diff --git a/lc_proxy/block_cache.nim b/lc_proxy/block_cache.nim new file mode 100644 index 000000000..a65ddb932 --- /dev/null +++ b/lc_proxy/block_cache.nim @@ -0,0 +1,62 @@ +# light client proxy +# Copyright (c) 2022 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.push raises: [Defect].} + +import + std/tables, + web3/ethtypes, + stew/[results, keyed_queue] + + +## payloads received through block gossip (and validated by light client). +## Payloads are stored in order of arrival. When cache is full the oldest +## payload is deleted first. +type BlockCache* = ref object + max: int + blocks: KeyedQueue[BlockHash, ExecutionPayloadV1] + +proc `==`(x, y: Quantity): bool {.borrow, noSideEffect.} + +proc new*(T: type BlockCache, max: uint32): T = + let maxAsInt = int(max) + return BlockCache( + max: maxAsInt, + blocks: KeyedQueue[BlockHash, ExecutionPayloadV1].init(maxAsInt) + ) + +func len*(self: BlockCache): int = + return len(self.blocks) + +func isEmpty*(self: BlockCache): bool = + return len(self.blocks) == 0 + +proc add*(self: BlockCache, payload: ExecutionPayloadV1) = + if self.blocks.hasKey(payload.blockHash): + return + + if len(self.blocks) >= self.max: + discard self.blocks.shift() + + discard self.blocks.append(payload.blockHash, payload) + +proc latest*(self: BlockCache): results.Opt[ExecutionPayloadV1] = + let latestPair = ? self.blocks.last() + return Opt.some(latestPair.data) + +proc getByNumber*( + self: BlockCache, + number: Quantity): Opt[ExecutionPayloadV1] = + + var payloadResult: Opt[ExecutionPayloadV1] + + for payload in self.blocks.prevValues: + if payload.blockNumber == number: + payloadResult = Opt.some(payload) + break + + return payloadResult diff --git a/lc_proxy/lc_proxy.nim b/lc_proxy/lc_proxy.nim index 6b607a56f..981345769 100644 --- a/lc_proxy/lc_proxy.nim +++ b/lc_proxy/lc_proxy.nim @@ -23,7 +23,8 @@ import beacon_chain/[light_client, nimbus_binary_common, version], ../nimbus/rpc/cors, ./rpc/rpc_eth_lc_api, - ./lc_proxy_conf + ./lc_proxy_conf, + ./block_cache from beacon_chain/gossip_processing/block_processor import newExecutionPayload from beacon_chain/gossip_processing/eth2_processor import toValidationResult @@ -94,6 +95,8 @@ proc run() {.raises: [Exception, Defect].} = forkDigests, getBeaconTime, genesis_validators_root ) + blockCache = BlockCache.new(uint32(64)) + # TODO: for now we serve all cross origin requests authHooks = @[httpCors(@[])] @@ -105,7 +108,7 @@ proc run() {.raises: [Exception, Defect].} = authHooks ) - lcProxy = LightClientRpcProxy.new(rpcProxy, chainId) + lcProxy = LightClientRpcProxy.new(rpcProxy, blockCache, chainId) optimisticHandler = proc(signedBlock: ForkedMsgTrustedSignedBeaconBlock): Future[void] {.async.} = @@ -116,7 +119,7 @@ proc run() {.raises: [Exception, Defect].} = when stateFork >= BeaconStateFork.Bellatrix: if blck.message.is_execution_block: template payload(): auto = blck.message.body.execution_payload - lcProxy.executionPayload.ok payload.asEngineExecutionPayload() + blockCache.add(payload.asEngineExecutionPayload()) else: discard return diff --git a/lc_proxy/rpc/rpc_eth_lc_api.nim b/lc_proxy/rpc/rpc_eth_lc_api.nim index 64448bb6f..9ace14977 100644 --- a/lc_proxy/rpc/rpc_eth_lc_api.nim +++ b/lc_proxy/rpc/rpc_eth_lc_api.nim @@ -8,8 +8,9 @@ {.push raises: [Defect].} import + std/strutils, stint, - stew/byteutils, + stew/[byteutils, results], chronicles, json_rpc/[rpcproxy, rpcserver, rpcclient], web3, @@ -17,7 +18,8 @@ import beacon_chain/eth1/eth1_monitor, beacon_chain/networking/network_metadata, beacon_chain/spec/forks, - ../validate_proof + ../validate_proof, + ../block_cache export forks @@ -35,26 +37,79 @@ template encodeHexData(value: UInt256): HexDataStr = template encodeQuantity(value: Quantity): HexQuantityStr = hexQuantityStr(encodeQuantity(value.uint64)) -type LightClientRpcProxy* = ref object - proxy: RpcProxy - executionPayload*: Opt[ExecutionPayloadV1] - chainId: Quantity +type + LightClientRpcProxy* = ref object + proxy: RpcProxy + blockCache: BlockCache + chainId: Quantity -template checkPreconditions(payload: Opt[ExecutionPayloadV1], quantityTag: string) = - if payload.isNone(): + QuantityTagKind = enum + LatestBlock, BlockNumber + + QuantityTag = object + case kind: QuantityTagKind + of LatestBlock: + discard + of BlockNumber: + blockNumber: Quantity + +func parseHexIntResult(tag: string): Result[uint64, string] = + try: + ok(parseHexInt(tag).uint64) + except ValueError as e: + err(e.msg) + +func parseHexQuantity(tag: string): Result[Quantity, string] = + let hexQuantity = hexQuantityStr(tag) + if validate(hexQuantity): + let parsed = ? parseHexIntResult(tag) + return ok(Quantity(parsed)) + else: + return err("Invalid Etheruem Hex quantity.") + +func parseQuantityTag(blockTag: string): Result[QuantityTag, string] = + let tag = blockTag.toLowerAscii + case tag + of "latest": + return ok(QuantityTag(kind: LatestBlock)) + else: + let quantity = ? parseHexQuantity(tag) + return ok(QuantityTag(kind: BlockNumber, blockNumber: quantity)) + +template checkPreconditions(proxy: LightClientRpcProxy) = + if proxy.blockCache.isEmpty(): raise newException(ValueError, "Syncing") - if quantityTag != "latest": - # TODO: for now we support only latest block, as its semantically most straight - # forward, i.e it is last received and a valid ExecutionPayloadV1. - # Ultimately we could keep track of n last valid payloads and support number - # queries for this set of blocks. - # `Pending` could be mapped to some optimistic header with the block - # fetched on demand. - raise newException(ValueError, "Only latest block is supported") - template rpcClient(lcProxy: LightClientRpcProxy): RpcClient = lcProxy.proxy.getClient() +proc getPayloadByTag( + proxy: LightClientRpcProxy, + quantityTag: string): ExecutionPayloadV1 {.raises: [ValueError, Defect].} = + checkPreconditions(proxy) + + let tagResult = parseQuantityTag(quantityTag) + + if tagResult.isErr: + raise newException(ValueError, tagResult.error) + + let tag = tagResult.get() + + var payload: ExecutionPayloadV1 + + case tag.kind + of LatestBlock: + # this will always be ok as we always validate that cache is not empty + payload = proxy.blockCache.latest.get + of BlockNumber: + let payLoadResult = proxy.blockCache.getByNumber(tag.blockNumber) + if payLoadResult.isErr(): + raise newException( + ValueError, "Block not stored in cache " & $tag.blockNumber + ) + payload = payLoadResult.get + + return payload + proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) = template payload(): Opt[ExecutionPayloadV1] = lcProxy.executionPayload @@ -63,21 +118,17 @@ proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) = lcProxy.proxy.rpc("eth_blockNumber") do() -> HexQuantityStr: ## Returns the number of most recent block. - if payload.isNone: - raise newException(ValueError, "Syncing") + checkPreconditions(lcProxy) - return encodeQuantity(payload.get.blockNumber) + return encodeQuantity(lcProxy.blockCache.latest.get.blockNumber) - # TODO quantity tag should be better typed lcProxy.proxy.rpc("eth_getBalance") do(address: Address, quantityTag: string) -> HexQuantityStr: - checkPreconditions(payload, quantityTag) - # When requesting state for `latest` block number, we need to translate # `latest` to actual block number as `latest` on proxy and on data provider # can mean different blocks and ultimatly piece received piece of state # must by validated against correct state root let - executionPayload = payload.get + executionPayload = lcProxy.getPayloadByTag(quantityTag) blockNumber = executionPayload.blockNumber.uint64 info "Forwarding get_Balance", executionBn = blockNumber @@ -100,10 +151,8 @@ proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) = raise newException(ValueError, accountResult.error) lcProxy.proxy.rpc("eth_getStorageAt") do(address: Address, slot: HexDataStr, quantityTag: string) -> HexDataStr: - checkPreconditions(payload, quantityTag) - let - executionPayload = payload.get + executionPayload = lcProxy.getPayloadByTag(quantityTag) uslot = UInt256.fromHex(slot.string) blockNumber = executionPayload.blockNumber.uint64 @@ -132,10 +181,12 @@ proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) = proc new*( T: type LightClientRpcProxy, proxy: RpcProxy, + blockCache: BlockCache, chainId: Quantity): T = return LightClientRpcProxy( proxy: proxy, + blockCache: blockCache, chainId: chainId )