From 270ce41d5c6aa214d697237c91852e52a9f7b121 Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Tue, 27 Dec 2022 15:25:20 +0100 Subject: [PATCH] Add light client bridge (#1386) * Add light client bridge binary --- Makefile | 4 + fluffy/beacon_light_client_bridge.nim | 230 ++++++++++++++++++ .../beacon_light_client_bridge_conf.nim | 165 +++++++++++++ .../light_client_content.nim | 13 + nimbus.nimble | 3 + 5 files changed, 415 insertions(+) create mode 100644 fluffy/beacon_light_client_bridge.nim create mode 100644 fluffy/network/beacon_light_client/beacon_light_client_bridge_conf.nim diff --git a/Makefile b/Makefile index ef7774d3b..8545e9e83 100644 --- a/Makefile +++ b/Makefile @@ -203,6 +203,10 @@ fluffy: | build deps echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim fluffy $(NIM_PARAMS) nimbus.nims +lc-bridge: | build deps + echo -e $(BUILD_MSG) "build/$@" && \ + $(ENV_SCRIPT) nim lc_bridge $(NIM_PARAMS) nimbus.nims + # primitive reproducibility test fluffy-test-reproducibility: + [ -e build/fluffy ] || $(MAKE) V=0 fluffy; \ diff --git a/fluffy/beacon_light_client_bridge.nim b/fluffy/beacon_light_client_bridge.nim new file mode 100644 index 000000000..07952ab52 --- /dev/null +++ b/fluffy/beacon_light_client_bridge.nim @@ -0,0 +1,230 @@ +# Nimbus +# 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 + 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: [Defect].} + + # 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 new file mode 100644 index 000000000..83ca1c506 --- /dev/null +++ b/fluffy/network/beacon_light_client/beacon_light_client_bridge_conf.nim @@ -0,0 +1,165 @@ +# beacon light client bridge +# 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 + 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/network/beacon_light_client/light_client_content.nim b/fluffy/network/beacon_light_client/light_client_content.nim index f2a1b5d7a..b1d042207 100644 --- a/fluffy/network/beacon_light_client/light_client_content.nim +++ b/fluffy/network/beacon_light_client/light_client_content.nim @@ -226,6 +226,19 @@ proc decodeLightClientUpdatesForked*( return ok(updates) + +func bootstrapContentKey*(bh: Digest): ContentKey = + ContentKey( + contentType: lightClientBootstrap, + lightClientBootstrapKey: LightClientBootstrapKey(blockHash: bh) + ) + +func updateContentKey*(startPeriod: uint64, count: uint64): ContentKey = + ContentKey( + contentType: lightClientUpdate, + lightClientUpdateKey: LightClientUpdateKey(startPeriod: startPeriod, count: count) + ) + func finalityUpdateContentKey*(finalSlot: uint64, optimisticSlot: uint64): ContentKey = ContentKey( contentType: lightClientFinalityUpdate, diff --git a/nimbus.nimble b/nimbus.nimble index 679227e90..c3eec8cc1 100644 --- a/nimbus.nimble +++ b/nimbus.nimble @@ -74,6 +74,9 @@ 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.