diff --git a/fluffy/network/beacon/beacon_db.nim b/fluffy/network/beacon/beacon_db.nim index a578efb03..ebd2d70e7 100644 --- a/fluffy/network/beacon/beacon_db.nim +++ b/fluffy/network/beacon/beacon_db.nim @@ -40,7 +40,7 @@ type kv: KvStoreRef bestUpdates: BestLightClientUpdateStore forkDigests: ForkDigests - cfg: RuntimeConfig + cfg*: RuntimeConfig finalityUpdateCache: Opt[LightClientFinalityUpdateCache] optimisticUpdateCache: Opt[LightClientOptimisticUpdateCache] diff --git a/fluffy/network/beacon/beacon_network.nim b/fluffy/network/beacon/beacon_network.nim index 4fc0ff145..83e786a21 100644 --- a/fluffy/network/beacon/beacon_network.nim +++ b/fluffy/network/beacon/beacon_network.nim @@ -16,7 +16,7 @@ import beacon_chain/spec/datatypes/[phase0, altair, bellatrix], beacon_chain/gossip_processing/light_client_processor, ../wire/[portal_protocol, portal_stream, portal_protocol_config], - "."/[beacon_content, beacon_db, beacon_chain_historical_summaries] + "."/[beacon_content, beacon_db, beacon_validation, beacon_chain_historical_summaries] export beacon_content, beacon_db @@ -29,6 +29,7 @@ type BeaconNetwork* = ref object processor*: ref LightClientProcessor contentQueue*: AsyncQueue[(Opt[NodeId], ContentKeysList, seq[seq[byte]])] forkDigests*: ForkDigests + trustedBlockRoot: Opt[Eth2Digest] processContentLoop: Future[void] func toContentIdHandler(contentKey: ContentKeyByteList): results.Opt[ContentId] = @@ -71,9 +72,9 @@ proc getContent( if contentRes.isNone(): warn "Failed fetching content from the beacon chain network", contentKey = contentKeyEncoded - return Opt.none(seq[byte]) + Opt.none(seq[byte]) else: - return Opt.some(contentRes.value().content) + Opt.some(contentRes.value().content) proc getLightClientBootstrap*( n: BeaconNetwork, trustedRoot: Digest @@ -113,11 +114,11 @@ proc getLightClientUpdatesByRange*( decodingResult = decodeLightClientUpdatesByRange(n.forkDigests, updates) if decodingResult.isErr(): - return Opt.none(ForkedLightClientUpdateList) + Opt.none(ForkedLightClientUpdateList) else: # TODO Not doing validation for now, as probably it should be done by layer # above - return Opt.some(decodingResult.value()) + Opt.some(decodingResult.value()) proc getLightClientFinalityUpdate*( n: BeaconNetwork, finalizedSlot: uint64 @@ -159,9 +160,9 @@ proc getLightClientOptimisticUpdate*( decodeLightClientOptimisticUpdateForked(n.forkDigests, optimisticUpdate) if decodingResult.isErr(): - return Opt.none(ForkedLightClientOptimisticUpdate) + Opt.none(ForkedLightClientOptimisticUpdate) else: - return Opt.some(decodingResult.value()) + Opt.some(decodingResult.value()) proc getHistoricalSummaries*( n: BeaconNetwork, epoch: uint64 @@ -175,9 +176,9 @@ proc getHistoricalSummaries*( return Opt.none(HistoricalSummaries) if n.validateHistoricalSummaries(summariesWithProof).isOk(): - return Opt.some(summariesWithProof.historical_summaries) + Opt.some(summariesWithProof.historical_summaries) else: - return Opt.none(HistoricalSummaries) + Opt.none(HistoricalSummaries) proc new*( T: type BeaconNetwork, @@ -186,6 +187,7 @@ proc new*( beaconDb: BeaconDb, streamManager: StreamManager, forkDigests: ForkDigests, + trustedBlockRoot: Opt[Eth2Digest], bootstrapRecords: openArray[Record] = [], portalConfig: PortalProtocolConfig = defaultPortalProtocolConfig, ): T = @@ -220,6 +222,7 @@ proc new*( beaconDb: beaconDb, contentQueue: contentQueue, forkDigests: forkDigests, + trustedBlockRoot: trustedBlockRoot, ) proc validateContent( @@ -232,22 +235,47 @@ proc validateContent( of unused: raiseAssert "Should not be used and fail at decoding" of lightClientBootstrap: - let decodingResult = decodeLightClientBootstrapForked(n.forkDigests, content) - if decodingResult.isOk: - # TODO: - # Currently only verifying if the content can be decoded. - # Later on we need to either provide a list of acceptable bootstraps (not - # really scalable and requires quite some configuration) or find some - # way to proof these. - # They could be proven at moment of creation by checking finality update - # its finalized_header. And verifying the current_sync_committee with the - # header state root and current_sync_committee_branch? - # Perhaps can be expanded to being able to verify back fill by storing - # also the past beacon headers (This is sorta stored in a proof format - # for history network also) - ok() - else: - err("Error decoding content: " & decodingResult.error) + let bootstrap = decodeLightClientBootstrapForked(n.forkDigests, content).valueOr: + return err("Error decoding bootstrap: " & error) + + withForkyBootstrap(bootstrap): + when lcDataFork > LightClientDataFork.None: + # Try getting last finality update from db. If the node is LC synced + # this data should be there. Then check is done to see if the headers + # are the same. + # Note that this will only work for newly created LC bootstraps. If + # backfill of bootstraps is to be supported, they need to be provided + # with a proof against historical summaries. + # See also: + # https://github.com/ethereum/portal-network-specs/issues/296 + let finalityUpdate = n.beaconDb.getLastFinalityUpdate() + if finalityUpdate.isOk(): + withForkyFinalityUpdate(finalityUpdate.value): + when lcDataFork > LightClientDataFork.None: + if forkyFinalityUpdate.finalized_header.beacon != + forkyBootstrap.header.beacon: + return err("Bootstrap header does not match recent finalized header") + + if forkyBootstrap.isValidBootstrap(n.beaconDb.cfg): + ok() + else: + err("Error validating LC bootstrap") + else: + err("No LC data before Altair") + elif n.trustedBlockRoot.isSome(): + # If not yet synced, try trusted block root + let blockRoot = hash_tree_root(forkyBootstrap.header.beacon) + if blockRoot != n.trustedBlockRoot.get(): + return err("Bootstrap header does not match trusted block root") + + if forkyBootstrap.isValidBootstrap(n.beaconDb.cfg): + ok() + else: + err("Error validating LC bootstrap") + else: + err("Cannot validate LC bootstrap") + else: + err("No LC data before Altair") of lightClientUpdate: let decodingResult = decodeLightClientUpdatesByRange(n.forkDigests, content) if decodingResult.isOk: diff --git a/fluffy/network/beacon/beacon_validation.nim b/fluffy/network/beacon/beacon_validation.nim new file mode 100644 index 000000000..9724221b6 --- /dev/null +++ b/fluffy/network/beacon/beacon_validation.nim @@ -0,0 +1,26 @@ +# Fluffy +# Copyright (c) 2024 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 + stew/bitops2, + beacon_chain/spec/presets, + beacon_chain/spec/forks, + beacon_chain/spec/forks_light_client + +func isValidBootstrap*(bootstrap: ForkyLightClientBootstrap, cfg: RuntimeConfig): bool = + ## Verify if the bootstrap is valid. This does not verify if the header is + ## part of the canonical chain. + is_valid_light_client_header(bootstrap.header, cfg) and + is_valid_merkle_branch( + hash_tree_root(bootstrap.current_sync_committee), + bootstrap.current_sync_committee_branch, + log2trunc(altair.CURRENT_SYNC_COMMITTEE_GINDEX), + get_subtree_index(altair.CURRENT_SYNC_COMMITTEE_GINDEX), + bootstrap.header.beacon.state_root, + ) diff --git a/fluffy/portal_node.nim b/fluffy/portal_node.nim index 6cd71372c..9382e726f 100644 --- a/fluffy/portal_node.nim +++ b/fluffy/portal_node.nim @@ -107,6 +107,7 @@ proc new*( beaconDb, streamManager, networkData.forks, + config.trustedBlockRoot, bootstrapRecords = bootstrapRecords, portalConfig = config.portalConfig, ) diff --git a/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim b/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim index 1015b889c..96a71d514 100644 --- a/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim +++ b/fluffy/tests/beacon_network_tests/beacon_test_helpers.nim @@ -24,8 +24,14 @@ proc newLCNode*( node = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(port)) db = BeaconDb.new(networkData, "", inMemory = true) streamManager = StreamManager.new(node) - network = - BeaconNetwork.new(PortalNetwork.none, node, db, streamManager, networkData.forks) + network = BeaconNetwork.new( + PortalNetwork.none, + node, + db, + streamManager, + networkData.forks, + Opt.none(Eth2Digest), + ) return BeaconNode(discoveryProtocol: node, beaconNetwork: network) diff --git a/fluffy/tools/portal_bridge/portal_bridge_beacon.nim b/fluffy/tools/portal_bridge/portal_bridge_beacon.nim index d57d87522..0263380a8 100644 --- a/fluffy/tools/portal_bridge/portal_bridge_beacon.nim +++ b/fluffy/tools/portal_bridge/portal_bridge_beacon.nim @@ -140,7 +140,7 @@ proc gossipLCFinalityUpdate( portalRpcClient: RpcClient, cfg: RuntimeConfig, forkDigests: ref ForkDigests, -): Future[Result[Slot, string]] {.async.} = +): Future[Result[(Slot, Eth2Digest), string]] {.async.} = var update = try: info "Downloading LC finality update" @@ -155,6 +155,7 @@ proc gossipLCFinalityUpdate( when lcDataFork > LightClientDataFork.None: let finalizedSlot = forkyObject.finalized_header.beacon.slot + blockRoot = hash_tree_root(forkyObject.finalized_header.beacon) contentKey = encode(finalityUpdateContentKey(finalizedSlot.uint64)) forkDigest = forkDigestAtEpoch( forkDigests[], epoch(forkyObject.attested_header.beacon.slot), cfg @@ -176,7 +177,7 @@ proc gossipLCFinalityUpdate( let res = await GossipRpcAndClose() if res.isOk(): - return ok(finalizedSlot) + return ok((finalizedSlot, blockRoot)) else: return err(res.error) else: @@ -394,7 +395,14 @@ proc runBeacon*(config: PortalBridgeConf) {.raises: [CatchableError].} = if res.isErr(): warn "Error gossiping LC finality update", error = res.error else: - lastFinalityUpdateEpoch = epoch(res.get()) + let (slot, blockRoot) = res.value() + lastFinalityUpdateEpoch = epoch(slot) + let res = await gossipLCBootstrapUpdate( + restClient, portalRpcClient, blockRoot, cfg, forkDigests + ) + + if res.isErr(): + warn "Error gossiping LC bootstrap", error = res.error let res2 = await gossipHistoricalSummaries( restClient, portalRpcClient, cfg, forkDigests