translate only methods with block params

This commit is contained in:
Felipe Andrade 2023-06-02 13:07:18 -07:00
parent 924030dd70
commit a3cf38f8b2
4 changed files with 149 additions and 108 deletions

@ -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
}

@ -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)
}
}
}
}
// replace the original request with the derived requests
if len(derivedRequests) > 0 {
rpcReqs = derivedRequests
}
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 {
for _, rpcReq := range rpcReqs {
if rpcReq.Method == ConsensusGetReceiptsMethod {
return nil, ErrConsensusGetReceiptsCantBeBatched
params[0].BlockHash = bnh.BlockHash
}
translatedParams = mustMarshalJSON(params)
default:
return nil, ErrConsensusGetReceiptsInvalidTarget
}
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) {

@ -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) {

@ -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(