Add basic validation for LC bootstraps + portal_bridge changes (#2527)

- Add basic validation for LC bootstrap gossip, validating either
by trusted block root (only 1) when not synced, or by comparing
with the header of the latest finality update when synced.

- Update portal_bridge beacon to also gossip bootstraps into the
network on each end of epoch.
This commit is contained in:
Kim De Mey 2024-07-25 20:15:26 +02:00 committed by GitHub
parent 254bda365f
commit 7e2a636717
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 100 additions and 31 deletions

View File

@ -40,7 +40,7 @@ type
kv: KvStoreRef
bestUpdates: BestLightClientUpdateStore
forkDigests: ForkDigests
cfg: RuntimeConfig
cfg*: RuntimeConfig
finalityUpdateCache: Opt[LightClientFinalityUpdateCache]
optimisticUpdateCache: Opt[LightClientOptimisticUpdateCache]

View File

@ -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:

View File

@ -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,
)

View File

@ -107,6 +107,7 @@ proc new*(
beaconDb,
streamManager,
networkData.forks,
config.trustedBlockRoot,
bootstrapRecords = bootstrapRecords,
portalConfig = config.portalConfig,
)

View File

@ -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)

View File

@ -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