From 11fc2de060815d7f73863b96c95c03b89c6bbb4e Mon Sep 17 00:00:00 2001 From: Kim De Mey Date: Fri, 17 Mar 2023 10:19:17 +0100 Subject: [PATCH] Prepare Fluffy for beacon light client bridge (#1506) --- fluffy/fluffy.nim | 213 ++++++++++-------- .../beacon_light_client.nim | 2 +- .../beacon_light_client_content.nim | 13 +- fluffy/rpc/rpc_calls/rpc_portal_calls.nim | 26 +++ 4 files changed, 157 insertions(+), 97 deletions(-) diff --git a/fluffy/fluffy.nim b/fluffy/fluffy.nim index 8bb89ef64..98a2f8458 100644 --- a/fluffy/fluffy.nim +++ b/fluffy/fluffy.nim @@ -11,7 +11,7 @@ import std/os, confutils, confutils/std/net, chronicles, chronicles/topics_registry, chronos, metrics, metrics/chronos_httpserver, json_rpc/clients/httpclient, - json_rpc/rpcproxy, stew/[byteutils, io2], + json_rpc/rpcproxy, stew/[byteutils, io2, results], eth/keys, eth/net/nat, eth/p2p/discoveryv5/protocol as discv5_protocol, beacon_chain/beacon_clock, @@ -46,6 +46,70 @@ proc initializeBridgeClient(maybeUri: Option[string]): Option[BridgeClient] = notice "Failed to initialize bridge client", error = err.msg return none(BridgeClient) +proc initBeaconLightClient( + network: LightClientNetwork, networkData: NetworkInitData, + trustedBlockRoot: Option[Eth2Digest]): LightClient = + let + getBeaconTime = networkData.clock.getBeaconTimeFn() + + refDigests = newClone networkData.forks + + lc = LightClient.new( + network, + network.portalProtocol.baseProtocol.rng, + networkData.metadata.cfg, + refDigests, + getBeaconTime, + networkData.genesis_validators_root, + LightClientFinalizationMode.Optimistic + ) + + # TODO: For now just log new headers. Ultimately we should also use callbacks + # for each lc object to save them to db and offer them to the network. + # TODO-2: The above statement sounds that this work should really be done at a + # later lower, and these callbacks are rather for use for the "application". + proc onFinalizedHeader( + lightClient: LightClient, finalizedHeader: ForkedLightClientHeader) = + withForkyHeader(finalizedHeader): + when lcDataFork > LightClientDataFork.None: + info "New LC finalized header", + finalized_header = shortLog(forkyHeader) + + proc onOptimisticHeader( + lightClient: LightClient, optimisticHeader: ForkedLightClientHeader) = + withForkyHeader(optimisticHeader): + when lcDataFork > LightClientDataFork.None: + info "New LC optimistic header", + optimistic_header = shortLog(forkyHeader) + + lc.onFinalizedHeader = onFinalizedHeader + lc.onOptimisticHeader = onOptimisticHeader + lc.trustedBlockRoot = 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()) + + # asyncSpawn runOnSecondLoop() + + lc + proc run(config: PortalConf) {.raises: [CatchableError].} = # Make sure dataDir exists let pathExists = createPath(config.dataDir.string) @@ -113,8 +177,10 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = ) streamManager = StreamManager.new(d) - stateNetwork = StateNetwork.new(d, db, streamManager, - bootstrapRecords = bootstrapRecords, portalConfig = portalConfig) + stateNetwork = Opt.some(StateNetwork.new( + d, db, streamManager, + bootstrapRecords = bootstrapRecords, + portalConfig = portalConfig)) accumulator = # Building an accumulator from header epoch files takes > 2m30s and is @@ -132,8 +198,31 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = except SszError as err: raiseAssert "Invalid baked-in accumulator: " & err.msg - historyNetwork = HistoryNetwork.new(d, db, streamManager, accumulator, - bootstrapRecords = bootstrapRecords, portalConfig = portalConfig) + historyNetwork = Opt.some(HistoryNetwork.new( + d, db, streamManager, accumulator, + bootstrapRecords = bootstrapRecords, + portalConfig = portalConfig)) + + beaconLightClient = + # TODO: Currently disabled by default as it is not sufficiently polished. + # Eventually this should be always-on functionality. + if config.trustedBlockRoot.isSome(): + let + # Fluffy works only over mainnet data currently + networkData = loadNetworkData("mainnet") + beaconLightClientDb = LightClientDb.new( + config.dataDir / "lightClientDb") + lightClientNetwork = LightClientNetwork.new( + d, + beaconLightClientDb, + streamManager, + networkData.forks, + bootstrapRecords = bootstrapRecords) + + Opt.some(initBeaconLightClient( + lightClientNetwork, networkData, config.trustedBlockRoot)) + else: + Opt.none(LightClient) # TODO: If no new network key is generated then we should first check if an # enr file exists, and in the case it does read out the seqNum from it and @@ -143,6 +232,8 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = fatal "Failed to write the enr file", file = enrFile quit 1 + + ## Start metrics HTTP server if config.metricsEnabled: let address = config.metricsAddress @@ -155,101 +246,39 @@ proc run(config: PortalConf) {.raises: [CatchableError].} = # TODO: Ideally we don't have the Exception here except Exception as exc: raiseAssert exc.msg + ## Starting the different networks. + d.start() + if stateNetwork.isSome(): + stateNetwork.get().start() + if historyNetwork.isSome(): + historyNetwork.get().start() + if beaconLightClient.isSome(): + let lc = beaconLightClient.get() + lc.network.start() + lc.start() + + ## Starting the JSON-RPC APIs if config.rpcEnabled: let ta = initTAddress(config.rpcAddress, config.rpcPort) var rpcHttpServerWithProxy = RpcProxy.new([ta], config.proxyUri) - rpcHttpServerWithProxy.installEthApiHandlers(historyNetwork) rpcHttpServerWithProxy.installDiscoveryApiHandlers(d) - rpcHttpServerWithProxy.installPortalApiHandlers(stateNetwork.portalProtocol, "state") - rpcHttpServerWithProxy.installPortalApiHandlers(historyNetwork.portalProtocol, "history") - rpcHttpServerWithProxy.installPortalDebugApiHandlers(stateNetwork.portalProtocol, "state") - rpcHttpServerWithProxy.installPortalDebugApiHandlers(historyNetwork.portalProtocol, "history") - # TODO for now we can only proxy to local node (or remote one without ssl) to make it possible - # to call infura https://github.com/status-im/nim-json-rpc/pull/101 needs to get merged for http client to support https/ + if stateNetwork.isSome(): + rpcHttpServerWithProxy.installPortalApiHandlers( + stateNetwork.get().portalProtocol, "state") + if historyNetwork.isSome(): + rpcHttpServerWithProxy.installEthApiHandlers(historyNetwork.get()) + rpcHttpServerWithProxy.installPortalApiHandlers( + historyNetwork.get().portalProtocol, "history") + rpcHttpServerWithProxy.installPortalDebugApiHandlers( + historyNetwork.get().portalProtocol, "history") + if beaconLightClient.isSome(): + rpcHttpServerWithProxy.installPortalApiHandlers( + beaconLightClient.get().network.portalProtocol, "beaconLightClient") + # TODO: Test proxy with remote node over HTTPS waitFor rpcHttpServerWithProxy.start() 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: ForkedLightClientHeader) = - withForkyHeader(finalizedHeader): - when lcDataFork > LightClientDataFork.None: - info "New LC finalized header", - finalized_header = shortLog(forkyHeader) - - proc onOptimisticHeader( - lightClient: LightClient, optimisticHeader: ForkedLightClientHeader) = - withForkyHeader(optimisticHeader): - when lcDataFork > LightClientDataFork.None: - info "New LC optimistic header", - optimistic_header = shortLog(forkyHeader) - - 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() - runForever() when isMainModule: diff --git a/fluffy/network/beacon_light_client/beacon_light_client.nim b/fluffy/network/beacon_light_client/beacon_light_client.nim index 068508b20..af328951a 100644 --- a/fluffy/network/beacon_light_client/beacon_light_client.nim +++ b/fluffy/network/beacon_light_client/beacon_light_client.nim @@ -27,7 +27,7 @@ type gcsafe, raises: [].} LightClient* = ref object - network: LightClientNetwork + network*: LightClientNetwork cfg: RuntimeConfig forkDigests: ref ForkDigests getBeaconTime: GetBeaconTimeFn diff --git a/fluffy/network/beacon_light_client/beacon_light_client_content.nim b/fluffy/network/beacon_light_client/beacon_light_client_content.nim index 1965dd41d..f64a198df 100644 --- a/fluffy/network/beacon_light_client/beacon_light_client_content.nim +++ b/fluffy/network/beacon_light_client/beacon_light_client_content.nim @@ -103,10 +103,15 @@ func decodeSsz*(input: openArray[byte], T: type): Result[T, string] = except SszError as e: err(e.msg) -# TODO: Not sure at this point how this API should look best, but the current -# version is a bit weird as it provides both a Forked object and a forkDigest -# Lets see when we get to used it in the bridge, might require something -# like `forkDigestAtEpoch` instead. +# Yes, this API is odd as you pass a SomeForkedLightClientObject yet still have +# to also pass the ForkDigest. This is because we can't just select the right +# digest through the LightClientDataFork here as LightClientDataFork and +# ConsensusFork are not mapped 1-to-1. There is loss of fork data. +# This means we need to get the ConsensusFork directly, which is possible by +# passing the epoch (slot) from the object through `forkDigestAtEpoch`. This +# however requires the runtime config which is part of the `Eth2Node` object. +# Not something we would like to include as a parameter here, so we stick with +# just passing the forkDigest and doing the work outside of this encode call. func encodeForkedLightClientObject*( obj: SomeForkedLightClientObject, forkDigest: ForkDigest): seq[byte] = diff --git a/fluffy/rpc/rpc_calls/rpc_portal_calls.nim b/fluffy/rpc/rpc_calls/rpc_portal_calls.nim index 73bb4e887..54ac8852d 100644 --- a/fluffy/rpc/rpc_calls/rpc_portal_calls.nim +++ b/fluffy/rpc/rpc_calls/rpc_portal_calls.nim @@ -49,3 +49,29 @@ proc portal_historyRecursiveFindContent(contentKey: string): string proc portal_historyStore(contentKey: string, contentValue: string): bool proc portal_historyLocalContent(contentKey: string): string proc portal_historyGossip(contentKey: string, contentValue: string): int + +## Portal Beacon Light Client Network json-rpc calls +proc portal_beaconLightClientNodeInfo(): NodeInfo +proc portal_beaconLightClientRoutingTableInfo(): RoutingTableInfo +proc portal_beaconLightClientAddEnr(enr: Record): bool +proc portal_beaconLightClientAddEnrs(enrs: seq[Record]): bool +proc portal_beaconLightClientGetEnr(nodeId: NodeId): Record +proc portal_beaconLightClientDeleteEnr(nodeId: NodeId): bool +proc portal_beaconLightClientLookupEnr(nodeId: NodeId): Record +proc portal_beaconLightClientPing(enr: Record): tuple[ + enrSeq: uint64, customPayload: string] +proc portal_beaconLightClientFindNodes(enr: Record): seq[Record] +proc portal_beaconLightClientFindContent(enr: Record, contentKey: string): tuple[ + connectionId: Option[string], + content: Option[string], + enrs: Option[seq[Record]]] +proc portal_beaconLightClientFindContentFull(enr: Record, contentKey: string): tuple[ + content: Option[string], + enrs: Option[seq[Record]]] +proc portal_beaconLightClientOffer( + enr: Record, contentKey: string, contentValue: string): string +proc portal_beaconLightClientRecursiveFindNodes(nodeId: NodeId): seq[Record] +proc portal_beaconLightClientRecursiveFindContent(contentKey: string): string +proc portal_beaconLightClientStore(contentKey: string, contentValue: string): bool +proc portal_beaconLightClientLocalContent(contentKey: string): string +proc portal_beaconLightClientGossip(contentKey: string, contentValue: string): int