From 33881542a99ff23474651444bb5d975bdb7c201b Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Thu, 1 Jun 2023 13:16:40 -0700 Subject: [PATCH 1/6] feat(proxyd): add consensus_getReceipts meta method --- proxyd/proxyd/README.md | 53 +++++++ proxyd/proxyd/backend.go | 150 ++++++++++++++++-- proxyd/proxyd/config.go | 26 +-- proxyd/proxyd/example.config.toml | 5 +- proxyd/proxyd/go.mod | 1 + proxyd/proxyd/go.sum | 2 + .../integration_tests/consensus_test.go | 146 +++++++++++++++++ .../integration_tests/testdata/consensus.toml | 1 + .../testdata/consensus_responses.yml | 27 ++++ proxyd/proxyd/proxyd.go | 26 ++- proxyd/proxyd/rewriter.go | 34 ++++ proxyd/proxyd/server.go | 7 + .../tools/mockserver/handler/handler.go | 7 +- 13 files changed, 458 insertions(+), 27 deletions(-) diff --git a/proxyd/proxyd/README.md b/proxyd/proxyd/README.md index d3bfd50..0a3c715 100644 --- a/proxyd/proxyd/README.md +++ b/proxyd/proxyd/README.md @@ -89,6 +89,59 @@ 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 can specify their preferred method to fetch receipts with `consensus_receipts_target`. + +This method takes **both** the blockNumberOrHash **and** list of transaction hashes to fetch the receipts, +and then after selecting the backend to serve the request, +it translates to the correct target with the appropriate parameters. + +Note that only one of the parameters will be actually used depending on the target. + +Request params +```json +{ + "jsonrpc":"2.0", + "id": 1, + "params": { + "blockNumberOrHash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", + "transactions": [ + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331", + "0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b" + ] + } +} +``` + +It currently supports translation to the following targets: +* `debug_getRawReceipts(blockOrHash)` (default) +* `alchemy_getTransactionReceipts(blockOrHash)` +* `eth_getTransactionReceipt(txHash)` batched + +The selected target is returned in the response, in a wrapped result. + +Response +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "method": "eth_getTransactionReceipt", + "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..439bb47 100644 --- a/proxyd/proxyd/backend.go +++ b/proxyd/proxyd/backend.go @@ -7,6 +7,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/google/uuid" "io" "math" "math/rand" @@ -97,6 +100,8 @@ var ( } ErrBackendUnexpectedJSONRPC = errors.New("backend returned an unexpected JSON-RPC response") + + ErrConsensusGetReceiptsCantBeBatched = errors.New("consensus_getReceipts cannot be batched") ) 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,33 @@ 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 ReceiptsTargetEthTransactionReceipt = "eth_getTransactionReceipt" +const ReceiptsTargetDebugGetRawReceipts = "debug_getRawReceipts" +const ReceiptsTargetGetTransactionReceipts = "alchemy_getTransactionReceipts" + +type ConsensusGetReceiptsReq struct { + BlockOrHash *rpc.BlockNumberOrHash `json:"blockOrHash"` + Transactions []common.Hash `json:"transactions"` +} + +type ConsensusGetReceiptsRes struct { + Method string `json:"method"` + Result interface{} `json:"result"` +} + func NewBackend( name string, rpcURL string, @@ -266,9 +293,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 +302,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 +329,13 @@ 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.Debug( + "Received unsupported batch request 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 +413,66 @@ 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() + originalRequests := rpcReqs + translatedReqs := make(map[string]*RPCReq, len(rpcReqs)) + derivedRequests := make([]*RPCReq, 0, 0) + // translate consensus_getReceipts to receipts target + // right now we only support non-batched + if !isBatch { + for _, rpcReq := range rpcReqs { + if rpcReq.Method == ConsensusGetReceiptsMethod { + translatedReqs[string(rpcReq.ID)] = rpcReq + rpcReq.Method = b.receiptsTarget + var reqParams []ConsensusGetReceiptsReq + err := json.Unmarshal(rpcReq.Params, &reqParams) + if err != nil { + return nil, ErrInvalidRequest("invalid request") + } + bnh := reqParams[0].BlockOrHash + switch b.receiptsTarget { + case ReceiptsTargetDebugGetRawReceipts, + ReceiptsTargetGetTransactionReceipts: // block or hash + params := make([]string, 1) + if bnh.BlockNumber != nil { + params[0] = bnh.BlockNumber.String() + } else { + params[0] = bnh.BlockHash.Hex() + } + rawParams := mustMarshalJSON(params) + rpcReq.Params = rawParams + case ReceiptsTargetEthTransactionReceipt: // list of tx hashes + for _, txHash := range reqParams[0].Transactions { + params := make([]common.Hash, 1) + params[0] = txHash + rawParams := mustMarshalJSON(params) + randomID := mustMarshalJSON(uuid.New().String()) + dReq := &RPCReq{ + JSONRPC: rpcReq.JSONRPC, + Method: ReceiptsTargetEthTransactionReceipt, + Params: rawParams, + ID: randomID, + } + derivedRequests = append(derivedRequests, dReq) + } + } + } + } + // replace the original request with the derived requests + if len(derivedRequests) > 0 { + rpcReqs = derivedRequests + } + } else { + for _, rpcReq := range rpcReqs { + if rpcReq.Method == ConsensusGetReceiptsMethod { + return nil, ErrConsensusGetReceiptsCantBeBatched + } + } + } + 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 +536,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 +559,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 +568,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 +577,38 @@ 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 = ConsensusGetReceiptsRes{ + Method: translatedReq.Method, + Result: res.Result, + } + } + } + sortBatchRPCResponse(rpcReqs, rpcRes) + + // if the translated requests originated derived requests, wrap their results + if len(derivedRequests) > 0 { + results := make([]interface{}, 0, len(rpcRes)) + for _, res := range rpcRes { + results = append(results, res.Result) + } + + wrappedRes := &RPCRes{ + JSONRPC: originalRequests[0].JSONRPC, + Result: ConsensusGetReceiptsRes{ + Method: rpcReqs[0].Method, + Result: results, + }, + ID: originalRequests[0].ID, + } + + rpcRes = []*RPCRes{wrappedRes} + } + + return rpcRes, nil } // IsHealthy checks if the backend is able to serve traffic, based on dynamic parameters @@ -604,6 +727,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, ErrConsensusGetReceiptsCantBeBatched) { + return nil, err + } if errors.Is(err, ErrMethodNotWhitelisted) { return nil, err } 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..b089d3d 100644 --- a/proxyd/proxyd/integration_tests/consensus_test.go +++ b/proxyd/proxyd/integration_tests/consensus_test.go @@ -784,6 +784,152 @@ 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() + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", []interface{}{map[string]interface{}{ + "blockOrHash": "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.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() + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", []interface{}{map[string]interface{}{ + "blockOrHash": "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.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() + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", []interface{}{map[string]interface{}{ + "blockOrHash": "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.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", func(t *testing.T) { + reset() + useOnlyNode1() + + 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{}{map[string]interface{}{ + "blockOrHash": "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]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + + 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 eth_getTransactionReceipt batched", func(t *testing.T) { + reset() + useOnlyNode1() + + nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("eth_getTransactionReceipt")) + defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts")) + + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", []interface{}{map[string]interface{}{ + "blockOrHash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", + "transactions": []string{ + "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5", + "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c6", + "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c7", + "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c8", + }}}) + 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, 4, len(reqJsonMap)) + for _, req := range reqJsonMap { + require.Equal(t, "eth_getTransactionReceipt", req["method"]) + } + require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5", reqJsonMap[0]["params"].([]interface{})[0]) + require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c6", reqJsonMap[1]["params"].([]interface{})[0]) + require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c7", reqJsonMap[2]["params"].([]interface{})[0]) + require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c8", reqJsonMap[3]["params"].([]interface{})[0]) + + var resJsonMap map[string]interface{} + err = json.Unmarshal(resRaw, &resJsonMap) + + require.Equal(t, "eth_getTransactionReceipt", resJsonMap["result"].(map[string]interface{})["method"].(string)) + require.Equal(t, 4, len(resJsonMap["result"].(map[string]interface{})["result"].([]interface{}))) + for _, res := range resJsonMap["result"].(map[string]interface{})["result"].([]interface{}) { + require.Equal(t, "eth_getTransactionReceipt", res.(map[string]interface{})["_"]) + } + + }) + + 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{}{"0x102"}), + 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..4a523f6 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,20 @@ func Start(config *Config) (*Server, func(), error) { return srv, shutdownFunc, nil } +func validateReceiptsTarget(val string) (string, error) { + if val == "" { + val = "debug_getRawReceipts" + } + switch val { + case "debug_getRawReceipts", + "eth_getTransactionReceipt", + "alchemy_getTransactionReceipts": + 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..6c2840b 100644 --- a/proxyd/proxyd/rewriter.go +++ b/proxyd/proxyd/rewriter.go @@ -63,6 +63,8 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul case "eth_getLogs", "eth_newFilter": return rewriteRange(rctx, req, res, 0) + case "consensus_getReceipts": + return rewriteGetReceiptsParams(rctx, req, res) case "debug_getRawReceipts": return rewriteParam(rctx, req, res, 0, true) case "eth_getBalance", @@ -82,6 +84,38 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul return RewriteNone, nil } +func rewriteGetReceiptsParams(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResult, error) { + var p []interface{} + err := json.Unmarshal(req.Params, &p) + if err != nil { + return RewriteOverrideError, err + } + + if len(p) != 1 { + return RewriteNone, nil + } + + if m, ok := p[0].(map[string]interface{}); !ok || m["blockOrHash"] == nil { + return RewriteNone, nil + } + + val, rw, err := rewriteTag(rctx, p[0].(map[string]interface{})["blockOrHash"].(string)) + if err != nil { + return RewriteOverrideError, err + } + + if rw { + p[0].(map[string]interface{})["blockOrHash"] = val + paramRaw, err := json.Marshal(p) + if err != nil { + return RewriteOverrideError, err + } + req.Params = paramRaw + return RewriteOverrideRequest, nil + } + return RewriteNone, nil +} + func rewriteParam(rctx RewriteContext, req *RPCReq, res *RPCRes, pos int, required bool) (RewriteResult, error) { var p []interface{} err := json.Unmarshal(req.Params, &p) diff --git a/proxyd/proxyd/server.go b/proxyd/proxyd/server.go index 3ca10af..8b3a2e8 100644 --- a/proxyd/proxyd/server.go +++ b/proxyd/proxyd/server.go @@ -347,6 +347,10 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { writeRPCError(ctx, w, nil, ErrGatewayTimeout) return } + if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) { + writeRPCError(ctx, w, nil, ErrInvalidRequest(err.Error())) + return + } if err != nil { writeRPCError(ctx, w, nil, ErrInternal) return @@ -485,6 +489,9 @@ 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) { + 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..99c28c5 100644 --- a/proxyd/proxyd/tools/mockserver/handler/handler.go +++ b/proxyd/proxyd/tools/mockserver/handler/handler.go @@ -88,7 +88,12 @@ 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) + idJson, _ := json.Marshal(r["id"]) + rpcRes.ID = idJson + res, _ := json.Marshal(rpcRes) + responses = append(responses, string(res)) } } From eb0fc1a837bd1d6aef802e1ef5c2fce890334063 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Thu, 1 Jun 2023 13:36:13 -0700 Subject: [PATCH 2/6] lint --- proxyd/proxyd/backend.go | 9 +++++---- proxyd/proxyd/integration_tests/consensus_test.go | 2 ++ proxyd/proxyd/tools/mockserver/handler/handler.go | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/proxyd/proxyd/backend.go b/proxyd/proxyd/backend.go index 439bb47..6bc92e4 100644 --- a/proxyd/proxyd/backend.go +++ b/proxyd/proxyd/backend.go @@ -7,9 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/rpc" - "github.com/google/uuid" "io" "math" "math/rand" @@ -20,6 +17,10 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/google/uuid" + sw "github.com/ethereum-optimism/optimism/proxyd/pkg/avg-sliding-window" "github.com/ethereum/go-ethereum/log" @@ -415,7 +416,7 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool originalRequests := rpcReqs translatedReqs := make(map[string]*RPCReq, len(rpcReqs)) - derivedRequests := make([]*RPCReq, 0, 0) + derivedRequests := make([]*RPCReq, 0) // translate consensus_getReceipts to receipts target // right now we only support non-batched if !isBatch { diff --git a/proxyd/proxyd/integration_tests/consensus_test.go b/proxyd/proxyd/integration_tests/consensus_test.go index b089d3d..a0ae8d4 100644 --- a/proxyd/proxyd/integration_tests/consensus_test.go +++ b/proxyd/proxyd/integration_tests/consensus_test.go @@ -802,6 +802,7 @@ func TestConsensus(t *testing.T) { 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{})["_"]) @@ -824,6 +825,7 @@ func TestConsensus(t *testing.T) { 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{})["_"]) diff --git a/proxyd/proxyd/tools/mockserver/handler/handler.go b/proxyd/proxyd/tools/mockserver/handler/handler.go index 99c28c5..0f6880c 100644 --- a/proxyd/proxyd/tools/mockserver/handler/handler.go +++ b/proxyd/proxyd/tools/mockserver/handler/handler.go @@ -90,6 +90,9 @@ func (mh *MockedHandler) Handler(w http.ResponseWriter, req *http.Request) { if 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) From 924030dd707b9d8e507954d2cd1f30871d0c07fc Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Thu, 1 Jun 2023 13:40:03 -0700 Subject: [PATCH 3/6] lint --- proxyd/proxyd/integration_tests/consensus_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proxyd/proxyd/integration_tests/consensus_test.go b/proxyd/proxyd/integration_tests/consensus_test.go index a0ae8d4..f55dbe9 100644 --- a/proxyd/proxyd/integration_tests/consensus_test.go +++ b/proxyd/proxyd/integration_tests/consensus_test.go @@ -848,6 +848,7 @@ func TestConsensus(t *testing.T) { 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{})["_"]) @@ -874,6 +875,7 @@ func TestConsensus(t *testing.T) { 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{})["_"]) @@ -912,6 +914,7 @@ func TestConsensus(t *testing.T) { var resJsonMap map[string]interface{} err = json.Unmarshal(resRaw, &resJsonMap) + require.NoError(t, err) require.Equal(t, "eth_getTransactionReceipt", resJsonMap["result"].(map[string]interface{})["method"].(string)) require.Equal(t, 4, len(resJsonMap["result"].(map[string]interface{})["result"].([]interface{}))) From a3cf38f8b268b01ec04ef51c6bd5b40a69732b86 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Fri, 2 Jun 2023 13:07:18 -0700 Subject: [PATCH 4/6] translate only methods with block params --- proxyd/proxyd/README.md | 27 ++-- proxyd/proxyd/backend.go | 121 +++++++++--------- .../integration_tests/consensus_test.go | 98 ++++++++++---- proxyd/proxyd/server.go | 11 +- 4 files changed, 149 insertions(+), 108 deletions(-) diff --git a/proxyd/proxyd/README.md b/proxyd/proxyd/README.md index 0a3c715..e3ec1ca 100644 --- a/proxyd/proxyd/README.md +++ b/proxyd/proxyd/README.md @@ -95,43 +95,36 @@ 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 can specify their preferred method to fetch receipts with `consensus_receipts_target`. +Each backend specifies their preferred method to fetch receipts with `consensus_receipts_target` config, +which will be translated from `consensus_getReceipts`. -This method takes **both** the blockNumberOrHash **and** list of transaction hashes to fetch the receipts, -and then after selecting the backend to serve the request, -it translates to the correct target with the appropriate parameters. +This method takes a `blockNumberOrHash` (i.e. `tag|qty|hash`) +and returns the receipts for all transactions in the block. -Note that only one of the parameters will be actually used depending on the target. - -Request params +Request example ```json { "jsonrpc":"2.0", "id": 1, - "params": { - "blockNumberOrHash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", - "transactions": [ - "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331", - "0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b" - ] - } + "params": ["0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"] } ``` It currently supports translation to the following targets: * `debug_getRawReceipts(blockOrHash)` (default) * `alchemy_getTransactionReceipts(blockOrHash)` -* `eth_getTransactionReceipt(txHash)` batched +* `parity_getBlockReceipts(blockOrHash)` +* `eth_getBlockReceipts(blockOrHash)` The selected target is returned in the response, in a wrapped result. -Response +Response example ```json { "jsonrpc": "2.0", "id": 1, "result": { - "method": "eth_getTransactionReceipt", + "method": "debug_getRawReceipts", "result": { // the actual raw result from backend } diff --git a/proxyd/proxyd/backend.go b/proxyd/proxyd/backend.go index 6bc92e4..6fc1552 100644 --- a/proxyd/proxyd/backend.go +++ b/proxyd/proxyd/backend.go @@ -17,11 +17,9 @@ import ( "sync" "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/google/uuid" - - sw "github.com/ethereum-optimism/optimism/proxyd/pkg/avg-sliding-window" "github.com/ethereum/go-ethereum/log" "github.com/gorilla/websocket" @@ -103,6 +101,7 @@ 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 { @@ -252,20 +251,28 @@ type indexedReqRes struct { } const ConsensusGetReceiptsMethod = "consensus_getReceipts" -const ReceiptsTargetEthTransactionReceipt = "eth_getTransactionReceipt" -const ReceiptsTargetDebugGetRawReceipts = "debug_getRawReceipts" -const ReceiptsTargetGetTransactionReceipts = "alchemy_getTransactionReceipts" -type ConsensusGetReceiptsReq struct { +const ReceiptsTargetDebugGetRawReceipts = "debug_getRawReceipts" +const ReceiptsTargetAlchemyGetTransactionReceipts = "alchemy_getTransactionReceipts" +const ReceiptsTargetParityGetTransactionReceipts = "parity_getBlockReceipts" +const ReceiptsTargetEthGetTransactionReceipts = "eth_getBlockReceipts" + +type ConsensusGetReceiptsRequest struct { BlockOrHash *rpc.BlockNumberOrHash `json:"blockOrHash"` Transactions []common.Hash `json:"transactions"` } -type ConsensusGetReceiptsRes struct { +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, @@ -331,12 +338,19 @@ func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([] switch err { case nil: // do nothing case ErrConsensusGetReceiptsCantBeBatched: - log.Debug( + 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 @@ -414,58 +428,56 @@ 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() - originalRequests := rpcReqs translatedReqs := make(map[string]*RPCReq, len(rpcReqs)) - derivedRequests := make([]*RPCReq, 0) // translate consensus_getReceipts to receipts target // right now we only support non-batched - if !isBatch { + 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 []ConsensusGetReceiptsReq + var reqParams []ConsensusGetReceiptsRequest err := json.Unmarshal(rpcReq.Params, &reqParams) if err != nil { return nil, ErrInvalidRequest("invalid request") } bnh := reqParams[0].BlockOrHash - switch b.receiptsTarget { + + var translatedParams []byte + switch rpcReq.Method { case ReceiptsTargetDebugGetRawReceipts, - ReceiptsTargetGetTransactionReceipts: // block or hash + ReceiptsTargetEthGetTransactionReceipts, + ReceiptsTargetParityGetTransactionReceipts: + // conventional methods use an array of strings having either block number or block hash + // i.e. ["0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"] params := make([]string, 1) if bnh.BlockNumber != nil { params[0] = bnh.BlockNumber.String() } else { params[0] = bnh.BlockHash.Hex() } - rawParams := mustMarshalJSON(params) - rpcReq.Params = rawParams - case ReceiptsTargetEthTransactionReceipt: // list of tx hashes - for _, txHash := range reqParams[0].Transactions { - params := make([]common.Hash, 1) - params[0] = txHash - rawParams := mustMarshalJSON(params) - randomID := mustMarshalJSON(uuid.New().String()) - dReq := &RPCReq{ - JSONRPC: rpcReq.JSONRPC, - Method: ReceiptsTargetEthTransactionReceipt, - Params: rawParams, - ID: randomID, - } - derivedRequests = append(derivedRequests, dReq) + 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 bnh.BlockNumber != nil { + params[0].BlockNumber = bnh.BlockNumber + } else { + params[0].BlockHash = bnh.BlockHash } + translatedParams = mustMarshalJSON(params) + default: + return nil, ErrConsensusGetReceiptsInvalidTarget } - } - } - // replace the original request with the derived requests - if len(derivedRequests) > 0 { - rpcReqs = derivedRequests - } - } else { - for _, rpcReq := range rpcReqs { - if rpcReq.Method == ConsensusGetReceiptsMethod { - return nil, ErrConsensusGetReceiptsCantBeBatched + + rpcReq.Params = translatedParams } } } @@ -582,33 +594,15 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool for _, res := range rpcRes { translatedReq, exist := translatedReqs[string(res.ID)] if exist { - res.Result = ConsensusGetReceiptsRes{ + res.Result = ConsensusGetReceiptsResult{ Method: translatedReq.Method, Result: res.Result, } } } + sortBatchRPCResponse(rpcReqs, rpcRes) - // if the translated requests originated derived requests, wrap their results - if len(derivedRequests) > 0 { - results := make([]interface{}, 0, len(rpcRes)) - for _, res := range rpcRes { - results = append(results, res.Result) - } - - wrappedRes := &RPCRes{ - JSONRPC: originalRequests[0].JSONRPC, - Result: ConsensusGetReceiptsRes{ - Method: rpcReqs[0].Method, - Result: results, - }, - ID: originalRequests[0].ID, - } - - rpcRes = []*RPCRes{wrappedRes} - } - return rpcRes, nil } @@ -728,10 +722,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, ErrConsensusGetReceiptsCantBeBatched) { - return nil, err - } - 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/integration_tests/consensus_test.go b/proxyd/proxyd/integration_tests/consensus_test.go index f55dbe9..f4166a3 100644 --- a/proxyd/proxyd/integration_tests/consensus_test.go +++ b/proxyd/proxyd/integration_tests/consensus_test.go @@ -788,6 +788,10 @@ func TestConsensus(t *testing.T) { 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{}{map[string]interface{}{ "blockOrHash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"}}) @@ -811,6 +815,10 @@ func TestConsensus(t *testing.T) { 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{}{map[string]interface{}{ "blockOrHash": "latest"}}) @@ -834,6 +842,10 @@ func TestConsensus(t *testing.T) { 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{}{map[string]interface{}{ "blockOrHash": "0x55"}}) @@ -854,9 +866,13 @@ func TestConsensus(t *testing.T) { require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) }) - t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts", func(t *testing.T) { + 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")) @@ -871,7 +887,7 @@ func TestConsensus(t *testing.T) { require.NoError(t, err) require.Equal(t, "alchemy_getTransactionReceipts", reqJsonMap["method"]) - require.Equal(t, "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", reqJsonMap["params"].([]interface{})[0]) + require.Equal(t, "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", reqJsonMap["params"].([]interface{})[0].(map[string]interface{})["blockHash"]) var resJsonMap map[string]interface{} err = json.Unmarshal(resRaw, &resJsonMap) @@ -881,47 +897,79 @@ func TestConsensus(t *testing.T) { require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"]) }) - t.Run("translate consensus_getReceipts to eth_getTransactionReceipt batched", func(t *testing.T) { + t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts with block number", func(t *testing.T) { reset() useOnlyNode1() + update() - nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("eth_getTransactionReceipt")) + // 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{}{map[string]interface{}{ - "blockOrHash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", - "transactions": []string{ - "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5", - "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c6", - "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c7", - "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c8", - }}}) + "blockOrHash": "0x55"}}) require.NoError(t, err) require.Equal(t, 200, statusCode) - var reqJsonMap []map[string]interface{} + var reqJsonMap map[string]interface{} err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &reqJsonMap) require.NoError(t, err) - require.Equal(t, 4, len(reqJsonMap)) - for _, req := range reqJsonMap { - require.Equal(t, "eth_getTransactionReceipt", req["method"]) - } - require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5", reqJsonMap[0]["params"].([]interface{})[0]) - require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c6", reqJsonMap[1]["params"].([]interface{})[0]) - require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c7", reqJsonMap[2]["params"].([]interface{})[0]) - require.Equal(t, "0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c8", reqJsonMap[3]["params"].([]interface{})[0]) + 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, "eth_getTransactionReceipt", resJsonMap["result"].(map[string]interface{})["method"].(string)) - require.Equal(t, 4, len(resJsonMap["result"].(map[string]interface{})["result"].([]interface{}))) - for _, res := range resJsonMap["result"].(map[string]interface{})["result"].([]interface{}) { - require.Equal(t, "eth_getTransactionReceipt", res.(map[string]interface{})["_"]) - } + 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{}{map[string]interface{}{ + "blockOrHash": "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{}{map[string]interface{}{ + "blockOrHash": "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) { diff --git a/proxyd/proxyd/server.go b/proxyd/proxyd/server.go index 8b3a2e8..88b031c 100644 --- a/proxyd/proxyd/server.go +++ b/proxyd/proxyd/server.go @@ -347,7 +347,8 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { writeRPCError(ctx, w, nil, ErrGatewayTimeout) return } - if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) { + if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) || + errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) { writeRPCError(ctx, w, nil, ErrInvalidRequest(err.Error())) return } @@ -364,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 } @@ -489,7 +495,8 @@ 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) { + if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) || + errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) { return nil, false, err } log.Error( From 804a57566f8da74dd38c7ee79cf1ba6674e37de7 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Fri, 2 Jun 2023 13:45:42 -0700 Subject: [PATCH 5/6] fix input --- proxyd/proxyd/backend.go | 20 ++++------- .../integration_tests/consensus_test.go | 36 +++++++++++-------- proxyd/proxyd/rewriter.go | 36 +------------------ 3 files changed, 29 insertions(+), 63 deletions(-) diff --git a/proxyd/proxyd/backend.go b/proxyd/proxyd/backend.go index 6fc1552..9c4234f 100644 --- a/proxyd/proxyd/backend.go +++ b/proxyd/proxyd/backend.go @@ -257,11 +257,6 @@ const ReceiptsTargetAlchemyGetTransactionReceipts = "alchemy_getTransactionRecei const ReceiptsTargetParityGetTransactionReceipts = "parity_getBlockReceipts" const ReceiptsTargetEthGetTransactionReceipts = "eth_getBlockReceipts" -type ConsensusGetReceiptsRequest struct { - BlockOrHash *rpc.BlockNumberOrHash `json:"blockOrHash"` - Transactions []common.Hash `json:"transactions"` -} - type ConsensusGetReceiptsResult struct { Method string `json:"method"` Result interface{} `json:"result"` @@ -442,12 +437,11 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool if rpcReq.Method == ConsensusGetReceiptsMethod { translatedReqs[string(rpcReq.ID)] = rpcReq rpcReq.Method = b.receiptsTarget - var reqParams []ConsensusGetReceiptsRequest + var reqParams []rpc.BlockNumberOrHash err := json.Unmarshal(rpcReq.Params, &reqParams) if err != nil { return nil, ErrInvalidRequest("invalid request") } - bnh := reqParams[0].BlockOrHash var translatedParams []byte switch rpcReq.Method { @@ -457,20 +451,20 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool // conventional methods use an array of strings having either block number or block hash // i.e. ["0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"] params := make([]string, 1) - if bnh.BlockNumber != nil { - params[0] = bnh.BlockNumber.String() + if reqParams[0].BlockNumber != nil { + params[0] = reqParams[0].BlockNumber.String() } else { - params[0] = bnh.BlockHash.Hex() + 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 bnh.BlockNumber != nil { - params[0].BlockNumber = bnh.BlockNumber + if reqParams[0].BlockNumber != nil { + params[0].BlockNumber = reqParams[0].BlockNumber } else { - params[0].BlockHash = bnh.BlockHash + params[0].BlockHash = reqParams[0].BlockHash } translatedParams = mustMarshalJSON(params) default: diff --git a/proxyd/proxyd/integration_tests/consensus_test.go b/proxyd/proxyd/integration_tests/consensus_test.go index f4166a3..1b37ef7 100644 --- a/proxyd/proxyd/integration_tests/consensus_test.go +++ b/proxyd/proxyd/integration_tests/consensus_test.go @@ -793,8 +793,8 @@ func TestConsensus(t *testing.T) { // reset request counts nodes["node1"].mockBackend.Reset() - resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", []interface{}{map[string]interface{}{ - "blockOrHash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"}}) + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"}) require.NoError(t, err) require.Equal(t, 200, statusCode) @@ -820,8 +820,9 @@ func TestConsensus(t *testing.T) { // reset request counts nodes["node1"].mockBackend.Reset() - resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", []interface{}{map[string]interface{}{ - "blockOrHash": "latest"}}) + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"latest"}) + require.NoError(t, err) require.Equal(t, 200, statusCode) @@ -847,8 +848,9 @@ func TestConsensus(t *testing.T) { // reset request counts nodes["node1"].mockBackend.Reset() - resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", []interface{}{map[string]interface{}{ - "blockOrHash": "0x55"}}) + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0x55"}) + require.NoError(t, err) require.Equal(t, 200, statusCode) @@ -877,8 +879,9 @@ func TestConsensus(t *testing.T) { 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{}{map[string]interface{}{ - "blockOrHash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"}}) + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"}) + require.NoError(t, err) require.Equal(t, 200, statusCode) @@ -908,8 +911,9 @@ func TestConsensus(t *testing.T) { 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{}{map[string]interface{}{ - "blockOrHash": "0x55"}}) + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"0x55"}) + require.NoError(t, err) require.Equal(t, 200, statusCode) @@ -939,8 +943,9 @@ func TestConsensus(t *testing.T) { 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{}{map[string]interface{}{ - "blockOrHash": "latest"}}) + resRaw, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"latest"}) + require.NoError(t, err) require.Equal(t, 200, statusCode) @@ -966,8 +971,9 @@ func TestConsensus(t *testing.T) { 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{}{map[string]interface{}{ - "blockOrHash": "latest"}}) + _, statusCode, err := client.SendRPC("consensus_getReceipts", + []interface{}{"latest"}) + require.NoError(t, err) require.Equal(t, 400, statusCode) }) @@ -978,7 +984,7 @@ func TestConsensus(t *testing.T) { _, statusCode, err := client.SendBatchRPC( NewRPCReq("1", "eth_getBlockByNumber", []interface{}{"latest"}), - NewRPCReq("2", "consensus_getReceipts", []interface{}{"0x102"}), + NewRPCReq("2", "consensus_getReceipts", []interface{}{"0x55"}), NewRPCReq("3", "eth_getBlockByNumber", []interface{}{"0xe1"})) require.NoError(t, err) require.Equal(t, 400, statusCode) diff --git a/proxyd/proxyd/rewriter.go b/proxyd/proxyd/rewriter.go index 6c2840b..b00bc51 100644 --- a/proxyd/proxyd/rewriter.go +++ b/proxyd/proxyd/rewriter.go @@ -63,9 +63,7 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul case "eth_getLogs", "eth_newFilter": return rewriteRange(rctx, req, res, 0) - case "consensus_getReceipts": - return rewriteGetReceiptsParams(rctx, req, res) - case "debug_getRawReceipts": + case "debug_getRawReceipts", "consensus_getReceipts": return rewriteParam(rctx, req, res, 0, true) case "eth_getBalance", "eth_getCode", @@ -84,38 +82,6 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul return RewriteNone, nil } -func rewriteGetReceiptsParams(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResult, error) { - var p []interface{} - err := json.Unmarshal(req.Params, &p) - if err != nil { - return RewriteOverrideError, err - } - - if len(p) != 1 { - return RewriteNone, nil - } - - if m, ok := p[0].(map[string]interface{}); !ok || m["blockOrHash"] == nil { - return RewriteNone, nil - } - - val, rw, err := rewriteTag(rctx, p[0].(map[string]interface{})["blockOrHash"].(string)) - if err != nil { - return RewriteOverrideError, err - } - - if rw { - p[0].(map[string]interface{})["blockOrHash"] = val - paramRaw, err := json.Marshal(p) - if err != nil { - return RewriteOverrideError, err - } - req.Params = paramRaw - return RewriteOverrideRequest, nil - } - return RewriteNone, nil -} - func rewriteParam(rctx RewriteContext, req *RPCReq, res *RPCRes, pos int, required bool) (RewriteResult, error) { var p []interface{} err := json.Unmarshal(req.Params, &p) From 3b56ec9d9edd81827d8a6afc3d0bc3245c183139 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Fri, 2 Jun 2023 14:11:49 -0700 Subject: [PATCH 6/6] fix targets --- proxyd/proxyd/proxyd.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/proxyd/proxyd/proxyd.go b/proxyd/proxyd/proxyd.go index 4a523f6..f1e9275 100644 --- a/proxyd/proxyd/proxyd.go +++ b/proxyd/proxyd/proxyd.go @@ -328,12 +328,13 @@ func Start(config *Config) (*Server, func(), error) { func validateReceiptsTarget(val string) (string, error) { if val == "" { - val = "debug_getRawReceipts" + val = ReceiptsTargetDebugGetRawReceipts } switch val { - case "debug_getRawReceipts", - "eth_getTransactionReceipt", - "alchemy_getTransactionReceipts": + case ReceiptsTargetDebugGetRawReceipts, + ReceiptsTargetAlchemyGetTransactionReceipts, + ReceiptsTargetEthGetTransactionReceipts, + ReceiptsTargetParityGetTransactionReceipts: return val, nil default: return "", fmt.Errorf("invalid receipts target: %s", val)