nimbus-eth1/nimbus_verified_proxy/rpc/rpc_eth_api.nim

322 lines
9.5 KiB
Nim

# nimbus_verified_proxy
# Copyright (c) 2022-2023 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [].}
import
std/[strutils, typetraits],
stint,
stew/[byteutils, results],
chronicles,
json_rpc/[rpcproxy, rpcserver, rpcclient],
eth/common/eth_types as etypes,
web3,
web3/[primitives, eth_api_types],
beacon_chain/el/el_manager,
beacon_chain/networking/network_metadata,
beacon_chain/spec/forks,
./rpc_utils,
../validate_proof,
../block_cache
export forks
logScope:
topics = "verified_proxy"
proc `==`(x, y: Quantity): bool {.borrow, noSideEffect.}
type
VerifiedRpcProxy* = ref object
proxy: RpcProxy
blockCache: BlockCache
chainId: Quantity
QuantityTagKind = enum
LatestBlock, BlockNumber
BlockTag = eth_api_types.RtBlockIdentifier
QuantityTag = object
case kind: QuantityTagKind
of LatestBlock:
discard
of BlockNumber:
blockNumber: Quantity
func parseQuantityTag(blockTag: BlockTag): Result[QuantityTag, string] =
if blockTag.kind == bidAlias:
let tag = blockTag.alias.toLowerAscii
case tag
of "latest":
return ok(QuantityTag(kind: LatestBlock))
else:
return err("Unsupported blockTag: " & tag)
else:
let quantity = blockTag.number.Quantity
return ok(QuantityTag(kind: BlockNumber, blockNumber: quantity))
template checkPreconditions(proxy: VerifiedRpcProxy) =
if proxy.blockCache.isEmpty():
raise newException(ValueError, "Syncing")
template rpcClient(lcProxy: VerifiedRpcProxy): RpcClient =
lcProxy.proxy.getClient()
proc getPayloadByTag(
proxy: VerifiedRpcProxy,
quantityTag: BlockTag):
results.Opt[ExecutionData] {.raises: [ValueError].} =
checkPreconditions(proxy)
let tagResult = parseQuantityTag(quantityTag)
if tagResult.isErr:
raise newException(ValueError, tagResult.error)
let tag = tagResult.get()
case tag.kind
of LatestBlock:
# this will always return some block, as we always checkPreconditions
return proxy.blockCache.latest
of BlockNumber:
return proxy.blockCache.getByNumber(tag.blockNumber)
proc getPayloadByTagOrThrow(
proxy: VerifiedRpcProxy,
quantityTag: BlockTag): ExecutionData {.raises: [ValueError].} =
let tagResult = getPayloadByTag(proxy, quantityTag)
if tagResult.isErr:
raise newException(ValueError, "No block stored for given tag " & $quantityTag)
return tagResult.get()
proc installEthApiHandlers*(lcProxy: VerifiedRpcProxy) =
lcProxy.proxy.rpc("eth_chainId") do() -> Quantity:
return lcProxy.chainId
lcProxy.proxy.rpc("eth_blockNumber") do() -> Quantity:
## Returns the number of the most recent block.
checkPreconditions(lcProxy)
return lcProxy.blockCache.latest.get.blockNumber
lcProxy.proxy.rpc("eth_getBalance") do(
address: Address, quantityTag: BlockTag) -> UInt256:
# When requesting state for `latest` block number, we need to translate
# `latest` to actual block number as `latest` on proxy and on data provider
# can mean different blocks and ultimatly piece received piece of state
# must by validated against correct state root
let
executionPayload = lcProxy.getPayloadByTagOrThrow(quantityTag)
blockNumber = executionPayload.blockNumber.uint64
info "Forwarding eth_getBalance call", blockNumber
let proof = await lcProxy.rpcClient.eth_getProof(
address, @[], blockId(blockNumber))
let accountResult = getAccountFromProof(
executionPayload.stateRoot,
proof.address,
proof.balance,
proof.nonce,
proof.codeHash,
proof.storageHash,
proof.accountProof
)
if accountResult.isOk():
return accountResult.get.balance
else:
raise newException(ValueError, accountResult.error)
lcProxy.proxy.rpc("eth_getStorageAt") do(
address: Address, slot: UInt256, quantityTag: BlockTag) -> UInt256:
let
executionPayload = lcProxy.getPayloadByTagOrThrow(quantityTag)
blockNumber = executionPayload.blockNumber.uint64
info "Forwarding eth_getStorageAt", blockNumber
let proof = await lcProxy.rpcClient.eth_getProof(
address, @[slot], blockId(blockNumber))
let dataResult = getStorageData(executionPayload.stateRoot, slot, proof)
if dataResult.isOk():
let slotValue = dataResult.get()
return slotValue
else:
raise newException(ValueError, dataResult.error)
lcProxy.proxy.rpc("eth_getTransactionCount") do(
address: Address, quantityTag: BlockTag) -> Quantity:
let
executionPayload = lcProxy.getPayloadByTagOrThrow(quantityTag)
blockNumber = executionPayload.blockNumber.uint64
info "Forwarding eth_getTransactionCount", blockNumber
let proof = await lcProxy.rpcClient.eth_getProof(
address, @[], blockId(blockNumber))
let accountResult = getAccountFromProof(
executionPayload.stateRoot,
proof.address,
proof.balance,
proof.nonce,
proof.codeHash,
proof.storageHash,
proof.accountProof
)
if accountResult.isOk():
return Quantity(accountResult.get.nonce)
else:
raise newException(ValueError, accountResult.error)
lcProxy.proxy.rpc("eth_getCode") do(
address: Address, quantityTag: BlockTag) -> seq[byte]:
let
executionPayload = lcProxy.getPayloadByTagOrThrow(quantityTag)
blockNumber = executionPayload.blockNumber.uint64
let
proof = await lcProxy.rpcClient.eth_getProof(
address, @[], blockId(blockNumber))
accountResult = getAccountFromProof(
executionPayload.stateRoot,
proof.address,
proof.balance,
proof.nonce,
proof.codeHash,
proof.storageHash,
proof.accountProof
)
if accountResult.isErr():
raise newException(ValueError, accountResult.error)
let account = accountResult.get()
if account.codeHash == etypes.EMPTY_CODE_HASH:
# account does not have any code, return empty hex data
return @[]
info "Forwarding eth_getCode", blockNumber
let code = await lcProxy.rpcClient.eth_getCode(
address,
blockId(blockNumber)
)
if isValidCode(account, code):
return code
else:
raise newException(ValueError,
"Received code which does not match the account code hash")
# TODO:
# Following methods are forwarded directly to the web3 provider and therefore
# are not validated in any way.
lcProxy.proxy.registerProxyMethod("net_version")
lcProxy.proxy.registerProxyMethod("eth_call")
lcProxy.proxy.registerProxyMethod("eth_sendRawTransaction")
lcProxy.proxy.registerProxyMethod("eth_getTransactionReceipt")
# TODO currently we do not handle fullTransactions flag. It require updates on
# nim-web3 side
lcProxy.proxy.rpc("eth_getBlockByNumber") do(
quantityTag: BlockTag, fullTransactions: bool) -> Option[BlockObject]:
let executionPayload = lcProxy.getPayloadByTag(quantityTag)
if executionPayload.isErr:
return none(BlockObject)
return some(asBlockObject(executionPayload.get()))
lcProxy.proxy.rpc("eth_getBlockByHash") do(
blockHash: BlockHash, fullTransactions: bool) -> Option[BlockObject]:
let executionPayload = lcProxy.blockCache.getPayloadByHash(blockHash)
if executionPayload.isErr:
return none(BlockObject)
return some(asBlockObject(executionPayload.get()))
proc new*(
T: type VerifiedRpcProxy,
proxy: RpcProxy,
blockCache: BlockCache,
chainId: Quantity): T =
VerifiedRpcProxy(
proxy: proxy,
blockCache: blockCache,
chainId: chainId)
# Used to be in eth1_monitor.nim; not sure why it was deleted,
# so I copied it here. --Adam
template awaitWithRetries*[T](lazyFutExpr: Future[T],
retries = 3,
timeout = 60.seconds): untyped =
const
reqType = astToStr(lazyFutExpr)
var
retryDelayMs = 16000
f: Future[T]
attempts = 0
while true:
f = lazyFutExpr
yield f or sleepAsync(timeout)
if not f.finished:
await cancelAndWait(f)
elif f.failed:
when not (f.error of CatchableError):
static: doAssert false, "f.error not CatchableError"
debug "Web3 request failed", req = reqType, err = f.error.msg
else:
break
inc attempts
if attempts >= retries:
var errorMsg = reqType & " failed " & $retries & " times"
if f.failed: errorMsg &= ". Last error: " & f.error.msg
raise newException(DataProviderFailure, errorMsg)
await sleepAsync(chronos.milliseconds(retryDelayMs))
retryDelayMs *= 2
read(f)
proc verifyChaindId*(p: VerifiedRpcProxy): Future[void] {.async.} =
let localId = p.chainId
# retry 2 times, if the data provider fails despite the re-tries, propagate
# exception to the caller.
let providerId = awaitWithRetries(
p.rpcClient.eth_chainId(),
retries = 2,
timeout = seconds(30)
)
# This is a chain/network mismatch error between the Nimbus verified proxy and
# the application using it. Fail fast to avoid misusage. The user must fix
# the configuration.
if localId != providerId:
fatal "The specified data provider serves data for a different chain",
expectedChain = distinctBase(localId),
providerChain = distinctBase(providerId)
quit 1
return