package proxyd import ( "encoding/json" "errors" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" ) type RewriteContext struct { latest hexutil.Uint64 safe hexutil.Uint64 finalized hexutil.Uint64 maxBlockRange uint64 } type RewriteResult uint8 const ( // RewriteNone means request should be forwarded as-is RewriteNone RewriteResult = iota // RewriteOverrideError means there was an error attempting to rewrite RewriteOverrideError // RewriteOverrideRequest means the modified request should be forwarded to the backend RewriteOverrideRequest // RewriteOverrideResponse means to skip calling the backend and serve the overridden response RewriteOverrideResponse ) var ( ErrRewriteBlockOutOfRange = errors.New("block is out of range") ErrRewriteRangeTooLarge = errors.New("block range is too large") ) // RewriteTags modifies the request and the response based on block tags func RewriteTags(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResult, error) { rw, err := RewriteResponse(rctx, req, res) if rw == RewriteOverrideResponse { return rw, err } return RewriteRequest(rctx, req, res) } // RewriteResponse modifies the response object to comply with the rewrite context // after the method has been called at the backend // RewriteResult informs the decision of the rewrite func RewriteResponse(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResult, error) { switch req.Method { case "eth_blockNumber": res.Result = rctx.latest return RewriteOverrideResponse, nil } return RewriteNone, nil } // RewriteRequest modifies the request object to comply with the rewrite context // before the method has been called at the backend // it returns false if nothing was changed func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResult, error) { switch req.Method { case "eth_getLogs", "eth_newFilter": return rewriteRange(rctx, req, res, 0) case "debug_getRawReceipts", "consensus_getReceipts": return rewriteParam(rctx, req, res, 0, true, false) case "eth_getBalance", "eth_getCode", "eth_getTransactionCount", "eth_call": return rewriteParam(rctx, req, res, 1, false, true) case "eth_getStorageAt", "eth_getProof": return rewriteParam(rctx, req, res, 2, false, true) case "eth_getBlockTransactionCountByNumber", "eth_getUncleCountByBlockNumber", "eth_getBlockByNumber", "eth_getTransactionByBlockNumberAndIndex", "eth_getUncleByBlockNumberAndIndex": return rewriteParam(rctx, req, res, 0, false, false) } return RewriteNone, nil } func rewriteParam(rctx RewriteContext, req *RPCReq, res *RPCRes, pos int, required bool, blockNrOrHash bool) (RewriteResult, error) { var p []interface{} err := json.Unmarshal(req.Params, &p) if err != nil { return RewriteOverrideError, err } // we assume latest if the param is missing, // and we don't rewrite if there is not enough params if len(p) == pos && !required { p = append(p, "latest") } else if len(p) <= pos { return RewriteNone, nil } // support for https://eips.ethereum.org/EIPS/eip-1898 var val interface{} var rw bool if blockNrOrHash { bnh, err := remarshalBlockNumberOrHash(p[pos]) if err != nil { // fallback to string s, ok := p[pos].(string) if ok { val, rw, err = rewriteTag(rctx, s) if err != nil { return RewriteOverrideError, err } } else { return RewriteOverrideError, errors.New("expected BlockNumberOrHash or string") } } else { val, rw, err = rewriteTagBlockNumberOrHash(rctx, bnh) if err != nil { return RewriteOverrideError, err } } } else { s, ok := p[pos].(string) if !ok { return RewriteOverrideError, errors.New("expected string") } val, rw, err = rewriteTag(rctx, s) if err != nil { return RewriteOverrideError, err } } if rw { p[pos] = val paramRaw, err := json.Marshal(p) if err != nil { return RewriteOverrideError, err } req.Params = paramRaw return RewriteOverrideRequest, nil } return RewriteNone, nil } func rewriteRange(rctx RewriteContext, req *RPCReq, res *RPCRes, pos int) (RewriteResult, error) { var p []map[string]interface{} err := json.Unmarshal(req.Params, &p) if err != nil { return RewriteOverrideError, err } // if either fromBlock or toBlock is defined, default the other to "latest" if unset _, hasFrom := p[pos]["fromBlock"] _, hasTo := p[pos]["toBlock"] if hasFrom && !hasTo { p[pos]["toBlock"] = "latest" } else if hasTo && !hasFrom { p[pos]["fromBlock"] = "latest" } modifiedFrom, err := rewriteTagMap(rctx, p[pos], "fromBlock") if err != nil { return RewriteOverrideError, err } modifiedTo, err := rewriteTagMap(rctx, p[pos], "toBlock") if err != nil { return RewriteOverrideError, err } if rctx.maxBlockRange > 0 && (hasFrom || hasTo) { from, err := blockNumber(p[pos], "fromBlock", uint64(rctx.latest)) if err != nil { return RewriteOverrideError, err } to, err := blockNumber(p[pos], "toBlock", uint64(rctx.latest)) if err != nil { return RewriteOverrideError, err } if to-from > rctx.maxBlockRange { return RewriteOverrideError, ErrRewriteRangeTooLarge } } // if any of the fields the request have been changed, re-marshal the params if modifiedFrom || modifiedTo { paramsRaw, err := json.Marshal(p) req.Params = paramsRaw if err != nil { return RewriteOverrideError, err } return RewriteOverrideRequest, nil } return RewriteNone, nil } func blockNumber(m map[string]interface{}, key string, latest uint64) (uint64, error) { current, ok := m[key].(string) if !ok { return 0, errors.New("expected string") } // the latest/safe/finalized tags are already replaced by rewriteTag if current == "earliest" { return 0, nil } if current == "pending" { return latest + 1, nil } return hexutil.DecodeUint64(current) } func rewriteTagMap(rctx RewriteContext, m map[string]interface{}, key string) (bool, error) { if m[key] == nil || m[key] == "" { return false, nil } current, ok := m[key].(string) if !ok { return false, errors.New("expected string") } val, rw, err := rewriteTag(rctx, current) if err != nil { return false, err } if rw { m[key] = val return true, nil } return false, nil } func remarshalBlockNumberOrHash(current interface{}) (*rpc.BlockNumberOrHash, error) { jv, err := json.Marshal(current) if err != nil { return nil, err } var bnh rpc.BlockNumberOrHash err = bnh.UnmarshalJSON(jv) if err != nil { return nil, err } return &bnh, nil } func rewriteTag(rctx RewriteContext, current string) (string, bool, error) { bnh, err := remarshalBlockNumberOrHash(current) if err != nil { return "", false, err } // this is a hash, not a block if bnh.BlockNumber == nil { return current, false, nil } switch *bnh.BlockNumber { case rpc.PendingBlockNumber, rpc.EarliestBlockNumber: return current, false, nil case rpc.FinalizedBlockNumber: return rctx.finalized.String(), true, nil case rpc.SafeBlockNumber: return rctx.safe.String(), true, nil case rpc.LatestBlockNumber: return rctx.latest.String(), true, nil default: if bnh.BlockNumber.Int64() > int64(rctx.latest) { return "", false, ErrRewriteBlockOutOfRange } } return current, false, nil } func rewriteTagBlockNumberOrHash(rctx RewriteContext, current *rpc.BlockNumberOrHash) (*rpc.BlockNumberOrHash, bool, error) { // this is a hash, not a block number if current.BlockNumber == nil { return current, false, nil } switch *current.BlockNumber { case rpc.PendingBlockNumber, rpc.EarliestBlockNumber: return current, false, nil case rpc.FinalizedBlockNumber: bn := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(rctx.finalized)) return &bn, true, nil case rpc.SafeBlockNumber: bn := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(rctx.safe)) return &bn, true, nil case rpc.LatestBlockNumber: bn := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(rctx.latest)) return &bn, true, nil default: if current.BlockNumber.Int64() > int64(rctx.latest) { return nil, false, ErrRewriteBlockOutOfRange } } return current, false, nil }