# Fluffy # Copyright (c) 2021-2024 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/rpcserver, chronicles, web3/conversions, # sigh, for FixedBytes marshalling web3/eth_api_types, web3/primitives as web3types, eth/common/eth_types, beacon_chain/spec/forks, ../common/common_utils, ../network/history/[history_network, history_content], ../network/state/[state_network, state_content, state_endpoints], ../network/beacon/beacon_light_client from ../../nimbus/transaction import getSender, ValidationError from ../../nimbus/rpc/filters import headerBloomFilter, deriveLogs, filterLogs from ../../nimbus/beacon/web3_eth_conv import w3Addr, w3Hash, ethHash # Subset of Ethereum execution JSON-RPC API: # https://ethereum.github.io/execution-apis/api-documentation/ # # Currently supported subset: # - eth_chainId # - eth_getBlockByHash # - eth_getBlockByNumber # - eth_getBlockTransactionCountByHash # - eth_getLogs - Partially: only requests by block hash # # 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: eth_api_types.Hash256): eth_types.Hash256 = result.data = value.bytes func init*( T: type TransactionObject, tx: eth_types.Transaction, header: eth_types.BlockHeader, txIndex: int, ): T {.raises: [ValidationError].} = TransactionObject( blockHash: Opt.some(w3Hash header.blockHash), blockNumber: Opt.some(eth_api_types.BlockNumber(header.number)), `from`: w3Addr tx.getSender(), gas: Quantity(tx.gasLimit), gasPrice: Quantity(tx.gasPrice), hash: w3Hash tx.rlpHash, input: tx.payload, nonce: Quantity(tx.nonce), to: Opt.some(w3Addr tx.destination), transactionIndex: Opt.some(Quantity(txIndex)), value: tx.value, v: Quantity(tx.V), r: tx.R, s: tx.S, `type`: Opt.some(Quantity(tx.txType)), maxFeePerGas: Opt.some(Quantity(tx.maxFeePerGas)), maxPriorityFeePerGas: Opt.some(Quantity(tx.maxPriorityFeePerGas)), ) # Note: Similar as `populateBlockObject` from rpc_utils, but lacking the # total difficulty func init*( T: type BlockObject, header: eth_types.BlockHeader, body: BlockBody, fullTx = true, isUncle = false, ): T {.raises: [ValidationError].} = let blockHash = header.blockHash var blockObject = BlockObject( number: eth_api_types.BlockNumber(header.number), hash: w3Hash blockHash, parentHash: w3Hash header.parentHash, nonce: Opt.some(FixedBytes[8](header.nonce)), sha3Uncles: w3Hash header.ommersHash, logsBloom: FixedBytes[256] header.logsBloom, transactionsRoot: w3Hash header.txRoot, stateRoot: w3Hash header.stateRoot, receiptsRoot: w3Hash header.receiptsRoot, miner: w3Addr header.coinbase, difficulty: header.difficulty, extraData: HistoricExtraData 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: UInt256.low(), gasLimit: Quantity(header.gasLimit), gasUsed: Quantity(header.gasUsed), timestamp: Quantity(header.timestamp), ) let size = sizeof(BlockHeader) - sizeof(Blob) + header.extraData.len blockObject.size = Quantity(size.uint) if not isUncle: blockObject.uncles = body.uncles.map( proc(h: eth_types.BlockHeader): eth_api_types.Hash256 = w3Hash h.blockHash ) if fullTx: var i = 0 for tx in body.transactions: # ValidationError from tx.getSender in TransactionObject.init blockObject.transactions.add txOrHash(TransactionObject.init(tx, header, i)) inc i else: for tx in body.transactions: blockObject.transactions.add txOrHash(w3Hash rlpHash(tx)) blockObject proc installEthApiHandlers*( rpcServer: RpcServer, historyNetwork: HistoryNetwork, beaconLightClient: Opt[LightClient], stateNetwork: Opt[StateNetwork], ) = # Supported API through the Portal Network rpcServer.rpc("eth_chainId") do() -> Quantity: # The Portal Network can only support MainNet at the moment, so always return # 1 return Quantity(uint64(1)) rpcServer.rpc("eth_getBlockByHash") do( data: eth_api_types.Hash256, fullTransactions: bool ) -> Opt[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 Opt.none(BlockObject) return Opt.some(BlockObject.init(header, body, fullTransactions)) rpcServer.rpc("eth_getBlockByNumber") do( quantityTag: RtBlockIdentifier, fullTransactions: bool ) -> Opt[BlockObject]: if quantityTag.kind == bidAlias: let tag = quantityTag.alias.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 Opt.none(BlockObject) return Opt.some(BlockObject.init(header, body, fullTransactions)) 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 Opt.none(BlockObject) return Opt.some(BlockObject.init(header, body, fullTransactions)) else: raise newException(ValueError, "Not available before Capella - not synced?") of "pending": raise newException(ValueError, "Pending tag not yet implemented") else: raise newException(ValueError, "Unsupported block tag " & tag) else: let blockNumber = quantityTag.number.uint64 (header, body) = (await historyNetwork.getBlock(blockNumber)).valueOr: return Opt.none(BlockObject) return Opt.some(BlockObject.init(header, body, fullTransactions)) rpcServer.rpc("eth_getBlockTransactionCountByHash") do( data: eth_api_types.Hash256 ) -> Quantity: ## 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 Quantity(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. # rpcServer.rpc("eth_getTransactionReceipt") do( # data: EthHashStr) -> Opt[ReceiptObject]: rpcServer.rpc("eth_getLogs") do(filterOptions: FilterOptions) -> seq[LogObject]: if filterOptions.blockHash.isNone(): # Currently only queries by blockhash are supported. # TODO: Can impolement range queries by block number now. raise newException( ValueError, "Unsupported query: Only `blockHash` queries are currently supported", ) let hash = ethHash 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 @[] rpcServer.rpc("eth_getBalance") do( data: web3Types.Address, quantityTag: RtBlockIdentifier ) -> UInt256: ## Returns the balance of the account of given address. ## ## data: address to check for balance. ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns integer of the current balance in wei. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") if quantityTag.kind == bidAlias: # TODO: Implement raise newException(ValueError, "tag not yet implemented") let blockNumber = quantityTag.number.uint64 balance = (await sn.getBalance(blockNumber, data.EthAddress)).valueOr: raise newException(ValueError, "Unable to get balance") return balance rpcServer.rpc("eth_getTransactionCount") do( data: web3Types.Address, quantityTag: RtBlockIdentifier ) -> Quantity: ## Returns the number of transactions sent from an address. ## ## data: address. ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns integer of the number of transactions send from this address. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") if quantityTag.kind == bidAlias: # TODO: Implement raise newException(ValueError, "tag not yet implemented") let blockNumber = quantityTag.number.uint64 nonce = (await sn.getTransactionCount(blockNumber, data.EthAddress)).valueOr: raise newException(ValueError, "Unable to get transaction count") return nonce.Quantity rpcServer.rpc("eth_getStorageAt") do( data: web3Types.Address, slot: UInt256, quantityTag: RtBlockIdentifier ) -> FixedBytes[32]: ## Returns the value from a storage position at a given address. ## ## data: address of the storage. ## slot: integer of the position in the storage. ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns: the value at this storage position. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") if quantityTag.kind == bidAlias: # TODO: Implement raise newException(ValueError, "tag not yet implemented") let blockNumber = quantityTag.number.uint64 slotValue = (await sn.getStorageAt(blockNumber, data.EthAddress, slot)).valueOr: raise newException(ValueError, "Unable to get storage slot") return FixedBytes[32](slotValue.toBytesBE()) rpcServer.rpc("eth_getCode") do( data: web3Types.Address, quantityTag: RtBlockIdentifier ) -> seq[byte]: ## Returns code at a given address. ## ## data: address ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns the code from the given address. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") if quantityTag.kind == bidAlias: # TODO: Implement raise newException(ValueError, "tag not yet implemented") let blockNumber = quantityTag.number.uint64 bytecode = (await sn.getCode(blockNumber, data.EthAddress)).valueOr: raise newException(ValueError, "Unable to get code") return bytecode.asSeq() rpcServer.rpc("eth_getProof") do( data: web3Types.Address, slots: seq[UInt256], quantityTag: RtBlockIdentifier ) -> ProofResponse: ## Returns information about an account and storage slots along with account ## and storage proofs which prove the existence of the values in the state. ## See spec here: https://eips.ethereum.org/EIPS/eip-1186 ## ## data: address of the account. ## slots: integers of the positions in the storage to return. ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns: the proof response containing the account, account proof and storage proof let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") if quantityTag.kind == bidAlias: # TODO: Implement raise newException(ValueError, "tag not yet implemented") let blockNumber = quantityTag.number.uint64 proofs = (await sn.getProofs(blockNumber, data.EthAddress, slots)).valueOr: raise newException(ValueError, "Unable to get proofs") var storageProof = newSeqOfCap[StorageProof](slots.len) for i, slot in slots: let (slotKey, slotValue) = proofs.slots[i] storageProof.add( StorageProof( key: slotKey, value: slotValue, proof: seq[RlpEncodedBytes](proofs.slotProofs[i]), ) ) return ProofResponse( address: data, accountProof: seq[RlpEncodedBytes](proofs.accountProof), balance: proofs.account.balance, nonce: web3types.Quantity(proofs.account.nonce), codeHash: web3types.Hash256(proofs.account.codeHash.data), storageHash: web3types.Hash256(proofs.account.storageRoot.data), storageProof: storageProof, ) # TODO: Should we move these debug methods into a separate debug rpcServer? rpcServer.rpc("debug_getBalanceByStateRoot") do( data: web3Types.Address, stateRoot: web3types.Hash256 ) -> UInt256: ## Returns the balance of the account of given address. ## ## data: address to check for balance. ## stateRoot: the state root used to search the state trie. ## Returns integer of the current balance in wei. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") let balance = ( await sn.getBalanceByStateRoot( KeccakHash.fromBytes(stateRoot.bytes()), data.EthAddress ) ).valueOr: raise newException(ValueError, "Unable to get balance") return balance rpcServer.rpc("debug_getTransactionCountByStateRoot") do( data: web3Types.Address, stateRoot: web3types.Hash256 ) -> Quantity: ## Returns the number of transactions sent from an address. ## ## data: address. ## stateRoot: the state root used to search the state trie. ## Returns integer of the number of transactions send from this address. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") let nonce = ( await sn.getTransactionCountByStateRoot( KeccakHash.fromBytes(stateRoot.bytes()), data.EthAddress ) ).valueOr: raise newException(ValueError, "Unable to get transaction count") return nonce.Quantity rpcServer.rpc("debug_getStorageAtByStateRoot") do( data: web3Types.Address, slot: UInt256, stateRoot: web3types.Hash256 ) -> FixedBytes[32]: ## Returns the value from a storage position at a given address. ## ## data: address of the storage. ## slot: integer of the position in the storage. ## stateRoot: the state root used to search the state trie. ## Returns: the value at this storage position. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") let slotValue = ( await sn.getStorageAtByStateRoot( KeccakHash.fromBytes(stateRoot.bytes()), data.EthAddress, slot ) ).valueOr: raise newException(ValueError, "Unable to get storage slot") return FixedBytes[32](slotValue.toBytesBE()) rpcServer.rpc("debug_getCodeByStateRoot") do( data: web3Types.Address, stateRoot: web3types.Hash256 ) -> seq[byte]: ## Returns code at a given address. ## ## data: address ## stateRoot: the state root used to search the state trie. ## Returns the code from the given address. let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") let bytecode = ( await sn.getCodeByStateRoot( KeccakHash.fromBytes(stateRoot.bytes()), data.EthAddress ) ).valueOr: raise newException(ValueError, "Unable to get code") return bytecode.asSeq() rpcServer.rpc("debug_getProofByStateRoot") do( data: web3Types.Address, slots: seq[UInt256], stateRoot: web3types.Hash256 ) -> ProofResponse: ## Returns information about an account and storage slots along with account ## and storage proofs which prove the existence of the values in the state. ## ## data: address of the account. ## slots: integers of the positions in the storage to return. ## stateRoot: the state root used to search the state trie. ## Returns: the proof response containing the account, account proof and storage proof let sn = stateNetwork.valueOr: raise newException(ValueError, "State sub-network not enabled") let proofs = ( await sn.getProofsByStateRoot( KeccakHash.fromBytes(stateRoot.bytes()), data.EthAddress, slots ) ).valueOr: raise newException(ValueError, "Unable to get proofs") var storageProof = newSeqOfCap[StorageProof](slots.len) for i, slot in slots: let (slotKey, slotValue) = proofs.slots[i] storageProof.add( StorageProof( key: slotKey, value: slotValue, proof: seq[RlpEncodedBytes](proofs.slotProofs[i]), ) ) return ProofResponse( address: data, accountProof: seq[RlpEncodedBytes](proofs.accountProof), balance: proofs.account.balance, nonce: web3types.Quantity(proofs.account.nonce), codeHash: web3types.Hash256(proofs.account.codeHash.data), storageHash: web3types.Hash256(proofs.account.storageRoot.data), storageProof: storageProof, )