mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-13 05:44:40 +00:00
c0d52ba179
Both types are not safe and require validation/conversion from rpc implementer. This PR change it to safer types and delegate the conversion and validation to the rpc library.
322 lines
9.5 KiB
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
|
|
|