nimbus-eth1/lc_proxy/rpc/rpc_eth_lc_api.nim

239 lines
7.4 KiB
Nim

# ligh client proxy
# Copyright (c) 2022 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: [Defect].}
import
std/strutils,
stint,
stew/[byteutils, results],
chronicles,
json_rpc/[rpcproxy, rpcserver, rpcclient],
web3,
web3/[ethhexstrings, ethtypes],
beacon_chain/eth1/eth1_monitor,
beacon_chain/networking/network_metadata,
beacon_chain/spec/forks,
../validate_proof,
../block_cache
export forks
logScope:
topics = "light_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 encodeQuantity(value: Quantity): HexQuantityStr =
hexQuantityStr(encodeQuantity(value.uint64))
type
LightClientRpcProxy* = 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 Etheruem 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: LightClientRpcProxy) =
if proxy.blockCache.isEmpty():
raise newException(ValueError, "Syncing")
template rpcClient(lcProxy: LightClientRpcProxy): RpcClient = lcProxy.proxy.getClient()
proc getPayloadByTag(
proxy: LightClientRpcProxy,
quantityTag: string): ExecutionPayloadV1 {.raises: [ValueError, Defect].} =
checkPreconditions(proxy)
let tagResult = parseQuantityTag(quantityTag)
if tagResult.isErr:
raise newException(ValueError, tagResult.error)
let tag = tagResult.get()
var payload: ExecutionPayloadV1
case tag.kind
of LatestBlock:
# this will always be ok as we always validate that cache is not empty
payload = proxy.blockCache.latest.get
of BlockNumber:
let payLoadResult = proxy.blockCache.getByNumber(tag.blockNumber)
if payLoadResult.isErr():
raise newException(
ValueError, "Block not stored in cache " & $tag.blockNumber
)
payload = payLoadResult.get
return payload
proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) =
template payload(): Opt[ExecutionPayloadV1] = lcProxy.executionPayload
lcProxy.proxy.rpc("eth_chainId") do() -> HexQuantityStr:
return encodeQuantity(lcProxy.chainId)
lcProxy.proxy.rpc("eth_blockNumber") do() -> HexQuantityStr:
## Returns the number of 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.getPayloadByTag(quantityTag)
blockNumber = executionPayload.blockNumber.uint64
info "Forwarding get_Balance", executionBn = 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.getPayloadByTag(quantityTag)
uslot = UInt256.fromHex(slot.string)
blockNumber = executionPayload.blockNumber.uint64
info "Forwarding eth_getStorageAt", executionBn = 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.getPayloadByTag(quantityTag)
blockNumber = executionPayload.blockNumber.uint64
info "Forwarding eth_getTransactionCount", executionBn = 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)
# TODO This methods are forwarded directly to provider therefore thay 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 Respond to this calls using BlockCache
lcProxy.proxy.registerProxyMethod("eth_getBlockByNumber")
lcProxy.proxy.registerProxyMethod("eth_getBlockByHash")
proc new*(
T: type LightClientRpcProxy,
proxy: RpcProxy,
blockCache: BlockCache,
chainId: Quantity): T =
return LightClientRpcProxy(
proxy: proxy,
blockCache: blockCache,
chainId: chainId
)
proc verifyChaindId*(p: LightClientRpcProxy): Future[void] {.async.} =
let localId = p.chainId
# retry 2 times, if the data provider will fail despite re-tries, propagate
# exception to the caller.
let providerId = awaitWithRetries(
p.rpcClient.eth_chainId(),
retries = 2,
timeout = seconds(30)
)
# this configuration error, in theory we could allow proxy to chung on, but
# it would only mislead the user. It is better to fail fast here.
if localId != providerId:
fatal "The specified data provider serves data for a different chain",
expectedChain = distinctBase(localId),
providerChain = distinctBase(providerId)
quit 1
return