diff --git a/fluffy/conf.nim b/fluffy/conf.nim index d0fc2391a..324a5b6c6 100644 --- a/fluffy/conf.nim +++ b/fluffy/conf.nim @@ -12,6 +12,8 @@ import uri, confutils, confutils/std/net, chronicles, eth/keys, eth/net/nat, eth/p2p/discoveryv5/[enr, node], json_rpc/rpcproxy, + nimcrypto/hash, + stew/byteutils, ./network/wire/portal_protocol_config proc defaultDataDir*(): string = @@ -40,6 +42,8 @@ const defaultStorageSizeDesc* = $defaultStorageSize type + TrustedDigest* = MDigest[32 * 8] + PortalCmd* = enum noCommand @@ -208,12 +212,25 @@ type defaultValueDesc: $defaultStorageSizeDesc name: "storage-size" .}: uint32 + trustedBlockRoot* {. + desc: "Recent trusted finalized block root to initialize the consensus light client from. " & + "If not provided by the user, portal light client will be disabled." + defaultValue: none(TrustedDigest) + name: "trusted-block-root" .}: Option[TrustedDigest] + case cmd* {. command defaultValue: noCommand .}: PortalCmd of noCommand: discard +func parseCmdArg*(T: type TrustedDigest, input: string): T + {.raises: [ValueError, Defect].} = + TrustedDigest.fromHex(input) + +func completeCmdArg*(T: type TrustedDigest, input: string): seq[string] = + return @[] + proc parseCmdArg*(T: type enr.Record, p: TaintedString): T {.raises: [Defect, ConfigurationError].} = if not fromURI(result, p): diff --git a/fluffy/fluffy.nim b/fluffy/fluffy.nim index 56c5034d7..0172a7f72 100644 --- a/fluffy/fluffy.nim +++ b/fluffy/fluffy.nim @@ -14,11 +14,22 @@ import json_rpc/rpcproxy, stew/[byteutils, io2], eth/keys, eth/net/nat, eth/p2p/discoveryv5/protocol as discv5_protocol, + beacon_chain/beacon_clock, + beacon_chain/spec/forks, + beacon_chain/spec/datatypes/altair, + beacon_chain/gossip_processing/light_client_processor, ./conf, ./network_metadata, ./common/common_utils, ./rpc/[rpc_eth_api, bridge_client, rpc_discovery_api, rpc_portal_api, rpc_portal_debug_api], ./network/state/[state_network, state_content], ./network/history/[history_network, history_content], + ./network/beacon_light_client/[ + light_client_init_loader, + light_client_content, + beacon_light_client, + light_client_db, + light_client_network + ], ./network/wire/[portal_stream, portal_protocol_config], ./data/[history_data_seeding, history_data_parser], ./content_db @@ -163,6 +174,78 @@ proc run(config: PortalConf) {.raises: [CatchableError, Defect].} = let bridgeClient = initializeBridgeClient(config.bridgeUri) d.start() + + # TODO: Currently disabled by default as it is not stable/polished enough, + # ultimatetely this should probably be always on. + if config.trustedBlockRoot.isSome(): + # fluffy light client works only over mainnet data + let + networkData = loadNetworkData("mainnet") + + db = LightClientDb.new(config.dataDir / "lightClientDb") + + lightClientNetwork = LightClientNetwork.new( + d, + db, + streamManager, + networkData.forks, + bootstrapRecords = bootstrapRecords) + + getBeaconTime = networkData.clock.getBeaconTimeFn() + + refDigests = newClone networkData.forks + + lc = LightClient.new( + lightClientNetwork, + rng, + networkData.metadata.cfg, + refDigests, + getBeaconTime, + networkData.genesis_validators_root, + LightClientFinalizationMode.Optimistic + ) + + # TODO: For now just log headers. Ultimately we should also use callbacks for each + # lc object to save them to db and offer them to the network. + proc onFinalizedHeader( + lightClient: LightClient, finalizedHeader: BeaconBlockHeader) = + info "New LC finalized header", + finalized_header = shortLog(finalizedHeader) + + proc onOptimisticHeader( + lightClient: LightClient, optimisticHeader: BeaconBlockHeader) = + info "New LC optimistic header", + optimistic_header = shortLog(optimisticHeader) + + lc.onFinalizedHeader = onFinalizedHeader + lc.onOptimisticHeader = onOptimisticHeader + lc.trustedBlockRoot = config.trustedBlockRoot + + proc onSecond(time: Moment) = + let wallSlot = getBeaconTime().slotOrZero() + # TODO this is a place to enable/disable gossip based on the current status + # of light client + # lc.updateGossipStatus(wallSlot + 1) + + 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 + onSecond(start) + let finished = chronos.now(chronos.Moment) + let processingTime = finished - afterSleep + trace "onSecond task completed", sleepTime, processingTime + + onSecond(Moment.now()) + + lightClientNetwork.start() + lc.start() + + asyncSpawn runOnSecondLoop() + historyNetwork.start() stateNetwork.start() diff --git a/fluffy/network/beacon_light_client/beacon_light_client.nim b/fluffy/network/beacon_light_client/beacon_light_client.nim index ea01d8bf1..f3790c81e 100644 --- a/fluffy/network/beacon_light_client/beacon_light_client.nim +++ b/fluffy/network/beacon_light_client/beacon_light_client.nim @@ -5,8 +5,11 @@ # * 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. +when (NimMajor, NimMinor) < (1, 4): + {.push raises: [Defect].} +else: + {.push raises: [].} -{.push raises: [Defect].} import chronicles, diff --git a/fluffy/network/beacon_light_client/light_client_init_loader.nim b/fluffy/network/beacon_light_client/light_client_init_loader.nim new file mode 100644 index 000000000..b35b7694c --- /dev/null +++ b/fluffy/network/beacon_light_client/light_client_init_loader.nim @@ -0,0 +1,56 @@ +# Nimbus - Portal Network +# 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. + +when (NimMajor, NimMinor) < (1, 4): + {.push raises: [Defect].} +else: + {.push raises: [].} + +import + beacon_chain/networking/network_metadata, + beacon_chain/spec/forks, + beacon_chain/spec/datatypes/altair, + beacon_chain/beacon_clock, + beacon_chain/nimbus_binary_common, + beacon_chain/conf + +type + NetworkInitData* = object + clock*: BeaconClock + metaData*: Eth2NetworkMetadata + forks*: ForkDigests + genesis_validators_root*: Eth2Digest + +proc loadNetworkData*(networkName: string): NetworkInitData {.raises: [CatchableError, Defect].}= + let + metadata = + try: + loadEth2Network(some("mainnet")) + except CatchableError as exc: + raiseAssert(exc.msg) + + genesisState = + try: + template genesisData(): auto = metadata.genesisData + newClone(readSszForkedHashedBeaconState( + metadata.cfg, genesisData.toOpenArrayByte(genesisData.low, genesisData.high))) + except CatchableError as err: + raiseAssert "Invalid baked-in state: " & err.msg + + beaconClock = BeaconClock.init(getStateField(genesisState[], genesis_time)) + + genesis_validators_root = + getStateField(genesisState[], genesis_validators_root) + + forks = newClone ForkDigests.init(metadata.cfg, genesis_validators_root) + + return NetworkInitData( + clock: beaconClock, + metaData: metaData, + forks: forks[], + genesis_validators_root: genesis_validators_root + ) diff --git a/fluffy/rpc/rpc_eth_api.nim b/fluffy/rpc/rpc_eth_api.nim index a3ba26876..24be5860d 100644 --- a/fluffy/rpc/rpc_eth_api.nim +++ b/fluffy/rpc/rpc_eth_api.nim @@ -14,7 +14,11 @@ import eth/[common/eth_types, rlp], ../../nimbus/rpc/[rpc_types, hexstrings, filters], ../../nimbus/transaction, - ../../nimbus/common/chain_config, + # TODO: this is a bit weird but having this import makes beacon_light_client + # to fail compilation due throwing undeclared `CatchableError` in + # `vendor/nimbus-eth2/beacon_chain/spec/keystore.nim`. This is most probably + # caused by `readValue` clashing ? + # ../../nimbus/common/chain_config ../network/history/[history_network, history_content] # Subset of Eth JSON-RPC API: https://eth.wiki/json-rpc/API @@ -190,8 +194,9 @@ proc installEthApiHandlers*( # Supported API through the Portal Network rpcServerWithProxy.rpc("eth_chainId") do() -> HexQuantityStr: - # The Portal Network can only support MainNet at the moment - return encodeQuantity(distinctBase(MainNet.ChainId)) + # The Portal Network can only support MainNet at the moment, so always return + # 1 + return encodeQuantity(uint64(1)) rpcServerWithProxy.rpc("eth_getBlockByHash") do( data: EthHashStr, fullTransactions: bool) -> Option[BlockObject]: