diff --git a/execution_chain/utils/debug.nim b/execution_chain/utils/debug.nim index a7dec382b2..231a60d765 100644 --- a/execution_chain/utils/debug.nim +++ b/execution_chain/utils/debug.nim @@ -114,7 +114,7 @@ proc `$`(acl: transactions.AccessList): string = result.add " * " & $ap.address & "\n" for i, k in ap.storageKeys: result.add " - " & k.toHex - if i < ap.storageKeys.len-1: + if i < ap.storageKeys.len - 1: result.add "\n" proc debug*(tx: Transaction): string = diff --git a/nimbus_verified_proxy/c_frontend.nim b/nimbus_verified_proxy/c_frontend.nim new file mode 100644 index 0000000000..0daad6cba0 --- /dev/null +++ b/nimbus_verified_proxy/c_frontend.nim @@ -0,0 +1,197 @@ +# nimbus_verified_proxy +# Copyright (c) 2025 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: [], gcsafe.} + +import + stint, + std/strutils, + json_rpc/[rpcserver, rpcproxy], + web3/[eth_api, eth_api_types], + ../execution_chain/rpc/cors, + ./engine/types, + ./nimbus_verified_proxy_conf + +type JsonRpcServer* = ref object + case kind*: ClientKind #we reuse clientKind for servers also + of Http: + httpServer: RpcHttpServer + of WebSocket: + wsServer: RpcWebSocketServer + +proc init*( + T: type JsonRpcServer, url: Web3Url +): JsonRpcServer {.raises: [JsonRpcError, ValueError, TransportAddressError].} = + let + auth = @[httpCors(@[])] # TODO: for now we serve all cross origin requests + parsedUrl = parseUri(url.web3Url) + hostname = if parsedUrl.hostname == "": "127.0.0.1" else: parsedUrl.hostname + port = + if parsedUrl.port == "": + 8545 + else: + parseInt(parsedUrl.port) + listenAddress = initTAddress(hostname, port) + + case url.kind + of HttpUrl: + JsonRpcServer( + kind: Http, httpServer: newRpcHttpServer([listenAddress], RpcRouter.init(), auth) + ) + of WsUrl: + let server = + JsonRpcServer(kind: WebSocket, wsServer: newRpcWebSocketServer(listenAddress)) + + server.wsServer.router = RpcRouter.init() + server + +func getServer(server: JsonRpcServer): RpcServer = + case server.kind + of Http: server.httpServer + of WebSocket: server.wsServer + +proc start*(server: JsonRpcServer): Result[void, string] = + try: + case server.kind + of Http: + server.httpServer.start() + of WebSocket: + server.wsServer.start() + except CatchableError as e: + return err(e.msg) + + ok() + +proc injectEngineFrontend*(server: JsonRpcServer, frontend: EthApiFrontend) = + server.getServer().rpc("eth_blockNumber") do() -> uint64: + await frontend.eth_blockNumber() + + server.getServer().rpc("eth_getBalance") do( + address: Address, quantityTag: BlockTag + ) -> UInt256: + await frontend.eth_getBalance(address, quantityTag) + + server.getServer().rpc("eth_getStorageAt") do( + address: Address, slot: UInt256, quantityTag: BlockTag + ) -> FixedBytes[32]: + await frontend.eth_getStorageAt(address, slot, quantityTag) + + server.getServer().rpc("eth_getTransactionCount") do( + address: Address, quantityTag: BlockTag + ) -> Quantity: + await frontend.eth_getTransactionCount(address, quantityTag) + + server.getServer().rpc("eth_getCode") do( + address: Address, quantityTag: BlockTag + ) -> seq[byte]: + await frontend.eth_getCode(address, quantityTag) + + server.getServer().rpc("eth_getBlockByHash") do( + blockHash: Hash32, fullTransactions: bool + ) -> BlockObject: + await frontend.eth_getBlockByHash(blockHash, fullTransactions) + + server.getServer().rpc("eth_getBlockByNumber") do( + blockTag: BlockTag, fullTransactions: bool + ) -> BlockObject: + await frontend.eth_getBlockByNumber(blockTag, fullTransactions) + + server.getServer().rpc("eth_getUncleCountByBlockNumber") do( + blockTag: BlockTag + ) -> Quantity: + await frontend.eth_getUncleCountByBlockNumber(blockTag) + + server.getServer().rpc("eth_getUncleCountByBlockHash") do( + blockHash: Hash32 + ) -> Quantity: + await frontend.eth_getUncleCountByBlockHash(blockHash) + + server.getServer().rpc("eth_getBlockTransactionCountByNumber") do( + blockTag: BlockTag + ) -> Quantity: + await frontend.eth_getBlockTransactionCountByNumber(blockTag) + + server.getServer().rpc("eth_getBlockTransactionCountByHash") do( + blockHash: Hash32 + ) -> Quantity: + await frontend.eth_getBlockTransactionCountByHash(blockHash) + + server.getServer().rpc("eth_getTransactionByBlockNumberAndIndex") do( + blockTag: BlockTag, index: Quantity + ) -> TransactionObject: + await frontend.eth_getTransactionByBlockNumberAndIndex(blockTag, index) + + server.getServer().rpc("eth_getTransactionByBlockHashAndIndex") do( + blockHash: Hash32, index: Quantity + ) -> TransactionObject: + await frontend.eth_getTransactionByBlockHashAndIndex(blockHash, index) + + server.getServer().rpc("eth_call") do( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] + ) -> seq[byte]: + await frontend.eth_call(tx, blockTag, optimisticStateFetch.get(true)) + + server.getServer().rpc("eth_createAccessList") do( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] + ) -> AccessListResult: + await frontend.eth_createAccessList(tx, blockTag, optimisticStateFetch.get(true)) + + server.getServer().rpc("eth_estimateGas") do( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] + ) -> Quantity: + await frontend.eth_estimateGas(tx, blockTag, optimisticStateFetch.get(true)) + + server.getServer().rpc("eth_getTransactionByHash") do( + txHash: Hash32 + ) -> TransactionObject: + await frontend.eth_getTransactionByHash(txHash) + + server.getServer().rpc("eth_getBlockReceipts") do( + blockTag: BlockTag + ) -> Opt[seq[ReceiptObject]]: + await frontend.eth_getBlockReceipts(blockTag) + + server.getServer().rpc("eth_getTransactionReceipt") do( + txHash: Hash32 + ) -> ReceiptObject: + await frontend.eth_getTransactionReceipt(txHash) + + server.getServer().rpc("eth_getLogs") do( + filterOptions: FilterOptions + ) -> seq[LogObject]: + await frontend.eth_getLogs(filterOptions) + + server.getServer().rpc("eth_newFilter") do(filterOptions: FilterOptions) -> string: + await frontend.eth_newFilter(filterOptions) + + server.getServer().rpc("eth_uninstallFilter") do(filterId: string) -> bool: + await frontend.eth_uninstallFilter(filterId) + + server.getServer().rpc("eth_getFilterLogs") do(filterId: string) -> seq[LogObject]: + await frontend.eth_getFilterLogs(filterId) + + server.getServer().rpc("eth_getFilterChanges") do(filterId: string) -> seq[LogObject]: + await frontend.eth_getFilterChanges(filterId) + + server.getServer().rpc("eth_blobBaseFee") do() -> UInt256: + await frontend.eth_blobBaseFee() + + server.getServer().rpc("eth_gasPrice") do() -> Quantity: + await frontend.eth_gasPrice() + + server.getServer().rpc("eth_maxPriorityFeePerGas") do() -> Quantity: + await frontend.eth_maxPriorityFeePerGas() + +proc stop*(server: JsonRpcServer) {.async: (raises: [CancelledError]).} = + try: + case server.kind + of Http: + await server.httpServer.closeWait() + of WebSocket: + await server.wsServer.closeWait() + except CatchableError as e: + raise newException(CancelledError, e.msg) diff --git a/nimbus_verified_proxy/rpc/accounts.nim b/nimbus_verified_proxy/engine/accounts.nim similarity index 82% rename from nimbus_verified_proxy/rpc/accounts.nim rename to nimbus_verified_proxy/engine/accounts.nim index e2ae67b19f..0dd22b9a6d 100644 --- a/nimbus_verified_proxy/rpc/accounts.nim +++ b/nimbus_verified_proxy/engine/accounts.nim @@ -18,7 +18,7 @@ import json_rpc/[rpcserver, rpcclient], web3/[primitives, eth_api_types, eth_api], ../../execution_chain/beacon/web3_eth_conv, - ../types + ./types proc getAccountFromProof*( stateRoot: Hash32, @@ -99,14 +99,14 @@ proc getStorageFromProof*( getStorageFromProof(account, storageProof) proc getAccount*( - lcProxy: VerifiedRpcProxy, + engine: RpcVerificationEngine, address: Address, blockNumber: base.BlockNumber, stateRoot: Root, ): Future[Result[Account, string]] {.async: (raises: []).} = let cacheKey = (stateRoot, address) - cachedAcc = lcProxy.accountsCache.get(cacheKey) + cachedAcc = engine.accountsCache.get(cacheKey) if cachedAcc.isSome(): return ok(cachedAcc.get()) @@ -115,7 +115,7 @@ proc getAccount*( let proof = try: - await lcProxy.rpcClient.eth_getProof(address, @[], blockId(blockNumber)) + await engine.backend.eth_getProof(address, @[], blockId(blockNumber)) except CatchableError as e: return err(e.msg) @@ -125,18 +125,18 @@ proc getAccount*( ) if account.isOk(): - lcProxy.accountsCache.put(cacheKey, account.get()) + engine.accountsCache.put(cacheKey, account.get()) return account proc getCode*( - lcProxy: VerifiedRpcProxy, + engine: RpcVerificationEngine, address: Address, blockNumber: base.BlockNumber, stateRoot: Root, -): Future[Result[seq[byte], string]] {.async.} = +): Future[Result[seq[byte], string]] {.async: (raises: []).} = # get verified account details for the address at blockNumber - let account = (await lcProxy.getAccount(address, blockNumber, stateRoot)).valueOr: + let account = (await engine.getAccount(address, blockNumber, stateRoot)).valueOr: return err(error) # if the account does not have any code, return empty hex data @@ -145,7 +145,7 @@ proc getCode*( let cacheKey = (stateRoot, address) - cachedCode = lcProxy.codeCache.get(cacheKey) + cachedCode = engine.codeCache.get(cacheKey) if cachedCode.isSome(): return ok(cachedCode.get()) @@ -153,20 +153,20 @@ proc getCode*( let code = try: - await lcProxy.rpcClient.eth_getCode(address, blockId(blockNumber)) + await engine.backend.eth_getCode(address, blockId(blockNumber)) except CatchableError as e: return err(e.msg) # verify the byte code. since we verified the account against # the state root we just need to verify the code hash if account.codeHash == keccak256(code): - lcProxy.codeCache.put(cacheKey, code) + engine.codeCache.put(cacheKey, code) return ok(code) else: return err("received code doesn't match the account code hash") proc getStorageAt*( - lcProxy: VerifiedRpcProxy, + engine: RpcVerificationEngine, address: Address, slot: UInt256, blockNumber: base.BlockNumber, @@ -174,7 +174,7 @@ proc getStorageAt*( ): Future[Result[UInt256, string]] {.async: (raises: []).} = let cacheKey = (stateRoot, address, slot) - cachedSlotValue = lcProxy.storageCache.get(cacheKey) + cachedSlotValue = engine.storageCache.get(cacheKey) if cachedSlotValue.isSome(): return ok(cachedSlotValue.get()) @@ -183,19 +183,19 @@ proc getStorageAt*( let proof = try: - await lcProxy.rpcClient.eth_getProof(address, @[slot], blockId(blockNumber)) + await engine.backend.eth_getProof(address, @[slot], blockId(blockNumber)) except CatchableError as e: return err(e.msg) slotValue = getStorageFromProof(stateRoot, slot, proof) if slotValue.isOk(): - lcProxy.storageCache.put(cacheKey, slotValue.get()) + engine.storageCache.put(cacheKey, slotValue.get()) return slotValue proc populateCachesForAccountAndSlots( - lcProxy: VerifiedRpcProxy, + engine: RpcVerificationEngine, address: Address, slots: seq[UInt256], blockNumber: base.BlockNumber, @@ -204,18 +204,16 @@ proc populateCachesForAccountAndSlots( var slotsToFetch: seq[UInt256] for s in slots: let storageCacheKey = (stateRoot, address, s) - if lcProxy.storageCache.get(storageCacheKey).isNone(): + if engine.storageCache.get(storageCacheKey).isNone(): slotsToFetch.add(s) let accountCacheKey = (stateRoot, address) - if lcProxy.accountsCache.get(accountCacheKey).isNone() or slotsToFetch.len() > 0: + if engine.accountsCache.get(accountCacheKey).isNone() or slotsToFetch.len() > 0: let proof = try: - await lcProxy.rpcClient.eth_getProof( - address, slotsToFetch, blockId(blockNumber) - ) + await engine.backend.eth_getProof(address, slotsToFetch, blockId(blockNumber)) except CatchableError as e: return err(e.msg) account = getAccountFromProof( @@ -224,33 +222,33 @@ proc populateCachesForAccountAndSlots( ) if account.isOk(): - lcProxy.accountsCache.put(accountCacheKey, account.get()) + engine.accountsCache.put(accountCacheKey, account.get()) for i, s in slotsToFetch: let slotValue = getStorageFromProof(stateRoot, s, proof, i) if slotValue.isOk(): let storageCacheKey = (stateRoot, address, s) - lcProxy.storageCache.put(storageCacheKey, slotValue.get()) + engine.storageCache.put(storageCacheKey, slotValue.get()) ok() proc populateCachesUsingAccessList*( - lcProxy: VerifiedRpcProxy, + engine: RpcVerificationEngine, blockNumber: base.BlockNumber, stateRoot: Root, tx: TransactionArgs, ): Future[Result[void, string]] {.async: (raises: []).} = let accessListRes: AccessListResult = try: - await lcProxy.rpcClient.eth_createAccessList(tx, blockId(blockNumber)) + await engine.backend.eth_createAccessList(tx, blockId(blockNumber)) except CatchableError as e: return err(e.msg) var futs = newSeqOfCap[Future[Result[void, string]]](accessListRes.accessList.len()) for accessPair in accessListRes.accessList: let slots = accessPair.storageKeys.mapIt(UInt256.fromBytesBE(it.data)) - futs.add lcProxy.populateCachesForAccountAndSlots( + futs.add engine.populateCachesForAccountAndSlots( accessPair.address, slots, blockNumber, stateRoot ) diff --git a/nimbus_verified_proxy/rpc/blocks.nim b/nimbus_verified_proxy/engine/blocks.nim similarity index 73% rename from nimbus_verified_proxy/rpc/blocks.nim rename to nimbus_verified_proxy/engine/blocks.nim index a03dc5f661..332386fc17 100644 --- a/nimbus_verified_proxy/rpc/blocks.nim +++ b/nimbus_verified_proxy/engine/blocks.nim @@ -17,26 +17,26 @@ import eth/rlp, eth/trie/[ordered_trie, trie_defs], ../../execution_chain/beacon/web3_eth_conv, - ../types, - ../header_store, + ./types, + ./header_store, ./transactions proc resolveBlockTag*( - vp: VerifiedRpcProxy, blockTag: BlockTag + engine: RpcVerificationEngine, blockTag: BlockTag ): Result[BlockTag, string] = if blockTag.kind == bidAlias: let tag = blockTag.alias.toLowerAscii() case tag of "latest": - let hLatest = vp.headerStore.latest.valueOr: + let hLatest = engine.headerStore.latest.valueOr: return err("Couldn't get the latest block number from header store") ok(BlockTag(kind: bidNumber, number: Quantity(hLatest.number))) of "finalized": - let hFinalized = vp.headerStore.finalized.valueOr: + let hFinalized = engine.headerStore.finalized.valueOr: return err("Couldn't get the latest block number from header store") ok(BlockTag(kind: bidNumber, number: Quantity(hFinalized.number))) of "earliest": - let hEarliest = vp.headerStore.earliest.valueOr: + let hEarliest = engine.headerStore.earliest.valueOr: return err("Couldn't get the latest block number from header store") ok(BlockTag(kind: bidNumber, number: Quantity(hEarliest.number))) else: @@ -73,7 +73,7 @@ func convHeader*(blk: eth_api_types.BlockObject): Header = ) proc walkBlocks( - vp: VerifiedRpcProxy, + engine: RpcVerificationEngine, sourceNum: base.BlockNumber, targetNum: base.BlockNumber, sourceHash: Hash32, @@ -83,20 +83,20 @@ proc walkBlocks( info "Starting block walk to verify requested block", blockHash = targetHash let numBlocks = sourceNum - targetNum - if numBlocks > vp.maxBlockWalk: + if numBlocks > engine.maxBlockWalk: return err( - "Cannot query more than " & $vp.maxBlockWalk & + "Cannot query more than " & $engine.maxBlockWalk & " to verify the chain for the requested block" ) for i in 0 ..< numBlocks: let nextHeader = - if vp.headerStore.contains(nextHash): - vp.headerStore.get(nextHash).get() + if engine.headerStore.contains(nextHash): + engine.headerStore.get(nextHash).get() else: let blk = try: - await vp.rpcClient.eth_getBlockByHash(nextHash, false) + await engine.backend.eth_getBlockByHash(nextHash, false) except CatchableError as e: return err( "Couldn't get block " & $nextHash & " during the chain traversal: " & e.msg @@ -122,19 +122,19 @@ proc walkBlocks( err("the requested block is not part of the canonical chain") proc verifyHeader( - vp: VerifiedRpcProxy, header: Header, hash: Hash32 -): Future[Result[void, string]] {.async.} = + engine: RpcVerificationEngine, header: Header, hash: Hash32 +): Future[Result[void, string]] {.async: (raises: []).} = # verify calculated hash with the requested hash if header.computeBlockHash != hash: return err("hashed block header doesn't match with blk.hash(downloaded)") - if not vp.headerStore.contains(hash): - let latestHeader = vp.headerStore.latest.valueOr: + if not engine.headerStore.contains(hash): + let latestHeader = engine.headerStore.latest.valueOr: return err("Couldn't get the latest header, syncing in progress") # walk blocks backwards(time) from source to target ?( - await vp.walkBlocks( + await engine.walkBlocks( latestHeader.number, header.number, latestHeader.parentHash, hash ) ) @@ -142,11 +142,11 @@ proc verifyHeader( ok() proc verifyBlock( - vp: VerifiedRpcProxy, blk: BlockObject, fullTransactions: bool -): Future[Result[void, string]] {.async.} = + engine: RpcVerificationEngine, blk: BlockObject, fullTransactions: bool +): Future[Result[void, string]] {.async: (raises: []).} = let header = convHeader(blk) - ?(await vp.verifyHeader(header, blk.hash)) + ?(await engine.verifyHeader(header, blk.hash)) # verify transactions if fullTransactions: @@ -163,12 +163,12 @@ proc verifyBlock( ok() proc getBlock*( - vp: VerifiedRpcProxy, blockHash: Hash32, fullTransactions: bool -): Future[Result[BlockObject, string]] {.async.} = + engine: RpcVerificationEngine, blockHash: Hash32, fullTransactions: bool +): Future[Result[BlockObject, string]] {.async: (raises: []).} = # get the target block let blk = try: - await vp.rpcClient.eth_getBlockByHash(blockHash, fullTransactions) + await engine.backend.eth_getBlockByHash(blockHash, fullTransactions) except CatchableError as e: return err(e.msg) @@ -177,20 +177,20 @@ proc getBlock*( return err("the downloaded block hash doesn't match with the requested hash") # verify the block - ?(await vp.verifyBlock(blk, fullTransactions)) + ?(await engine.verifyBlock(blk, fullTransactions)) ok(blk) proc getBlock*( - vp: VerifiedRpcProxy, blockTag: BlockTag, fullTransactions: bool -): Future[Result[BlockObject, string]] {.async.} = - let numberTag = vp.resolveBlockTag(blockTag).valueOr: + engine: RpcVerificationEngine, blockTag: BlockTag, fullTransactions: bool +): Future[Result[BlockObject, string]] {.async: (raises: []).} = + let numberTag = engine.resolveBlockTag(blockTag).valueOr: return err(error) # get the target block let blk = try: - await vp.rpcClient.eth_getBlockByNumber(numberTag, fullTransactions) + await engine.backend.eth_getBlockByNumber(numberTag, fullTransactions) except CatchableError as e: return err(e.msg) @@ -199,14 +199,14 @@ proc getBlock*( err("the downloaded block number doesn't match with the requested block number") # verify the block - ?(await vp.verifyBlock(blk, fullTransactions)) + ?(await engine.verifyBlock(blk, fullTransactions)) ok(blk) proc getHeader*( - vp: VerifiedRpcProxy, blockHash: Hash32 -): Future[Result[Header, string]] {.async.} = - let cachedHeader = vp.headerStore.get(blockHash) + engine: RpcVerificationEngine, blockHash: Hash32 +): Future[Result[Header, string]] {.async: (raises: []).} = + let cachedHeader = engine.headerStore.get(blockHash) if cachedHeader.isNone(): debug "did not find the header in the cache", blockHash = blockHash @@ -216,7 +216,7 @@ proc getHeader*( # get the target block let blk = try: - await vp.rpcClient.eth_getBlockByHash(blockHash, false) + await engine.backend.eth_getBlockByHash(blockHash, false) except CatchableError as e: return err(e.msg) @@ -225,18 +225,18 @@ proc getHeader*( if blockHash != blk.hash: return err("the blk.hash(downloaded) doesn't match with the provided hash") - ?(await vp.verifyHeader(header, blockHash)) + ?(await engine.verifyHeader(header, blockHash)) ok(header) proc getHeader*( - vp: VerifiedRpcProxy, blockTag: BlockTag -): Future[Result[Header, string]] {.async.} = + engine: RpcVerificationEngine, blockTag: BlockTag +): Future[Result[Header, string]] {.async: (raises: []).} = let - numberTag = vp.resolveBlockTag(blockTag).valueOr: + numberTag = engine.resolveBlockTag(blockTag).valueOr: return err(error) n = distinctBase(numberTag.number) - cachedHeader = vp.headerStore.get(n) + cachedHeader = engine.headerStore.get(n) if cachedHeader.isNone(): debug "did not find the header in the cache", blockTag = blockTag @@ -246,7 +246,7 @@ proc getHeader*( # get the target block let blk = try: - await vp.rpcClient.eth_getBlockByNumber(numberTag, false) + await engine.backend.eth_getBlockByNumber(numberTag, false) except CatchableError as e: return err(e.msg) @@ -256,6 +256,6 @@ proc getHeader*( return err("the downloaded block number doesn't match with the requested block number") - ?(await vp.verifyHeader(header, blk.hash)) + ?(await engine.verifyHeader(header, blk.hash)) ok(header) diff --git a/nimbus_verified_proxy/engine/engine.nim b/nimbus_verified_proxy/engine/engine.nim new file mode 100644 index 0000000000..5888282262 --- /dev/null +++ b/nimbus_verified_proxy/engine/engine.nim @@ -0,0 +1,31 @@ +# nimbus_verified_proxy +# Copyright (c) 2022-2025 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: [], gcsafe.} + +import ./types, ./utils, ./rpc_frontend, ./header_store, ./evm + +proc init*( + T: type RpcVerificationEngine, config: RpcVerificationEngineConf +): T {.raises: [ValueError].} = + let engine = RpcVerificationEngine( + chainId: config.chainId, + maxBlockWalk: config.maxBlockWalk, + headerStore: HeaderStore.new(config.headerStoreLen), + accountsCache: AccountsCache.init(config.accountCacheLen), + codeCache: CodeCache.init(config.codeCacheLen), + storageCache: StorageCache.init(config.storageCacheLen), + ) + + engine.registerDefaultFrontend() + + let networkId = chainIdToNetworkId(config.chainId).valueOr: + raise newException(ValueError, error) + # since AsyncEvm requires a few transport methods (getStorage, getCode etc.) for initialization, we initialize the proxy first then the evm within it + engine.evm = AsyncEvm.init(engine.toAsyncEvmStateBackend(), networkId) + + engine diff --git a/nimbus_verified_proxy/rpc/evm.nim b/nimbus_verified_proxy/engine/evm.nim similarity index 70% rename from nimbus_verified_proxy/rpc/evm.nim rename to nimbus_verified_proxy/engine/evm.nim index cb11cb8797..a3dd79ccc1 100644 --- a/nimbus_verified_proxy/rpc/evm.nim +++ b/nimbus_verified_proxy/engine/evm.nim @@ -12,25 +12,24 @@ import ../../execution_chain/evm/async_evm_backend, ../../execution_chain/evm/async_evm, ./accounts, - ../header_store, - ../types + ./header_store, + ./types logScope: topics = "verified_proxy_evm" export async_evm, async_evm_backend -proc toAsyncEvmStateBackend*(vp: VerifiedRpcProxy): AsyncEvmStateBackend = +proc toAsyncEvmStateBackend*(engine: RpcVerificationEngine): AsyncEvmStateBackend = let accProc = proc( header: Header, address: Address ): Future[Opt[Account]] {.async: (raises: [CancelledError]).} = let account = try: - (await vp.getAccount(address, header.number, header.stateRoot)) - except CatchableError: - error "error getting account" - return Opt.none(Account) + (await engine.getAccount(address, header.number, header.stateRoot)) + except CatchableError as e: + raise newException(CancelledError, e.msg) if account.isOk(): return Opt.some(account.get()) @@ -42,10 +41,9 @@ proc toAsyncEvmStateBackend*(vp: VerifiedRpcProxy): AsyncEvmStateBackend = ): Future[Opt[UInt256]] {.async: (raises: [CancelledError]).} = let storageSlot = try: - (await vp.getStorageAt(address, slotKey, header.number, header.stateRoot)) - except CatchableError: - error "error getting storage" - return Opt.none(UInt256) + (await engine.getStorageAt(address, slotKey, header.number, header.stateRoot)) + except CatchableError as e: + raise newException(CancelledError, e.msg) if storageSlot.isOk(): return Opt.some(storageSlot.get()) @@ -57,10 +55,9 @@ proc toAsyncEvmStateBackend*(vp: VerifiedRpcProxy): AsyncEvmStateBackend = ): Future[Opt[seq[byte]]] {.async: (raises: [CancelledError]).} = let code = try: - (await vp.getCode(address, header.number, header.stateRoot)) - except CatchableError: - error "error getting code" - return Opt.none(seq[byte]) + (await engine.getCode(address, header.number, header.stateRoot)) + except CatchableError as e: + raise newException(CancelledError, e.msg) if code.isOk(): return Opt.some(code.get()) @@ -70,6 +67,6 @@ proc toAsyncEvmStateBackend*(vp: VerifiedRpcProxy): AsyncEvmStateBackend = blockHashProc = proc( header: Header, number: BlockNumber ): Future[Opt[Hash32]] {.async: (raises: [CancelledError]).} = - vp.headerStore.getHash(number) + engine.headerStore.getHash(number) AsyncEvmStateBackend.init(accProc, storageProc, codeProc, blockHashProc) diff --git a/nimbus_verified_proxy/rpc/fees.nim b/nimbus_verified_proxy/engine/fees.nim similarity index 82% rename from nimbus_verified_proxy/rpc/fees.nim rename to nimbus_verified_proxy/engine/fees.nim index afcd3dd855..8119d96ab3 100644 --- a/nimbus_verified_proxy/rpc/fees.nim +++ b/nimbus_verified_proxy/engine/fees.nim @@ -14,7 +14,7 @@ import web3/[eth_api_types, eth_api], std/algorithm, ../../execution_chain/beacon/web3_eth_conv, - ../types, + ./types, ./blocks, ./transactions @@ -34,10 +34,12 @@ func median(prices: var openArray[GasInt]): GasInt = # default case return GasInt(0) -proc suggestGasPrice*(vp: VerifiedRpcProxy): Future[Result[GasInt, string]] {.async.} = +proc suggestGasPrice*( + engine: RpcVerificationEngine +): Future[Result[GasInt, string]] {.async: (raises: []).} = const minGasPrice = 30_000_000_000.GasInt let - blk = (await vp.getBlock(blockId("latest"), true)).valueOr: + blk = (await engine.getBlock(blockId("latest"), true)).valueOr: return err(error) txs = blk.transactions.toTransactions().valueOr: return err(error) @@ -50,10 +52,10 @@ proc suggestGasPrice*(vp: VerifiedRpcProxy): Future[Result[GasInt, string]] {.as ok(max(minGasPrice, median(prices))) proc suggestMaxPriorityGasPrice*( - vp: VerifiedRpcProxy -): Future[Result[GasInt, string]] {.async.} = + engine: RpcVerificationEngine +): Future[Result[GasInt, string]] {.async: (raises: []).} = let - blk = (await vp.getBlock(blockId("latest"), true)).valueOr: + blk = (await engine.getBlock(blockId("latest"), true)).valueOr: return err(error) txs = blk.transactions.toTransactions().valueOr: return err(error) diff --git a/nimbus_verified_proxy/header_store.nim b/nimbus_verified_proxy/engine/header_store.nim similarity index 98% rename from nimbus_verified_proxy/header_store.nim rename to nimbus_verified_proxy/engine/header_store.nim index 178ab0ac0f..f41b14673e 100644 --- a/nimbus_verified_proxy/header_store.nim +++ b/nimbus_verified_proxy/engine/header_store.nim @@ -28,7 +28,7 @@ type HeaderStore* = ref object earliest: Opt[Header] earliestHash: Opt[Hash32] -func convLCHeader*(lcHeader: ForkedLightClientHeader): Result[Header, string] = +func convLCHeader(lcHeader: ForkedLightClientHeader): Result[Header, string] = withForkyHeader(lcHeader): when lcDataFork > LightClientDataFork.Altair: template p(): auto = diff --git a/nimbus_verified_proxy/rpc/receipts.nim b/nimbus_verified_proxy/engine/receipts.nim similarity index 74% rename from nimbus_verified_proxy/rpc/receipts.nim rename to nimbus_verified_proxy/engine/receipts.nim index a2f915df98..6d618aab40 100644 --- a/nimbus_verified_proxy/rpc/receipts.nim +++ b/nimbus_verified_proxy/engine/receipts.nim @@ -16,7 +16,7 @@ import web3/[eth_api_types, eth_api], ../../execution_chain/beacon/web3_eth_conv, ../../execution_chain/rpc/filters, - ../types, + ./types, ./blocks template toLog(lg: LogObject): Log = @@ -43,11 +43,11 @@ func toReceipts(recs: openArray[ReceiptObject]): seq[Receipt] = recs.mapIt(it.toReceipt) proc getReceipts( - vp: VerifiedRpcProxy, header: Header, blockTag: types.BlockTag -): Future[Result[seq[ReceiptObject], string]] {.async.} = + engine: RpcVerificationEngine, header: Header, blockTag: types.BlockTag +): Future[Result[seq[ReceiptObject], string]] {.async: (raises: []).} = let rxs = try: - await vp.rpcClient.eth_getBlockReceipts(blockTag) + await engine.backend.eth_getBlockReceipts(blockTag) except CatchableError as e: return err(e.msg) if rxs.isSome(): @@ -60,41 +60,41 @@ proc getReceipts( return ok(rxs.get()) proc getReceipts*( - vp: VerifiedRpcProxy, blockTag: types.BlockTag -): Future[Result[seq[ReceiptObject], string]] {.async.} = + engine: RpcVerificationEngine, blockTag: types.BlockTag +): Future[Result[seq[ReceiptObject], string]] {.async: (raises: []).} = let - header = (await vp.getHeader(blockTag)).valueOr: + header = (await engine.getHeader(blockTag)).valueOr: return err(error) # all other tags are automatically resolved while getting the header numberTag = types.BlockTag( kind: BlockIdentifierKind.bidNumber, number: Quantity(header.number) ) - await vp.getReceipts(header, numberTag) + await engine.getReceipts(header, numberTag) proc getReceipts*( - vp: VerifiedRpcProxy, blockHash: Hash32 -): Future[Result[seq[ReceiptObject], string]] {.async.} = + engine: RpcVerificationEngine, blockHash: Hash32 +): Future[Result[seq[ReceiptObject], string]] {.async: (raises: []).} = let - header = (await vp.getHeader(blockHash)).valueOr: + header = (await engine.getHeader(blockHash)).valueOr: return err(error) numberTag = types.BlockTag( kind: BlockIdentifierKind.bidNumber, number: Quantity(header.number) ) - await vp.getReceipts(header, numberTag) + await engine.getReceipts(header, numberTag) proc resolveFilterTags*( - vp: VerifiedRpcProxy, filter: FilterOptions + engine: RpcVerificationEngine, filter: FilterOptions ): Result[FilterOptions, string] = if filter.blockHash.isSome(): return ok(filter) let fromBlock = filter.fromBlock.get(types.BlockTag(kind: bidAlias, alias: "latest")) toBlock = filter.toBlock.get(types.BlockTag(kind: bidAlias, alias: "latest")) - fromBlockNumberTag = vp.resolveBlockTag(fromBlock).valueOr: + fromBlockNumberTag = engine.resolveBlockTag(fromBlock).valueOr: return err(error) - toBlockNumberTag = vp.resolveBlockTag(toBlock).valueOr: + toBlockNumberTag = engine.resolveBlockTag(toBlock).valueOr: return err(error) return ok( @@ -108,8 +108,8 @@ proc resolveFilterTags*( ) proc verifyLogs*( - vp: VerifiedRpcProxy, filter: FilterOptions, logObjs: seq[LogObject] -): Future[Result[void, string]] {.async.} = + engine: RpcVerificationEngine, filter: FilterOptions, logObjs: seq[LogObject] +): Future[Result[void, string]] {.async: (raises: []).} = # store block hashes contains the logs so that we can batch receipt requests var prevBlockHash: Hash32 @@ -121,7 +121,7 @@ proc verifyLogs*( # exploit sequentiality of logs if prevBlockHash != lg.blockHash.get(): # TODO: a cache will solve downloading the same block receipts for multiple logs - rxs = (await vp.getReceipts(lg.blockHash.get())).valueOr: + rxs = (await engine.getReceipts(lg.blockHash.get())).valueOr: return err(error) prevBlockHash = lg.blockHash.get() let @@ -141,17 +141,17 @@ proc verifyLogs*( ok() proc getLogs*( - vp: VerifiedRpcProxy, filter: FilterOptions -): Future[Result[seq[LogObject], string]] {.async.} = + engine: RpcVerificationEngine, filter: FilterOptions +): Future[Result[seq[LogObject], string]] {.async: (raises: []).} = let - resolvedFilter = vp.resolveFilterTags(filter).valueOr: + resolvedFilter = engine.resolveFilterTags(filter).valueOr: return err(error) logObjs = try: - await vp.rpcClient.eth_getLogs(resolvedFilter) + await engine.backend.eth_getLogs(resolvedFilter) except CatchableError as e: return err(e.msg) - ?(await vp.verifyLogs(resolvedFilter, logObjs)) + ?(await engine.verifyLogs(resolvedFilter, logObjs)) return ok(logObjs) diff --git a/nimbus_verified_proxy/engine/rpc_frontend.nim b/nimbus_verified_proxy/engine/rpc_frontend.nim new file mode 100644 index 0000000000..87839762eb --- /dev/null +++ b/nimbus_verified_proxy/engine/rpc_frontend.nim @@ -0,0 +1,413 @@ +# nimbus_verified_proxy +# Copyright (c) 2022-2025 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: [], gcsafe.} + +import + results, + stew/byteutils, + nimcrypto/sysrand, + json_rpc/[rpcserver, rpcclient], + eth/common/accounts, + web3/[eth_api, eth_api_types], + ../../execution_chain/core/eip4844, + ../../execution_chain/common/common, + ./types, + ./header_store, + ./accounts, + ./blocks, + ./evm, + ./transactions, + ./receipts, + ./fees + +proc registerDefaultFrontend*(engine: RpcVerificationEngine) = + engine.frontend.eth_chainId = proc(): Future[UInt256] {. + async: (raises: [ValueError]) + .} = + engine.chainId + + engine.frontend.eth_blockNumber = proc(): Future[uint64] {. + async: (raises: [ValueError]) + .} = + ## Returns the number of the most recent block. + let latest = engine.headerStore.latest.valueOr: + raise newException(ValueError, "Syncing") + + latest.number.uint64 + + engine.frontend.eth_getBalance = proc( + address: Address, quantityTag: BlockTag + ): Future[UInt256] {.async: (raises: [ValueError]).} = + let + header = (await engine.getHeader(quantityTag)).valueOr: + raise newException(ValueError, error) + account = (await engine.getAccount(address, header.number, header.stateRoot)).valueOr: + raise newException(ValueError, error) + + account.balance + + engine.frontend.eth_getStorageAt = proc( + address: Address, slot: UInt256, quantityTag: BlockTag + ): Future[FixedBytes[32]] {.async: (raises: [ValueError]).} = + let + header = (await engine.getHeader(quantityTag)).valueOr: + raise newException(ValueError, error) + storage = ( + await engine.getStorageAt(address, slot, header.number, header.stateRoot) + ).valueOr: + raise newException(ValueError, error) + + storage.to(Bytes32) + + engine.frontend.eth_getTransactionCount = proc( + address: Address, quantityTag: BlockTag + ): Future[Quantity] {.async: (raises: [ValueError]).} = + let + header = (await engine.getHeader(quantityTag)).valueOr: + raise newException(ValueError, error) + account = (await engine.getAccount(address, header.number, header.stateRoot)).valueOr: + raise newException(ValueError, error) + + Quantity(account.nonce) + + engine.frontend.eth_getCode = proc( + address: Address, quantityTag: BlockTag + ): Future[seq[byte]] {.async: (raises: [ValueError]).} = + let + header = (await engine.getHeader(quantityTag)).valueOr: + raise newException(ValueError, error) + code = (await engine.getCode(address, header.number, header.stateRoot)).valueOr: + raise newException(ValueError, error) + + code + + engine.frontend.eth_getBlockByHash = proc( + blockHash: Hash32, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [ValueError]).} = + (await engine.getBlock(blockHash, fullTransactions)).valueOr: + raise newException(ValueError, error) + + engine.frontend.eth_getBlockByNumber = proc( + blockTag: BlockTag, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [ValueError]).} = + (await engine.getBlock(blockTag, fullTransactions)).valueOr: + raise newException(ValueError, error) + + engine.frontend.eth_getUncleCountByBlockNumber = proc( + blockTag: BlockTag + ): Future[Quantity] {.async: (raises: [ValueError]).} = + let blk = (await engine.getBlock(blockTag, false)).valueOr: + raise newException(ValueError, error) + + Quantity(blk.uncles.len()) + + engine.frontend.eth_getUncleCountByBlockHash = proc( + blockHash: Hash32 + ): Future[Quantity] {.async: (raises: [ValueError]).} = + let blk = (await engine.getBlock(blockHash, false)).valueOr: + raise newException(ValueError, error) + + Quantity(blk.uncles.len()) + + engine.frontend.eth_getBlockTransactionCountByNumber = proc( + blockTag: BlockTag + ): Future[Quantity] {.async: (raises: [ValueError]).} = + let blk = (await engine.getBlock(blockTag, true)).valueOr: + raise newException(ValueError, error) + + Quantity(blk.transactions.len) + + engine.frontend.eth_getBlockTransactionCountByHash = proc( + blockHash: Hash32 + ): Future[Quantity] {.async: (raises: [ValueError]).} = + let blk = (await engine.getBlock(blockHash, true)).valueOr: + raise newException(ValueError, error) + + Quantity(blk.transactions.len) + + engine.frontend.eth_getTransactionByBlockNumberAndIndex = proc( + blockTag: BlockTag, index: Quantity + ): Future[TransactionObject] {.async: (raises: [ValueError]).} = + let blk = (await engine.getBlock(blockTag, true)).valueOr: + raise newException(ValueError, error) + + if distinctBase(index) >= uint64(blk.transactions.len): + raise newException(ValueError, "provided transaction index is outside bounds") + let x = blk.transactions[distinctBase(index)] + + doAssert x.kind == tohTx + + x.tx + + engine.frontend.eth_getTransactionByBlockHashAndIndex = proc( + blockHash: Hash32, index: Quantity + ): Future[TransactionObject] {.async: (raises: [ValueError]).} = + let blk = (await engine.getBlock(blockHash, true)).valueOr: + raise newException(ValueError, error) + + if distinctBase(index) >= uint64(blk.transactions.len): + raise newException(ValueError, "provided transaction index is outside bounds") + let x = blk.transactions[distinctBase(index)] + + doAssert x.kind == tohTx + + x.tx + + engine.frontend.eth_call = proc( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: bool = true + ): Future[seq[byte]] {.async: (raises: [CancelledError, ValueError]).} = + if tx.to.isNone(): + raise newException(ValueError, "to address is required") + + let header = (await engine.getHeader(blockTag)).valueOr: + raise newException(ValueError, error) + + # Start fetching code to get it in the code cache + discard engine.getCode(tx.to.get(), header.number, header.stateRoot) + + # As a performance optimisation we concurrently pre-fetch the state needed + # for the call by calling eth_createAccessList and then using the returned + # access list keys to fetch the required state using eth_getProof. + (await engine.populateCachesUsingAccessList(header.number, header.stateRoot, tx)).isOkOr: + raise newException(ValueError, error) + + let callResult = (await engine.evm.call(header, tx, optimisticStateFetch)).valueOr: + raise newException(ValueError, error) + + if callResult.error.len() > 0: + raise newException(ValueError, callResult.error) + + return callResult.output + + engine.frontend.eth_createAccessList = proc( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: bool = true + ): Future[AccessListResult] {.async: (raises: [CancelledError, ValueError]).} = + if tx.to.isNone(): + raise newException(ValueError, "to address is required") + + let header = (await engine.getHeader(blockTag)).valueOr: + raise newException(ValueError, error) + + # Start fetching code to get it in the code cache + discard engine.getCode(tx.to.get(), header.number, header.stateRoot) + + # As a performance optimisation we concurrently pre-fetch the state needed + # for the call by calling eth_createAccessList and then using the returned + # access list keys to fetch the required state using eth_getProof. + (await engine.populateCachesUsingAccessList(header.number, header.stateRoot, tx)).isOkOr: + raise newException(ValueError, error) + + let (accessList, error, gasUsed) = ( + await engine.evm.createAccessList(header, tx, optimisticStateFetch) + ).valueOr: + raise newException(ValueError, error) + + return + AccessListResult(accessList: accessList, error: error, gasUsed: gasUsed.Quantity) + + engine.frontend.eth_estimateGas = proc( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: bool = true + ): Future[Quantity] {.async: (raises: [CancelledError, ValueError]).} = + if tx.to.isNone(): + raise newException(ValueError, "to address is required") + + let header = (await engine.getHeader(blockTag)).valueOr: + raise newException(ValueError, error) + + # Start fetching code to get it in the code cache + discard engine.getCode(tx.to.get(), header.number, header.stateRoot) + + # As a performance optimisation we concurrently pre-fetch the state needed + # for the call by calling eth_createAccessList and then using the returned + # access list keys to fetch the required state using eth_getProof. + (await engine.populateCachesUsingAccessList(header.number, header.stateRoot, tx)).isOkOr: + raise newException(ValueError, error) + + let gasEstimate = (await engine.evm.estimateGas(header, tx, optimisticStateFetch)).valueOr: + raise newException(ValueError, error) + + return gasEstimate.Quantity + + engine.frontend.eth_getTransactionByHash = proc( + txHash: Hash32 + ): Future[TransactionObject] {.async: (raises: [ValueError]).} = + let tx = + try: + await engine.backend.eth_getTransactionByHash(txHash) + except CatchableError as e: + raise newException(ValueError, e.msg) + + if tx.hash != txHash: + raise newException( + ValueError, + "the downloaded transaction hash doesn't match the requested transaction hash", + ) + + if not checkTxHash(tx, txHash): + raise + newException(ValueError, "the transaction doesn't hash to the provided hash") + + return tx + + engine.frontend.eth_getBlockReceipts = proc( + blockTag: BlockTag + ): Future[Opt[seq[ReceiptObject]]] {.async: (raises: [ValueError]).} = + let rxs = (await engine.getReceipts(blockTag)).valueOr: + raise newException(ValueError, error) + return Opt.some(rxs) + + engine.frontend.eth_getTransactionReceipt = proc( + txHash: Hash32 + ): Future[ReceiptObject] {.async: (raises: [ValueError]).} = + let + rx = + try: + await engine.backend.eth_getTransactionReceipt(txHash) + except CatchableError as e: + raise newException(ValueError, e.msg) + rxs = (await engine.getReceipts(rx.blockHash)).valueOr: + raise newException(ValueError, error) + + for r in rxs: + if r.transactionHash == txHash: + return r + + raise newException(ValueError, "receipt couldn't be verified") + + engine.frontend.eth_getLogs = proc( + filterOptions: FilterOptions + ): Future[seq[LogObject]] {.async: (raises: [ValueError]).} = + (await engine.getLogs(filterOptions)).valueOr: + raise newException(ValueError, error) + + engine.frontend.eth_newFilter = proc( + filterOptions: FilterOptions + ): Future[string] {.async: (raises: [ValueError]).} = + if engine.filterStore.len >= MAX_FILTERS: + raise newException(ValueError, "FilterStore already full") + + var + id: array[8, byte] # 64bits + strId: string + + for i in 0 .. (MAX_ID_TRIES + 1): + if randomBytes(id) != len(id): + raise newException( + ValueError, "Couldn't generate a random identifier for the filter" + ) + + strId = toHex(id) + + if not engine.filterStore.contains(strId): + break + + if i >= MAX_ID_TRIES: + raise + newException(ValueError, "Couldn't create a unique identifier for the filter") + + engine.filterStore[strId] = + FilterStoreItem(filter: filterOptions, blockMarker: Opt.none(Quantity)) + + return strId + + engine.frontend.eth_uninstallFilter = proc( + filterId: string + ): Future[bool] {.async: (raises: [ValueError]).} = + if filterId in engine.filterStore: + engine.filterStore.del(filterId) + return true + + return false + + engine.frontend.eth_getFilterLogs = proc( + filterId: string + ): Future[seq[LogObject]] {.async: (raises: [ValueError]).} = + if filterId notin engine.filterStore: + raise newException(ValueError, "Filter doesn't exist") + + (await engine.getLogs(engine.filterStore[filterId].filter)).valueOr: + raise newException(ValueError, error) + + engine.frontend.eth_getFilterChanges = proc( + filterId: string + ): Future[seq[LogObject]] {.async: (raises: [ValueError]).} = + if filterId notin engine.filterStore: + raise newException(ValueError, "Filter doesn't exist") + + let + filterItem = engine.filterStore[filterId] + filter = engine.resolveFilterTags(filterItem.filter).valueOr: + raise newException(ValueError, error) + # after resolving toBlock is always some and a number tag + toBlock = filter.toBlock.get().number + + if filterItem.blockMarker.isSome() and toBlock <= filterItem.blockMarker.get(): + raise newException(ValueError, "No changes for the filter since the last query") + + let + fromBlock = + if filterItem.blockMarker.isSome(): + Opt.some( + types.BlockTag(kind: bidNumber, number: filterItem.blockMarker.get()) + ) + else: + filter.fromBlock + + changesFilter = FilterOptions( + fromBlock: fromBlock, + toBlock: filter.toBlock, + address: filter.address, + topics: filter.topics, + blockHash: filter.blockHash, + ) + logObjs = (await engine.getLogs(changesFilter)).valueOr: + raise newException(ValueError, error) + + # all logs verified so we can update blockMarker + engine.filterStore[filterId].blockMarker = Opt.some(toBlock) + + return logObjs + + engine.frontend.eth_blobBaseFee = proc(): Future[UInt256] {. + async: (raises: [ValueError]) + .} = + let com = CommonRef.new( + DefaultDbMemory.newCoreDbRef(), + taskpool = nil, + config = chainConfigForNetwork(engine.chainId), + initializeDb = false, + statelessProviderEnabled = true, # Enables collection of witness keys + ) + + let header = (await engine.getHeader(blockId("latest"))).valueOr: + raise newException(ValueError, error) + + if header.blobGasUsed.isNone(): + raise newException(ValueError, "blobGasUsed missing from latest header") + if header.excessBlobGas.isNone(): + raise newException(ValueError, "excessBlobGas missing from latest header") + let blobBaseFee = + getBlobBaseFee(header.excessBlobGas.get, com, com.toEVMFork(header)) * + header.blobGasUsed.get.u256 + return blobBaseFee + + engine.frontend.eth_gasPrice = proc(): Future[Quantity] {. + async: (raises: [ValueError]) + .} = + let suggestedPrice = (await engine.suggestGasPrice()).valueOr: + raise newException(ValueError, error) + + Quantity(suggestedPrice.uint64) + + engine.frontend.eth_maxPriorityFeePerGas = proc(): Future[Quantity] {. + async: (raises: [ValueError]) + .} = + let suggestedPrice = (await engine.suggestMaxPriorityGasPrice()).valueOr: + raise newException(ValueError, error) + + Quantity(suggestedPrice.uint64) diff --git a/nimbus_verified_proxy/rpc/transactions.nim b/nimbus_verified_proxy/engine/transactions.nim similarity index 100% rename from nimbus_verified_proxy/rpc/transactions.nim rename to nimbus_verified_proxy/engine/transactions.nim diff --git a/nimbus_verified_proxy/engine/types.nim b/nimbus_verified_proxy/engine/types.nim new file mode 100644 index 0000000000..9ace401ee6 --- /dev/null +++ b/nimbus_verified_proxy/engine/types.nim @@ -0,0 +1,181 @@ +# nimbus_verified_proxy +# Copyright (c) 2025 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: [], gcsafe.} + +import + std/tables, + json_rpc/rpcclient, + web3/[eth_api, eth_api_types], + stint, + minilru, + ../../execution_chain/evm/async_evm, + ./header_store + +export minilru + +const + MAX_ID_TRIES* = 10 + MAX_FILTERS* = 256 + +type + AccountsCacheKey* = (Root, Address) + AccountsCache* = LruCache[AccountsCacheKey, Account] + + CodeCacheKey* = (Root, Address) + CodeCache* = LruCache[CodeCacheKey, seq[byte]] + + StorageCacheKey* = (Root, Address, UInt256) + StorageCache* = LruCache[StorageCacheKey, UInt256] + + BlockTag* = eth_api_types.RtBlockIdentifier + + # Backend API + EthApiBackend* = object + eth_chainId*: proc(): Future[UInt256] {.async: (raises: [CancelledError]).} + eth_getBlockByHash*: proc( + blkHash: Hash32, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [CancelledError]).} + eth_getBlockByNumber*: proc( + blkNum: BlockTag, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [CancelledError]).} + eth_getProof*: proc( + address: Address, slots: seq[UInt256], blockId: BlockTag + ): Future[ProofResponse] {.async: (raises: [CancelledError]).} + eth_createAccessList*: proc( + args: TransactionArgs, blockId: BlockTag + ): Future[AccessListResult] {.async: (raises: [CancelledError]).} + eth_getCode*: proc(address: Address, blockId: BlockTag): Future[seq[byte]] {. + async: (raises: [CancelledError]) + .} + eth_getBlockReceipts*: proc(blockId: BlockTag): Future[Opt[seq[ReceiptObject]]] {. + async: (raises: [CancelledError]) + .} + eth_getTransactionReceipt*: + proc(txHash: Hash32): Future[ReceiptObject] {.async: (raises: [CancelledError]).} + eth_getTransactionByHash*: proc(txHash: Hash32): Future[TransactionObject] {. + async: (raises: [CancelledError]) + .} + eth_getLogs*: proc(filterOptions: FilterOptions): Future[seq[LogObject]] {. + async: (raises: [CancelledError]) + .} + + # Frontend API + EthApiFrontend* = object # Chain + eth_chainId*: proc(): Future[UInt256] {.async: (raises: [ValueError]).} + eth_blockNumber*: proc(): Future[uint64] {.async: (raises: [ValueError]).} + + # State + eth_getBalance*: proc(address: Address, blockId: BlockTag): Future[UInt256] {. + async: (raises: [ValueError]) + .} + eth_getStorageAt*: proc( + address: Address, slot: UInt256, blockId: BlockTag + ): Future[FixedBytes[32]] {.async: (raises: [ValueError]).} + eth_getTransactionCount*: proc( + address: Address, blockId: BlockTag + ): Future[Quantity] {.async: (raises: [ValueError]).} + eth_getCode*: proc(address: Address, blockId: BlockTag): Future[seq[byte]] {. + async: (raises: [ValueError]) + .} + eth_getProof*: proc( + address: Address, slots: seq[UInt256], blockId: BlockTag + ): Future[ProofResponse] {.async: (raises: [ValueError]).} + + # Block + eth_getBlockByHash*: proc( + blkHash: Hash32, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [ValueError]).} + eth_getBlockByNumber*: proc( + blkNum: BlockTag, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [ValueError]).} + eth_getUncleCountByBlockHash*: + proc(blkHash: Hash32): Future[Quantity] {.async: (raises: [ValueError]).} + eth_getUncleCountByBlockNumber*: + proc(blkNum: BlockTag): Future[Quantity] {.async: (raises: [ValueError]).} + eth_getBlockTransactionCountByHash*: + proc(blkHash: Hash32): Future[Quantity] {.async: (raises: [ValueError]).} + eth_getBlockTransactionCountByNumber*: + proc(blkNum: BlockTag): Future[Quantity] {.async: (raises: [ValueError]).} + + # Transaction + eth_getTransactionByBlockHashAndIndex*: proc( + blkHash: Hash32, index: Quantity + ): Future[TransactionObject] {.async: (raises: [ValueError]).} + eth_getTransactionByBlockNumberAndIndex*: proc( + blkNum: BlockTag, index: Quantity + ): Future[TransactionObject] {.async: (raises: [ValueError]).} + eth_getTransactionByHash*: + proc(txHash: Hash32): Future[TransactionObject] {.async: (raises: [ValueError]).} + + # EVM + eth_call*: proc( + args: TransactionArgs, blockId: BlockTag, optimisticFetch: bool = true + ): Future[seq[byte]] {.async: (raises: [CancelledError, ValueError]).} + eth_createAccessList*: proc( + args: TransactionArgs, blockId: BlockTag, optimisticFetch: bool = true + ): Future[AccessListResult] {.async: (raises: [CancelledError, ValueError]).} + eth_estimateGas*: proc( + args: TransactionArgs, blockId: BlockTag, optimisticFetch: bool = true + ): Future[Quantity] {.async: (raises: [CancelledError, ValueError]).} + + # Receipts + eth_getBlockReceipts*: proc(blockId: BlockTag): Future[Opt[seq[ReceiptObject]]] {. + async: (raises: [ValueError]) + .} + eth_getTransactionReceipt*: + proc(txHash: Hash32): Future[ReceiptObject] {.async: (raises: [ValueError]).} + eth_getLogs*: proc(filterOptions: FilterOptions): Future[seq[LogObject]] {. + async: (raises: [ValueError]) + .} + eth_newFilter*: proc(filterOptions: FilterOptions): Future[string] {. + async: (raises: [ValueError]) + .} + eth_uninstallFilter*: + proc(filterId: string): Future[bool] {.async: (raises: [ValueError]).} + eth_getFilterLogs*: + proc(filterId: string): Future[seq[LogObject]] {.async: (raises: [ValueError]).} + eth_getFilterChanges*: + proc(filterId: string): Future[seq[LogObject]] {.async: (raises: [ValueError]).} + + # Fee-based + eth_blobBaseFee*: proc(): Future[UInt256] {.async: (raises: [ValueError]).} + eth_gasPrice*: proc(): Future[Quantity] {.async: (raises: [ValueError]).} + eth_maxPriorityFeePerGas*: + proc(): Future[Quantity] {.async: (raises: [ValueError]).} + + FilterStoreItem* = object + filter*: FilterOptions + blockMarker*: Opt[Quantity] + + RpcVerificationEngine* = ref object + evm*: AsyncEvm + + # chain stores + headerStore*: HeaderStore + filterStore*: Table[string, FilterStoreItem] + + # state caches + accountsCache*: AccountsCache + codeCache*: CodeCache + storageCache*: StorageCache + + # interfaces + backend*: EthApiBackend + frontend*: EthApiFrontend + + # config items + chainId*: UInt256 + maxBlockWalk*: uint64 + + RpcVerificationEngineConf* = ref object + chainId*: UInt256 + maxBlockWalk*: uint64 + headerStoreLen*: int + accountCacheLen*: int + codeCacheLen*: int + storageCacheLen*: int diff --git a/nimbus_verified_proxy/engine/utils.nim b/nimbus_verified_proxy/engine/utils.nim new file mode 100644 index 0000000000..87eb50ac1d --- /dev/null +++ b/nimbus_verified_proxy/engine/utils.nim @@ -0,0 +1,22 @@ +# nimbus_verified_proxy +# Copyright (c) 2025 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: [], gcsafe.} + +import results, stint + +func chainIdToNetworkId*(chainId: UInt256): Result[UInt256, string] = + if chainId == 1.u256: # mainnet + ok(1.u256) + elif chainId == 11155111.u256: # sepolia + ok(11155111.u256) + elif chainId == 17000.u256: # holesky + ok(17000.u256) + elif chainId == 560048.u256: # hoodi + ok(560048.u256) + else: + err("Unknown chainId") diff --git a/nimbus_verified_proxy/json_rpc_backend.nim b/nimbus_verified_proxy/json_rpc_backend.nim new file mode 100644 index 0000000000..e19e80d9ba --- /dev/null +++ b/nimbus_verified_proxy/json_rpc_backend.nim @@ -0,0 +1,148 @@ +# nimbus_verified_proxy +# Copyright (c) 2025 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: [], gcsafe.} + +import + stint, + json_rpc/[rpcclient, rpcproxy], + web3/[eth_api, eth_api_types], + ./engine/types, + ./nimbus_verified_proxy_conf + +type JsonRpcClient* = ref object + url: string + case kind*: ClientKind + of Http: + httpClient: RpcHttpClient + of WebSocket: + wsClient: RpcWebSocketClient + +proc init*(T: type JsonRpcClient, url: Web3Url): JsonRpcClient = + case url.kind + of HttpUrl: + JsonRpcClient(kind: Http, httpClient: newRpcHttpClient(), url: url.web3Url) + of WsUrl: + JsonRpcClient(kind: WebSocket, wsClient: newRpcWebSocketClient(), url: url.web3Url) + +proc start*( + client: JsonRpcClient +): Future[Result[void, string]] {.async: (raises: []).} = + try: + case client.kind + of Http: + await client.httpClient.connect(client.url) + of WebSocket: + await client.wsClient.connect(uri = client.url, compression = false, flags = {}) + except CatchableError as e: + return err(e.msg) + + ok() + +template getClient(client: JsonRpcClient): RpcClient = + case client.kind + of Http: client.httpClient + of WebSocket: client.wsClient + +proc getEthApiBackend*(client: JsonRpcClient): EthApiBackend = + let + ethChainIdProc = proc(): Future[UInt256] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_chainId() + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getBlockByHashProc = proc( + blkHash: Hash32, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getBlockByHash(blkHash, fullTransactions) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getBlockByNumberProc = proc( + blkNum: BlockTag, fullTransactions: bool + ): Future[BlockObject] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getBlockByNumber(blkNum, fullTransactions) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getProofProc = proc( + address: Address, slots: seq[UInt256], blockId: BlockTag + ): Future[ProofResponse] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getProof(address, slots, blockId) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + createAccessListProc = proc( + args: TransactionArgs, blockId: BlockTag + ): Future[AccessListResult] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_createAccessList(args, blockId) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getCodeProc = proc( + address: Address, blockId: BlockTag + ): Future[seq[byte]] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getCode(address, blockId) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getTransactionByHashProc = proc( + txHash: Hash32 + ): Future[TransactionObject] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getTransactionByHash(txHash) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getTransactionReceiptProc = proc( + txHash: Hash32 + ): Future[ReceiptObject] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getTransactionReceipt(txHash) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getBlockReceiptsProc = proc( + blockId: BlockTag + ): Future[Opt[seq[ReceiptObject]]] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getBlockReceipts(blockId) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getLogsProc = proc( + filterOptions: FilterOptions + ): Future[seq[LogObject]] {.async: (raises: [CancelledError]).} = + try: + await client.getClient().eth_getLogs(filterOptions) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + EthApiBackend( + eth_chainId: ethChainIdProc, + eth_getBlockByHash: getBlockByHashProc, + eth_getBlockByNumber: getBlockByNumberProc, + eth_getProof: getProofProc, + eth_createAccessList: createAccessListProc, + eth_getCode: getCodeProc, + eth_getBlockReceipts: getBlockReceiptsProc, + eth_getLogs: getLogsProc, + eth_getTransactionByHash: getTransactionByHashProc, + eth_getTransactionReceipt: getTransactionReceiptProc, + ) + +proc stop*(client: JsonRpcClient) {.async: (raises: [CancelledError]).} = + try: + await client.getClient().close() + except CatchableError: + raise newException(CancelledError, "coudln't close the json rpc client") diff --git a/nimbus_verified_proxy/json_rpc_frontend.nim b/nimbus_verified_proxy/json_rpc_frontend.nim new file mode 100644 index 0000000000..0daad6cba0 --- /dev/null +++ b/nimbus_verified_proxy/json_rpc_frontend.nim @@ -0,0 +1,197 @@ +# nimbus_verified_proxy +# Copyright (c) 2025 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: [], gcsafe.} + +import + stint, + std/strutils, + json_rpc/[rpcserver, rpcproxy], + web3/[eth_api, eth_api_types], + ../execution_chain/rpc/cors, + ./engine/types, + ./nimbus_verified_proxy_conf + +type JsonRpcServer* = ref object + case kind*: ClientKind #we reuse clientKind for servers also + of Http: + httpServer: RpcHttpServer + of WebSocket: + wsServer: RpcWebSocketServer + +proc init*( + T: type JsonRpcServer, url: Web3Url +): JsonRpcServer {.raises: [JsonRpcError, ValueError, TransportAddressError].} = + let + auth = @[httpCors(@[])] # TODO: for now we serve all cross origin requests + parsedUrl = parseUri(url.web3Url) + hostname = if parsedUrl.hostname == "": "127.0.0.1" else: parsedUrl.hostname + port = + if parsedUrl.port == "": + 8545 + else: + parseInt(parsedUrl.port) + listenAddress = initTAddress(hostname, port) + + case url.kind + of HttpUrl: + JsonRpcServer( + kind: Http, httpServer: newRpcHttpServer([listenAddress], RpcRouter.init(), auth) + ) + of WsUrl: + let server = + JsonRpcServer(kind: WebSocket, wsServer: newRpcWebSocketServer(listenAddress)) + + server.wsServer.router = RpcRouter.init() + server + +func getServer(server: JsonRpcServer): RpcServer = + case server.kind + of Http: server.httpServer + of WebSocket: server.wsServer + +proc start*(server: JsonRpcServer): Result[void, string] = + try: + case server.kind + of Http: + server.httpServer.start() + of WebSocket: + server.wsServer.start() + except CatchableError as e: + return err(e.msg) + + ok() + +proc injectEngineFrontend*(server: JsonRpcServer, frontend: EthApiFrontend) = + server.getServer().rpc("eth_blockNumber") do() -> uint64: + await frontend.eth_blockNumber() + + server.getServer().rpc("eth_getBalance") do( + address: Address, quantityTag: BlockTag + ) -> UInt256: + await frontend.eth_getBalance(address, quantityTag) + + server.getServer().rpc("eth_getStorageAt") do( + address: Address, slot: UInt256, quantityTag: BlockTag + ) -> FixedBytes[32]: + await frontend.eth_getStorageAt(address, slot, quantityTag) + + server.getServer().rpc("eth_getTransactionCount") do( + address: Address, quantityTag: BlockTag + ) -> Quantity: + await frontend.eth_getTransactionCount(address, quantityTag) + + server.getServer().rpc("eth_getCode") do( + address: Address, quantityTag: BlockTag + ) -> seq[byte]: + await frontend.eth_getCode(address, quantityTag) + + server.getServer().rpc("eth_getBlockByHash") do( + blockHash: Hash32, fullTransactions: bool + ) -> BlockObject: + await frontend.eth_getBlockByHash(blockHash, fullTransactions) + + server.getServer().rpc("eth_getBlockByNumber") do( + blockTag: BlockTag, fullTransactions: bool + ) -> BlockObject: + await frontend.eth_getBlockByNumber(blockTag, fullTransactions) + + server.getServer().rpc("eth_getUncleCountByBlockNumber") do( + blockTag: BlockTag + ) -> Quantity: + await frontend.eth_getUncleCountByBlockNumber(blockTag) + + server.getServer().rpc("eth_getUncleCountByBlockHash") do( + blockHash: Hash32 + ) -> Quantity: + await frontend.eth_getUncleCountByBlockHash(blockHash) + + server.getServer().rpc("eth_getBlockTransactionCountByNumber") do( + blockTag: BlockTag + ) -> Quantity: + await frontend.eth_getBlockTransactionCountByNumber(blockTag) + + server.getServer().rpc("eth_getBlockTransactionCountByHash") do( + blockHash: Hash32 + ) -> Quantity: + await frontend.eth_getBlockTransactionCountByHash(blockHash) + + server.getServer().rpc("eth_getTransactionByBlockNumberAndIndex") do( + blockTag: BlockTag, index: Quantity + ) -> TransactionObject: + await frontend.eth_getTransactionByBlockNumberAndIndex(blockTag, index) + + server.getServer().rpc("eth_getTransactionByBlockHashAndIndex") do( + blockHash: Hash32, index: Quantity + ) -> TransactionObject: + await frontend.eth_getTransactionByBlockHashAndIndex(blockHash, index) + + server.getServer().rpc("eth_call") do( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] + ) -> seq[byte]: + await frontend.eth_call(tx, blockTag, optimisticStateFetch.get(true)) + + server.getServer().rpc("eth_createAccessList") do( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] + ) -> AccessListResult: + await frontend.eth_createAccessList(tx, blockTag, optimisticStateFetch.get(true)) + + server.getServer().rpc("eth_estimateGas") do( + tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] + ) -> Quantity: + await frontend.eth_estimateGas(tx, blockTag, optimisticStateFetch.get(true)) + + server.getServer().rpc("eth_getTransactionByHash") do( + txHash: Hash32 + ) -> TransactionObject: + await frontend.eth_getTransactionByHash(txHash) + + server.getServer().rpc("eth_getBlockReceipts") do( + blockTag: BlockTag + ) -> Opt[seq[ReceiptObject]]: + await frontend.eth_getBlockReceipts(blockTag) + + server.getServer().rpc("eth_getTransactionReceipt") do( + txHash: Hash32 + ) -> ReceiptObject: + await frontend.eth_getTransactionReceipt(txHash) + + server.getServer().rpc("eth_getLogs") do( + filterOptions: FilterOptions + ) -> seq[LogObject]: + await frontend.eth_getLogs(filterOptions) + + server.getServer().rpc("eth_newFilter") do(filterOptions: FilterOptions) -> string: + await frontend.eth_newFilter(filterOptions) + + server.getServer().rpc("eth_uninstallFilter") do(filterId: string) -> bool: + await frontend.eth_uninstallFilter(filterId) + + server.getServer().rpc("eth_getFilterLogs") do(filterId: string) -> seq[LogObject]: + await frontend.eth_getFilterLogs(filterId) + + server.getServer().rpc("eth_getFilterChanges") do(filterId: string) -> seq[LogObject]: + await frontend.eth_getFilterChanges(filterId) + + server.getServer().rpc("eth_blobBaseFee") do() -> UInt256: + await frontend.eth_blobBaseFee() + + server.getServer().rpc("eth_gasPrice") do() -> Quantity: + await frontend.eth_gasPrice() + + server.getServer().rpc("eth_maxPriorityFeePerGas") do() -> Quantity: + await frontend.eth_maxPriorityFeePerGas() + +proc stop*(server: JsonRpcServer) {.async: (raises: [CancelledError]).} = + try: + case server.kind + of Http: + await server.httpServer.closeWait() + of WebSocket: + await server.wsServer.closeWait() + except CatchableError as e: + raise newException(CancelledError, e.msg) diff --git a/nimbus_verified_proxy/libverifproxy/verifproxy.nim b/nimbus_verified_proxy/libverifproxy/verifproxy.nim index feb5ca28c0..5a70b0cfad 100644 --- a/nimbus_verified_proxy/libverifproxy/verifproxy.nim +++ b/nimbus_verified_proxy/libverifproxy/verifproxy.nim @@ -38,12 +38,11 @@ proc runContext(ctx: ptr Context) {.thread.} = let rpcAddr = jsonNode["RpcAddress"].getStr() let myConfig = VerifiedProxyConf( - rpcAddress: parseIpAddress(rpcAddr), listenAddress: some(defaultListenAddress), eth2Network: some(jsonNode["Eth2Network"].getStr()), trustedBlockRoot: Eth2Digest.fromHex(jsonNode["TrustedBlockRoot"].getStr()), - web3Url: parseCmdArg(Web3Url, jsonNode["Web3Url"].getStr()), - rpcPort: Port(jsonNode["RpcPort"].getInt()), + backendUrl: parseCmdArg(Web3Url, jsonNode["Web3Url"].getStr()), + frontendUrl: parseCmdArg(Web3Url, jsonNode["Web3Url"].getStr()), logLevel: jsonNode["LogLevel"].getStr(), maxPeers: 160, nat: NatConfig(hasExtIp: false, nat: NatAny), diff --git a/nimbus_verified_proxy/nimbus_verified_proxy.nim b/nimbus_verified_proxy/nimbus_verified_proxy.nim index e0edfc20cb..28079922dd 100644 --- a/nimbus_verified_proxy/nimbus_verified_proxy.nim +++ b/nimbus_verified_proxy/nimbus_verified_proxy.nim @@ -19,14 +19,14 @@ import beacon_chain/networking/topic_params, beacon_chain/spec/beaconstate, beacon_chain/[beacon_clock, buildinfo, light_client, nimbus_binary_common], - ../execution_chain/rpc/cors, ../execution_chain/common/common, - ./types, - ./rpc/evm, - ./rpc/rpc_eth_api, ./nimbus_verified_proxy_conf, - ./header_store, - ./rpc_api_backend, + ./engine/engine, + ./engine/header_store, + ./engine/utils, + ./engine/types, + ./json_rpc_backend, + ./json_rpc_frontend, ../execution_chain/version_info type OnHeaderCallback* = proc(s: cstring, t: int) {.cdecl, raises: [], gcsafe.} @@ -40,6 +40,23 @@ proc cleanup*(ctx: ptr Context) = dealloc(ctx.configJson) freeShared(ctx) +proc verifyChaindId( + engine: RpcVerificationEngine +): Future[void] {.async: (raises: []).} = + let providerId = + try: + await engine.backend.eth_chainId() + except CatchableError: + 0.u256 + + # 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 engine.chainId != providerId: + fatal "The specified data provider serves data for a different chain", + expectedChain = engine.chainId, providerChain = providerId + quit 1 + func getConfiguredChainId(networkMetadata: Eth2NetworkMetadata): UInt256 = if networkMetadata.eth1Network.isSome(): let @@ -54,18 +71,6 @@ func getConfiguredChainId(networkMetadata: Eth2NetworkMetadata): UInt256 = else: return networkMetadata.cfg.DEPOSIT_CHAIN_ID.u256 -func chainIdToNetworkId(chainId: UInt256): Result[UInt256, string] = - if chainId == 1.u256: - ok(1.u256) - elif chainId == 11155111.u256: - ok(11155111.u256) - elif chainId == 17000.u256: - ok(17000.u256) - elif chainId == 560048.u256: - ok(560048.u256) - else: - return err("Unknown chainId") - proc run*( config: VerifiedProxyConf, ctx: ptr Context ) {.raises: [CatchableError], gcsafe.} = @@ -82,30 +87,32 @@ proc run*( let metadata = loadEth2Network(config.eth2Network) let - chainId = getConfiguredChainId(metadata) - authHooks = @[httpCors(@[])] # TODO: for now we serve all cross origin requests - # TODO: write a comment - clientConfig = config.web3url.asClientConfig() - - rpcProxy = RpcProxy.new( - [initTAddress(config.rpcAddress, config.rpcPort)], clientConfig, authHooks + engineConf = RpcVerificationEngineConf( + chainId: getConfiguredChainId(metadata), + maxBlockWalk: config.maxBlockWalk, + headerStoreLen: config.headerStoreLen, + accountCacheLen: config.accountCacheLen, + codeCacheLen: config.codeCacheLen, + storageCacheLen: config.storageCacheLen, ) + engine = RpcVerificationEngine.init(engineConf) + jsonRpcClient = JsonRpcClient.init(config.backendUrl) + jsonRpcServer = JsonRpcServer.init(config.frontendUrl) - # header cache contains headers downloaded from p2p - headerStore = HeaderStore.new(config.cacheLen) - - # TODO: add config object to verified proxy for future config options - verifiedProxy = - VerifiedRpcProxy.init(rpcProxy, headerStore, chainId, config.maxBlockWalk) + # the backend only needs the url to connect to + engine.backend = jsonRpcClient.getEthApiBackend() - networkId = chainIdToNetworkId(chainId).valueOr: - raise newException(ValueError, error) + # inject frontend + jsonRpcServer.injectEngineFrontend(engine.frontend) - verifiedProxy.evm = AsyncEvm.init(verifiedProxy.toAsyncEvmStateBackend(), networkId) - verifiedProxy.rpcClient = verifiedProxy.initNetworkApiBackend() + # start frontend and backend + var status = waitFor jsonRpcClient.start() + if status.isErr(): + raise newException(ValueError, status.error) - # add handlers that verify RPC calls /rpc/rpc_eth_api.nim - verifiedProxy.installEthApiHandlers() + status = jsonRpcServer.start() + if status.isErr(): + raise newException(ValueError, status.error) # just for short hand convenience template cfg(): auto = @@ -171,10 +178,8 @@ proc run*( # start the p2p network and rpcProxy waitFor network.startListening() waitFor network.start() - waitFor rpcProxy.start() - # verify chain id that the proxy is connected to - waitFor verifiedProxy.verifyChaindId() + waitFor engine.verifyChaindId() proc onFinalizedHeader( lightClient: LightClient, finalizedHeader: ForkedLightClientHeader @@ -182,7 +187,7 @@ proc run*( withForkyHeader(finalizedHeader): when lcDataFork > LightClientDataFork.Altair: info "New LC finalized header", finalized_header = shortLog(forkyHeader) - let res = headerStore.updateFinalized(finalizedHeader) + let res = engine.headerStore.updateFinalized(finalizedHeader) if res.isErr(): error "finalized header update error", error = res.error() @@ -201,7 +206,7 @@ proc run*( withForkyHeader(optimisticHeader): when lcDataFork > LightClientDataFork.Altair: info "New LC optimistic header", optimistic_header = shortLog(forkyHeader) - let res = headerStore.add(optimisticHeader) + let res = engine.headerStore.add(optimisticHeader) if res.isErr(): error "header store add error", error = res.error() @@ -300,7 +305,8 @@ proc run*( if ctx != nil and ctx.stop: # Cleanup waitFor network.stop() - waitFor rpcProxy.stop() + waitFor jsonRpcClient.stop() + waitFor jsonRpcServer.stop() ctx.cleanup() # Notify client that cleanup is finished ctx.onHeader(nil, 2) diff --git a/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim b/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim index 598bc41eaf..3579a2fe2a 100644 --- a/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim +++ b/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim @@ -57,17 +57,41 @@ type VerifiedProxyConf* = object defaultValueDesc: "mainnet" name: "network" .}: Option[string] - # In-Memory Cache Size - # In order to support the BLOCKHASH opcode for eth_call we need at least - # MAX_PREV_HEADER_DEPTH headers in the header cache. - cacheLen* {. + accountCacheLen* {. hidden, - desc: "Length of the header cache maintained in memory", - defaultValue: MAX_PREV_HEADER_DEPTH, - defaultValueDesc: "256", - name: "debug-cache-len" + desc: "Length of the accounts cache maintained in memory", + defaultValue: 128, + name: "debug-account-cache-len" .}: int + codeCacheLen* {. + hidden, + desc: "Length of the code cache maintained in memory", + defaultValue: 64, + name: "debug-code-cache-len" + .}: int + + storageCacheLen* {. + hidden, + desc: "Length of the storage cache maintained in memory", + defaultValue: 256, + name: "debug-storage-cache-len" + .}: int + + headerStoreLen* {. + hidden, + desc: "Length of the header store maintained in memory", + defaultValue: 256, + name: "debug-header-store-len" + .}: int + + maxBlockWalk* {. + hidden, + desc: "Maximum number of blocks that will be traversed to serve a request", + defaultValue: 1000, + name: "debug-max-walk" + .}: uint64 + # Consensus light sync # No default - Needs to be provided by the user trustedBlockRoot* {. @@ -76,19 +100,19 @@ type VerifiedProxyConf* = object # (Untrusted) web3 provider # No default - Needs to be provided by the user - web3url* {.desc: "URL of the web3 data provider", name: "web3-url".}: Web3Url - - # Local JSON-RPC server - rpcAddress* {. - desc: "Listening address of the JSON-RPC server", - defaultValue: defaultAdminListenAddress, - defaultValueDesc: $defaultAdminListenAddressDesc, - name: "rpc-address" - .}: IpAddress - - rpcPort* {. - desc: "Listening port of the JSON-RPC server", defaultValue: 8545, name: "rpc-port" - .}: Port + backendUrl* {. + desc: "URL of the web3 data provider", + name: "backend-url" + .}: Web3Url + + # Listening endpoint of the proxy + # (verified) web3 end + frontendUrl* {. + desc: "URL for the listening end of the proxy - [http/ws]://[address]:[port]", + defaultValue: Web3Url(kind: HttpUrl, web3Url: "http://127.0.0.1:8545"), + defaultValueDesc: "http://127.0.0.1:8545", + name: "frontend-url" + .}: Web3Url # Libp2p bootstrapNodes* {. @@ -124,14 +148,6 @@ type VerifiedProxyConf* = object name: "max-peers" .}: int - maxBlockWalk* {. - hidden, - desc: "Maximum number of blocks that will be queried to serve a request", - defaultValue: 1000, - defaultValueDesc: "1000", - name: "debug-max-walk" - .}: uint64 - hardMaxPeers* {. desc: "The maximum number of peers to connect to. Defaults to maxPeers * 1.5" name: "hard-max-peers" .}: Option[int] @@ -203,7 +219,7 @@ func asLightClientConf*(pc: VerifiedProxyConf): LightClientConf = discv5Enabled: pc.discv5Enabled, directPeers: pc.directPeers, trustedBlockRoot: pc.trustedBlockRoot, - web3Urls: @[EngineApiUrlConfigValue(url: pc.web3url.web3Url)], + web3Urls: @[EngineApiUrlConfigValue(url: pc.backendUrl.web3Url)], jwtSecret: none(InputFile), stopAtEpoch: 0, ) diff --git a/nimbus_verified_proxy/rpc/rpc_eth_api.nim b/nimbus_verified_proxy/rpc/rpc_eth_api.nim deleted file mode 100644 index 03449b94f0..0000000000 --- a/nimbus_verified_proxy/rpc/rpc_eth_api.nim +++ /dev/null @@ -1,450 +0,0 @@ -# nimbus_verified_proxy -# Copyright (c) 2022-2025 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: [], gcsafe.} - -import - results, - chronicles, - stew/byteutils, - nimcrypto/sysrand, - json_rpc/[rpcserver, rpcclient, rpcproxy], - eth/common/accounts, - web3/[eth_api, eth_api_types], - ../../execution_chain/core/eip4844, - ../../execution_chain/common/common, - ../types, - ../header_store, - ./accounts, - ./blocks, - ./evm, - ./transactions, - ./receipts, - ./fees - -logScope: - topics = "verified_proxy" - -proc installEthApiHandlers*(vp: VerifiedRpcProxy) = - vp.proxy.rpc("eth_chainId") do() -> UInt256: - vp.chainId - - vp.proxy.rpc("eth_blockNumber") do() -> uint64: - ## Returns the number of the most recent block. - let latest = vp.headerStore.latest.valueOr: - raise newException(ValueError, "Syncing") - - latest.number.uint64 - - vp.proxy.rpc("eth_getBalance") do(address: Address, quantityTag: BlockTag) -> UInt256: - let - header = (await vp.getHeader(quantityTag)).valueOr: - raise newException(ValueError, error) - account = (await vp.getAccount(address, header.number, header.stateRoot)).valueOr: - raise newException(ValueError, error) - - account.balance - - vp.proxy.rpc("eth_getStorageAt") do( - address: Address, slot: UInt256, quantityTag: BlockTag - ) -> FixedBytes[32]: - let - header = (await vp.getHeader(quantityTag)).valueOr: - raise newException(ValueError, error) - storage = (await vp.getStorageAt(address, slot, header.number, header.stateRoot)).valueOr: - raise newException(ValueError, error) - - storage.to(Bytes32) - - vp.proxy.rpc("eth_getTransactionCount") do( - address: Address, quantityTag: BlockTag - ) -> Quantity: - let - header = (await vp.getHeader(quantityTag)).valueOr: - raise newException(ValueError, error) - account = (await vp.getAccount(address, header.number, header.stateRoot)).valueOr: - raise newException(ValueError, error) - - Quantity(account.nonce) - - vp.proxy.rpc("eth_getCode") do(address: Address, quantityTag: BlockTag) -> seq[byte]: - let - header = (await vp.getHeader(quantityTag)).valueOr: - raise newException(ValueError, error) - code = (await vp.getCode(address, header.number, header.stateRoot)).valueOr: - raise newException(ValueError, error) - - code - - vp.proxy.rpc("eth_getBlockByHash") do( - blockHash: Hash32, fullTransactions: bool - ) -> BlockObject: - (await vp.getBlock(blockHash, fullTransactions)).valueOr: - raise newException(ValueError, error) - - vp.proxy.rpc("eth_getBlockByNumber") do( - blockTag: BlockTag, fullTransactions: bool - ) -> BlockObject: - (await vp.getBlock(blockTag, fullTransactions)).valueOr: - raise newException(ValueError, error) - - vp.proxy.rpc("eth_getUncleCountByBlockNumber") do(blockTag: BlockTag) -> Quantity: - let blk = (await vp.getBlock(blockTag, false)).valueOr: - raise newException(ValueError, error) - - Quantity(blk.uncles.len()) - - vp.proxy.rpc("eth_getUncleCountByBlockHash") do(blockHash: Hash32) -> Quantity: - let blk = (await vp.getBlock(blockHash, false)).valueOr: - raise newException(ValueError, error) - - Quantity(blk.uncles.len()) - - vp.proxy.rpc("eth_getBlockTransactionCountByNumber") do( - blockTag: BlockTag - ) -> Quantity: - let blk = (await vp.getBlock(blockTag, true)).valueOr: - raise newException(ValueError, error) - - Quantity(blk.transactions.len) - - vp.proxy.rpc("eth_getBlockTransactionCountByHash") do(blockHash: Hash32) -> Quantity: - let blk = (await vp.getBlock(blockHash, true)).valueOr: - raise newException(ValueError, error) - - Quantity(blk.transactions.len) - - vp.proxy.rpc("eth_getTransactionByBlockNumberAndIndex") do( - blockTag: BlockTag, index: Quantity - ) -> TransactionObject: - let blk = (await vp.getBlock(blockTag, true)).valueOr: - raise newException(ValueError, error) - - if distinctBase(index) >= uint64(blk.transactions.len): - raise newException(ValueError, "provided transaction index is outside bounds") - let x = blk.transactions[distinctBase(index)] - - doAssert x.kind == tohTx - - x.tx - - vp.proxy.rpc("eth_getTransactionByBlockHashAndIndex") do( - blockHash: Hash32, index: Quantity - ) -> TransactionObject: - let blk = (await vp.getBlock(blockHash, true)).valueOr: - raise newException(ValueError, error) - - if distinctBase(index) >= uint64(blk.transactions.len): - raise newException(ValueError, "provided transaction index is outside bounds") - let x = blk.transactions[distinctBase(index)] - - doAssert x.kind == tohTx - - x.tx - - vp.proxy.rpc("eth_call") do( - tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] - ) -> seq[byte]: - if tx.to.isNone(): - raise newException(ValueError, "to address is required") - - let - header = (await vp.getHeader(blockTag)).valueOr: - raise newException(ValueError, error) - optimisticStateFetch = optimisticStateFetch.valueOr: - true - - # Start fetching code to get it in the code cache - discard vp.getCode(tx.to.get(), header.number, header.stateRoot) - - # As a performance optimisation we concurrently pre-fetch the state needed - # for the call by calling eth_createAccessList and then using the returned - # access list keys to fetch the required state using eth_getProof. - (await vp.populateCachesUsingAccessList(header.number, header.stateRoot, tx)).isOkOr: - raise newException(ValueError, error) - - let callResult = (await vp.evm.call(header, tx, optimisticStateFetch)).valueOr: - raise newException(ValueError, error) - - if callResult.error.len() > 0: - raise (ref ApplicationError)( - code: 3, - msg: callResult.error, - data: Opt.some(JsonString("\"" & callResult.output.to0xHex() & "\"")), - ) - - return callResult.output - - vp.proxy.rpc("eth_createAccessList") do( - tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] - ) -> AccessListResult: - if tx.to.isNone(): - raise newException(ValueError, "to address is required") - - let - header = (await vp.getHeader(blockTag)).valueOr: - raise newException(ValueError, error) - optimisticStateFetch = optimisticStateFetch.valueOr: - true - - # Start fetching code to get it in the code cache - discard vp.getCode(tx.to.get(), header.number, header.stateRoot) - - # As a performance optimisation we concurrently pre-fetch the state needed - # for the call by calling eth_createAccessList and then using the returned - # access list keys to fetch the required state using eth_getProof. - (await vp.populateCachesUsingAccessList(header.number, header.stateRoot, tx)).isOkOr: - raise newException(ValueError, error) - - let (accessList, error, gasUsed) = ( - await vp.evm.createAccessList(header, tx, optimisticStateFetch) - ).valueOr: - raise newException(ValueError, error) - - return - AccessListResult(accessList: accessList, error: error, gasUsed: gasUsed.Quantity) - - vp.proxy.rpc("eth_estimateGas") do( - tx: TransactionArgs, blockTag: BlockTag, optimisticStateFetch: Opt[bool] - ) -> Quantity: - if tx.to.isNone(): - raise newException(ValueError, "to address is required") - - let - header = (await vp.getHeader(blockTag)).valueOr: - raise newException(ValueError, error) - - optimisticStateFetch = optimisticStateFetch.valueOr: - true - - # Start fetching code to get it in the code cache - discard vp.getCode(tx.to.get(), header.number, header.stateRoot) - - # As a performance optimisation we concurrently pre-fetch the state needed - # for the call by calling eth_createAccessList and then using the returned - # access list keys to fetch the required state using eth_getProof. - (await vp.populateCachesUsingAccessList(header.number, header.stateRoot, tx)).isOkOr: - raise newException(ValueError, error) - - let gasEstimate = (await vp.evm.estimateGas(header, tx, optimisticStateFetch)).valueOr: - raise newException(ValueError, error) - - return gasEstimate.Quantity - - vp.proxy.rpc("eth_getTransactionByHash") do(txHash: Hash32) -> TransactionObject: - let tx = - try: - await vp.rpcClient.eth_getTransactionByHash(txHash) - except CatchableError as e: - raise newException(ValueError, e.msg) - - if tx.hash != txHash: - raise newException( - ValueError, - "the downloaded transaction hash doesn't match the requested transaction hash", - ) - - if not checkTxHash(tx, txHash): - raise - newException(ValueError, "the transaction doesn't hash to the provided hash") - - return tx - - vp.proxy.rpc("eth_getBlockReceipts") do(blockTag: BlockTag) -> Opt[seq[ReceiptObject]]: - let rxs = (await vp.getReceipts(blockTag)).valueOr: - raise newException(ValueError, error) - return Opt.some(rxs) - - vp.proxy.rpc("eth_getTransactionReceipt") do(txHash: Hash32) -> ReceiptObject: - let - rx = - try: - await vp.rpcClient.eth_getTransactionReceipt(txHash) - except CatchableError as e: - raise newException(ValueError, e.msg) - rxs = (await vp.getReceipts(rx.blockHash)).valueOr: - raise newException(ValueError, error) - - for r in rxs: - if r.transactionHash == txHash: - return r - - raise newException(ValueError, "receipt couldn't be verified") - - vp.proxy.rpc("eth_getLogs") do(filterOptions: FilterOptions) -> seq[LogObject]: - (await vp.getLogs(filterOptions)).valueOr: - raise newException(ValueError, error) - - vp.proxy.rpc("eth_newFilter") do(filterOptions: FilterOptions) -> string: - if vp.filterStore.len >= MAX_FILTERS: - raise newException(ValueError, "FilterStore already full") - - var - id: array[8, byte] # 64bits - strId: string - - for i in 0 .. (MAX_ID_TRIES + 1): - if randomBytes(id) != len(id): - raise newException( - ValueError, "Couldn't generate a random identifier for the filter" - ) - - strId = toHex(id) - - if not vp.filterStore.contains(strId): - break - - if i >= MAX_ID_TRIES: - raise - newException(ValueError, "Couldn't create a unique identifier for the filter") - - vp.filterStore[strId] = - FilterStoreItem(filter: filterOptions, blockMarker: Opt.none(Quantity)) - - return strId - - vp.proxy.rpc("eth_uninstallFilter") do(filterId: string) -> bool: - if filterId in vp.filterStore: - vp.filterStore.del(filterId) - return true - - return false - - vp.proxy.rpc("eth_getFilterLogs") do(filterId: string) -> seq[LogObject]: - if filterId notin vp.filterStore: - raise newException(ValueError, "Filter doesn't exist") - - (await vp.getLogs(vp.filterStore[filterId].filter)).valueOr: - raise newException(ValueError, error) - - vp.proxy.rpc("eth_getFilterChanges") do(filterId: string) -> seq[LogObject]: - if filterId notin vp.filterStore: - raise newException(ValueError, "Filter doesn't exist") - - let - filterItem = vp.filterStore[filterId] - filter = vp.resolveFilterTags(filterItem.filter).valueOr: - raise newException(ValueError, error) - # after resolving toBlock is always some and a number tag - toBlock = filter.toBlock.get().number - - if filterItem.blockMarker.isSome() and toBlock <= filterItem.blockMarker.get(): - raise newException(ValueError, "No changes for the filter since the last query") - - let - fromBlock = - if filterItem.blockMarker.isSome(): - Opt.some( - types.BlockTag(kind: bidNumber, number: filterItem.blockMarker.get()) - ) - else: - filter.fromBlock - - changesFilter = FilterOptions( - fromBlock: fromBlock, - toBlock: filter.toBlock, - address: filter.address, - topics: filter.topics, - blockHash: filter.blockHash, - ) - logObjs = (await vp.getLogs(changesFilter)).valueOr: - raise newException(ValueError, error) - - # all logs verified so we can update blockMarker - vp.filterStore[filterId].blockMarker = Opt.some(toBlock) - - return logObjs - - vp.proxy.rpc("eth_blobBaseFee") do() -> UInt256: - let com = CommonRef.new( - DefaultDbMemory.newCoreDbRef(), - taskpool = nil, - config = chainConfigForNetwork(vp.chainId), - initializeDb = false, - statelessProviderEnabled = true, # Enables collection of witness keys - ) - - let header = (await vp.getHeader(blockId("latest"))).valueOr: - raise newException(ValueError, error) - - if header.blobGasUsed.isNone(): - raise newException(ValueError, "blobGasUsed missing from latest header") - if header.excessBlobGas.isNone(): - raise newException(ValueError, "excessBlobGas missing from latest header") - let blobBaseFee = - getBlobBaseFee(header.excessBlobGas.get, com, com.toEVMFork(header)) * - header.blobGasUsed.get.u256 - return blobBaseFee - - vp.proxy.rpc("eth_gasPrice") do() -> Quantity: - let suggestedPrice = (await vp.suggestGasPrice()).valueOr: - raise newException(ValueError, error) - - Quantity(suggestedPrice.uint64) - - vp.proxy.rpc("eth_maxPriorityFeePerGas") do() -> Quantity: - let suggestedPrice = (await vp.suggestMaxPriorityGasPrice()).valueOr: - raise newException(ValueError, error) - - Quantity(suggestedPrice.uint64) - - # Following methods are forwarded directly to the web3 provider and therefore - # are not validated in any way. - vp.proxy.registerProxyMethod("net_version") - vp.proxy.registerProxyMethod("eth_sendRawTransaction") - -# 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(ValueError, 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 = localId, providerChain = providerId - quit 1 diff --git a/nimbus_verified_proxy/rpc_api_backend.nim b/nimbus_verified_proxy/rpc_api_backend.nim deleted file mode 100644 index 637cbf74c6..0000000000 --- a/nimbus_verified_proxy/rpc_api_backend.nim +++ /dev/null @@ -1,78 +0,0 @@ -# nimbus_verified_proxy -# Copyright (c) 2025 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: [], gcsafe.} - -import - json_rpc/[rpcproxy, rpcclient], - web3/[eth_api, eth_api_types], - stint, - std/json, - ./types - -proc initNetworkApiBackend*(vp: VerifiedRpcProxy): EthApiBackend = - let - ethChainIdProc = proc(): Future[UInt256] {.async.} = - await vp.proxy.getClient.eth_chainId() - - getBlockByHashProc = proc( - blkHash: Hash32, fullTransactions: bool - ): Future[BlockObject] {.async: (raw: true).} = - vp.proxy.getClient.eth_getBlockByHash(blkHash, fullTransactions) - - getBlockByNumberProc = proc( - blkNum: BlockTag, fullTransactions: bool - ): Future[BlockObject] {.async: (raw: true).} = - vp.proxy.getClient.eth_getBlockByNumber(blkNum, fullTransactions) - - getProofProc = proc( - address: Address, slots: seq[UInt256], blockId: BlockTag - ): Future[ProofResponse] {.async: (raw: true).} = - vp.proxy.getClient.eth_getProof(address, slots, blockId) - - createAccessListProc = proc( - args: TransactionArgs, blockId: BlockTag - ): Future[AccessListResult] {.async: (raw: true).} = - vp.proxy.getClient.eth_createAccessList(args, blockId) - - getCodeProc = proc( - address: Address, blockId: BlockTag - ): Future[seq[byte]] {.async: (raw: true).} = - vp.proxy.getClient.eth_getCode(address, blockId) - - getTransactionByHashProc = proc( - txHash: Hash32 - ): Future[TransactionObject] {.async: (raw: true).} = - vp.proxy.getClient.eth_getTransactionByHash(txHash) - - getTransactionReceiptProc = proc( - txHash: Hash32 - ): Future[ReceiptObject] {.async: (raw: true).} = - vp.proxy.getClient.eth_getTransactionReceipt(txHash) - - getBlockReceiptsProc = proc( - blockId: BlockTag - ): Future[Opt[seq[ReceiptObject]]] {.async: (raw: true).} = - vp.proxy.getClient.eth_getBlockReceipts(blockId) - - getLogsProc = proc( - filterOptions: FilterOptions - ): Future[seq[LogObject]] {.async: (raw: true).} = - vp.proxy.getClient.eth_getLogs(filterOptions) - - EthApiBackend( - eth_chainId: ethChainIdProc, - eth_getBlockByHash: getBlockByHashProc, - eth_getBlockByNumber: getBlockByNumberProc, - eth_getProof: getProofProc, - eth_createAccessList: createAccessListProc, - eth_getCode: getCodeProc, - eth_getBlockReceipts: getBlockReceiptsProc, - eth_getLogs: getLogsProc, - eth_getTransactionByHash: getTransactionByHashProc, - eth_getTransactionReceipt: getTransactionReceiptProc, - ) diff --git a/nimbus_verified_proxy/tests/test_api_backend.nim b/nimbus_verified_proxy/tests/test_api_backend.nim index 6999a208b1..84211371ea 100644 --- a/nimbus_verified_proxy/tests/test_api_backend.nim +++ b/nimbus_verified_proxy/tests/test_api_backend.nim @@ -11,7 +11,7 @@ import std/tables, web3/[eth_api, eth_api_types], stint, - ../types, + ../engine/types, json_rpc/[rpcserver, rpcclient] type @@ -186,66 +186,97 @@ func convToPartialBlock(blk: BlockObject): BlockObject = proc initTestApiBackend*(t: TestApiState): EthApiBackend = let - ethChainIdProc = proc(): Future[UInt256] {.async.} = + ethChainIdProc = proc(): Future[UInt256] {.async: (raises: [CancelledError]).} = return t.chainId getBlockByHashProc = proc( blkHash: Hash32, fullTransactions: bool - ): Future[BlockObject] {.async.} = - if fullTransactions: - return t.blocks[blkHash] - else: - return convToPartialBlock(t.blocks[blkHash]) + ): Future[BlockObject] {.async: (raises: [CancelledError]).} = + try: + if fullTransactions: + return t.blocks[blkHash] + else: + return convToPartialBlock(t.blocks[blkHash]) + except CatchableError as e: + raise newException(CancelledError, e.msg) getBlockByNumberProc = proc( blkNum: BlockTag, fullTransactions: bool - ): Future[BlockObject] {.async.} = - # we directly use number here because the verified proxy should never use aliases - let blkHash = t.nums[blkNum.number] - - if fullTransactions: - return t.blocks[blkHash] - else: - return convToPartialBlock(t.blocks[blkHash]) + ): Future[BlockObject] {.async: (raises: [CancelledError]).} = + try: + # we directly use number here because the verified proxy should never use aliases + let blkHash = t.nums[blkNum.number] + + if fullTransactions: + return t.blocks[blkHash] + else: + return convToPartialBlock(t.blocks[blkHash]) + except CatchableError as e: + raise newException(CancelledError, e.msg) getProofProc = proc( address: Address, slots: seq[UInt256], blkNum: BlockTag - ): Future[ProofResponse] {.async.} = - # we directly use number here because the verified proxy should never use aliases - let blkHash = t.nums[blkNum.number] - t.proofs[(address, slots, blkHash)] + ): Future[ProofResponse] {.async: (raises: [CancelledError]).} = + try: + # we directly use number here because the verified proxy should never use aliases + let blkHash = t.nums[blkNum.number] + t.proofs[(address, slots, blkHash)] + except CatchableError as e: + raise newException(CancelledError, e.msg) createAccessListProc = proc( args: TransactionArgs, blkNum: BlockTag - ): Future[AccessListResult] {.async.} = - # we directly use number here because the verified proxy should never use aliases - let blkHash = t.nums[blkNum.number] - t.accessLists[(args, blkHash)] + ): Future[AccessListResult] {.async: (raises: [CancelledError]).} = + try: + # we directly use number here because the verified proxy should never use aliases + let blkHash = t.nums[blkNum.number] + t.accessLists[(args, blkHash)] + except CatchableError as e: + raise newException(CancelledError, e.msg) getCodeProc = proc( address: Address, blkNum: BlockTag - ): Future[seq[byte]] {.async.} = - # we directly use number here because the verified proxy should never use aliases - let blkHash = t.nums[blkNum.number] - t.codes[(address, blkHash)] + ): Future[seq[byte]] {.async: (raises: [CancelledError]).} = + try: + # we directly use number here because the verified proxy should never use aliases + let blkHash = t.nums[blkNum.number] + t.codes[(address, blkHash)] + except CatchableError as e: + raise newException(CancelledError, e.msg) getBlockReceiptsProc = proc( blockId: BlockTag - ): Future[Opt[seq[ReceiptObject]]] {.async.} = - # we directly use number here because the verified proxy should never use aliases - let blkHash = t.nums[blockId.number] - Opt.some(t.blockReceipts[blkHash]) - - getLogsProc = proc(filterOptions: FilterOptions): Future[seq[LogObject]] {.async.} = - t.logs[filterOptions] + ): Future[Opt[seq[ReceiptObject]]] {.async: (raises: [CancelledError]).} = + try: + # we directly use number here because the verified proxy should never use aliases + let blkHash = t.nums[blockId.number] + Opt.some(t.blockReceipts[blkHash]) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + getLogsProc = proc( + filterOptions: FilterOptions + ): Future[seq[LogObject]] {.async: (raises: [CancelledError]).} = + try: + t.logs[filterOptions] + except CatchableError as e: + raise newException(CancelledError, e.msg) getTransactionByHashProc = proc( txHash: Hash32 - ): Future[TransactionObject] {.async.} = - t.transactions[txHash] + ): Future[TransactionObject] {.async: (raises: [CancelledError]).} = + try: + t.transactions[txHash] + except CatchableError as e: + raise newException(CancelledError, e.msg) - getTransactionReceiptProc = proc(txHash: Hash32): Future[ReceiptObject] {.async.} = - t.receipts[txHash] + getTransactionReceiptProc = proc( + txHash: Hash32 + ): Future[ReceiptObject] {.async: (raises: [CancelledError]).} = + try: + t.receipts[txHash] + except CatchableError as e: + raise newException(CancelledError, e.msg) EthApiBackend( eth_chainId: ethChainIdProc, diff --git a/nimbus_verified_proxy/tests/test_blocks.nim b/nimbus_verified_proxy/tests/test_blocks.nim index 19768bc564..2c149a800e 100644 --- a/nimbus_verified_proxy/tests/test_blocks.nim +++ b/nimbus_verified_proxy/tests/test_blocks.nim @@ -10,18 +10,18 @@ import unittest2, - json_rpc/[rpcclient, rpcserver, rpcproxy], + chronos, web3/[eth_api_types, eth_api], - ../header_store, - ../rpc/blocks, - ../types, + ../engine/header_store, + ../engine/blocks, + ../engine/types, ./test_utils, ./test_api_backend suite "test verified blocks": let ts = TestApiState.init(1.u256) - vp = startTestSetup(ts, 1, 9) # header store holds 1 and maxBlockWalk is 9 + engine = initTestEngine(ts, 1, 9) # header store holds 1 and maxBlockWalk is 9 test "check fetching blocks on every fork": let forkBlockNames = [ @@ -35,15 +35,14 @@ suite "test verified blocks": getBlockFromJson("nimbus_verified_proxy/tests/data/" & blockName & ".json") ts.loadBlock(blk) - check vp.headerStore.add(convHeader(blk), blk.hash).isOk() + check engine.headerStore.add(convHeader(blk), blk.hash).isOk() - # reuse verified proxy's internal client. Conveniently it is looped back to the proxy server - let verifiedBlk = waitFor vp.proxy.getClient().eth_getBlockByHash(blk.hash, true) + let verifiedBlk = waitFor engine.frontend.eth_getBlockByHash(blk.hash, true) check blk == verifiedBlk ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() test "check fetching blocks by number and tags": let @@ -55,28 +54,28 @@ suite "test verified blocks": hash = blk.hash ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() ts.loadBlock(blk) check: - vp.headerStore.add(convHeader(blk), blk.hash).isOk() - vp.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() + engine.headerStore.add(convHeader(blk), blk.hash).isOk() + engine.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() - var verifiedBlk = waitFor vp.proxy.getClient().eth_getBlockByNumber(numberTag, true) + var verifiedBlk = waitFor engine.frontend.eth_getBlockByNumber(numberTag, true) check blk == verifiedBlk - verifiedBlk = waitFor vp.proxy.getClient().eth_getBlockByNumber(finalTag, true) + verifiedBlk = waitFor engine.frontend.eth_getBlockByNumber(finalTag, true) check blk == verifiedBlk - verifiedBlk = waitFor vp.proxy.getClient().eth_getBlockByNumber(earliestTag, true) + verifiedBlk = waitFor engine.frontend.eth_getBlockByNumber(earliestTag, true) check blk == verifiedBlk - verifiedBlk = waitFor vp.proxy.getClient().eth_getBlockByNumber(latestTag, true) + verifiedBlk = waitFor engine.frontend.eth_getBlockByNumber(latestTag, true) check blk == verifiedBlk test "check block walk": ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() let targetBlockNum = 22431080 @@ -89,7 +88,7 @@ suite "test verified blocks": ts.loadBlock(blk) if i == sourceBlockNum: - check vp.headerStore.add(convHeader(blk), blk.hash).isOk() + check engine.headerStore.add(convHeader(blk), blk.hash).isOk() let unreachableTargetTag = @@ -101,22 +100,22 @@ suite "test verified blocks": # TODO: catch the exact error try: let verifiedBlk = - waitFor vp.proxy.getClient().eth_getBlockByNumber(unreachableTargetTag, true) + waitFor engine.frontend.eth_getBlockByNumber(unreachableTargetTag, true) check(false) - except CatchableError as e: + except CatchableError: check(true) # TODO: catch the exact error try: let verifiedBlk = - waitFor vp.proxy.getClient().eth_getBlockByNumber(reachableTargetTag, true) + waitFor engine.frontend.eth_getBlockByNumber(reachableTargetTag, true) check(true) - except CatchableError as e: + except CatchableError: check(false) test "check block related API methods": ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() let blk = getBlockFromJson("nimbus_verified_proxy/tests/data/Paris.json") @@ -124,20 +123,18 @@ suite "test verified blocks": hash = blk.hash ts.loadBlock(blk) - check vp.headerStore.add(convHeader(blk), blk.hash).isOk() + check engine.headerStore.add(convHeader(blk), blk.hash).isOk() let - uncleCountByHash = waitFor vp.proxy.getClient().eth_getUncleCountByBlockHash(hash) + uncleCountByHash = waitFor engine.frontend.eth_getUncleCountByBlockHash(hash) uncleCountByNum = - waitFor vp.proxy.getClient().eth_getUncleCountByBlockNumber(numberTag) - txCountByHash = - waitFor vp.proxy.getClient().eth_getBlockTransactionCountByHash(hash) + waitFor engine.frontend.eth_getUncleCountByBlockNumber(numberTag) + txCountByHash = waitFor engine.frontend.eth_getBlockTransactionCountByHash(hash) txCountByNum = - waitFor vp.proxy.getClient().eth_getBlockTransactionCountByNumber(numberTag) - txByHash = waitFor vp.proxy.getClient().eth_getTransactionByBlockHashAndIndex( - hash, Quantity(0) - ) - txByNum = waitFor vp.proxy.getClient().eth_getTransactionByBlockNumberAndIndex( + waitFor engine.frontend.eth_getBlockTransactionCountByNumber(numberTag) + txByHash = + waitFor engine.frontend.eth_getTransactionByBlockHashAndIndex(hash, Quantity(0)) + txByNum = waitFor engine.frontend.eth_getTransactionByBlockNumberAndIndex( numberTag, Quantity(0) ) @@ -150,5 +147,3 @@ suite "test verified blocks": check txByHash == blk.transactions[0].tx check txByHash == txByNum - - vp.stopTestSetup() diff --git a/nimbus_verified_proxy/tests/test_fees.nim b/nimbus_verified_proxy/tests/test_fees.nim index 996427774d..323be0c09b 100644 --- a/nimbus_verified_proxy/tests/test_fees.nim +++ b/nimbus_verified_proxy/tests/test_fees.nim @@ -10,12 +10,12 @@ import unittest2, + chronos, web3/[eth_api, eth_api_types], - json_rpc/[rpcclient, rpcserver, rpcproxy], eth/common/eth_types_rlp, - ../rpc/blocks, - ../types, - ../header_store, + ../engine/blocks, + ../engine/types, + ../engine/header_store, ./test_utils, ./test_api_backend @@ -23,15 +23,15 @@ suite "test fees verification": test "check api methods": let ts = TestApiState.init(1.u256) - vp = startTestSetup(ts, 1, 1, 8777) + engine = initTestEngine(ts, 1, 1) blk = getBlockFromJson("nimbus_verified_proxy/tests/data/Paris.json") ts.loadBlock(blk) - check vp.headerStore.add(convHeader(blk), blk.hash).isOk() + check engine.headerStore.add(convHeader(blk), blk.hash).isOk() let - gasPrice = waitFor vp.proxy.getClient().eth_gasPrice() - priorityFee = waitFor vp.proxy.getClient().eth_maxPriorityFeePerGas() + gasPrice = waitFor engine.frontend.eth_gasPrice() + priorityFee = waitFor engine.frontend.eth_maxPriorityFeePerGas() # we are only checking the API interface atm check: @@ -39,7 +39,7 @@ suite "test fees verification": priorityFee > Quantity(0) try: - let blobFee = waitFor vp.proxy.getClient().eth_blobBaseFee() + let blobFee = waitFor engine.frontend.eth_blobBaseFee() # blobs weren't enables on paris check false except CatchableError: @@ -47,17 +47,17 @@ suite "test fees verification": check true ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() let blk2 = getBlockFromJson("nimbus_verified_proxy/tests/data/Prague.json") ts.loadBlock(blk2) - check vp.headerStore.add(convHeader(blk2), blk2.hash).isOk() + check engine.headerStore.add(convHeader(blk2), blk2.hash).isOk() - let blobFeePrague = waitFor vp.proxy.getClient().eth_blobBaseFee() + let blobFeePrague = waitFor engine.frontend.eth_blobBaseFee() check: blobFeePrague > u256(0) ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() diff --git a/nimbus_verified_proxy/tests/test_header_store.nim b/nimbus_verified_proxy/tests/test_header_store.nim index f00ef36f61..0912624d61 100644 --- a/nimbus_verified_proxy/tests/test_header_store.nim +++ b/nimbus_verified_proxy/tests/test_header_store.nim @@ -10,9 +10,10 @@ import unittest2, + chronos, stint/endians2, eth/common/headers_rlp, - ../header_store, + ../engine/header_store, beacon_chain/spec/[forks, digest, helpers] func headerGenerator(number: int): ForkedLightClientHeader = diff --git a/nimbus_verified_proxy/tests/test_proof_validation.nim b/nimbus_verified_proxy/tests/test_proof_validation.nim index bde2afafa1..d4a4a0f7cc 100644 --- a/nimbus_verified_proxy/tests/test_proof_validation.nim +++ b/nimbus_verified_proxy/tests/test_proof_validation.nim @@ -9,7 +9,7 @@ {.push raises: [], gcsafe.} -import unittest2, stint, stew/byteutils, web3, ../rpc/accounts +import unittest2, stint, stew/byteutils, web3, ../engine/accounts suite "Merkle proof of inclusion validation": test "Validate account proof": diff --git a/nimbus_verified_proxy/tests/test_receipts.nim b/nimbus_verified_proxy/tests/test_receipts.nim index e6d4ed025f..54e7a326b0 100644 --- a/nimbus_verified_proxy/tests/test_receipts.nim +++ b/nimbus_verified_proxy/tests/test_receipts.nim @@ -10,19 +10,19 @@ import unittest2, + chronos, web3/[eth_api, eth_api_types], - json_rpc/[rpcclient, rpcserver, rpcproxy], eth/common/eth_types_rlp, - ../rpc/blocks, - ../types, - ../header_store, + ../engine/blocks, + ../engine/types, + ../engine/header_store, ./test_utils, ./test_api_backend suite "test receipts verification": let ts = TestApiState.init(1.u256) - vp = startTestSetup(ts, 1, 1, 8887) + engine = initTestEngine(ts, 1, 1) test "get receipts using block tags": let @@ -36,27 +36,27 @@ suite "test receipts verification": ts.loadBlockReceipts(blk, rxs) ts.loadReceipt(rxs[0].transactionHash, rxs[0]) check: - vp.headerStore.add(convHeader(blk), blk.hash).isOk() - vp.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() + engine.headerStore.add(convHeader(blk), blk.hash).isOk() + engine.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() - var verified = waitFor vp.proxy.getClient().eth_getBlockReceipts(numberTag) + var verified = waitFor engine.frontend.eth_getBlockReceipts(numberTag) check rxs == verified.get() - verified = waitFor vp.proxy.getClient().eth_getBlockReceipts(finalTag) + verified = waitFor engine.frontend.eth_getBlockReceipts(finalTag) check rxs == verified.get() - verified = waitFor vp.proxy.getClient().eth_getBlockReceipts(earliestTag) + verified = waitFor engine.frontend.eth_getBlockReceipts(earliestTag) check rxs == verified.get() - verified = waitFor vp.proxy.getClient().eth_getBlockReceipts(latestTag) + verified = waitFor engine.frontend.eth_getBlockReceipts(latestTag) check rxs == verified.get() let verifiedReceipt = - waitFor vp.proxy.getClient().eth_getTransactionReceipt(rxs[0].transactionHash) + waitFor engine.frontend.eth_getTransactionReceipt(rxs[0].transactionHash) check rxs[0] == verifiedReceipt ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() test "get logs using tags": let @@ -72,8 +72,8 @@ suite "test receipts verification": # update block tags because getLogs (uses)-> getReceipts (uses)-> getHeader ts.loadBlockReceipts(blk, rxs) check: - vp.headerStore.add(convHeader(blk), blk.hash).isOk() - vp.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() + engine.headerStore.add(convHeader(blk), blk.hash).isOk() + engine.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() for tag in tags: let filterOptions = FilterOptions( @@ -92,11 +92,11 @@ suite "test receipts verification": ) ts.loadLogs(filterOptions, logs) - let verifiedLogs = waitFor vp.proxy.getClient().eth_getLogs(filterOptions) + let verifiedLogs = waitFor engine.frontend.eth_getLogs(filterOptions) check verifiedLogs.len == logs.len ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() test "create filters and uninstall filters": # filter options without any tags would test resolving default "latest" @@ -114,15 +114,15 @@ suite "test receipts verification": let # create a filter - newFilter = waitFor vp.proxy.getClient().eth_newFilter(filterOptions) + newFilter = waitFor engine.frontend.eth_newFilter(filterOptions) # deleting will prove if the filter was created - delStatus = waitFor vp.proxy.getClient().eth_uninstallFilter(newFilter) + delStatus = waitFor engine.frontend.eth_uninstallFilter(newFilter) check delStatus let unknownFilterId = "thisisacorrectfilterid" - delStatus2 = waitFor vp.proxy.getClient().eth_uninstallFilter(newFilter) + delStatus2 = waitFor engine.frontend.eth_uninstallFilter(newFilter) check not delStatus2 @@ -136,8 +136,8 @@ suite "test receipts verification": ts.loadBlockReceipts(blk, rxs) check: - vp.headerStore.add(convHeader(blk), blk.hash).isOk() - vp.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() + engine.headerStore.add(convHeader(blk), blk.hash).isOk() + engine.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() # filter options without any tags would test resolving default "latest" let filterOptions = FilterOptions( @@ -156,19 +156,18 @@ suite "test receipts verification": let # create a filter - newFilter = waitFor vp.proxy.getClient().eth_newFilter(filterOptions) - filterLogs = waitFor vp.proxy.getClient().eth_getFilterLogs(newFilter) - filterChanges = waitFor vp.proxy.getClient().eth_getFilterChanges(newFilter) + newFilter = waitFor engine.frontend.eth_newFilter(filterOptions) + filterLogs = waitFor engine.frontend.eth_getFilterLogs(newFilter) + filterChanges = waitFor engine.frontend.eth_getFilterChanges(newFilter) check filterLogs.len == logs.len check filterChanges.len == logs.len try: - let againFilterChanges = - waitFor vp.proxy.getClient().eth_getFilterChanges(newFilter) + let againFilterChanges = waitFor engine.frontend.eth_getFilterChanges(newFilter) check false - except CatchableError as e: + except CatchableError: check true ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() diff --git a/nimbus_verified_proxy/tests/test_state.nim b/nimbus_verified_proxy/tests/test_state.nim index 8043865d2c..951d03fa09 100644 --- a/nimbus_verified_proxy/tests/test_state.nim +++ b/nimbus_verified_proxy/tests/test_state.nim @@ -10,20 +10,20 @@ import unittest2, + chronos, web3/[eth_api, eth_api_types], - json_rpc/[rpcclient, rpcserver, rpcproxy], stew/byteutils, eth/common/[base, eth_types_rlp], - ../rpc/blocks, - ../types, - ../header_store, + ../engine/blocks, + ../engine/types, + ../engine/header_store, ./test_utils, ./test_api_backend suite "test state verification": let ts = TestApiState.init(1.u256) - vp = startTestSetup(ts, 1, 1, 8897) + engine = initTestEngine(ts, 1, 1) test "test EVM-based methods": let @@ -56,20 +56,18 @@ suite "test state verification": ts.loadAccessList(tx, blk, accessList) check: - vp.headerStore.add(convHeader(blk), blk.hash).isOk() - vp.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() + engine.headerStore.add(convHeader(blk), blk.hash).isOk() + engine.headerStore.updateFinalized(convHeader(blk), blk.hash).isOk() let - verifiedBalance = waitFor vp.proxy.getClient().eth_getBalance(address, latestTag) + verifiedBalance = waitFor engine.frontend.eth_getBalance(address, latestTag) verifiedNonce = - waitFor vp.proxy.getClient().eth_getTransactionCount(address, latestTag) - verifiedCode = waitFor vp.proxy.getClient().eth_getCode(address, latestTag) - verifiedSlot = - waitFor vp.proxy.getClient().eth_getStorageAt(address, slot, latestTag) - verifiedCall = waitFor vp.proxy.getClient().eth_call(tx, latestTag) - verifiedAccessList = - waitFor vp.proxy.getClient().eth_createAccessList(tx, latestTag) - verifiedEstimate = waitFor vp.proxy.getClient().eth_estimateGas(tx, latestTag) + waitFor engine.frontend.eth_getTransactionCount(address, latestTag) + verifiedCode = waitFor engine.frontend.eth_getCode(address, latestTag) + verifiedSlot = waitFor engine.frontend.eth_getStorageAt(address, slot, latestTag) + verifiedCall = waitFor engine.frontend.eth_call(tx, latestTag) + verifiedAccessList = waitFor engine.frontend.eth_createAccessList(tx, latestTag) + verifiedEstimate = waitFor engine.frontend.eth_estimateGas(tx, latestTag) check: verifiedBalance == UInt256.fromHex("0x1d663f6a4afc5b01abb5d") @@ -85,4 +83,4 @@ suite "test state verification": verifiedEstimate == Quantity(22080) ts.clear() - vp.headerStore.clear() + engine.headerStore.clear() diff --git a/nimbus_verified_proxy/tests/test_transactions.nim b/nimbus_verified_proxy/tests/test_transactions.nim index f083d90cfd..10866f793c 100644 --- a/nimbus_verified_proxy/tests/test_transactions.nim +++ b/nimbus_verified_proxy/tests/test_transactions.nim @@ -10,10 +10,10 @@ import unittest2, + chronos, web3/[eth_api, eth_api_types], - json_rpc/[rpcclient, rpcserver, rpcproxy], eth/common/eth_types_rlp, - ../rpc/transactions, + ../engine/transactions, ./test_utils, ./test_api_backend @@ -34,16 +34,13 @@ suite "test transaction verification": test "check eth api methods": let ts = TestApiState.init(1.u256) - vp = startTestSetup(ts, 1, 1, 8888) + engine = initTestEngine(ts, 1, 1) # defining port 8888 is a hack for addr in use errors blk = getBlockFromJson("nimbus_verified_proxy/tests/data/Paris.json") for tx in blk.transactions: if tx.kind == tohTx: ts.loadTransaction(tx.tx.hash, tx.tx) - let verifiedTx = - waitFor vp.proxy.getClient().eth_getTransactionByHash(tx.tx.hash) + let verifiedTx = waitFor engine.frontend.eth_getTransactionByHash(tx.tx.hash) check verifiedTx == tx.tx ts.clear() - - vp.stopTestSetup() diff --git a/nimbus_verified_proxy/tests/test_utils.nim b/nimbus_verified_proxy/tests/test_utils.nim index 51a207be07..f64f8ade8f 100644 --- a/nimbus_verified_proxy/tests/test_utils.nim +++ b/nimbus_verified_proxy/tests/test_utils.nim @@ -10,17 +10,14 @@ import stint, - json_rpc/[rpcclient, rpcproxy, rpcserver, jsonmarshal], + chronos, + json_rpc/jsonmarshal, stew/[io2, byteutils], web3/[eth_api_types, conversions], eth/common/eth_types_rlp, - ../../execution_chain/rpc/cors, ../../execution_chain/common/common, - ../types, - ../rpc/evm, - ../rpc/rpc_eth_api, - ../nimbus_verified_proxy_conf, - ../header_store, + ../engine/types, + ../engine/engine, ./test_api_backend proc getBlockFromJson*(filepath: string): BlockObject {.raises: [SerializationError].} = @@ -72,27 +69,20 @@ template `==`*(rxs1: seq[ReceiptObject], rxs2: seq[ReceiptObject]): bool = template `==`*(logs1: seq[LogObject], logs2: seq[LogObject]): bool = JrpcConv.encode(logs1).JsonString == JrpcConv.encode(logs2).JsonString -proc startTestSetup*( - testState: TestApiState, headerCacheLen: int, maxBlockWalk: uint64, port: int = 8545 -): VerifiedRpcProxy {.raises: [CatchableError].} = +proc initTestEngine*( + testState: TestApiState, headerCacheLen: int, maxBlockWalk: uint64 +): RpcVerificationEngine {.raises: [CatchableError].} = let - chainId = 1.u256 - networkId = 1.u256 - authHooks = @[httpCors(@[])] # TODO: for now we serve all cross origin requests - web3Url = Web3Url(kind: Web3UrlKind.HttpUrl, web3Url: "http://127.0.0.1:" & $port) - clientConfig = web3Url.asClientConfig() - rpcProxy = RpcProxy.new([initTAddress("127.0.0.1", port)], clientConfig, authHooks) - headerStore = HeaderStore.new(headerCacheLen) - - vp = VerifiedRpcProxy.init(rpcProxy, headerStore, chainId, maxBlockWalk) - - vp.evm = AsyncEvm.init(vp.toAsyncEvmStateBackend(), networkId) - vp.rpcClient = initTestApiBackend(testState) - vp.installEthApiHandlers() - - waitFor vp.proxy.start() - waitFor vp.verifyChaindId() - return vp - -proc stopTestSetup*(vp: VerifiedRpcProxy) {.raises: [CatchableError].} = - waitFor vp.proxy.stop() + engineConf = RpcVerificationEngineConf( + chainId: 1.u256, + maxBlockWalk: maxBlockWalk, + headerStoreLen: headerCacheLen, + accountCacheLen: 1, + codeCacheLen: 1, + storageCacheLen: 1, + ) + engine = RpcVerificationEngine.init(engineConf) + + engine.backend = initTestApiBackend(testState) + + return engine diff --git a/nimbus_verified_proxy/types.nim b/nimbus_verified_proxy/types.nim deleted file mode 100644 index 7990947b58..0000000000 --- a/nimbus_verified_proxy/types.nim +++ /dev/null @@ -1,107 +0,0 @@ -# nimbus_verified_proxy -# Copyright (c) 2025 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: [], gcsafe.} - -import - std/tables, - json_rpc/[rpcproxy, rpcclient], - web3/[eth_api, eth_api_types], - stint, - minilru, - ./header_store, - ../execution_chain/evm/async_evm - -export minilru - -const - ACCOUNTS_CACHE_SIZE = 128 - CODE_CACHE_SIZE = 64 - STORAGE_CACHE_SIZE = 256 - MAX_ID_TRIES* = 10 - MAX_FILTERS* = 256 - -type - AccountsCacheKey* = (Root, Address) - AccountsCache* = LruCache[AccountsCacheKey, Account] - - CodeCacheKey* = (Root, Address) - CodeCache* = LruCache[CodeCacheKey, seq[byte]] - - StorageCacheKey* = (Root, Address, UInt256) - StorageCache* = LruCache[StorageCacheKey, UInt256] - - BlockTag* = eth_api_types.RtBlockIdentifier - - ChainIdProc* = proc(): Future[UInt256] {.async.} - GetBlockByHashProc* = - proc(blkHash: Hash32, fullTransactions: bool): Future[BlockObject] {.async.} - GetBlockByNumberProc* = - proc(blkNum: BlockTag, fullTransactions: bool): Future[BlockObject] {.async.} - GetProofProc* = proc( - address: Address, slots: seq[UInt256], blockId: BlockTag - ): Future[ProofResponse] {.async.} - CreateAccessListProc* = - proc(args: TransactionArgs, blockId: BlockTag): Future[AccessListResult] {.async.} - GetCodeProc* = proc(address: Address, blockId: BlockTag): Future[seq[byte]] {.async.} - GetBlockReceiptsProc = - proc(blockId: BlockTag): Future[Opt[seq[ReceiptObject]]] {.async.} - GetTransactionReceiptProc = proc(txHash: Hash32): Future[ReceiptObject] {.async.} - GetTransactionByHashProc = proc(txHash: Hash32): Future[TransactionObject] {.async.} - GetLogsProc = proc(filterOptions: FilterOptions): Future[seq[LogObject]] {.async.} - - EthApiBackend* = object - eth_chainId*: ChainIdProc - eth_getBlockByHash*: GetBlockByHashProc - eth_getBlockByNumber*: GetBlockByNumberProc - eth_getProof*: GetProofProc - eth_createAccessList*: CreateAccessListProc - eth_getCode*: GetCodeProc - eth_getBlockReceipts*: GetBlockReceiptsProc - eth_getTransactionReceipt*: GetTransactionReceiptProc - eth_getTransactionByHash*: GetTransactionByHashProc - eth_getLogs*: GetLogsProc - - FilterStoreItem* = object - filter*: FilterOptions - blockMarker*: Opt[Quantity] - - VerifiedRpcProxy* = ref object - evm*: AsyncEvm - proxy*: RpcProxy - headerStore*: HeaderStore - accountsCache*: AccountsCache - codeCache*: CodeCache - storageCache*: StorageCache - rpcClient*: EthApiBackend - - # TODO: when the list grows big add a config object instead - # config parameters - filterStore*: Table[string, FilterStoreItem] - chainId*: UInt256 - maxBlockWalk*: uint64 - -proc init*( - T: type VerifiedRpcProxy, - proxy: RpcProxy, - headerStore: HeaderStore, - chainId: UInt256, - maxBlockWalk: uint64, -): T = - VerifiedRpcProxy( - proxy: proxy, - headerStore: headerStore, - accountsCache: AccountsCache.init(ACCOUNTS_CACHE_SIZE), - codeCache: CodeCache.init(CODE_CACHE_SIZE), - storageCache: StorageCache.init(STORAGE_CACHE_SIZE), - chainId: chainId, - maxBlockWalk: maxBlockWalk, - ) - -createRpcSigsFromNim(RpcClient): - proc eth_estimateGas(args: TransactionArgs, blockTag: BlockTag): Quantity - proc eth_maxPriorityFeePerGas(): Quantity