From 84d9b9889f045f807faaf13bb98465494f4ebaf9 Mon Sep 17 00:00:00 2001 From: Kim De Mey Date: Thu, 23 Mar 2023 08:49:16 +0100 Subject: [PATCH] Add beacon chain lc content bridging to the Fluffy bridge (#1517) Also delete the old, obsolete bridge that only stores content into the db. --- fluffy/beacon_light_client_bridge.nim | 227 ------------------ .../beacon_light_client_bridge_conf.nim | 162 ------------- fluffy/tools/bridge/beacon_chain_bridge.nim | 159 +++++++++++- .../tools/bridge/beacon_chain_bridge_conf.nim | 12 +- nimbus.nimble | 3 - 5 files changed, 165 insertions(+), 398 deletions(-) delete mode 100644 fluffy/beacon_light_client_bridge.nim delete mode 100644 fluffy/network/beacon_light_client/beacon_light_client_bridge_conf.nim diff --git a/fluffy/beacon_light_client_bridge.nim b/fluffy/beacon_light_client_bridge.nim deleted file mode 100644 index e0fd2dc25..000000000 --- a/fluffy/beacon_light_client_bridge.nim +++ /dev/null @@ -1,227 +0,0 @@ -# Nimbus -# Copyright (c) 2022-2023 Status Research & Development GmbH -# Licensed and distributed under either of -# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). -# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). -# at your option. This file may not be copied, modified, or distributed except according to those terms. - -{.push raises: [].} - -import - std/[os, strutils], - chronicles, chronicles/chronos_tools, chronos, - eth/keys, - beacon_chain/eth1/eth1_monitor, - beacon_chain/gossip_processing/[optimistic_processor, light_client_processor], - beacon_chain/spec/beaconstate, - beacon_chain/spec/datatypes/[phase0, altair, bellatrix], - beacon_chain/[light_client, nimbus_binary_common, version], - "."/network/beacon_light_client/[ - light_client_db, - light_client_network, - light_client_content, - beacon_light_client_bridge_conf - ], - "."/network/wire/[portal_stream, portal_protocol_config, portal_protocol] - -# TODO Find what can throw exception -proc run() {.raises: [Exception, Defect].} = - {.pop.} - var config = makeBannerAndConfig( - "Beacon light client bridge " & fullVersionStr, BridgeConf) - {.push raises: [].} - - # Required as both Eth2Node and LightClient requires correct config type - var lcConfig = config.asLightClientConf() - - setupLogging(config.logLevel, config.logStdout, none(OutFile)) - - notice "Launching Beacon light client bridge", - version = fullVersionStr, cmdParams = commandLineParams(), config - - let - metadata = loadEth2Network(lcConfig.eth2Network) - - for node in metadata.bootstrapNodes: - lcConfig.bootstrapNodes.add node - - template cfg(): auto = metadata.cfg - - let - genesisState = - try: - template genesisData(): auto = metadata.genesisData - newClone(readSszForkedHashedBeaconState( - 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)) - - getBeaconTime = beaconClock.getBeaconTimeFn() - - genesis_validators_root = - getStateField(genesisState[], genesis_validators_root) - - forkDigests = newClone ForkDigests.init(cfg, genesis_validators_root) - - genesisBlockRoot = get_initial_beacon_block(genesisState[]).root - - rng = keys.newRng() - - netKeys = getRandomNetKeys(rng[]) - - network = createEth2Node( - rng, lcConfig, netKeys, cfg, - forkDigests, getBeaconTime, genesis_validators_root - ) - - streamManager = StreamManager.new(network.discovery) - - db = LightClientDb.new(lcConfig.dataDir / "db") - - lcNetwork = LightClientNetwork.new( - network.discovery, - db, - streamManager, - forkDigests[] - ) - - lightClient = createLightClient( - network, rng, lcConfig, cfg, forkDigests, getBeaconTime, - genesis_validators_root, LightClientFinalizationMode.Optimistic) - - info "Listening to incoming network requests" - network.initBeaconSync(cfg, forkDigests, genesisBlockRoot, getBeaconTime) - - lightClient.installMessageValidators() - waitFor network.startListening() - waitFor network.start() - lcNetwork.start() - - 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) - - # TODO Currently the only thing bridge does it to save all lc objects received - # from libp2p network to portal compatible database format. This way portal - # nodes can find this content in the network if bridge node is their neighbour. - # Ultimately bridge node should not only save objects into db, but also actively - # gossip them into the portal light client network. - proc onBootstrap( - lightClient: LightClient, - bootstrap: altair.LightClientBootstrap) = - info "New LC boostrap", - bootstrap, period = bootstrap.header.slot.sync_committee_period - - let - bh = hash_tree_root(bootstrap.header) - contentKey = encode(bootstrapContentKey(bh)) - contentId = toContentId(contentKey) - content = encodeBootstrapForked( - network.forkDigests.altair, - bootstrap - ) - lcNetwork.portalProtocol.storeContent( - contentKey, - contentId, - content - ) - - proc onLCUpdate(lightClient: LightClient, update: altair.LightClientUpdate) = - info "New LC update", - update, period = update.attested_header.slot.sync_committee_period - let - period = update.attested_header.slot.sync_committee_period - contentKey = encode(updateContentKey(period.uint64, uint64(1))) - contentId = toContentId(contentKey) - content = encodeLightClientUpdatesForked( - network.forkDigests.altair, - @[update] - ) - lcNetwork.portalProtocol.storeContent( - contentKey, - contentId, - content - ) - - proc onOptimisticUpdate( - lightClient: LightClient, - optUpdate: altair.LightClientOptimisticUpdate) = - info "New LC optimistic update", - optUpdate, period = optUpdate.attested_header.slot.sync_committee_period - let - slot = optUpdate.attested_header.slot - contentKey = encode(optimisticUpdateContentKey(slot.uint64)) - contentId = toContentId(contentKey) - content = encodeOptimisticUpdateForked( - network.forkDigests.altair, - optUpdate - ) - lcNetwork.portalProtocol.storeContent( - contentKey, - contentId, - content - ) - - proc onFinalityUpdate( - lightClient: LightClient, - finUpdate: altair.LightClientFinalityUpdate) = - info "New LC finality update", - finUpdate, period = finUpdate.attested_header.slot.sync_committee_period - let - finSlot = finUpdate.finalized_header.slot - optSlot = finUpdate.attested_header.slot - contentKey = encode(finalityUpdateContentKey(finSlot.uint64, optSlot.uint64)) - contentId = toContentId(contentKey) - content = encodeFinalityUpdateForked( - network.forkDigests.altair, - finUpdate - ) - lcNetwork.portalProtocol.storeContent( - contentKey, - contentId, - content - ) - - lightClient.onFinalizedHeader = onFinalizedHeader - lightClient.onOptimisticHeader = onOptimisticHeader - lightClient.trustedBlockRoot = some config.trustedBlockRoot - lightClient.bootstrapObserver = onBootstrap - lightClient.updateObserver = onLCUpdate - lightClient.finalityUpdateObserver = onFinalityUpdate - lightClient.optimisticUpdateObserver = onOptimisticUpdate - - proc onSecond(time: Moment) = - let wallSlot = getBeaconTime().slotOrZero() - lightClient.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()) - - lightClient.start() - - asyncSpawn runOnSecondLoop() - - while true: - poll() - -when isMainModule: - run() diff --git a/fluffy/network/beacon_light_client/beacon_light_client_bridge_conf.nim b/fluffy/network/beacon_light_client/beacon_light_client_bridge_conf.nim deleted file mode 100644 index 6be939bdd..000000000 --- a/fluffy/network/beacon_light_client/beacon_light_client_bridge_conf.nim +++ /dev/null @@ -1,162 +0,0 @@ -# beacon light client bridge -# Copyright (c) 2022-2023 Status Research & Development GmbH -# Licensed and distributed under either of -# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). -# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). -# at your option. This file may not be copied, modified, or distributed except according to those terms. - -{.push raises: [].} - -import - std/os, - json_serialization/std/net, - beacon_chain/light_client, - beacon_chain/conf - -export net, conf - -proc defaultBridgeDataDir*(): string = - let dataDir = when defined(windows): - "AppData" / "Roaming" / "BeaconLightClientBridge" - elif defined(macosx): - "Library" / "Application Support" / "BeaconLightClientBridge" - else: - ".cache" / "beacon-ligh-client-bridge" - - getHomeDir() / dataDir - -const - defaultBridgeDataDirDesc* = defaultBridgeDataDir() - -type BridgeConf* = object - # Config - configFile* {. - 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 - - 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 - - # Storage - dataDir* {. - desc: "The directory where beacon light client bridge will store all data" - defaultValue: defaultBridgeDataDir() - defaultValueDesc: $defaultBridgeDataDirDesc - abbr: "d" - name: "data-dir" .}: OutDir - - # Consensus light sync - # No default - Needs to be provided by the user - trustedBlockRoot* {. - desc: "Recent trusted finalized block root to initialize the consensus light client from" - name: "trusted-block-root" .}: Eth2Digest - - # 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" - defaultValue: defaultListenAddress - defaultValueDesc: $defaultListenAddressDesc - name: "listen-address" .}: ValidIpAddress - - 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] - - -func asLightClientConf*(pc: BridgeConf): LightClientConf = - return LightClientConf( - configFile: pc.configFile, - logLevel: pc.logLevel, - logStdout: pc.logStdout, - logFile: none(OutFile), - dataDir: pc.dataDir, - # Portal networks are defined only over mainnet therefore bridging makes - # sense only for mainnet - eth2Network: some("mainnet"), - 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: @[], - jwtSecret: none(string), - stopAtEpoch: 0 - ) diff --git a/fluffy/tools/bridge/beacon_chain_bridge.nim b/fluffy/tools/bridge/beacon_chain_bridge.nim index 67594f968..1fefe5d5d 100644 --- a/fluffy/tools/bridge/beacon_chain_bridge.nim +++ b/fluffy/tools/bridge/beacon_chain_bridge.nim @@ -82,6 +82,7 @@ import libp2p/protocols/pubsub/errors, ../../rpc/portal_rpc_client, ../../network/history/history_content, + ../../network/beacon_light_client/beacon_light_client_content, ../../common/common_types, ./beacon_chain_bridge_conf @@ -157,6 +158,10 @@ proc asPortalBlockData*( (hash, headerWithProof, body) +func forkDigestAtEpoch( + forkDigests: ForkDigests, epoch: Epoch, cfg: RuntimeConfig): ForkDigest = + forkDigests.atEpoch(epoch, cfg) + proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = # Required as both Eth2Node and LightClient requires correct config type var lcConfig = config.asLightClientConf() @@ -225,12 +230,12 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = blockhash = history_content.`$`hash block: # gossip header - let contentKey = ContentKey.init(blockHeader, hash) + let contentKey = history_content.ContentKey.init(blockHeader, hash) let encodedContentKey = contentKey.encode.asSeq() try: let peers = await rpcHttpclient.portal_historyGossip( - encodedContentKey.toHex(), + toHex(encodedContentKey), SSZ.encode(headerWithProof).toHex()) info "Block header gossiped", peers, contentKey = encodedContentKey.toHex() @@ -245,7 +250,7 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = await sleepAsync(1.seconds) block: # gossip block - let contentKey = ContentKey.init(blockBody, hash) + let contentKey = history_content.ContentKey.init(blockBody, hash) let encodedContentKey = contentKey.encode.asSeq() try: @@ -267,6 +272,148 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = network, rng, lcConfig, cfg, forkDigests, getBeaconTime, genesis_validators_root, LightClientFinalizationMode.Optimistic) + ### Beacon Light Client content bridging specific callbacks + proc onBootstrap( + lightClient: LightClient, + bootstrap: ForkedLightClientBootstrap) = + withForkyObject(bootstrap): + when lcDataFork > LightClientDataFork.None: + info "New Beacon LC bootstrap", + forkyObject, slot = forkyObject.header.beacon.slot + + let + root = hash_tree_root(forkyObject.header) + contentKey = encode(bootstrapContentKey(root)) + contentId = beacon_light_client_content.toContentId(contentKey) + forkDigest = forkDigestAtEpoch( + forkDigests[], epoch(forkyObject.header.beacon.slot), cfg) + content = encodeBootstrapForked( + forkDigest, + bootstrap + ) + + proc GossipRpcAndClose() {.async.} = + try: + let + contentKeyHex = contentKey.asSeq().toHex() + peers = await rpcHttpclient.portal_beaconLightClientGossip( + contentKeyHex, + content.toHex()) + info "Beacon LC bootstrap gossiped", peers, + contentKey = contentKeyHex + except CatchableError as e: + error "JSON-RPC error", error = $e.msg + + await rpcHttpclient.close() + + asyncSpawn(GossipRpcAndClose()) + + proc onUpdate(lightClient: LightClient, update: ForkedLightClientUpdate) = + withForkyObject(update): + when lcDataFork > LightClientDataFork.None: + info "New Beacon LC update", + update, slot = forkyObject.attested_header.beacon.slot + + let + period = forkyObject.attested_header.beacon.slot.sync_committee_period + contentKey = encode(updateContentKey(period.uint64, uint64(1))) + contentId = beacon_light_client_content.toContentId(contentKey) + forkDigest = forkDigestAtEpoch( + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) + content = encodeLightClientUpdatesForked( + forkDigest, + @[update] + ) + + proc GossipRpcAndClose() {.async.} = + try: + let + contentKeyHex = contentKey.asSeq().toHex() + peers = await rpcHttpclient.portal_beaconLightClientGossip( + contentKeyHex, + content.toHex()) + info "Beacon LC bootstrap gossiped", peers, + contentKey = contentKeyHex + except CatchableError as e: + error "JSON-RPC error", error = $e.msg + + await rpcHttpclient.close() + + asyncSpawn(GossipRpcAndClose()) + + proc onOptimisticUpdate( + lightClient: LightClient, + update: ForkedLightClientOptimisticUpdate) = + withForkyObject(update): + when lcDataFork > LightClientDataFork.None: + info "New Beacon LC optimistic update", + update, slot = forkyObject.attested_header.beacon.slot + + let + slot = forkyObject.attested_header.beacon.slot + contentKey = encode(optimisticUpdateContentKey(slot.uint64)) + contentId = beacon_light_client_content.toContentId(contentKey) + forkDigest = forkDigestAtEpoch( + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) + content = encodeOptimisticUpdateForked( + forkDigest, + update + ) + + proc GossipRpcAndClose() {.async.} = + try: + let + contentKeyHex = contentKey.asSeq().toHex() + peers = await rpcHttpclient.portal_beaconLightClientGossip( + contentKeyHex, + content.toHex()) + info "Beacon LC bootstrap gossiped", peers, + contentKey = contentKeyHex + except CatchableError as e: + error "JSON-RPC error", error = $e.msg + + await rpcHttpclient.close() + + asyncSpawn(GossipRpcAndClose()) + + proc onFinalityUpdate( + lightClient: LightClient, + update: ForkedLightClientFinalityUpdate) = + withForkyObject(update): + when lcDataFork > LightClientDataFork.None: + info "New Beacon LC finality update", + update, slot = forkyObject.attested_header.beacon.slot + let + finalizedSlot = forkyObject.finalized_header.beacon.slot + optimisticSlot = forkyObject.attested_header.beacon.slot + contentKey = encode(finalityUpdateContentKey( + finalizedSlot.uint64, optimisticSlot.uint64)) + contentId = beacon_light_client_content.toContentId(contentKey) + forkDigest = forkDigestAtEpoch( + forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg) + content = encodeFinalityUpdateForked( + forkDigest, + update + ) + + proc GossipRpcAndClose() {.async.} = + try: + let + contentKeyHex = contentKey.asSeq().toHex() + peers = await rpcHttpclient.portal_beaconLightClientGossip( + contentKeyHex, + content.toHex()) + info "Beacon LC bootstrap gossiped", peers, + contentKey = contentKeyHex + except CatchableError as e: + error "JSON-RPC error", error = $e.msg + + await rpcHttpclient.close() + + asyncSpawn(GossipRpcAndClose()) + + ### + waitFor rpcHttpclient.connect(config.rpcAddress, Port(config.rpcPort), false) info "Listening to incoming network requests" @@ -310,6 +457,12 @@ proc run(config: BeaconBridgeConf) {.raises: [CatchableError].} = lightClient.onOptimisticHeader = onOptimisticHeader lightClient.trustedBlockRoot = some config.trustedBlockRoot + if config.beaconLightClient: + lightClient.bootstrapObserver = onBootstrap + lightClient.updateObserver = onUpdate + lightClient.finalityUpdateObserver = onFinalityUpdate + lightClient.optimisticUpdateObserver = onOptimisticUpdate + func shouldSyncOptimistically(wallSlot: Slot): bool = let optimisticHeader = lightClient.optimisticHeader withForkyHeader(optimisticHeader): diff --git a/fluffy/tools/bridge/beacon_chain_bridge_conf.nim b/fluffy/tools/bridge/beacon_chain_bridge_conf.nim index 81bee94fe..6c8adab7f 100644 --- a/fluffy/tools/bridge/beacon_chain_bridge_conf.nim +++ b/fluffy/tools/bridge/beacon_chain_bridge_conf.nim @@ -18,11 +18,11 @@ export net, conf proc defaultVerifiedProxyDataDir*(): string = let dataDir = when defined(windows): - "AppData" / "Roaming" / "NimbusVerifiedProxy" + "AppData" / "Roaming" / "FluffyBeaconChainBridge" elif defined(macosx): - "Library" / "Application Support" / "NimbusVerifiedProxy" + "Library" / "Application Support" / "FluffyBeaconChainBridge" else: - ".cache" / "nimbus-verified-proxy" + ".cache" / "fluffy-beacon-chain-bridge" getHomeDir() / dataDir @@ -64,6 +64,12 @@ type BeaconBridgeConf* = object abbr: "d" name: "data-dir" .}: OutDir + # Bridge options + beaconLightClient* {. + desc: "Enable beacon light client content bridging" + defaultValue: false + name: "beacon-light-client" .}: bool + # Network eth2Network* {. desc: "The Eth2 network to join" diff --git a/nimbus.nimble b/nimbus.nimble index 6050f0e2a..155bb0280 100644 --- a/nimbus.nimble +++ b/nimbus.nimble @@ -74,9 +74,6 @@ task test_evm, "Run EVM tests": task fluffy, "Build fluffy": buildBinary "fluffy", "fluffy/", "-d:chronicles_log_level=TRACE -d:chronosStrictException -d:PREFER_BLST_SHA256=false" -task lc_bridge, "Build light client bridge": - buildBinary "beacon_light_client_bridge", "fluffy/", "-d:chronicles_log_level=TRACE -d:chronosStrictException -d:PREFER_BLST_SHA256=false -d:libp2p_pki_schemes=secp256k1" - task fluffy_test, "Run fluffy tests": # Need the nimbus_db_backend in state network tests as we need a Hexary to # start from, even though it only uses the MemoryDb.