From a3cf38f8b268b01ec04ef51c6bd5b40a69732b86 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Fri, 2 Jun 2023 13:07:18 -0700 Subject: [PATCH] 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(