344 lines
10 KiB
Nim
344 lines
10 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,
|
|
stint,
|
|
stew/[byteutils, results],
|
|
chronicles,
|
|
json_rpc/[rpcproxy, rpcserver, rpcclient],
|
|
eth/common/eth_types as etypes,
|
|
web3,
|
|
web3/[ethhexstrings, ethtypes],
|
|
beacon_chain/eth1/eth1_monitor,
|
|
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.}
|
|
|
|
template encodeQuantity(value: UInt256): HexQuantityStr =
|
|
hexQuantityStr("0x" & value.toHex())
|
|
|
|
template encodeHexData(value: UInt256): HexDataStr =
|
|
hexDataStr("0x" & toBytesBE(value).toHex)
|
|
|
|
template bytesToHex(bytes: seq[byte]): HexDataStr =
|
|
hexDataStr("0x" & toHex(bytes))
|
|
|
|
template encodeQuantity(value: Quantity): HexQuantityStr =
|
|
hexQuantityStr(encodeQuantity(value.uint64))
|
|
|
|
type
|
|
VerifiedRpcProxy* = ref object
|
|
proxy: RpcProxy
|
|
blockCache: BlockCache
|
|
chainId: Quantity
|
|
|
|
QuantityTagKind = enum
|
|
LatestBlock, BlockNumber
|
|
|
|
QuantityTag = object
|
|
case kind: QuantityTagKind
|
|
of LatestBlock:
|
|
discard
|
|
of BlockNumber:
|
|
blockNumber: Quantity
|
|
|
|
func parseHexIntResult(tag: string): Result[uint64, string] =
|
|
try:
|
|
ok(parseHexInt(tag).uint64)
|
|
except ValueError as e:
|
|
err(e.msg)
|
|
|
|
func parseHexQuantity(tag: string): Result[Quantity, string] =
|
|
let hexQuantity = hexQuantityStr(tag)
|
|
if validate(hexQuantity):
|
|
let parsed = ? parseHexIntResult(tag)
|
|
return ok(Quantity(parsed))
|
|
else:
|
|
return err("Invalid hex quantity.")
|
|
|
|
func parseQuantityTag(blockTag: string): Result[QuantityTag, string] =
|
|
let tag = blockTag.toLowerAscii
|
|
case tag
|
|
of "latest":
|
|
return ok(QuantityTag(kind: LatestBlock))
|
|
else:
|
|
let quantity = ? parseHexQuantity(tag)
|
|
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: string):
|
|
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: string): 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() -> HexQuantityStr:
|
|
return encodeQuantity(lcProxy.chainId)
|
|
|
|
lcProxy.proxy.rpc("eth_blockNumber") do() -> HexQuantityStr:
|
|
## Returns the number of the most recent block.
|
|
checkPreconditions(lcProxy)
|
|
|
|
return encodeQuantity(lcProxy.blockCache.latest.get.blockNumber)
|
|
|
|
lcProxy.proxy.rpc("eth_getBalance") do(
|
|
address: Address, quantityTag: string) -> HexQuantityStr:
|
|
# 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 encodeQuantity(accountResult.get.balance)
|
|
else:
|
|
raise newException(ValueError, accountResult.error)
|
|
|
|
lcProxy.proxy.rpc("eth_getStorageAt") do(
|
|
address: Address, slot: HexDataStr, quantityTag: string) -> HexDataStr:
|
|
let
|
|
executionPayload = lcProxy.getPayloadByTagOrThrow(quantityTag)
|
|
uslot = UInt256.fromHex(slot.string)
|
|
blockNumber = executionPayload.blockNumber.uint64
|
|
|
|
info "Forwarding eth_getStorageAt", blockNumber
|
|
|
|
let proof = await lcProxy.rpcClient.eth_getProof(
|
|
address, @[uslot], blockId(blockNumber))
|
|
|
|
let dataResult = getStorageData(executionPayload.stateRoot, uslot, proof)
|
|
|
|
if dataResult.isOk():
|
|
let slotValue = dataResult.get()
|
|
return encodeHexData(slotValue)
|
|
else:
|
|
raise newException(ValueError, dataResult.error)
|
|
|
|
lcProxy.proxy.rpc("eth_getTransactionCount") do(
|
|
address: Address, quantityTag: string) -> HexQuantityStr:
|
|
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 hexQuantityStr(encodeQuantity(accountResult.get.nonce))
|
|
else:
|
|
raise newException(ValueError, accountResult.error)
|
|
|
|
lcProxy.proxy.rpc("eth_getCode") do(
|
|
address: Address, quantityTag: string) -> HexDataStr:
|
|
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 hexDataStr("0x")
|
|
|
|
info "Forwarding eth_getCode", blockNumber
|
|
|
|
let code = await lcProxy.rpcClient.eth_getCode(
|
|
address,
|
|
blockId(blockNumber)
|
|
)
|
|
|
|
if isValidCode(account, code):
|
|
return bytesToHex(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: string, 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
|
|
|