diff --git a/proxyd/proxyd/README.md b/proxyd/proxyd/README.md index d3bfd50..e3ec1ca 100644 --- a/proxyd/proxyd/README.md +++ b/proxyd/proxyd/README.md @@ -89,6 +89,52 @@ Cache use Redis and can be enabled for the following immutable methods: * `eth_getUncleByBlockHashAndIndex` * `debug_getRawReceipts` (block hash only) +## Meta method `consensus_getReceipts` + +To support backends with different specifications in the same backend group, +proxyd exposes a convenient method to fetch receipts abstracting away +what specific backend will serve the request. + +Each backend specifies their preferred method to fetch receipts with `consensus_receipts_target` config, +which will be translated from `consensus_getReceipts`. + +This method takes a `blockNumberOrHash` (i.e. `tag|qty|hash`) +and returns the receipts for all transactions in the block. + +Request example +```json +{ + "jsonrpc":"2.0", + "id": 1, + "params": ["0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"] +} +``` + +It currently supports translation to the following targets: +* `debug_getRawReceipts(blockOrHash)` (default) +* `alchemy_getTransactionReceipts(blockOrHash)` +* `parity_getBlockReceipts(blockOrHash)` +* `eth_getBlockReceipts(blockOrHash)` + +The selected target is returned in the response, in a wrapped result. + +Response example +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "method": "debug_getRawReceipts", + "result": { + // the actual raw result from backend + } + } +} +``` + +See [op-node receipt fetcher](https://github.com/ethereum-optimism/optimism/blob/186e46a47647a51a658e699e9ff047d39444c2de/op-node/sources/receipts.go#L186-L253). + + ## Metrics See `metrics.go` for a list of all available metrics. diff --git a/proxyd/proxyd/backend.go b/proxyd/proxyd/backend.go index 07c9fa2..9c4234f 100644 --- a/proxyd/proxyd/backend.go +++ b/proxyd/proxyd/backend.go @@ -18,6 +18,8 @@ import ( "time" sw "github.com/ethereum-optimism/optimism/proxyd/pkg/avg-sliding-window" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/log" "github.com/gorilla/websocket" @@ -97,6 +99,9 @@ var ( } ErrBackendUnexpectedJSONRPC = errors.New("backend returned an unexpected JSON-RPC response") + + ErrConsensusGetReceiptsCantBeBatched = errors.New("consensus_getReceipts cannot be batched") + ErrConsensusGetReceiptsInvalidTarget = errors.New("unsupported consensus_receipts_target") ) func ErrInvalidRequest(msg string) *RPCErr { @@ -118,6 +123,7 @@ func ErrInvalidParams(msg string) *RPCErr { type Backend struct { Name string rpcURL string + receiptsTarget string wsURL string authUsername string authPassword string @@ -208,7 +214,7 @@ func WithProxydIP(ip string) BackendOpt { } } -func WithSkipPeerCountCheck(skipPeerCountCheck bool) BackendOpt { +func WithConsensusSkipPeerCountCheck(skipPeerCountCheck bool) BackendOpt { return func(b *Backend) { b.skipPeerCountCheck = skipPeerCountCheck } @@ -232,12 +238,36 @@ func WithMaxErrorRateThreshold(maxErrorRateThreshold float64) BackendOpt { } } +func WithConsensusReceiptTarget(receiptsTarget string) BackendOpt { + return func(b *Backend) { + b.receiptsTarget = receiptsTarget + } +} + type indexedReqRes struct { index int req *RPCReq res *RPCRes } +const ConsensusGetReceiptsMethod = "consensus_getReceipts" + +const ReceiptsTargetDebugGetRawReceipts = "debug_getRawReceipts" +const ReceiptsTargetAlchemyGetTransactionReceipts = "alchemy_getTransactionReceipts" +const ReceiptsTargetParityGetTransactionReceipts = "parity_getBlockReceipts" +const ReceiptsTargetEthGetTransactionReceipts = "eth_getBlockReceipts" + +type ConsensusGetReceiptsResult struct { + Method string `json:"method"` + Result interface{} `json:"result"` +} + +// BlockHashOrNumberParameter is a non-conventional wrapper used by alchemy_getTransactionReceipts +type BlockHashOrNumberParameter struct { + BlockHash *common.Hash `json:"blockHash"` + BlockNumber *rpc.BlockNumber `json:"blockNumber"` +} + func NewBackend( name string, rpcURL string, @@ -266,9 +296,7 @@ func NewBackend( networkErrorsSlidingWindow: sw.NewSlidingWindow(), } - for _, opt := range opts { - opt(backend) - } + backend.Override(opts...) if !backend.stripTrailingXFF && backend.proxydIP == "" { log.Warn("proxied requests' XFF header will not contain the proxyd ip address") @@ -277,6 +305,12 @@ func NewBackend( return backend } +func (b *Backend) Override(opts ...BackendOpt) { + for _, opt := range opts { + opt(b) + } +} + func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([]*RPCRes, error) { var lastError error // <= to account for the first attempt not technically being @@ -298,6 +332,20 @@ func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([] res, err := b.doForward(ctx, reqs, isBatch) switch err { case nil: // do nothing + case ErrConsensusGetReceiptsCantBeBatched: + log.Warn( + "Received unsupported batch request for consensus_getReceipts", + "name", b.Name, + "req_id", GetReqID(ctx), + "err", err, + ) + case ErrConsensusGetReceiptsInvalidTarget: + log.Error( + "Unsupported consensus_receipts_target for consensus_getReceipts", + "name", b.Name, + "req_id", GetReqID(ctx), + "err", err, + ) // ErrBackendUnexpectedJSONRPC occurs because infura responds with a single JSON-RPC object // to a batch request whenever any Request Object in the batch would induce a partial error. // We don't label the backend offline in this case. But the error is still returned to @@ -375,11 +423,63 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool // we are concerned about network error rates, so we record 1 request independently of how many are in the batch b.networkRequestsSlidingWindow.Incr() + translatedReqs := make(map[string]*RPCReq, len(rpcReqs)) + // translate consensus_getReceipts to receipts target + // right now we only support non-batched + if isBatch { + for _, rpcReq := range rpcReqs { + if rpcReq.Method == ConsensusGetReceiptsMethod { + return nil, ErrConsensusGetReceiptsCantBeBatched + } + } + } else { + for _, rpcReq := range rpcReqs { + if rpcReq.Method == ConsensusGetReceiptsMethod { + translatedReqs[string(rpcReq.ID)] = rpcReq + rpcReq.Method = b.receiptsTarget + var reqParams []rpc.BlockNumberOrHash + err := json.Unmarshal(rpcReq.Params, &reqParams) + if err != nil { + return nil, ErrInvalidRequest("invalid request") + } + + var translatedParams []byte + switch rpcReq.Method { + case ReceiptsTargetDebugGetRawReceipts, + ReceiptsTargetEthGetTransactionReceipts, + ReceiptsTargetParityGetTransactionReceipts: + // conventional methods use an array of strings having either block number or block hash + // i.e. ["0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"] + params := make([]string, 1) + if reqParams[0].BlockNumber != nil { + params[0] = reqParams[0].BlockNumber.String() + } else { + params[0] = reqParams[0].BlockHash.Hex() + } + translatedParams = mustMarshalJSON(params) + case ReceiptsTargetAlchemyGetTransactionReceipts: + // alchemy uses an array of object with either block number or block hash + // i.e. [{ blockHash: "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b" }] + params := make([]BlockHashOrNumberParameter, 1) + if reqParams[0].BlockNumber != nil { + params[0].BlockNumber = reqParams[0].BlockNumber + } else { + params[0].BlockHash = reqParams[0].BlockHash + } + translatedParams = mustMarshalJSON(params) + default: + return nil, ErrConsensusGetReceiptsInvalidTarget + } + + rpcReq.Params = translatedParams + } + } + } + isSingleElementBatch := len(rpcReqs) == 1 // Single element batches are unwrapped before being sent // since Alchemy handles single requests better than batches. - var body []byte if isSingleElementBatch { body = mustMarshalJSON(rpcReqs[0]) @@ -443,17 +543,17 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool return nil, wrapErr(err, "error reading response body") } - var res []*RPCRes + var rpcRes []*RPCRes if isSingleElementBatch { var singleRes RPCRes if err := json.Unmarshal(resB, &singleRes); err != nil { return nil, ErrBackendBadResponse } - res = []*RPCRes{ + rpcRes = []*RPCRes{ &singleRes, } } else { - if err := json.Unmarshal(resB, &res); err != nil { + if err := json.Unmarshal(resB, &rpcRes); err != nil { // Infura may return a single JSON-RPC response if, for example, the batch contains a request for an unsupported method if responseIsNotBatched(resB) { b.networkErrorsSlidingWindow.Incr() @@ -466,7 +566,7 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool } } - if len(rpcReqs) != len(res) { + if len(rpcReqs) != len(rpcRes) { b.networkErrorsSlidingWindow.Incr() RecordBackendNetworkErrorRateSlidingWindow(b, b.ErrorRate()) return nil, ErrBackendUnexpectedJSONRPC @@ -475,7 +575,7 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool // capture the HTTP status code in the response. this will only // ever be 400 given the status check on line 318 above. if httpRes.StatusCode != 200 { - for _, res := range res { + for _, res := range rpcRes { res.Error.HTTPErrorCode = httpRes.StatusCode } } @@ -484,8 +584,20 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool RecordBackendNetworkLatencyAverageSlidingWindow(b, time.Duration(b.latencySlidingWindow.Avg())) RecordBackendNetworkErrorRateSlidingWindow(b, b.ErrorRate()) - sortBatchRPCResponse(rpcReqs, res) - return res, nil + // enrich the response with the actual request method + for _, res := range rpcRes { + translatedReq, exist := translatedReqs[string(res.ID)] + if exist { + res.Result = ConsensusGetReceiptsResult{ + Method: translatedReq.Method, + Result: res.Result, + } + } + } + + sortBatchRPCResponse(rpcReqs, rpcRes) + + return rpcRes, nil } // IsHealthy checks if the backend is able to serve traffic, based on dynamic parameters @@ -604,7 +716,9 @@ func (bg *BackendGroup) Forward(ctx context.Context, rpcReqs []*RPCReq, isBatch if len(rpcReqs) > 0 { res, err = back.Forward(ctx, rpcReqs, isBatch) - if errors.Is(err, ErrMethodNotWhitelisted) { + if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) || + errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) || + errors.Is(err, ErrMethodNotWhitelisted) { return nil, err } if errors.Is(err, ErrBackendOffline) { diff --git a/proxyd/proxyd/config.go b/proxyd/proxyd/config.go index 0e75769..bd58f62 100644 --- a/proxyd/proxyd/config.go +++ b/proxyd/proxyd/config.go @@ -79,18 +79,20 @@ type BackendOptions struct { } type BackendConfig struct { - Username string `toml:"username"` - Password string `toml:"password"` - RPCURL string `toml:"rpc_url"` - WSURL string `toml:"ws_url"` - WSPort int `toml:"ws_port"` - MaxRPS int `toml:"max_rps"` - MaxWSConns int `toml:"max_ws_conns"` - CAFile string `toml:"ca_file"` - ClientCertFile string `toml:"client_cert_file"` - ClientKeyFile string `toml:"client_key_file"` - StripTrailingXFF bool `toml:"strip_trailing_xff"` - SkipPeerCountCheck bool `toml:"consensus_skip_peer_count"` + Username string `toml:"username"` + Password string `toml:"password"` + RPCURL string `toml:"rpc_url"` + WSURL string `toml:"ws_url"` + WSPort int `toml:"ws_port"` + MaxRPS int `toml:"max_rps"` + MaxWSConns int `toml:"max_ws_conns"` + CAFile string `toml:"ca_file"` + ClientCertFile string `toml:"client_cert_file"` + ClientKeyFile string `toml:"client_key_file"` + StripTrailingXFF bool `toml:"strip_trailing_xff"` + + ConsensusSkipPeerCountCheck bool `toml:"consensus_skip_peer_count"` + ConsensusReceiptsTarget string `toml:"consensus_receipts_target"` } type BackendsConfig map[string]*BackendConfig diff --git a/proxyd/proxyd/example.config.toml b/proxyd/proxyd/example.config.toml index 19c6c26..cce4896 100644 --- a/proxyd/proxyd/example.config.toml +++ b/proxyd/proxyd/example.config.toml @@ -74,7 +74,9 @@ client_cert_file = "" client_key_file = "" # Allows backends to skip peer count checking, default false # consensus_skip_peer_count = true - +# Specified the target method to get receipts, default "debug_getRawReceipts" +# See https://github.com/ethereum-optimism/optimism/blob/186e46a47647a51a658e699e9ff047d39444c2de/op-node/sources/receipts.go#L186-L253 +consensus_receipts_target = "eth_getBlockReceipts" [backends.alchemy] rpc_url = "" @@ -83,6 +85,7 @@ username = "" password = "" max_rps = 3 max_ws_conns = 1 +consensus_receipts_target = "alchemy_getTransactionReceipts" [backend_groups] [backend_groups.main] diff --git a/proxyd/proxyd/go.mod b/proxyd/proxyd/go.mod index d7b1c89..605a887 100644 --- a/proxyd/proxyd/go.mod +++ b/proxyd/proxyd/go.mod @@ -9,6 +9,7 @@ require ( github.com/ethereum/go-ethereum v1.12.0 github.com/go-redis/redis/v8 v8.11.4 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb + github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d diff --git a/proxyd/proxyd/go.sum b/proxyd/proxyd/go.sum index c9927a4..a6edcee 100644 --- a/proxyd/proxyd/go.sum +++ b/proxyd/proxyd/go.sum @@ -157,6 +157,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/proxyd/proxyd/integration_tests/consensus_test.go b/proxyd/proxyd/integration_tests/consensus_test.go index f59358e..1b37ef7 100644 --- a/proxyd/proxyd/integration_tests/consensus_test.go +++ b/proxyd/proxyd/integration_tests/consensus_test.go @@ -784,6 +784,211 @@ func TestConsensus(t *testing.T) { // dont rewrite for 0xe1 require.Equal(t, "0xe1", jsonMap[2]["result"].(map[string]interface{})["number"]) }) + + t.Run("translate consensus_getReceipts to debug_getRawReceipts", func(t *testing.T) { + reset() + useOnlyNode1() + update() + + // reset request counts + nodes["node1"].mockBackend.Reset() + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"}) + require.NoError(t, err) + require.Equal(t, 200, statusCode) + + var jsonMap map[string]interface{} + err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &jsonMap) + require.NoError(t, err) + require.Equal(t, "debug_getRawReceipts", jsonMap["method"]) + require.Equal(t, "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", jsonMap["params"].([]interface{})[0]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + require.NoError(t, err) + + require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string)) + require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) + }) + + t.Run("translate consensus_getReceipts to debug_getRawReceipts with latest block tag", func(t *testing.T) { + reset() + useOnlyNode1() + update() + + // reset request counts + nodes["node1"].mockBackend.Reset() + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"latest"}) + + require.NoError(t, err) + require.Equal(t, 200, statusCode) + + var jsonMap map[string]interface{} + err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &jsonMap) + require.NoError(t, err) + require.Equal(t, "debug_getRawReceipts", jsonMap["method"]) + require.Equal(t, "0x101", jsonMap["params"].([]interface{})[0]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + require.NoError(t, err) + + require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string)) + require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) + }) + + t.Run("translate consensus_getReceipts to debug_getRawReceipts with block number", func(t *testing.T) { + reset() + useOnlyNode1() + update() + + // reset request counts + nodes["node1"].mockBackend.Reset() + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0x55"}) + + require.NoError(t, err) + require.Equal(t, 200, statusCode) + + var jsonMap map[string]interface{} + err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &jsonMap) + require.NoError(t, err) + require.Equal(t, "debug_getRawReceipts", jsonMap["method"]) + require.Equal(t, "0x55", jsonMap["params"].([]interface{})[0]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + require.NoError(t, err) + + require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string)) + require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) + }) + + t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts with block hash", func(t *testing.T) { + reset() + useOnlyNode1() + update() + + // reset request counts + nodes["node1"].mockBackend.Reset() + + nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("alchemy_getTransactionReceipts")) + defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts")) + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"}) + + require.NoError(t, err) + require.Equal(t, 200, statusCode) + + var reqJsonMap map[string]interface{} + err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &reqJsonMap) + + require.NoError(t, err) + require.Equal(t, "alchemy_getTransactionReceipts", reqJsonMap["method"]) + require.Equal(t, "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", reqJsonMap["params"].([]interface{})[0].(map[string]interface{})["blockHash"]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + require.NoError(t, err) + + require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string)) + require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) + }) + + t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts with block number", func(t *testing.T) { + reset() + useOnlyNode1() + update() + + // reset request counts + nodes["node1"].mockBackend.Reset() + + nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("alchemy_getTransactionReceipts")) + defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts")) + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0x55"}) + + require.NoError(t, err) + require.Equal(t, 200, statusCode) + + var reqJsonMap map[string]interface{} + err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &reqJsonMap) + + require.NoError(t, err) + require.Equal(t, "alchemy_getTransactionReceipts", reqJsonMap["method"]) + require.Equal(t, "0x55", reqJsonMap["params"].([]interface{})[0].(map[string]interface{})["blockNumber"]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + require.NoError(t, err) + + require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string)) + require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) + }) + + t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts with latest block tag", func(t *testing.T) { + reset() + useOnlyNode1() + update() + + // reset request counts + nodes["node1"].mockBackend.Reset() + + nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("alchemy_getTransactionReceipts")) + defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts")) + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"latest"}) + + require.NoError(t, err) + require.Equal(t, 200, statusCode) + + var reqJsonMap map[string]interface{} + err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &reqJsonMap) + + require.NoError(t, err) + require.Equal(t, "alchemy_getTransactionReceipts", reqJsonMap["method"]) + require.Equal(t, "0x101", reqJsonMap["params"].([]interface{})[0].(map[string]interface{})["blockNumber"]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + require.NoError(t, err) + + require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string)) + require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) + }) + + t.Run("translate consensus_getReceipts to unsupported consensus_receipts_target", func(t *testing.T) { + reset() + useOnlyNode1() + + nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("unsupported_consensus_receipts_target")) + defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts")) + + _, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"latest"}) + + require.NoError(t, err) + require.Equal(t, 400, statusCode) + }) + + t.Run("consensus_getReceipts should not be used in a batch", func(t *testing.T) { + reset() + useOnlyNode1() + + _, statusCode, err := client.SendBatchRPC( + NewRPCReq("1", "eth_getBlockByNumber", []interface{}{"latest"}), + NewRPCReq("2", "consensus_getReceipts", []interface{}{"0x55"}), + NewRPCReq("3", "eth_getBlockByNumber", []interface{}{"0xe1"})) + require.NoError(t, err) + require.Equal(t, 400, statusCode) + }) } func buildResponse(result interface{}) string { diff --git a/proxyd/proxyd/integration_tests/testdata/consensus.toml b/proxyd/proxyd/integration_tests/testdata/consensus.toml index 6d8fdf5..bb13036 100644 --- a/proxyd/proxyd/integration_tests/testdata/consensus.toml +++ b/proxyd/proxyd/integration_tests/testdata/consensus.toml @@ -27,3 +27,4 @@ eth_call = "node" eth_chainId = "node" eth_blockNumber = "node" eth_getBlockByNumber = "node" +consensus_getReceipts = "node" diff --git a/proxyd/proxyd/integration_tests/testdata/consensus_responses.yml b/proxyd/proxyd/integration_tests/testdata/consensus_responses.yml index ee413c5..e4720b0 100644 --- a/proxyd/proxyd/integration_tests/testdata/consensus_responses.yml +++ b/proxyd/proxyd/integration_tests/testdata/consensus_responses.yml @@ -184,3 +184,30 @@ "number": "0xd1" } } +- method: debug_getRawReceipts + response: > + { + "jsonrpc": "2.0", + "id": 67, + "result": { + "_": "debug_getRawReceipts" + } + } +- method: eth_getTransactionReceipt + response: > + { + "jsonrpc": "2.0", + "id": 67, + "result": { + "_": "eth_getTransactionReceipt" + } + } +- method: alchemy_getTransactionReceipts + response: > + { + "jsonrpc": "2.0", + "id": 67, + "result": { + "_": "alchemy_getTransactionReceipts" + } + } diff --git a/proxyd/proxyd/proxyd.go b/proxyd/proxyd/proxyd.go index afb27bc..f1e9275 100644 --- a/proxyd/proxyd/proxyd.go +++ b/proxyd/proxyd/proxyd.go @@ -141,7 +141,17 @@ func Start(config *Config) (*Server, func(), error) { opts = append(opts, WithStrippedTrailingXFF()) } opts = append(opts, WithProxydIP(os.Getenv("PROXYD_IP"))) - opts = append(opts, WithSkipPeerCountCheck(cfg.SkipPeerCountCheck)) + opts = append(opts, WithConsensusSkipPeerCountCheck(cfg.ConsensusSkipPeerCountCheck)) + + receiptsTarget, err := ReadFromEnvOrConfig(cfg.ConsensusReceiptsTarget) + if err != nil { + return nil, nil, err + } + receiptsTarget, err = validateReceiptsTarget(receiptsTarget) + if err != nil { + return nil, nil, err + } + opts = append(opts, WithConsensusReceiptTarget(receiptsTarget)) back := NewBackend(name, rpcURL, wsURL, rpcRequestSemaphore, opts...) backendNames = append(backendNames, name) @@ -316,6 +326,21 @@ func Start(config *Config) (*Server, func(), error) { return srv, shutdownFunc, nil } +func validateReceiptsTarget(val string) (string, error) { + if val == "" { + val = ReceiptsTargetDebugGetRawReceipts + } + switch val { + case ReceiptsTargetDebugGetRawReceipts, + ReceiptsTargetAlchemyGetTransactionReceipts, + ReceiptsTargetEthGetTransactionReceipts, + ReceiptsTargetParityGetTransactionReceipts: + return val, nil + default: + return "", fmt.Errorf("invalid receipts target: %s", val) + } +} + func secondsToDuration(seconds int) time.Duration { return time.Duration(seconds) * time.Second } diff --git a/proxyd/proxyd/rewriter.go b/proxyd/proxyd/rewriter.go index 71dd361..b00bc51 100644 --- a/proxyd/proxyd/rewriter.go +++ b/proxyd/proxyd/rewriter.go @@ -63,7 +63,7 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul case "eth_getLogs", "eth_newFilter": return rewriteRange(rctx, req, res, 0) - case "debug_getRawReceipts": + case "debug_getRawReceipts", "consensus_getReceipts": return rewriteParam(rctx, req, res, 0, true) case "eth_getBalance", "eth_getCode", diff --git a/proxyd/proxyd/server.go b/proxyd/proxyd/server.go index 3ca10af..88b031c 100644 --- a/proxyd/proxyd/server.go +++ b/proxyd/proxyd/server.go @@ -347,6 +347,11 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { writeRPCError(ctx, w, nil, ErrGatewayTimeout) return } + if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) || + errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) { + writeRPCError(ctx, w, nil, ErrInvalidRequest(err.Error())) + return + } if err != nil { writeRPCError(ctx, w, nil, ErrInternal) return @@ -360,6 +365,11 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { rawBody := json.RawMessage(body) backendRes, cached, err := s.handleBatchRPC(ctx, []json.RawMessage{rawBody}, isLimited, false) if err != nil { + if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) || + errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) { + writeRPCError(ctx, w, nil, ErrInvalidRequest(err.Error())) + return + } writeRPCError(ctx, w, nil, ErrInternal) return } @@ -485,6 +495,10 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL elems := cacheMisses[start:end] res, err := s.BackendGroups[group.backendGroup].Forward(ctx, createBatchRequest(elems), isBatch) if err != nil { + if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) || + errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) { + return nil, false, err + } log.Error( "error forwarding RPC batch", "batch_size", len(elems), diff --git a/proxyd/proxyd/tools/mockserver/handler/handler.go b/proxyd/proxyd/tools/mockserver/handler/handler.go index 8a78a7e..0f6880c 100644 --- a/proxyd/proxyd/tools/mockserver/handler/handler.go +++ b/proxyd/proxyd/tools/mockserver/handler/handler.go @@ -88,7 +88,15 @@ func (mh *MockedHandler) Handler(w http.ResponseWriter, req *http.Request) { } } if selectedResponse != "" { - responses = append(responses, selectedResponse) + var rpcRes proxyd.RPCRes + err = json.Unmarshal([]byte(selectedResponse), &rpcRes) + if err != nil { + panic(err) + } + idJson, _ := json.Marshal(r["id"]) + rpcRes.ID = idJson + res, _ := json.Marshal(rpcRes) + responses = append(responses, string(res)) } }