341 lines
13 KiB
Nim
341 lines
13 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2021-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/[times, sequtils, strutils, typetraits],
|
|
json_rpc/[rpcproxy, rpcserver], stew/byteutils,
|
|
web3/conversions, # sigh, for FixedBytes marshalling
|
|
eth/[common/eth_types, rlp],
|
|
beacon_chain/spec/forks,
|
|
../../nimbus/rpc/[rpc_types, hexstrings, filters],
|
|
../../nimbus/transaction,
|
|
# TODO: this is a bit weird but having this import makes beacon_light_client
|
|
# to fail compilation due throwing undeclared `CatchableError` in
|
|
# `vendor/nimbus-eth2/beacon_chain/spec/keystore.nim`. This is most probably
|
|
# caused by `readValue` clashing ?
|
|
# ../../nimbus/common/chain_config
|
|
../network/history/[history_network, history_content],
|
|
../network/beacon_light_client/beacon_light_client
|
|
|
|
# Subset of Eth JSON-RPC API: https://eth.wiki/json-rpc/API
|
|
# Supported subset will eventually be found here:
|
|
# https://github.com/ethereum/stateless-ethereum-specs/blob/master/portal-network.md#json-rpc-api
|
|
#
|
|
# In order to already support these calls before every part of the Portal
|
|
# Network is up, one plan is to get the data directly from an external client
|
|
# through RPC calls. Practically just playing a proxy to that client.
|
|
# Can be done by just forwarding the rpc call, or by adding a call here, but
|
|
# that would introduce a unnecessary serializing/deserializing step.
|
|
|
|
# Some similar code as from nimbus `rpc_utils`, but avoiding that import as it
|
|
# brings in a lot more. Should restructure `rpc_utils` a bit before using that.
|
|
func toHash*(value: array[32, byte]): Hash256 =
|
|
result.data = value
|
|
|
|
func toHash*(value: EthHashStr): Hash256 {.raises: [ValueError].} =
|
|
hexToPaddedByteArray[32](value.string).toHash
|
|
|
|
func init*(
|
|
T: type TransactionObject,
|
|
tx: eth_types.Transaction, header: BlockHeader, txIndex: int):
|
|
T {.raises: [ValidationError].} =
|
|
TransactionObject(
|
|
blockHash: some(header.blockHash),
|
|
blockNumber: some(encodeQuantity(header.blockNumber)),
|
|
`from`: tx.getSender(),
|
|
gas: encodeQuantity(tx.gasLimit.uint64),
|
|
gasPrice: encodeQuantity(tx.gasPrice.uint64),
|
|
hash: tx.rlpHash,
|
|
input: tx.payload,
|
|
nonce: encodeQuantity(tx.nonce.uint64),
|
|
to: some(tx.destination),
|
|
transactionIndex: some(encodeQuantity(txIndex.uint64)),
|
|
value: encodeQuantity(tx.value),
|
|
v: encodeQuantity(tx.V.uint),
|
|
r: encodeQuantity(tx.R),
|
|
s: encodeQuantity(tx.S),
|
|
`type`: encodeQuantity(tx.txType.uint64),
|
|
maxFeePerGas: encodeQuantity(tx.maxFee.uint64),
|
|
maxPriorityFeePerGas: encodeQuantity(tx.maxPriorityFee.uint64),
|
|
)
|
|
|
|
# Note: Similar as `populateBlockObject` from rpc_utils, but lacking the
|
|
# total difficulty
|
|
func init*(
|
|
T: type BlockObject,
|
|
header: BlockHeader, body: BlockBody,
|
|
fullTx = true, isUncle = false):
|
|
T {.raises: [ValidationError].} =
|
|
let blockHash = header.blockHash
|
|
|
|
var blockObject = BlockObject(
|
|
number: some(encodeQuantity(header.blockNumber)),
|
|
hash: some(blockHash),
|
|
parentHash: header.parentHash,
|
|
nonce: some(hexDataStr(header.nonce)),
|
|
sha3Uncles: header.ommersHash,
|
|
logsBloom: FixedBytes[256] header.bloom,
|
|
transactionsRoot: header.txRoot,
|
|
stateRoot: header.stateRoot,
|
|
receiptsRoot: header.receiptRoot,
|
|
miner: header.coinbase,
|
|
difficulty: encodeQuantity(header.difficulty),
|
|
extraData: hexDataStr(header.extraData),
|
|
# TODO: This is optional according to
|
|
# https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/eth1.0-apis/assembled-spec/openrpc.json
|
|
# So we should probably change `BlockObject`.
|
|
totalDifficulty: encodeQuantity(UInt256.low()),
|
|
gasLimit: encodeQuantity(header.gasLimit.uint64),
|
|
gasUsed: encodeQuantity(header.gasUsed.uint64),
|
|
timestamp: encodeQuantity(header.timestamp.uint64)
|
|
)
|
|
|
|
let size = sizeof(BlockHeader) - sizeof(Blob) + header.extraData.len
|
|
blockObject.size = encodeQuantity(size.uint)
|
|
|
|
if not isUncle:
|
|
blockObject.uncles =
|
|
body.uncles.map(proc(h: BlockHeader): Hash256 = h.blockHash)
|
|
|
|
if fullTx:
|
|
var i = 0
|
|
for tx in body.transactions:
|
|
# ValidationError from tx.getSender in TransactionObject.init
|
|
blockObject.transactions.add %(TransactionObject.init(tx, header, i))
|
|
inc i
|
|
else:
|
|
for tx in body.transactions:
|
|
blockObject.transactions.add %(keccakHash(rlp.encode(tx)))
|
|
|
|
blockObject
|
|
|
|
proc installEthApiHandlers*(
|
|
# Currently only HistoryNetwork needed, later we might want a master object
|
|
# holding all the networks.
|
|
rpcServerWithProxy: var RpcProxy, historyNetwork: HistoryNetwork,
|
|
beaconLightClient: Opt[LightClient])
|
|
{.raises: [CatchableError].} =
|
|
|
|
# Supported API
|
|
rpcServerWithProxy.registerProxyMethod("eth_blockNumber")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_call")
|
|
|
|
# rpcServerWithProxy.registerProxyMethod("eth_chainId")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_estimateGas")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_feeHistory")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getBalance")
|
|
|
|
# rpcServerWithProxy.registerProxyMethod("eth_getBlockByHash")
|
|
|
|
# rpcServerWithProxy.registerProxyMethod("eth_getBlockByNumber")
|
|
|
|
# rpcServerWithProxy.registerProxyMethod("eth_getBlockTransactionCountByHash")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getBlockTransactionCountByNumber")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getCode")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getRawTransactionByHash")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getRawTransactionByBlockHashAndIndex")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getRawTransactionByBlockNumberAndIndex")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getStorageAt")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getTransactionByBlockHashAndIndex")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getTransactionByBlockNumberAndIndex")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getTransactionByHash")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getTransactionCount")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getTransactionReceipt")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getUncleByBlockHashAndIndex")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getUncleByBlockNumberAndIndex")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getUncleCountByBlockHash")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getUncleCountByBlockNumber")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getProof")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_sendRawTransaction")
|
|
|
|
# Optional API
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_gasPrice")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getFilterChanges")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_getFilterLogs")
|
|
|
|
# rpcServerWithProxy.registerProxyMethod("eth_getLogs")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_newBlockFilter")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_newFilter")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_newPendingTransactionFilter")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_pendingTransactions")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_syncing")
|
|
|
|
rpcServerWithProxy.registerProxyMethod("eth_uninstallFilter")
|
|
|
|
# Supported API through the Portal Network
|
|
|
|
rpcServerWithProxy.rpc("eth_chainId") do() -> HexQuantityStr:
|
|
# The Portal Network can only support MainNet at the moment, so always return
|
|
# 1
|
|
return encodeQuantity(uint64(1))
|
|
|
|
rpcServerWithProxy.rpc("eth_getBlockByHash") do(
|
|
data: EthHashStr, fullTransactions: bool) -> Option[BlockObject]:
|
|
## Returns information about a block by hash.
|
|
##
|
|
## data: Hash of a block.
|
|
## fullTransactions: If true it returns the full transaction objects, if
|
|
## false only the hashes of the transactions.
|
|
##
|
|
## Returns BlockObject or nil when no block was found.
|
|
let
|
|
blockHash = data.toHash()
|
|
(header, body) = (await historyNetwork.getBlock(blockHash)).valueOr:
|
|
return none(BlockObject)
|
|
|
|
return some(BlockObject.init(header, body))
|
|
|
|
rpcServerWithProxy.rpc("eth_getBlockByNumber") do(
|
|
quantityTag: string, fullTransactions: bool) -> Option[BlockObject]:
|
|
let tag = quantityTag.toLowerAscii
|
|
case tag
|
|
of "latest":
|
|
# TODO:
|
|
# I assume this would refer to the content in the latest optimistic update
|
|
# in case the majority treshold is not met. And if it is met it is the
|
|
# same as the safe version?
|
|
raise newException(ValueError, "Latest tag not yet implemented")
|
|
of "earliest":
|
|
raise newException(ValueError, "Earliest tag not yet implemented")
|
|
of "safe":
|
|
if beaconLightClient.isNone():
|
|
raise newException(ValueError, "Safe tag not yet implemented")
|
|
|
|
withForkyStore(beaconLightClient.value().store[]):
|
|
when lcDataFork > LightClientDataFork.Altair:
|
|
let
|
|
blockHash = forkyStore.optimistic_header.execution.block_hash
|
|
(header, body) = (await historyNetwork.getBlock(blockHash)).valueOr:
|
|
return none(BlockObject)
|
|
|
|
return some(BlockObject.init(header, body))
|
|
else:
|
|
raise newException(
|
|
ValueError, "Not available before Capella - not synced?")
|
|
of "finalized":
|
|
if beaconLightClient.isNone():
|
|
raise newException(ValueError, "Finalized tag not yet implemented")
|
|
|
|
withForkyStore(beaconLightClient.value().store[]):
|
|
when lcDataFork > LightClientDataFork.Altair:
|
|
let
|
|
blockHash = forkyStore.finalized_header.execution.block_hash
|
|
(header, body) = (await historyNetwork.getBlock(blockHash)).valueOr:
|
|
return none(BlockObject)
|
|
|
|
return some(BlockObject.init(header, body))
|
|
else:
|
|
raise newException(
|
|
ValueError, "Not available before Capella - not synced?")
|
|
of "pending":
|
|
raise newException(ValueError, "Pending tag not yet implemented")
|
|
else:
|
|
if not isValidHexQuantity(quantityTag):
|
|
raise newException(ValueError, "Provided block number is not a hex number")
|
|
|
|
let
|
|
blockNumber = fromHex(UInt256, quantityTag)
|
|
maybeBlock = (await historyNetwork.getBlock(blockNumber)).valueOr:
|
|
raise newException(ValueError, error)
|
|
|
|
if maybeBlock.isNone():
|
|
return none(BlockObject)
|
|
else:
|
|
let (header, body) = maybeBlock.get()
|
|
return some(BlockObject.init(header, body))
|
|
|
|
rpcServerWithProxy.rpc("eth_getBlockTransactionCountByHash") do(
|
|
data: EthHashStr) -> HexQuantityStr:
|
|
## Returns the number of transactions in a block from a block matching the
|
|
## given block hash.
|
|
##
|
|
## data: hash of a block
|
|
## Returns integer of the number of transactions in this block.
|
|
let
|
|
blockHash = data.toHash()
|
|
(_, body) = (await historyNetwork.getBlock(blockHash)).valueOr:
|
|
raise newException(ValueError, "Could not find block with requested hash")
|
|
|
|
var txCount: uint = 0
|
|
for tx in body.transactions:
|
|
txCount.inc()
|
|
|
|
return encodeQuantity(txCount)
|
|
|
|
# Note: can't implement this yet as the fluffy node doesn't know the relation
|
|
# of tx hash -> block number -> block hash, in order to get the receipt
|
|
# from from the block with that block hash. The Canonical Indices Network
|
|
# would need to be implemented to get this information.
|
|
# rpcServerWithProxy.rpc("eth_getTransactionReceipt") do(
|
|
# data: EthHashStr) -> Option[ReceiptObject]:
|
|
|
|
rpcServerWithProxy.rpc("eth_getLogs") do(
|
|
filterOptions: FilterOptions) -> seq[FilterLog]:
|
|
if filterOptions.blockHash.isNone():
|
|
# Currently only queries by blockhash are supported.
|
|
# To support range queries the Indicies network is required.
|
|
raise newException(ValueError,
|
|
"Unsupported query: Only `blockHash` queries are currently supported")
|
|
|
|
let hash = filterOptions.blockHash.unsafeGet()
|
|
|
|
let header = (await historyNetwork.getVerifiedBlockHeader(hash)).valueOr:
|
|
raise newException(ValueError,
|
|
"Could not find header with requested hash")
|
|
|
|
if headerBloomFilter(header, filterOptions.address, filterOptions.topics):
|
|
# TODO: These queries could be done concurrently, investigate if there
|
|
# are no assumptions about usage of concurrent queries on portal
|
|
# wire protocol level
|
|
let
|
|
body = (await historyNetwork.getBlockBody(hash, header)).valueOr:
|
|
raise newException(ValueError,
|
|
"Could not find block body for requested hash")
|
|
receipts = (await historyNetwork.getReceipts(hash, header)).valueOr:
|
|
raise newException(ValueError,
|
|
"Could not find receipts for requested hash")
|
|
|
|
logs = deriveLogs(header, body.transactions, receipts)
|
|
filteredLogs = filterLogs(
|
|
logs, filterOptions.address, filterOptions.topics)
|
|
|
|
return filteredLogs
|
|
else:
|
|
# bloomfilter returned false, there are no logs matching the criteria
|
|
return @[]
|