From 6d698ea25f61b92b8687cdceeeb5e3595621947e Mon Sep 17 00:00:00 2001 From: Yashvardhan Kukreja Date: Wed, 9 Jul 2025 19:44:32 -0400 Subject: [PATCH 1/2] feat: force disable interop validation and remove interop error code awareness Signed-off-by: Yashvardhan Kukreja --- proxyd/backend.go | 173 ++++++++---------- proxyd/config.go | 1 + proxyd/go.mod | 26 +-- proxyd/go.sum | 42 +++-- .../interop_validation_test.go | 77 ++++++-- proxyd/interop_strategy.go | 5 +- proxyd/server.go | 8 +- 7 files changed, 190 insertions(+), 142 deletions(-) diff --git a/proxyd/backend.go b/proxyd/backend.go index c7b13070..3addfa9a 100644 --- a/proxyd/backend.go +++ b/proxyd/backend.go @@ -18,7 +18,6 @@ import ( "time" sw "github.com/ethereum-optimism/infra/proxyd/pkg/avg-sliding-window" - supervisorBackend "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend" supervisorTypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" @@ -30,9 +29,11 @@ import ( ) const ( - JSONRPCVersion = "2.0" - JSONRPCErrorInternal = -32000 - notFoundRpcError = -32601 + JSONRPCVersion = "2.0" + JSONRPCErrorInternal = -32000 + JSONRPCErrorInvalidRequest = -32600 + notFoundRpcError = -32601 + JSONRPCErrorInvalidParams = -32602 ) var ( @@ -165,115 +166,95 @@ Summary: -321500 MISSED_DATA -321501 DATA_CORRUPTION */ -var interopRPCErrorMap = map[error]*RPCErr{ - supervisorTypes.ErrUninitialized: { - Code: -320400, - HTTPErrorCode: 400, - }, - supervisorTypes.ErrSkipped: { - Code: -320500, - HTTPErrorCode: 422, - }, - supervisorTypes.ErrUnknownChain: { - Code: -320501, - HTTPErrorCode: 404, - }, - supervisorTypes.ErrConflict: { - Code: -320600, - HTTPErrorCode: 409, - }, - supervisorTypes.ErrIneffective: { - Code: -320601, - HTTPErrorCode: 422, - }, - supervisorTypes.ErrOutOfOrder: { - Code: -320900, - HTTPErrorCode: 409, - }, - supervisorTypes.ErrAwaitReplacementBlock: { - Code: -320901, - HTTPErrorCode: 409, - }, - supervisorTypes.ErrStop: { - Code: -321000, - HTTPErrorCode: 400, - }, - supervisorTypes.ErrOutOfScope: { - Code: -321100, - HTTPErrorCode: 400, - }, - supervisorTypes.ErrPreviousToFirst: { - Code: -321200, - HTTPErrorCode: 404, - }, - supervisorTypes.ErrFuture: { - Code: -321401, - HTTPErrorCode: 422, - }, - supervisorTypes.ErrNotExact: { - Code: -321500, - HTTPErrorCode: 404, - }, - supervisorTypes.ErrDataCorruption: { - Code: -321501, - HTTPErrorCode: 422, - }, - supervisorBackend.ErrUnexpectedMinSafetyLevel: { - Code: -32602, // invalid params - HTTPErrorCode: 400, - }, - errors.New("stopped acces-list check early"): { - Code: -32602, // invalid params - HTTPErrorCode: 400, - }, - errors.New("failed to read data"): { - Code: -32602, // invalid params - HTTPErrorCode: 400, - }, +func getInteropRPCErrorHttpCode(err error) (httpCode int, knownErr bool) { + knownErr = true + switch err.Error() { + case supervisorTypes.ErrUninitialized.Error(): + httpCode = 400 + case supervisorTypes.ErrSkipped.Error(): + httpCode = 422 + case supervisorTypes.ErrUnknownChain.Error(): + httpCode = 404 + case supervisorTypes.ErrConflict.Error(): + httpCode = 409 + case supervisorTypes.ErrIneffective.Error(): + httpCode = 422 + case supervisorTypes.ErrOutOfOrder.Error(): + httpCode = 409 + case supervisorTypes.ErrAwaitReplacementBlock.Error(): + httpCode = 409 + case supervisorTypes.ErrStop.Error(): + httpCode = 400 + case supervisorTypes.ErrOutOfScope.Error(): + httpCode = 400 + case supervisorTypes.ErrPreviousToFirst.Error(): + httpCode = 404 + case supervisorTypes.ErrFuture.Error(): + httpCode = 422 + case supervisorTypes.ErrNotExact.Error(): + httpCode = 404 + case supervisorTypes.ErrDataCorruption.Error(): + httpCode = 422 + default: + httpCode = 400 + knownErr = false + } + return } func ParseInteropError(err error) *RPCErr { - var fallbackErr *RPCErr + if rpcErr, ok := err.(*RPCErr); ok { + return rpcErr + } + httpErr, isHTTPError := err.(rpc.HTTPError) if !isHTTPError { - fallbackErr = &RPCErr{ + return &RPCErr{ Code: JSONRPCErrorInternal, Message: err.Error(), HTTPErrorCode: 500, } - } else { - // if the underlying error is a JSON-RPC error, overwrite it with the inherent error message body - var rpcErrJson rpcResJSON - unmarshalErr := json.Unmarshal(httpErr.Body, &rpcErrJson) - if unmarshalErr != nil { - fallbackErr = ErrInvalidParams(string(httpErr.Body)) - fallbackErr.HTTPErrorCode = httpErr.StatusCode - } else { - fallbackErr = &RPCErr{ - Code: rpcErrJson.Error.Code, - Message: rpcErrJson.Error.Message, - Data: rpcErrJson.Error.Data, - HTTPErrorCode: httpErr.StatusCode, - } - - err = fmt.Errorf(rpcErrJson.Error.Message) - } } - errStr := err.Error() - for errSubStr, errCodes := range interopRPCErrorMap { - if strings.Contains(errStr, errSubStr.Error()) { - interopParsedErr := errCodes.Clone() - interopParsedErr.Message = errStr - return interopParsedErr + // if the underlying error is a JSON-RPC error, overwrite it with the inherent error message body + var rpcResponse rpcResJSON + if unmarshalErr := json.Unmarshal(httpErr.Body, &rpcResponse); unmarshalErr == nil { + httpCode, knownErr := getInteropRPCErrorHttpCode(rpcResponse.Error) + if !knownErr { + httpCode = httpErr.StatusCode // fallback to the HTTP status code of the original error + } + rpcResponse.Error.HTTPErrorCode = httpCode + return rpcResponse.Error + } + var rpcErrResponse rpc.JsonError + if unmarshalErr := json.Unmarshal(httpErr.Body, &rpcErrResponse); unmarshalErr == nil { + var data json.RawMessage + if rpcErrResponse.Data != nil { + dataBytes, err := json.Marshal(rpcErrResponse.Data) + if err == nil { + data = json.RawMessage(dataBytes) + } else { + data = json.RawMessage([]byte(fmt.Sprintf("%+v", rpcErrResponse.Data))) + } + } + httpCode, _ := getInteropRPCErrorHttpCode(err) // no knownErr check as we're already want to fallback to 400 + return &RPCErr{ + Code: rpcErrResponse.Code, + Message: rpcErrResponse.Message, + Data: data, + HTTPErrorCode: httpCode, } } + + fallbackErr := ErrInvalidParams(string(httpErr.Body)) + fallbackErr.HTTPErrorCode = httpErr.StatusCode + return fallbackErr } func ErrInvalidRequest(msg string) *RPCErr { return &RPCErr{ - Code: -32600, + Code: JSONRPCErrorInvalidRequest, Message: msg, HTTPErrorCode: 400, } @@ -281,7 +262,7 @@ func ErrInvalidRequest(msg string) *RPCErr { func ErrInvalidParams(msg string) *RPCErr { return &RPCErr{ - Code: -32602, + Code: JSONRPCErrorInvalidParams, Message: msg, HTTPErrorCode: 400, } diff --git a/proxyd/config.go b/proxyd/config.go index 677a1942..5325a7f5 100644 --- a/proxyd/config.go +++ b/proxyd/config.go @@ -237,6 +237,7 @@ type InteropValidationConfig struct { ReqSizeLimit int `toml:"req_size_limit"` AccessListSizeLimit int `toml:"access_list_size_limit"` RateLimit SenderRateLimitConfig `toml:"sender_rate_limit"` + ForceDisableInteropValidation bool `toml:"force_disable_interop_validation"` } type InteropValidationStrategy string diff --git a/proxyd/go.mod b/proxyd/go.mod index f9fe9eb0..53ee8e74 100644 --- a/proxyd/go.mod +++ b/proxyd/go.mod @@ -1,15 +1,15 @@ module github.com/ethereum-optimism/infra/proxyd -go 1.22.0 +go 1.23.0 -toolchain go1.23.6 +toolchain go1.24.0 require ( github.com/BurntSushi/toml v1.5.0 github.com/alicebob/miniredis v2.5.0+incompatible github.com/emirpasic/gods v1.18.1 - github.com/ethereum-optimism/optimism v1.13.3-0.20250506125223-182c0424f6dc - github.com/ethereum/go-ethereum v1.15.3 + github.com/ethereum-optimism/optimism v1.13.5-0.20250709133933-ab50df748916 + github.com/ethereum/go-ethereum v1.15.11 github.com/go-redsync/redsync/v4 v4.10.0 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb github.com/gorilla/mux v1.8.0 @@ -22,7 +22,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a github.com/xaionaro-go/weightedshuffle v0.0.0-20211213010739-6a74fbc7d24a - golang.org/x/sync v0.10.0 + golang.org/x/sync v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -121,21 +121,21 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.14 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/urfave/cli/v2 v2.27.6 // indirect github.com/wlynxg/anet v0.0.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect - golang.org/x/crypto v0.32.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/crypto v0.35.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/net v0.34.0 // indirect + golang.org/x/net v0.36.0 // indirect golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.10.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect diff --git a/proxyd/go.sum b/proxyd/go.sum index 0b941882..5fce2f04 100644 --- a/proxyd/go.sum +++ b/proxyd/go.sum @@ -107,8 +107,8 @@ github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs= github.com/ethereum-optimism/op-geth v1.101503.4-rc.1 h1:ddcoFOmABL7bwz6b0pllSE4Org9iFTwMN6r1G6NRy3w= github.com/ethereum-optimism/op-geth v1.101503.4-rc.1/go.mod h1:QUo3fn+45vWqJWzJW+rIzRHUV7NmhhHLPdI87mAn1M8= -github.com/ethereum-optimism/optimism v1.13.3-0.20250506125223-182c0424f6dc h1:9oRI/aHPrnzILQX+HE2d1WmPAU19ByMZfXYBcg6iGa8= -github.com/ethereum-optimism/optimism v1.13.3-0.20250506125223-182c0424f6dc/go.mod h1:/GqvIKnHezaiugTjhamK8UmhqxVHDDskwb21FEUGucY= +github.com/ethereum-optimism/optimism v1.13.5-0.20250709133933-ab50df748916 h1:8EuqUX5Pq6yOUJhKBLOOYNTZ8HTA08faMBeUQU611j8= +github.com/ethereum-optimism/optimism v1.13.5-0.20250709133933-ab50df748916/go.mod h1:yImPc3xmqpN9hsIQivAaubX9ROSlGvhR0JURIQ2AZrU= github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= @@ -368,10 +368,10 @@ github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= @@ -388,8 +388,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -402,8 +404,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -428,8 +430,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -437,8 +439,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -477,8 +479,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -488,10 +490,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/proxyd/integration_tests/interop_validation_test.go b/proxyd/integration_tests/interop_validation_test.go index e83d35fa..97fa70dd 100644 --- a/proxyd/integration_tests/interop_validation_test.go +++ b/proxyd/integration_tests/interop_validation_test.go @@ -85,11 +85,11 @@ func TestInteropValidation_NormalFlow(t *testing.T) { require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", goodBackend.URL())) - errResp1 := fmt.Sprintf(errResTmpl, -32000, supervisorTypes.ErrConflict.Error()) + errResp1 := fmt.Sprintf(errResTmpl, -320600, supervisorTypes.ErrConflict.Error()) badValidatingBackend1 := NewMockBackend(SingleResponseHandler(409, errResp1)) defer badValidatingBackend1.Close() - errResp2 := fmt.Sprintf(errResTmpl, -32000, supervisorTypes.ErrDataCorruption.Error()) + errResp2 := fmt.Sprintf(errResTmpl, -321501, supervisorTypes.ErrDataCorruption.Error()) badValidatingBackend2 := NewMockBackend(SingleResponseHandler(400, errResp2)) defer badValidatingBackend2.Close() @@ -111,12 +111,13 @@ func TestInteropValidation_NormalFlow(t *testing.T) { jsonResponse []byte } type testCase struct { - name string - strategy proxyd.InteropValidationStrategy - urls []string - expectedResp respDetails - possibilities []respDetails - multiplePossibilities bool + name string + strategy proxyd.InteropValidationStrategy + urls []string + expectedResp respDetails + possibilities []respDetails + multiplePossibilities bool + forceDisableInteropValidation bool } cases := []testCase{ { @@ -128,6 +129,16 @@ func TestInteropValidation_NormalFlow(t *testing.T) { jsonResponse: []byte(expectedErrResp1), }, }, + { + name: "first-supervisor strategy with first url returning success but interop validation is disabled", + strategy: proxyd.FirstSupervisorStrategy, + urls: []string{goodSupervisorUrl}, + expectedResp: respDetails{ + code: 403, + jsonResponse: []byte(fmt.Sprintf(errResTmpl, -32600, "interop transactions are not allowed")), + }, + forceDisableInteropValidation: true, + }, { name: "default strategy with first url returning success", strategy: proxyd.EmptyStrategy, @@ -137,6 +148,16 @@ func TestInteropValidation_NormalFlow(t *testing.T) { jsonResponse: []byte(dummyHealthyRes), }, }, + { + name: "default strategy with first url returning success but interop validation is disabled", + strategy: proxyd.EmptyStrategy, + urls: []string{goodSupervisorUrl}, + expectedResp: respDetails{ + code: 403, + jsonResponse: []byte(fmt.Sprintf(errResTmpl, -32600, "interop transactions are not allowed")), + }, + forceDisableInteropValidation: true, + }, { name: "multicall strategy with atleast one good url", strategy: proxyd.MulticallStrategy, @@ -171,6 +192,7 @@ func TestInteropValidation_NormalFlow(t *testing.T) { t.Run(c.name, func(t *testing.T) { config.InteropValidationConfig.Strategy = c.strategy config.InteropValidationConfig.Urls = c.urls + config.InteropValidationConfig.ForceDisableInteropValidation = c.forceDisableInteropValidation _, shutdown, err := proxyd.Start(config) require.NoError(t, err) defer shutdown() @@ -658,11 +680,11 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", goodBackend.URL())) - errResp1 := fmt.Sprintf(errResTmpl, -32000, supervisorTypes.ErrConflict.Error()) + errResp1 := fmt.Sprintf(errResTmpl, -320600, supervisorTypes.ErrConflict.Error()) badHealthyBackend1 := NewMockBackend(SingleResponseHandler(409, errResp1)) defer badHealthyBackend1.Close() - errResp2 := fmt.Sprintf(errResTmpl, -32000, supervisorTypes.ErrDataCorruption.Error()) + errResp2 := fmt.Sprintf(errResTmpl, -321501, supervisorTypes.ErrDataCorruption.Error()) badHealthyBackend2 := NewMockBackend(SingleResponseHandler(400, errResp2)) defer badHealthyBackend2.Close() @@ -729,6 +751,8 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( // should start with no requests made to any of the backends obviously assertExpectations(t, expectations) + proceed := false + // First request // expectation: // - unhealthyBackend1 should receive 1 request only to realise that it's unhealthy @@ -736,7 +760,7 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( // - badHealthyBackend1 should receive 1 request and return with a bad validation response yet representing a healthy response // - badHealthyBackend2, unhealthyBackend3 should receive 0 requests because of the request already have being treated by badHealthyBackend1 t.Run("First Request", func(t *testing.T) { - fmt.Println("\t\t- Request should go through unhealthyBackend1(backed called) -> unhealthyBackend2(backend called) -> badHealthyBackend1(backend called)\n ") + fmt.Println("\t\t- Request should go through unhealthyBackend1(backend called) -> unhealthyBackend2(backend called) -> badHealthyBackend1(backend called)\n ") sendRawTransaction := makeSendRawTransaction(fakeInteropReqParams) observedResp, observedCode, err := client.SendRequest(sendRawTransaction) @@ -755,8 +779,15 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( expectations.badHealthyBackend1++ assertExpectations(t, expectations) + proceed = true }) + if !proceed { + t.Fatal("First request did not proceed") + } + + proceed = false + // second request // expectation: // - next backend to be tried is badHealthyBackend2 (as per it's turn considering the fact that badHealthyBackend1 served the last request) @@ -777,8 +808,15 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( assertExpectations(t, expectations) time.Sleep(100 * time.Millisecond) + proceed = true }) + if !proceed { + t.Fatal("Second request did not proceed") + } + + proceed = false + // third request // expectation // - unhealthyBackend3 gets tried only to realise that it's unhealthy @@ -800,8 +838,15 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( expectations.badHealthyBackend1 += 1 // the request tries the badHealthyBackend1 again as the next available healthy backend assertExpectations(t, expectations) + proceed = true }) + if !proceed { + t.Fatal("Third request did not proceed") + } + + proceed = false + // fourth request: // expectation: // - the next backend to be tried should be badHealthyBackend2, which suddenly returns a healthy response incrementing its request count alone @@ -820,8 +865,13 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( expectations.badHealthyBackend2++ assertExpectations(t, expectations) + proceed = true }) + if !proceed { + t.Fatal("Fourth request did not proceed") + } + // wait for the unhealthiness timeout to expire before trying the next request fmt.Println("\nWaiting 10 seconds for the unhealthiness timeout to expire...") time.Sleep(10 * time.Second) // default one @@ -851,7 +901,12 @@ func TestInteropValidation_HealthAwareLoadBalancingStrategy_SomeHealthyBackends( expectations.badHealthyBackend1 += 1 assertExpectations(t, expectations) + proceed = true }) + + if !proceed { + t.Fatal("Fifth request did not proceed") + } } func TestInteropValidation_HealthAwareLoadBalancingStrategy_NoHealthyBackends_CustomUnhealthinessTimeout(t *testing.T) { diff --git a/proxyd/interop_strategy.go b/proxyd/interop_strategy.go index ec55901b..a93364d6 100644 --- a/proxyd/interop_strategy.go +++ b/proxyd/interop_strategy.go @@ -94,7 +94,10 @@ func (s *commonInteropStrategy) preflightChecksAndCleanupAccessList(ctx context. interopAccessList, err = validateAndDeduplicateInteropAccessList(interopAccessList) if err != nil { log.Error("error validating and deduplicating interop access list", "req_id", GetReqID(ctx), "error", err) - return nil, false, ParseInteropError(fmt.Errorf("failed to read data: %w", err)) + rpcErr := ParseInteropError(fmt.Errorf("failed to parse access list entries: %w", err)) + rpcErr.HTTPErrorCode = 400 + rpcErr.Code = JSONRPCErrorInvalidParams + return nil, false, rpcErr } } diff --git a/proxyd/server.go b/proxyd/server.go index 98a2a54e..dedb39bc 100644 --- a/proxyd/server.go +++ b/proxyd/server.go @@ -457,6 +457,13 @@ func (s *Server) validateInteropSendRpcRequest(ctx context.Context, tx *types.Tr if !isInterop { return nil } + if s.interopValidatingConfig.ForceDisableInteropValidation { + return &RPCErr{ + Code: JSONRPCErrorInvalidRequest, + Message: "interop transactions are not allowed", + HTTPErrorCode: 403, + } + } // at this point, we know it's an interop transaction worthy of being validated log.Info( "validating interop access list", @@ -589,7 +596,6 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL responses[i] = NewRPCErrorRes(parsedReq.ID, err) continue } - } id := string(parsedReq.ID) From b2b06ff2dd743d234a7d8e25433e1f37566c666f Mon Sep 17 00:00:00 2001 From: Yashvardhan Kukreja Date: Thu, 24 Jul 2025 15:11:33 -0400 Subject: [PATCH 2/2] auto stop backoff Signed-off-by: Yashvardhan Kukreja --- proxyd/backoff.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++ proxyd/interop.go | 8 +++++ proxyd/server.go | 11 +++++++ 3 files changed, 95 insertions(+) create mode 100644 proxyd/backoff.go diff --git a/proxyd/backoff.go b/proxyd/backoff.go new file mode 100644 index 00000000..b98a8580 --- /dev/null +++ b/proxyd/backoff.go @@ -0,0 +1,76 @@ +package proxyd + +import "time" + +type BackoffStrategy interface { + Backoff() + WithinBackoff() bool + LastBackoffTime() time.Time + Reset() + BackoffWait() time.Duration +} + +type StaticBackoff struct { + lastBackoffTime time.Time + interval time.Duration +} + +func (b *StaticBackoff) WithinBackoff() bool { + if b.lastBackoffTime.IsZero() { + return false + } + return time.Since(b.lastBackoffTime) < b.interval +} + +func (b *StaticBackoff) LastBackoffTime() time.Time { + return b.lastBackoffTime +} + +func (b *StaticBackoff) Reset() { + b.lastBackoffTime = time.Time{} +} + +func (b *StaticBackoff) BackoffWait() time.Duration { + if b.WithinBackoff() { + return b.interval - time.Since(b.lastBackoffTime) + } + return 0 +} + +type IncrementalBackoff struct { + lastBackoffTime time.Time + maxDuration time.Duration + stepInterval time.Duration + stepCount int +} + +func (b *IncrementalBackoff) nextWhen() { + dur := b.stepInterval * time.Duration(b.stepCount) + if dur > b.maxDuration { + return b.maxDuration + } + return dur +} + +func (b *IncrementalBackoff) WithinBackoff() bool { + if b.lastBackoffTime.IsZero() { + return false + } + return time.Since(b.lastBackoffTime) < b.maxDuration +} + +func (b *IncrementalBackoff) LastBackoffTime() time.Time { + return b.lastBackoffTime +} + +func (b *IncrementalBackoff) Reset() { + b.lastBackoffTime = time.Time{} + b.stepCount = 0 +} + +func (b *IncrementalBackoff) BackoffWait() time.Duration { + if b.WithinBackoff() { + return b.maxDuration - time.Since(b.lastBackoffTime) + } + return 0 +} diff --git a/proxyd/interop.go b/proxyd/interop.go index 3f3bdadb..82b3508b 100644 --- a/proxyd/interop.go +++ b/proxyd/interop.go @@ -3,6 +3,7 @@ package proxyd import ( "context" "fmt" + "strings" "time" supervisorTypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" @@ -63,3 +64,10 @@ func (s *Server) rateLimitInteropSender(ctx context.Context, tx *types.Transacti } return s.genericRateLimitSender(ctx, tx, s.interopSenderLim) } + +func IsAutoStop(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), supervisorTypes.ErrFailsafeEnabled.Error()) +} diff --git a/proxyd/server.go b/proxyd/server.go index dedb39bc..ff8826b6 100644 --- a/proxyd/server.go +++ b/proxyd/server.go @@ -86,6 +86,7 @@ type Server struct { rateLimitHeader string interopValidatingConfig InteropValidationConfig interopStrategy InteropStrategy + interopBackoffStrategy BackoffStrategy } type limiterFunc func(method string) bool @@ -464,6 +465,13 @@ func (s *Server) validateInteropSendRpcRequest(ctx context.Context, tx *types.Tr HTTPErrorCode: 403, } } + if s.interopBackoffStrategy != nil && s.interopBackoffStrategy.WithinBackoff() { + return &RPCErr{ + Code: JSONRPCErrorInvalidRequest, + Message: "auto-stop observed, please try again later", + HTTPErrorCode: 403, + } + } // at this point, we know it's an interop transaction worthy of being validated log.Info( "validating interop access list", @@ -486,6 +494,9 @@ func (s *Server) validateInteropSendRpcRequest(ctx context.Context, tx *types.Tr } else { log.Info("interop access list validation failed", "req_id", GetReqID(ctx), "tx_hash", tx.Hash(), "error", finalErr) } + if IsAutoStop(finalErr) { + s.interopBackoffStrategy.Backoff() + } return finalErr }