diff --git a/nimbus_verified_proxy/c_frontend.nim b/nimbus_verified_proxy/c_frontend.nim deleted file mode 100644 index 0daad6cba0..0000000000 --- a/nimbus_verified_proxy/c_frontend.nim +++ /dev/null @@ -1,197 +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 - 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/engine/transactions.nim b/nimbus_verified_proxy/engine/transactions.nim index 798d5798c9..a9f9000e5e 100644 --- a/nimbus_verified_proxy/engine/transactions.nim +++ b/nimbus_verified_proxy/engine/transactions.nim @@ -47,7 +47,7 @@ proc toTransactions*(txs: openArray[TxOrHash]): Result[seq[Transaction], string] return ok(convertedTxs) proc checkTxHash*(txObj: TransactionObject, txHash: Hash32): bool = - toTransaction(txObj).rlpHash == txHash + toTransaction(txObj).computeRlpHash == txHash proc verifyTransactions*( txRoot: Hash32, transactions: seq[TxOrHash] diff --git a/nimbus_verified_proxy/json_lc_backend.nim b/nimbus_verified_proxy/json_lc_backend.nim new file mode 100644 index 0000000000..65223ca721 --- /dev/null +++ b/nimbus_verified_proxy/json_lc_backend.nim @@ -0,0 +1,138 @@ +# 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, + chronos, + chronicles, + presto/client, + beacon_chain/spec/eth2_apis/rest_light_client_calls, + beacon_chain/spec/presets, + beacon_chain/spec/forks, + ./lc/lc_manager, + ./nimbus_verified_proxy_conf + +logScope: + topics = "SSZLCRestClient" + +const + MaxMessageBodyBytes* = 128 * 1024 * 1024 # 128 MB (JSON encoded) + BASE_URL = "/eth/v1/beacon/light_client" + +type + LCRestPeer = ref object + score: int + restClient: RestClientRef + + LCRestClient* = ref object + cfg: RuntimeConfig + forkDigests: ref ForkDigests + peers: seq[LCRestPeer] + urls: seq[string] + +func new*( + T: type LCRestClient, cfg: RuntimeConfig, forkDigests: ref ForkDigests +): LCRestClient = + LCRestClient(cfg: cfg, forkDigests: forkDigests, peers: @[]) + +proc addEndpoints*(client: LCRestClient, urlList: UrlList) {.raises: [ValueError].} = + for endpoint in urlList.urls: + if endpoint in client.urls: + continue + + let restClient = RestClientRef.new(endpoint).valueOr: + raise newException(ValueError, $error) + + client.peers.add(LCRestPeer(score: 0, restClient: restClient)) + client.urls.add(endpoint) + +proc closeAll*(client: LCRestClient) {.async: (raises: []).} = + for peer in client.peers: + await peer.restClient.closeWait() + + client.peers.setLen(0) + client.urls.setLen(0) + +proc getEthLCBackend*(client: LCRestClient): EthLCBackend = + let + getLCBootstrapProc = proc( + reqId: uint64, blockRoot: Eth2Digest + ): Future[NetRes[ForkedLightClientBootstrap]] {.async: (raises: [CancelledError]).} = + let + peer = client.peers[reqId mod uint64(client.peers.len)] + res = + try: + await peer.restClient.getLightClientBootstrap( + blockRoot, client.cfg, client.forkDigests + ) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + ok(res) + + getLCUpdatesProc = proc( + reqId: uint64, startPeriod: SyncCommitteePeriod, count: uint64 + ): Future[LightClientUpdatesByRangeResponse] {.async: (raises: [CancelledError]).} = + let + peer = client.peers[reqId mod uint64(client.peers.len)] + res = + try: + await peer.restClient.getLightClientUpdatesByRange( + startPeriod, count, client.cfg, client.forkDigests + ) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + ok(res) + + getLCFinalityProc = proc( + reqId: uint64 + ): Future[NetRes[ForkedLightClientFinalityUpdate]] {. + async: (raises: [CancelledError]) + .} = + let + peer = client.peers[reqId mod uint64(client.peers.len)] + res = + try: + await peer.restClient.getLightClientFinalityUpdate( + client.cfg, client.forkDigests + ) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + ok(res) + + getLCOptimisticProc = proc( + reqId: uint64 + ): Future[NetRes[ForkedLightClientOptimisticUpdate]] {. + async: (raises: [CancelledError]) + .} = + let + peer = client.peers[reqId mod uint64(client.peers.len)] + res = + try: + await peer.restClient.getLightClientOptimisticUpdate( + client.cfg, client.forkDigests + ) + except CatchableError as e: + raise newException(CancelledError, e.msg) + + ok(res) + + updateScoreProc = proc(reqId: uint64, value: int) = + let peer = client.peers[reqId mod uint64(client.peers.len)] + peer.score += value + + EthLCBackend( + getLightClientBootstrap: getLCBootstrapProc, + getLightClientUpdatesByRange: getLCUpdatesProc, + getLightClientFinalityUpdate: getLCFinalityProc, + getLightClientOptimisticUpdate: getLCOptimisticProc, + updateScore: updateScoreProc, + ) diff --git a/nimbus_verified_proxy/lc/lc.nim b/nimbus_verified_proxy/lc/lc.nim new file mode 100644 index 0000000000..a46f35af61 --- /dev/null +++ b/nimbus_verified_proxy/lc/lc.nim @@ -0,0 +1,158 @@ +# 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: [].} + +import + chronicles, + chronos, + beacon_chain/gossip_processing/light_client_processor, + beacon_chain/beacon_clock, + ./lc_manager # use the modified light client manager + +type + LightClientHeaderCallback* = proc( + lightClient: LightClient, header: ForkedLightClientHeader + ) {.gcsafe, raises: [].} + + LightClient* = ref object + cfg: RuntimeConfig + forkDigests: ref ForkDigests + getBeaconTime*: GetBeaconTimeFn + store*: ref ForkedLightClientStore + processor*: ref LightClientProcessor + manager: LightClientManager + onFinalizedHeader*, onOptimisticHeader*: LightClientHeaderCallback + trustedBlockRoot*: Option[Eth2Digest] + +func getFinalizedHeader*(lightClient: LightClient): ForkedLightClientHeader = + withForkyStore(lightClient.store[]): + when lcDataFork > LightClientDataFork.None: + var header = ForkedLightClientHeader(kind: lcDataFork) + header.forky(lcDataFork) = forkyStore.finalized_header + header + else: + default(ForkedLightClientHeader) + +func getOptimisticHeader*(lightClient: LightClient): ForkedLightClientHeader = + withForkyStore(lightClient.store[]): + when lcDataFork > LightClientDataFork.None: + var header = ForkedLightClientHeader(kind: lcDataFork) + header.forky(lcDataFork) = forkyStore.optimistic_header + header + else: + default(ForkedLightClientHeader) + +proc new*( + T: type LightClient, + rng: ref HmacDrbgContext, + cfg: RuntimeConfig, + forkDigests: ref ForkDigests, + getBeaconTime: GetBeaconTimeFn, + genesis_validators_root: Eth2Digest, + finalizationMode: LightClientFinalizationMode, +): T = + let lightClient = LightClient( + cfg: cfg, + forkDigests: forkDigests, + getBeaconTime: getBeaconTime, + store: (ref ForkedLightClientStore)(), + ) + + func getTrustedBlockRoot(): Option[Eth2Digest] = + lightClient.trustedBlockRoot + + proc onStoreInitialized() = + discard + + proc onFinalizedHeader() = + if lightClient.onFinalizedHeader != nil: + lightClient.onFinalizedHeader(lightClient, lightClient.getFinalizedHeader) + + proc onOptimisticHeader() = + if lightClient.onOptimisticHeader != nil: + lightClient.onOptimisticHeader(lightClient, lightClient.getOptimisticHeader) + + # initialize without dumping + lightClient.processor = LightClientProcessor.new( + false, ".", ".", cfg, genesis_validators_root, finalizationMode, lightClient.store, + getBeaconTime, getTrustedBlockRoot, onStoreInitialized, onFinalizedHeader, + onOptimisticHeader, + ) + + proc lightClientVerifier( + obj: SomeForkedLightClientObject + ): Future[Result[void, LightClientVerifierError]] {. + async: (raises: [CancelledError], raw: true) + .} = + let resfut = Future[Result[void, LightClientVerifierError]] + .Raising([CancelledError]) + .init("lightClientVerifier") + lightClient.processor[].addObject(MsgSource.gossip, obj, resfut) + resfut + + proc bootstrapVerifier(obj: ForkedLightClientBootstrap): auto = + lightClientVerifier(obj) + + proc updateVerifier(obj: ForkedLightClientUpdate): auto = + lightClientVerifier(obj) + + proc finalityVerifier(obj: ForkedLightClientFinalityUpdate): auto = + lightClientVerifier(obj) + + proc optimisticVerifier(obj: ForkedLightClientOptimisticUpdate): auto = + lightClientVerifier(obj) + + func isLightClientStoreInitialized(): bool = + lightClient.store[].kind > LightClientDataFork.None + + func isNextSyncCommitteeKnown(): bool = + withForkyStore(lightClient.store[]): + when lcDataFork > LightClientDataFork.None: + forkyStore.is_next_sync_committee_known + else: + false + + func getFinalizedSlot(): Slot = + withForkyStore(lightClient.store[]): + when lcDataFork > LightClientDataFork.None: + forkyStore.finalized_header.beacon.slot + else: + GENESIS_SLOT + + func getOptimisticSlot(): Slot = + withForkyStore(lightClient.store[]): + when lcDataFork > LightClientDataFork.None: + forkyStore.optimistic_header.beacon.slot + else: + GENESIS_SLOT + + lightClient.manager = LightClientManager.init( + rng, getTrustedBlockRoot, bootstrapVerifier, updateVerifier, finalityVerifier, + optimisticVerifier, isLightClientStoreInitialized, isNextSyncCommitteeKnown, + getFinalizedSlot, getOptimisticSlot, getBeaconTime, + ) + + lightClient + +proc setBackend*(lightClient: LightClient, backend: EthLCBackend) = + lightClient.manager.backend = backend + +proc start*(lightClient: LightClient) = + info "Starting beacon light client", trusted_block_root = lightClient.trustedBlockRoot + lightClient.manager.start() + +proc stop*(lightClient: LightClient) {.async: (raises: []).} = + info "Stopping beacon light client" + await lightClient.manager.stop() + +proc resetToFinalizedHeader*( + lightClient: LightClient, + header: ForkedLightClientHeader, + current_sync_committee: SyncCommittee, +) = + lightClient.processor[].resetToFinalizedHeader(header, current_sync_committee) diff --git a/nimbus_verified_proxy/lc/lc_manager.nim b/nimbus_verified_proxy/lc/lc_manager.nim new file mode 100644 index 0000000000..acce8ab69e --- /dev/null +++ b/nimbus_verified_proxy/lc/lc_manager.nim @@ -0,0 +1,393 @@ +# 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: [].} + +import chronos, chronicles +import + beacon_chain/beacon_clock, + beacon_chain/networking/peer_scores, + beacon_chain/sync/[light_client_sync_helpers, sync_manager] + +logScope: + topics = "lcman" + +const MAX_REQUEST_LIGHT_CLIENT_UPDATES = 128 + +type + Nothing = object + ResponseError = object of CatchableError + Endpoint[K, V] = (K, V) # https://github.com/nim-lang/Nim/issues/19531 + Bootstrap = Endpoint[Eth2Digest, ForkedLightClientBootstrap] + UpdatesByRange = Endpoint[ + tuple[startPeriod: SyncCommitteePeriod, count: uint64], ForkedLightClientUpdate + ] + FinalityUpdate = Endpoint[Nothing, ForkedLightClientFinalityUpdate] + OptimisticUpdate = Endpoint[Nothing, ForkedLightClientOptimisticUpdate] + + NetRes*[T] = Result[T, void] + ValueVerifier[V] = proc(v: V): Future[Result[void, LightClientVerifierError]] {. + async: (raises: [CancelledError]) + .} + BootstrapVerifier* = ValueVerifier[ForkedLightClientBootstrap] + UpdateVerifier* = ValueVerifier[ForkedLightClientUpdate] + FinalityUpdateVerifier* = ValueVerifier[ForkedLightClientFinalityUpdate] + OptimisticUpdateVerifier* = ValueVerifier[ForkedLightClientOptimisticUpdate] + + GetTrustedBlockRootCallback* = proc(): Option[Eth2Digest] {.gcsafe, raises: [].} + GetBoolCallback* = proc(): bool {.gcsafe, raises: [].} + GetSlotCallback* = proc(): Slot {.gcsafe, raises: [].} + + LightClientUpdatesByRangeResponse* = NetRes[seq[ForkedLightClientUpdate]] + + LightClientBootstrapProc = proc( + id: uint64, blockRoot: Eth2Digest + ): Future[NetRes[ForkedLightClientBootstrap]] {.async: (raises: [CancelledError]).} + LightClientUpdatesByRangeProc = proc( + id: uint64, startPeriod: SyncCommitteePeriod, count: uint64 + ): Future[LightClientUpdatesByRangeResponse] {.async: (raises: [CancelledError]).} + LightClientFinalityUpdateProc = proc( + id: uint64 + ): Future[NetRes[ForkedLightClientFinalityUpdate]] {. + async: (raises: [CancelledError]) + .} + LightClientOptimisticUpdateProc = proc( + id: uint64 + ): Future[NetRes[ForkedLightClientOptimisticUpdate]] {. + async: (raises: [CancelledError]) + .} + UpdateScoreProc = proc(id: uint64, value: int) {.gcsafe, raises: [].} + + EthLCBackend* = object + getLightClientBootstrap*: LightClientBootstrapProc + getLightClientUpdatesByRange*: LightClientUpdatesByRangeProc + getLightClientFinalityUpdate*: LightClientFinalityUpdateProc + getLightClientOptimisticUpdate*: LightClientOptimisticUpdateProc + updateScore*: UpdateScoreProc + + LightClientManager* = object + rng: ref HmacDrbgContext + backend*: EthLCBackend + getTrustedBlockRoot: GetTrustedBlockRootCallback + bootstrapVerifier: BootstrapVerifier + updateVerifier: UpdateVerifier + finalityUpdateVerifier: FinalityUpdateVerifier + optimisticUpdateVerifier: OptimisticUpdateVerifier + isLightClientStoreInitialized: GetBoolCallback + isNextSyncCommitteeKnown: GetBoolCallback + getFinalizedSlot: GetSlotCallback + getOptimisticSlot: GetSlotCallback + getBeaconTime: GetBeaconTimeFn + loopFuture: Future[void].Raising([CancelledError]) + +func init*( + T: type LightClientManager, + rng: ref HmacDrbgContext, + getTrustedBlockRoot: GetTrustedBlockRootCallback, + bootstrapVerifier: BootstrapVerifier, + updateVerifier: UpdateVerifier, + finalityUpdateVerifier: FinalityUpdateVerifier, + optimisticUpdateVerifier: OptimisticUpdateVerifier, + isLightClientStoreInitialized: GetBoolCallback, + isNextSyncCommitteeKnown: GetBoolCallback, + getFinalizedSlot: GetSlotCallback, + getOptimisticSlot: GetSlotCallback, + getBeaconTime: GetBeaconTimeFn, +): LightClientManager = + ## Initialize light client manager. + LightClientManager( + rng: rng, + getTrustedBlockRoot: getTrustedBlockRoot, + bootstrapVerifier: bootstrapVerifier, + updateVerifier: updateVerifier, + finalityUpdateVerifier: finalityUpdateVerifier, + optimisticUpdateVerifier: optimisticUpdateVerifier, + isLightClientStoreInitialized: isLightClientStoreInitialized, + isNextSyncCommitteeKnown: isNextSyncCommitteeKnown, + getFinalizedSlot: getFinalizedSlot, + getOptimisticSlot: getOptimisticSlot, + getBeaconTime: getBeaconTime, + ) + +# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.3/specs/altair/light-client/p2p-interface.md#getlightclientbootstrap +proc doRequest( + e: typedesc[Bootstrap], backend: EthLCBackend, reqId: uint64, blockRoot: Eth2Digest +): Future[NetRes[ForkedLightClientBootstrap]] {. + async: (raises: [CancelledError], raw: true) +.} = + backend.getLightClientBootstrap(reqId, blockRoot) + +# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.3/specs/altair/light-client/p2p-interface.md#lightclientupdatesbyrange +proc doRequest( + e: typedesc[UpdatesByRange], + backend: EthLCBackend, + reqId: uint64, + key: tuple[startPeriod: SyncCommitteePeriod, count: uint64], +): Future[LightClientUpdatesByRangeResponse] {. + async: (raises: [ResponseError, CancelledError]) +.} = + let (startPeriod, count) = key + doAssert count > 0 and count <= MAX_REQUEST_LIGHT_CLIENT_UPDATES + let response = await backend.getLightClientUpdatesByRange(reqId, startPeriod, count) + if response.isOk: + let e = distinctBase(response.get).checkLightClientUpdates(startPeriod, count) + if e.isErr: + raise newException(ResponseError, e.error) + return response + +# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.3/specs/altair/light-client/p2p-interface.md#getlightclientfinalityupdate +proc doRequest( + e: typedesc[FinalityUpdate], backend: EthLCBackend, reqId: uint64 +): Future[NetRes[ForkedLightClientFinalityUpdate]] {. + async: (raises: [CancelledError], raw: true) +.} = + backend.getLightClientFinalityUpdate(reqId) + +# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.5/specs/altair/light-client/p2p-interface.md#getlightclientoptimisticupdate +proc doRequest( + e: typedesc[OptimisticUpdate], backend: EthLCBackend, reqId: uint64 +): Future[NetRes[ForkedLightClientOptimisticUpdate]] {. + async: (raises: [CancelledError], raw: true) +.} = + backend.getLightClientOptimisticUpdate(reqId) + +template valueVerifier[E]( + self: LightClientManager, e: typedesc[E] +): ValueVerifier[E.V] = + when E.V is ForkedLightClientBootstrap: + self.bootstrapVerifier + elif E.V is ForkedLightClientUpdate: + self.updateVerifier + elif E.V is ForkedLightClientFinalityUpdate: + self.finalityUpdateVerifier + elif E.V is ForkedLightClientOptimisticUpdate: + self.optimisticUpdateVerifier + else: + static: + doAssert false + +iterator values(v: auto): auto = + ## Local helper for `workerTask` to share the same implementation for both + ## scalar and aggregate values, by treating scalars as 1-length aggregates. + when v is seq: + for i in v: + yield i + else: + yield v + +proc workerTask[E]( + self: LightClientManager, e: typedesc[E], key: E.K +): Future[bool] {.async: (raises: [CancelledError]).} = + var + didProgress = false + reqId: uint64 + try: + self.rng[].generate(reqId) + + let value = + when E.K is Nothing: + await E.doRequest(self.backend, reqId) + else: + await E.doRequest(self.backend, reqId, key) + if value.isOk: + var applyReward = false + for val in value.get().values: + let res = await self.valueVerifier(E)(val) + if res.isErr: + case res.error + of LightClientVerifierError.MissingParent: + # Stop, requires different request to progress + return didProgress + of LightClientVerifierError.Duplicate: + # Ignore, a concurrent request may have already fulfilled this + when E.V is ForkedLightClientBootstrap: + didProgress = true + else: + discard + of LightClientVerifierError.UnviableFork: + # Descore, peer is on an incompatible fork version + withForkyObject(val): + when lcDataFork > LightClientDataFork.None: + notice "Received value from an unviable fork", + value = forkyObject, endpoint = E.name + else: + notice "Received value from an unviable fork", endpoint = E.name + self.backend.updateScore(reqId, PeerScoreUnviableFork) + return didProgress + of LightClientVerifierError.Invalid: + # Descore, received data is malformed + withForkyObject(val): + when lcDataFork > LightClientDataFork.None: + warn "Received invalid value", + value = forkyObject.shortLog, endpoint = E.name + else: + warn "Received invalid value", endpoint = E.name + self.backend.updateScore(reqId, PeerScoreBadValues) + return didProgress + else: + # Reward, peer returned something useful + applyReward = true + didProgress = true + if applyReward: + self.backend.updateScore(reqId, PeerScoreGoodValues) + else: + self.backend.updateScore(reqId, PeerScoreNoValues) + debug "Failed to receive value on request", value, endpoint = E.name + except ResponseError as exc: + self.backend.updateScore(reqId, PeerScoreBadValues) + warn "Received invalid response", error = exc.msg, endpoint = E.name + except CancelledError as exc: + raise exc + + return didProgress + +proc query[E]( + self: LightClientManager, e: typedesc[E], key: E.K +): Future[bool] {.async: (raises: [CancelledError]).} = + const NUM_WORKERS = 2 + var workers: array[NUM_WORKERS, Future[bool]] + + let progressFut = Future[void].Raising([CancelledError]).init("lcmanProgress") + var + numCompleted = 0 + success = false + maxCompleted = workers.len + + proc handleFinishedWorker(future: pointer) = + try: + let didProgress = cast[Future[bool]](future).read() + if didProgress and not progressFut.finished: + progressFut.complete() + success = true + except CatchableError: + discard + finally: + inc numCompleted + if numCompleted == maxCompleted: + progressFut.cancelSoon() + + # Start concurrent workers + for i in 0 ..< workers.len: + try: + workers[i] = self.workerTask(e, key) + workers[i].addCallback(handleFinishedWorker) + except CancelledError as exc: + raise exc + except CatchableError: + workers[i] = newFuture[bool]() + workers[i].complete(false) + + # Wait for any worker to report progress, or for all workers to finish + waitFor progressFut + + # cancel all workers + for i in 0 ..< NUM_WORKERS: + workers[i].cancelSoon() + + return success + +template query[E]( + self: LightClientManager, e: typedesc[E] +): Future[bool].Raising([CancelledError]) = + self.query(e, Nothing()) + +# https://github.com/ethereum/consensus-specs/blob/v1.5.0-beta.0/specs/altair/light-client/light-client.md#light-client-sync-process +proc loop(self: LightClientManager) {.async: (raises: [CancelledError]).} = + var + downloadOptimistic = true + downloadFinality = false + didOptimisticProgress = false + didFinalityProgress = false + + while true: + let + wallTime = self.getBeaconTime() + currentSlot = wallTime.slotOrZero() + currentEpoch = (currentSlot mod SLOTS_PER_EPOCH) + currentPeriod = currentSlot.sync_committee_period + finalizedSlot = self.getFinalizedSlot() + finalizedPeriod = finalizedSlot.sync_committee_period + finalizedEpoch = (finalizedSlot mod SLOTS_PER_EPOCH) + optimisticSlot = self.getOptimisticSlot() + optimisticPeriod = optimisticSlot.sync_committee_period + optimisitcEpoch = (optimisticSlot mod SLOTS_PER_EPOCH) + + # Obtain bootstrap data once a trusted block root is supplied + if not self.isLightClientStoreInitialized(): + let trustedBlockRoot = self.getTrustedBlockRoot() + + # reattempt bootstrap download in 2 seconds + if trustedBlockRoot.isNone: + debug "TrustedBlockRoot unavaialble re-attempting bootstrap download" + await sleepAsync(chronos.seconds(2)) + continue + + let didProgress = await self.query(Bootstrap, trustedBlockRoot.get) + + # reattempt bootstrap download in 2 seconds + if not didProgress: + debug "Re-attempting bootstrap download" + await sleepAsync(chronos.seconds(2)) + + continue + + # check and download sync committee updates + if finalizedPeriod == optimisticPeriod and not self.isNextSyncCommitteeKnown(): + if finalizedPeriod >= currentPeriod: + debug "Downloading light client sync committee updates", + start_period = finalizedPeriod, count = 1 + discard await self.query( + UpdatesByRange, (startPeriod: finalizedPeriod, count: uint64(1)) + ) + else: + let count = + min(currentPeriod - finalizedPeriod, MAX_REQUEST_LIGHT_CLIENT_UPDATES) + debug "Downloading light client sync committee updates", + start_period = finalizedPeriod, count = count + discard await self.query( + UpdatesByRange, (startPeriod: finalizedPeriod, count: uint64(count)) + ) + elif finalizedPeriod + 1 < currentPeriod: + let count = + min(currentPeriod - (finalizedPeriod + 1), MAX_REQUEST_LIGHT_CLIENT_UPDATES) + debug "Downloading light client sync committee updates", + start_period = finalizedPeriod, count = count + discard await self.query( + UpdatesByRange, (startPeriod: finalizedPeriod, count: uint64(count)) + ) + + # check and download optimistic update + if optimisticSlot < currentSlot: + debug "Downloading light client optimistic updates", slot = currentSlot + let didProgress = await self.query(OptimisticUpdate) + if not didProgress: + # retry in 2 seconds + await sleepAsync(chronos.seconds(2)) + continue + + # check and download finality update + if currentEpoch > finalizedEpoch + 2: + debug "Downloading light client finality updates", slot = currentSlot + let didProgress = await self.query(FinalityUpdate) + if not didProgress: + # retry in two seconds + await sleepAsync(chronos.seconds(2)) + continue + + # check for updates every slot + await sleepAsync(chronos.seconds(int64(SECONDS_PER_SLOT))) + +proc start*(self: var LightClientManager) = + ## Start light client manager's loop. + doAssert self.loopFuture == nil + self.loopFuture = self.loop() + +proc stop*(self: var LightClientManager) {.async: (raises: []).} = + ## Stop light client manager's loop. + if self.loopFuture != nil: + await noCancel self.loopFuture.cancelAndWait() + self.loopFuture = nil diff --git a/nimbus_verified_proxy/libverifproxy/verifproxy.nim b/nimbus_verified_proxy/libverifproxy/verifproxy.nim index 5a70b0cfad..b17ffd556d 100644 --- a/nimbus_verified_proxy/libverifproxy/verifproxy.nim +++ b/nimbus_verified_proxy/libverifproxy/verifproxy.nim @@ -7,7 +7,6 @@ import std/[atomics, json, net], - eth/net/nat, beacon_chain/spec/[digest, network], beacon_chain/nimbus_binary_common, ../nimbus_verified_proxy, @@ -38,20 +37,14 @@ proc runContext(ctx: ptr Context) {.thread.} = let rpcAddr = jsonNode["RpcAddress"].getStr() let myConfig = VerifiedProxyConf( - listenAddress: some(defaultListenAddress), eth2Network: some(jsonNode["Eth2Network"].getStr()), trustedBlockRoot: Eth2Digest.fromHex(jsonNode["TrustedBlockRoot"].getStr()), - backendUrl: parseCmdArg(Web3Url, jsonNode["Web3Url"].getStr()), - frontendUrl: parseCmdArg(Web3Url, jsonNode["Web3Url"].getStr()), + backendUrl: parseCmdArg(Web3Url, jsonNode["backendUrl"].getStr()), + frontendUrl: parseCmdArg(Web3Url, jsonNode["frontendUrl"].getStr()), + lcEndpoints: parseCmdArg(UrlList, jsonNode["lcEndpoints"].getStr()), logLevel: jsonNode["LogLevel"].getStr(), - maxPeers: 160, - nat: NatConfig(hasExtIp: false, nat: NatAny), logStdout: StdoutLogKind.Auto, dataDirFlag: none(OutDir), - tcpPort: Port(defaultEth2TcpPort), - udpPort: Port(defaultEth2TcpPort), - agentString: "nimbus", - discv5Enabled: true, ) run(myConfig, ctx) diff --git a/nimbus_verified_proxy/nimbus_verified_proxy.nim b/nimbus_verified_proxy/nimbus_verified_proxy.nim index 28079922dd..6be8bea40f 100644 --- a/nimbus_verified_proxy/nimbus_verified_proxy.nim +++ b/nimbus_verified_proxy/nimbus_verified_proxy.nim @@ -14,17 +14,19 @@ import confutils, eth/common/[keys, eth_types_rlp], json_rpc/rpcproxy, - beacon_chain/gossip_processing/optimistic_processor, + beacon_chain/gossip_processing/light_client_processor, beacon_chain/networking/network_metadata, - beacon_chain/networking/topic_params, beacon_chain/spec/beaconstate, - beacon_chain/[beacon_clock, buildinfo, light_client, nimbus_binary_common], + beacon_chain/conf, + beacon_chain/[beacon_clock, buildinfo, nimbus_binary_common], ../execution_chain/common/common, ./nimbus_verified_proxy_conf, ./engine/engine, ./engine/header_store, ./engine/utils, ./engine/types, + ./lc/lc, + ./json_lc_backend, ./json_rpc_backend, ./json_rpc_frontend, ../execution_chain/version_info @@ -79,7 +81,7 @@ proc run*( try: notice "Launching Nimbus verified proxy", - version = fullVersionStr, cmdParams = commandLineParams(), config + version = FullVersionStr, cmdParams = commandLineParams(), config except Exception: notice "commandLineParams() exception" @@ -148,36 +150,21 @@ proc run*( genesisBlockRoot = get_initial_beacon_block(genesisState[]).root - # transform the config to fit as a light client config and as a p2p node(Eth2Node) config - var lcConfig = config.asLightClientConf() - for node in metadata.bootstrapNodes: - lcConfig.bootstrapNodes.add node - - # create new network keys, create a p2p node(Eth2Node) and create a light client - let rng = keys.newRng() - netKeys = getRandomNetKeys(rng[]) - - network = createEth2Node( - rng, lcConfig, netKeys, cfg, forkDigests, getBeaconTime, genesis_validators_root - ) - # light client is set to optimistic finalization mode - lightClient = createLightClient( - network, rng, lcConfig, cfg, forkDigests, getBeaconTime, genesis_validators_root, + lightClient = LightClient.new( + rng, cfg, forkDigests, getBeaconTime, genesis_validators_root, LightClientFinalizationMode.Optimistic, ) - # registerbasic p2p protocols for maintaing peers ping/status/get_metadata/... etc. - network.registerProtocol( - PeerSync, - PeerSync.NetworkState.init(cfg, forkDigests, genesisBlockRoot, getBeaconTime), - ) + # REST client for json LC updates + lcRestClient = LCRestClient.new(cfg, forkDigests) + + # add endpoints to the client + lcRestClient.addEndpoints(config.lcEndpoints) + lightClient.setBackend(lcRestClient.getEthLCBackend()) - # start the p2p network and rpcProxy - waitFor network.startListening() - waitFor network.start() # verify chain id that the proxy is connected to waitFor engine.verifyChaindId() @@ -222,89 +209,16 @@ proc run*( lightClient.onFinalizedHeader = onFinalizedHeader lightClient.onOptimisticHeader = onOptimisticHeader lightClient.trustedBlockRoot = some config.trustedBlockRoot - lightClient.installMessageValidators() - - func shouldSyncOptimistically(wallSlot: Slot): bool = - let optimisticHeader = lightClient.optimisticHeader - withForkyHeader(optimisticHeader): - when lcDataFork > LightClientDataFork.None: - # Check whether light client has synced sufficiently close to wall slot - const maxAge = 2 * SLOTS_PER_EPOCH - forkyHeader.beacon.slot >= max(wallSlot, maxAge.Slot) - maxAge - else: - false - var blocksGossipState: GossipState - proc updateBlocksGossipStatus(slot: Slot) = - let - isBehind = not shouldSyncOptimistically(slot) - - targetGossipState = getTargetGossipState(slot.epoch, cfg, isBehind) - - template currentGossipState(): auto = - blocksGossipState - - if currentGossipState == targetGossipState: - return - - if currentGossipState.card == 0 and targetGossipState.card > 0: - debug "Enabling blocks topic subscriptions", wallSlot = slot, targetGossipState - elif currentGossipState.card > 0 and targetGossipState.card == 0: - debug "Disabling blocks topic subscriptions", wallSlot = slot - else: - # Individual forks added / removed - discard - - let - newGossipEpochs = targetGossipState - currentGossipState - oldGossipEpochs = currentGossipState - targetGossipState - - for gossipEpoch in oldGossipEpochs: - let forkDigest = forkDigests[].atEpoch(gossipEpoch, cfg) - network.unsubscribe(getBeaconBlocksTopic(forkDigest)) - - for gossipEpoch in newGossipEpochs: - let forkDigest = forkDigests[].atEpoch(gossipEpoch, cfg) - network.subscribe( - getBeaconBlocksTopic(forkDigest), - getBlockTopicParams(), - enableTopicMetrics = true, - ) - - blocksGossipState = targetGossipState - - proc updateGossipStatus(time: Moment) = - let wallSlot = getBeaconTime().slotOrZero() - updateBlocksGossipStatus(wallSlot + 1) - lightClient.updateGossipStatus(wallSlot + 1) - - # updates gossip status every second every second - proc runOnSecondLoop() {.async.} = - let sleepTime = chronos.seconds(1) - while true: - let start = chronos.now(chronos.Moment) - await chronos.sleepAsync(sleepTime) - let afterSleep = chronos.now(chronos.Moment) - let sleepTime = afterSleep - start - updateGossipStatus(start) - let finished = chronos.now(chronos.Moment) - let processingTime = finished - afterSleep - trace "onSecond task completed", sleepTime, processingTime - - # update gossip status before starting the light client - updateGossipStatus(Moment.now()) # start the light client lightClient.start() - # launch a async routine - asyncSpawn runOnSecondLoop() - # run an infinite loop and wait for a stop signal while true: poll() if ctx != nil and ctx.stop: # Cleanup - waitFor network.stop() + waitFor lcRestClient.closeAll() waitFor jsonRpcClient.stop() waitFor jsonRpcServer.stop() ctx.cleanup() diff --git a/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim b/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim index 3579a2fe2a..b549eb1e04 100644 --- a/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim +++ b/nimbus_verified_proxy/nimbus_verified_proxy_conf.nim @@ -12,7 +12,8 @@ import json_rpc/rpcproxy, # must be early (compilation annoyance) json_serialization/std/net, beacon_chain/conf_light_client, - beacon_chain/nimbus_binary_common + beacon_chain/nimbus_binary_common, + std/strutils export net @@ -25,37 +26,45 @@ type kind*: Web3UrlKind web3Url*: string + UrlList* = object + urls*: seq[string] + #!fmt: off type VerifiedProxyConf* = object # Config configFile* {. - desc: "Loads the configuration from a TOML file" - name: "config-file" .}: Option[InputFile] + desc: "Loads the configuration from a TOML file", + name: "config-file" + .}: Option[InputFile] # Logging logLevel* {. - desc: "Sets the log level" - defaultValue: "INFO" - name: "log-level" .}: string + desc: "Sets the log level", + defaultValue: "INFO", + name: "log-level" + .}: string logStdout* {. - hidden - desc: "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)" - defaultValueDesc: "auto" - defaultValue: StdoutLogKind.Auto - name: "log-format" .}: StdoutLogKind + hidden, + desc: "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)", + defaultValueDesc: "auto", + defaultValue: StdoutLogKind.Auto, + name: "log-format" + .}: StdoutLogKind # Storage dataDirFlag* {. - desc: "The directory where nimbus will store all blockchain data" - abbr: "d" - name: "data-dir" .}: Option[OutDir] + desc: "The directory where nimbus will store all blockchain data", + abbr: "d", + name: "data-dir" + .}: Option[OutDir] # Network eth2Network* {. - desc: "The Eth2 network to join" - defaultValueDesc: "mainnet" - name: "network" .}: Option[string] + desc: "The Eth2 network to join", + defaultValueDesc: "mainnet", + name: "network" + .}: Option[string] accountCacheLen* {. hidden, @@ -95,8 +104,9 @@ type VerifiedProxyConf* = object # Consensus light sync # No default - Needs to be provided by the user trustedBlockRoot* {. - desc: "Recent trusted finalized block root to initialize light client from" - name: "trusted-block-root" .}: Eth2Digest + desc: "Recent trusted finalized block root to initialize light client from", + name: "trusted-block-root" + .}: Eth2Digest # (Untrusted) web3 provider # No default - Needs to be provided by the user @@ -114,70 +124,12 @@ type VerifiedProxyConf* = object name: "frontend-url" .}: Web3Url - # Libp2p - bootstrapNodes* {. - desc: "Specifies one or more bootstrap nodes to use when connecting to the network" - abbr: "b" - name: "bootstrap-node" .}: seq[string] - - bootstrapNodesFile* {. - desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses" - defaultValue: "" - name: "bootstrap-file" .}: InputFile - - listenAddress* {. - desc: "Listening address for the Ethereum LibP2P and Discovery v5 traffic" - name: "listen-address" .}: Option[IpAddress] - - tcpPort* {. - desc: "Listening TCP port for Ethereum LibP2P traffic" - defaultValue: defaultEth2TcpPort - defaultValueDesc: $defaultEth2TcpPortDesc - name: "tcp-port" .}: Port - - udpPort* {. - desc: "Listening UDP port for node discovery" - defaultValue: defaultEth2TcpPort - defaultValueDesc: $defaultEth2TcpPortDesc - name: "udp-port" .}: Port - - # TODO: Select a lower amount of peers. - maxPeers* {. - desc: "The target number of peers to connect to", - defaultValue: 160, # 5 (fanout) * 64 (subnets) / 2 (subs) for a healthy mesh - name: "max-peers" - .}: int - - hardMaxPeers* {. - desc: "The maximum number of peers to connect to. Defaults to maxPeers * 1.5" - name: "hard-max-peers" .}: Option[int] - - nat* {. - desc: "Specify method to use for determining public address. " & - "Must be one of: any, none, upnp, pmp, extip:" - defaultValue: NatConfig(hasExtIp: false, nat: NatAny) - defaultValueDesc: "any" - name: "nat" .}: NatConfig - - enrAutoUpdate* {. - desc: "Discovery can automatically update its ENR with the IP address " & - "and UDP port as seen by other nodes it communicates with. " & - "This option allows to enable/disable this functionality" - defaultValue: false - name: "enr-auto-update" .}: bool - - agentString* {. - defaultValue: "nimbus", - desc: "Node agent string which is used as identifier in the LibP2P network", - name: "agent-string" - .}: string - - discv5Enabled* {.desc: "Enable Discovery v5", defaultValue: true, name: "discv5".}: - bool - - directPeers* {. - desc: "The list of priviledged, secure and known peers to connect and maintain the connection to, this requires a not random netkey-file. In the complete multiaddress format like: /ip4/
/tcp//p2p/. Peering agreements are established out of band and must be reciprocal." - name: "direct-peer" .}: seq[string] + # (Untrusted) web3 provider + # No default - Needs to be provided by the user + lcEndpoints* {. + desc: "command seperated URLs of the light client data provider", + name: "lc-endpoints" + .}: UrlList #!fmt: on @@ -195,34 +147,24 @@ proc parseCmdArg*(T: type Web3Url, p: string): T {.raises: [ValueError].} = ValueError, "Web3 url should have defined scheme (http/https/ws/wss)" ) +proc parseCmdArg*(T: type UrlList, p: string): T {.raises: [ValueError].} = + let urls = p.split(',') + + for u in urls: + let + parsed = parseUri(u) + normalizedScheme = parsed.scheme.toLowerAscii() + + if not (normalizedScheme == "http" or normalizedScheme == "https"): + raise newException(ValueError, "Light Client Endpoint should be a http(s) url") + + UrlList(urls: urls) + proc completeCmdArg*(T: type Web3Url, val: string): seq[string] = return @[] -func asLightClientConf*(pc: VerifiedProxyConf): LightClientConf = - return LightClientConf( - configFile: pc.configFile, - logLevel: pc.logLevel, - logStdout: pc.logStdout, - logFile: none(OutFile), - dataDirFlag: pc.dataDirFlag, - eth2Network: pc.eth2Network, - bootstrapNodes: pc.bootstrapNodes, - bootstrapNodesFile: pc.bootstrapNodesFile, - listenAddress: pc.listenAddress, - tcpPort: pc.tcpPort, - udpPort: pc.udpPort, - maxPeers: pc.maxPeers, - hardMaxPeers: pc.hardMaxPeers, - nat: pc.nat, - enrAutoUpdate: pc.enrAutoUpdate, - agentString: pc.agentString, - discv5Enabled: pc.discv5Enabled, - directPeers: pc.directPeers, - trustedBlockRoot: pc.trustedBlockRoot, - web3Urls: @[EngineApiUrlConfigValue(url: pc.backendUrl.web3Url)], - jwtSecret: none(InputFile), - stopAtEpoch: 0, - ) +proc completeCmdArg*(T: type UrlList, val: string): seq[string] = + return @[] # TODO: Cannot use ClientConfig in VerifiedProxyConf due to the fact that # it contain `set[TLSFlags]` which does not have proper toml serialization