Skip to content

Commit

Permalink
improve & refactor evm_rpc methods for reuse (#41)
Browse files Browse the repository at this point in the history
* add missing valid blocktags

* test invalid/unsupported block tag

* refactor ExtractBlockNumberFromEVMRPCRequest for reuse

will need the validation and block number extraction for height-based
routing. however, it will be in the critical path, so we need to not have
the bit that makes an additional evm request.

* add tests of cacheable param checks

* support & handle routing always-latest methods

some methods will always work if routed to a pruning cluster. do that!

* track & encode "empty" block tag requests separately

* refactor block tags to constants

* refactor & rename NoHistoryMethods

* refactor method param type checks to pure functions

* add test coverage of public decode methods
  • Loading branch information
pirtleshell authored Oct 11, 2023
1 parent 2f8fa7d commit 13a027b
Show file tree
Hide file tree
Showing 2 changed files with 273 additions and 43 deletions.
138 changes: 98 additions & 40 deletions decode/evm_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import (
cosmosmath "cosmossdk.io/math"
)

// These block tags are special strings used to reference blocks in JSON-RPC
// see https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block
const (
BlockTagLatest = "latest"
BlockTagPending = "pending"
BlockTagEarliest = "earliest"
BlockTagFinalized = "finalized"
BlockTagSafe = "safe"
// "empty" is not in the spec, it is our encoding for requests made with a nil block tag param.
BlockTagEmpty = "empty"
)

// Errors that might result from decoding parts or the whole of
// an EVM RPC request
var (
Expand All @@ -37,6 +49,18 @@ var CacheableByBlockNumberMethods = []string{
"eth_call",
}

// MethodHasBlockNumberParam returns true when the method expects a block number in the request parameters.
func MethodHasBlockNumberParam(method string) bool {
var includesBlockNumberParam bool
for _, cacheableByBlockNumberMethod := range CacheableByBlockNumberMethods {
if method == cacheableByBlockNumberMethod {
includesBlockNumberParam = true
break
}
}
return includesBlockNumberParam
}

// List of evm methods that can be cached by block hash
// and so are useful for converting and tracking the block hash associated with
// any requests invoking those methods to the matching block number
Expand All @@ -48,6 +72,52 @@ var CacheableByBlockHashMethods = []string{
"eth_getTransactionByBlockHashAndIndex",
}

// MethodHasBlockHashParam returns true when the method expects a block hash in the request parameters.
func MethodHasBlockHashParam(method string) bool {
var includesBlockHashParam bool
for _, cacheableByBlockHashMethod := range CacheableByBlockHashMethods {
if method == cacheableByBlockHashMethod {
includesBlockHashParam = true
break
}
}
return includesBlockHashParam
}

// NoHistoryMethods is a list of JSON-RPC methods that rely only on the present state of the chain.
// They can always be safely routed to an up-to-date pruning cluster.
var NoHistoryMethods = []string{
"web3_clientVersion",
"web3_sha3",
"net_version",
"net_listening",
"net_peerCount",
"eth_protocolVersion",
"eth_syncing",
"eth_coinbase",
"eth_chainId",
"eth_mining",
"eth_hashrate",
"eth_gasPrice",
"eth_accounts",
"eth_sign",
"eth_signTransaction",
"eth_sendTransaction",
"eth_sendRawTransaction",
}

// MethodRequiresNoHistory returns true when the JSON-RPC method always functions correctly
// when sent to the latest block.
// This is useful for determining if a request can be routed to a pruning cluster.
func MethodRequiresNoHistory(method string) bool {
for _, nonHistoricalMethod := range NoHistoryMethods {
if method == nonHistoricalMethod {
return true
}
}
return false
}

// List of evm methods that can be cached independent
// of block number (i.e. by block or transaction hash, filter id, or time period)
// TODO: break these out into separate list for methods that can be cached using the same key type
Expand Down Expand Up @@ -106,10 +176,18 @@ var MethodNameToBlockHashParamIndex = map[string]int{
// Mapping of string tag values used in the eth api to
// normalized int values that can be stored as the block number
// for the proxied request metric
// see https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block
var BlockTagToNumberCodec = map[string]int64{
"latest": -1,
"pending": -2,
"earliest": -3,
BlockTagLatest: -1,
BlockTagPending: -2,
BlockTagEarliest: -3,
BlockTagFinalized: -4,
BlockTagSafe: -5,
// "empty" is not part of the evm json-rpc spec
// it is our encoding for when no parameter is passed in as a block tag param
// usually, clients interpret an empty block tag to mean "latest"
// we track it separately here to more accurately track how users make requests
BlockTagEmpty: -6,
}

// EVMRPCRequest wraps expected values present in a request
Expand Down Expand Up @@ -143,37 +221,16 @@ func (r *EVMRPCRequestEnvelope) ExtractBlockNumberFromEVMRPCRequest(ctx context.
if r.Method == "" {
return 0, ErrInvalidEthAPIRequest
}

// validate this is a request with a block number param
var cacheableByBlockNumber bool
for _, cacheableByBlockNumberMethod := range CacheableByBlockNumberMethods {
if r.Method == cacheableByBlockNumberMethod {
cacheableByBlockNumber = true
break
}
// handle cacheable by block number
if MethodHasBlockNumberParam(r.Method) {
return ParseBlockNumberFromParams(r.Method, r.Params)
}

var cacheableByBlockHash bool
for _, cacheableByBlockHashMethod := range CacheableByBlockHashMethods {
if r.Method == cacheableByBlockHashMethod {
cacheableByBlockHash = true
break
}
}

if !cacheableByBlockNumber && !cacheableByBlockHash {
return 0, ErrUncachaebleByBlockNumberEthRequest
}

// parse block number using heuristics so byzantine
// they require their own consensus engine 😅
// https://ethereum.org/en/developers/docs/apis/json-rpc
// or at least a healthy level of [code coverage](./evm_rpc_test.go) ;-)
if cacheableByBlockNumber {
return parseBlockNumberFromParams(r.Method, r.Params)
// handle cacheable by block hash
if MethodHasBlockHashParam(r.Method) {
return lookupBlockNumberFromHashParam(ctx, evmClient, r.Method, r.Params)
}

return lookupBlockNumberFromHashParam(ctx, evmClient, r.Method, r.Params)
// handle unable to cached
return 0, ErrUncachaebleByBlockNumberEthRequest
}

// Generic method to lookup the block number
Expand Down Expand Up @@ -201,35 +258,36 @@ func lookupBlockNumberFromHashParam(ctx context.Context, evmClient *ethclient.Cl
}

// Generic method to parse the block number from a set of params
func parseBlockNumberFromParams(methodName string, params []interface{}) (int64, error) {
// errors if method does not have a block number in the param, or the param has an unexpected value
// block tags are encoded to an int64 according to the BlockTagToNumberCodec map.
func ParseBlockNumberFromParams(methodName string, params []interface{}) (int64, error) {
paramIndex, exists := MethodNameToBlockNumberParamIndex[methodName]

if !exists {
return 0, ErrUncachaebleByBlockNumberEthRequest
}

// capture requests made with empty block tag params
if params[paramIndex] == nil {
return BlockTagToNumberCodec["empty"], nil
}

tag, isString := params[paramIndex].(string)

if !isString {
return 0, fmt.Errorf(fmt.Sprintf("error decoding block number param from params %+v at index %d", params, paramIndex))
}

var blockNumber int64
tagEncoding, exists := BlockTagToNumberCodec[tag]
blockNumber, exists := BlockTagToNumberCodec[tag]

if !exists {
spaceint, valid := cosmosmath.NewIntFromString(tag)

if !valid {
return 0, fmt.Errorf(fmt.Sprintf("unable to parse tag %s to integer", tag))
}

blockNumber = spaceint.Int64()

return blockNumber, nil
}

blockNumber = tagEncoding

return blockNumber, nil
}
Loading

0 comments on commit 13a027b

Please sign in to comment.