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:
parent
254bda365f
commit
7e2a636717
|
@ -40,7 +40,7 @@ type
|
|||
kv: KvStoreRef
|
||||
bestUpdates: BestLightClientUpdateStore
|
||||
forkDigests: ForkDigests
|
||||
cfg: RuntimeConfig
|
||||
cfg*: RuntimeConfig
|
||||
finalityUpdateCache: Opt[LightClientFinalityUpdateCache]
|
||||
optimisticUpdateCache: Opt[LightClientOptimisticUpdateCache]
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -107,6 +107,7 @@ proc new*(
|
|||
beaconDb,
|
||||
streamManager,
|
||||
networkData.forks,
|
||||
config.trustedBlockRoot,
|
||||
bootstrapRecords = bootstrapRecords,
|
||||
portalConfig = config.portalConfig,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue