introduce types for LC merkle proofs (#3808)

Merkle proofs tend to have long underlying type definitions, e.g.,
`array[log2trunc(NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]`. For the
ones used in the LC sync protocol, dedicated types are introduced
to improve readability. Furthermore, the `CachedLightClientBootstrap`
wrapper that solely wrapped a merkle branch is eliminated.
This commit is contained in:
Etan Kissling 2022-06-28 07:52:23 +02:00 committed by GitHub
parent fd2040ae04
commit e8e9ce1aab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 59 additions and 72 deletions

View File

@ -37,20 +37,11 @@ type
CachedLightClientData* = object
## Cached data from historical non-finalized states to improve speed when
## creating future `LightClientUpdate` and `LightClientBootstrap` instances.
current_sync_committee_branch*:
array[log2trunc(altair.CURRENT_SYNC_COMMITTEE_INDEX), Eth2Digest]
next_sync_committee_branch*:
array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
current_sync_committee_branch*: altair.CurrentSyncCommitteeBranch
next_sync_committee_branch*: altair.NextSyncCommitteeBranch
finalized_slot*: Slot
finality_branch*:
array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest]
CachedLightClientBootstrap* = object
## Cached data from historical finalized epoch boundary blocks to improve
## speed when creating future `LightClientBootstrap` instances.
current_sync_committee_branch*:
array[log2trunc(altair.CURRENT_SYNC_COMMITTEE_INDEX), Eth2Digest]
finality_branch*: altair.FinalityBranch
LightClientDataCache* = object
data*: Table[BlockId, CachedLightClientData]
@ -58,19 +49,19 @@ type
## Key is the block ID of which the post state was used to get the data.
## Data stored for the finalized head block and all non-finalized blocks.
bootstrap*: Table[Slot, CachedLightClientBootstrap]
currentBranches*: Table[Slot, altair.CurrentSyncCommitteeBranch]
## Cached data for creating future `LightClientBootstrap` instances.
## Key is the block slot of which the post state was used to get the data.
## Data stored for all finalized epoch boundary blocks.
best*: Table[SyncCommitteePeriod, altair.LightClientUpdate]
bestUpdates*: Table[SyncCommitteePeriod, altair.LightClientUpdate]
## Stores the `LightClientUpdate` with the most `sync_committee_bits` per
## `SyncCommitteePeriod`. Sync committee finality gives precedence.
pendingBest*:
Table[(SyncCommitteePeriod, Eth2Digest), altair.LightClientUpdate]
## Same as `best`, but for `SyncCommitteePeriod` with not yet finalized
## `next_sync_committee`. Key is `(attested_period,
## Same as `bestUpdates`, but for `SyncCommitteePeriod` with not yet
## finalized `next_sync_committee`. Key is `(attested_period,
## hash_tree_root(current_sync_committee | next_sync_committee)`.
latest*: altair.LightClientFinalityUpdate
@ -85,7 +76,7 @@ type
# Light client data
cache*: LightClientDataCache
## Cached data to accelerate serving light client data
## Cached data to accelerate creating light client data
# -----------------------------------
# Config

View File

@ -137,13 +137,13 @@ func initLightClientDataStore*(
onLightClientOptimisticUpdate: onLCOptimisticUpdateCb)
func targetLightClientTailSlot(dag: ChainDAGRef): Slot =
## Earliest slot for which light client data is retained.
let
maxPeriods = dag.lcDataStore.maxPeriods
headPeriod = dag.head.slot.sync_committee_period
lowSlot = max(dag.tail.slot, dag.cfg.ALTAIR_FORK_EPOCH.start_slot)
tail = max(headPeriod + 1, maxPeriods.SyncCommitteePeriod) - maxPeriods
max(tail.start_slot, lowSlot)
## Earliest slot for which light client data is retained.
let
maxPeriods = dag.lcDataStore.maxPeriods
headPeriod = dag.head.slot.sync_committee_period
lowSlot = max(dag.tail.slot, dag.cfg.ALTAIR_FORK_EPOCH.start_slot)
tail = max(headPeriod + 1, maxPeriods.SyncCommitteePeriod) - maxPeriods
max(tail.start_slot, lowSlot)
func handleUnexpectedLightClientError(dag: ChainDAGRef, buggedSlot: Slot) =
## If there is an unexpected error, adjust `tailSlot` to keep track of the
@ -188,17 +188,16 @@ proc initLightClientBootstrapForPeriod(
bid = bsi.bid
boundarySlot = bid.slot.nextEpochBoundarySlot
if boundarySlot == nextBoundarySlot and bid.slot >= lowSlot and
not dag.lcDataStore.cache.bootstrap.hasKey(bid.slot):
not dag.lcDataStore.cache.currentBranches.hasKey(bid.slot):
if not dag.updateExistingState(
tmpState[], bid.atSlot, save = false, tmpCache):
dag.handleUnexpectedLightClientError(bid.slot)
continue
var cachedBootstrap {.noinit.}: CachedLightClientBootstrap
cachedBootstrap.current_sync_committee_branch = withState(tmpState[]):
let branch = withState(tmpState[]):
when stateFork >= BeaconStateFork.Altair:
state.data.build_proof(altair.CURRENT_SYNC_COMMITTEE_INDEX).get
else: raiseAssert "Unreachable"
dag.lcDataStore.cache.bootstrap[bid.slot] = cachedBootstrap
dag.lcDataStore.cache.currentBranches[bid.slot] = branch
proc initLightClientUpdateForPeriod(
dag: ChainDAGRef, period: SyncCommitteePeriod) =
@ -207,7 +206,7 @@ proc initLightClientUpdateForPeriod(
## Non-finalized blocks are processed incrementally.
if not dag.isNextSyncCommitteeFinalized(period):
return
if dag.lcDataStore.cache.best.hasKey(period):
if dag.lcDataStore.cache.bestUpdates.hasKey(period):
return
let startTick = Moment.now()
@ -217,7 +216,7 @@ proc initLightClientUpdateForPeriod(
# replicated on every `return`, and the log statement allocates another
# copy of the arguments on the stack for each instantiation (~1 MB stack!)
debug "Best historic LC update computed",
period, update = dag.lcDataStore.cache.best.getOrDefault(period),
period, update = dag.lcDataStore.cache.bestUpdates.getOrDefault(period),
computeDur = endTick - startTick
defer: logBest()
@ -271,7 +270,8 @@ proc initLightClientUpdateForPeriod(
maxParticipantsRes = dag.maxParticipantsBlock(highBid, lowSlot)
maxParticipantsBid = maxParticipantsRes.bid.valueOr:
if maxParticipantsRes.ok: # No single valid block exists in the period
dag.lcDataStore.cache.best[period] = default(altair.LightClientUpdate)
dag.lcDataStore.cache.bestUpdates[period] =
default(altair.LightClientUpdate)
return
# The block with highest participation may refer to a `finalized_checkpoint`
@ -360,7 +360,7 @@ proc initLightClientUpdateForPeriod(
update.sync_aggregate = blck.asSigned().message.body.sync_aggregate
else: raiseAssert "Unreachable"
update.signature_slot = signatureBid.slot
dag.lcDataStore.cache.best[period] = update
dag.lcDataStore.cache.bestUpdates[period] = update
proc getLightClientData(
dag: ChainDAGRef,
@ -507,7 +507,7 @@ proc createLightClientUpdates(
let isCommitteeFinalized = dag.isNextSyncCommitteeFinalized(attested_period)
var best =
if isCommitteeFinalized:
dag.lcDataStore.cache.best.getOrDefault(attested_period)
dag.lcDataStore.cache.bestUpdates.getOrDefault(attested_period)
else:
let key = (attested_period, state.syncCommitteeRoot)
dag.lcDataStore.cache.pendingBest.getOrDefault(key)
@ -543,7 +543,7 @@ proc createLightClientUpdates(
best.signature_slot = signature_slot
if isCommitteeFinalized:
dag.lcDataStore.cache.best[attested_period] = best
dag.lcDataStore.cache.bestUpdates[attested_period] = best
debug "Best LC update improved", period = attested_period, update = best
else:
let key = (attested_period, state.syncCommitteeRoot)
@ -683,7 +683,7 @@ proc processHeadChangeForLightClient*(dag: ChainDAGRef) =
if dag.head.slot < dag.lcDataStore.cache.tailSlot:
return
# Update `best` from `pendingBest` to ensure light client data
# Update `bestUpdates` from `pendingBest` to ensure light client data
# only refers to sync committees as selected by fork choice
let headPeriod = dag.head.slot.sync_committee_period
if not dag.isNextSyncCommitteeFinalized(headPeriod):
@ -699,12 +699,12 @@ proc processHeadChangeForLightClient*(dag: ChainDAGRef) =
dag.handleUnexpectedLightClientError(period.start_slot)
continue
key = (period, syncCommitteeRoot)
dag.lcDataStore.cache.best[period] =
dag.lcDataStore.cache.bestUpdates[period] =
dag.lcDataStore.cache.pendingBest.getOrDefault(key)
withState(dag.headState): # Common case separate to avoid `tmpState` copy
when stateFork >= BeaconStateFork.Altair:
let key = (headPeriod, state.syncCommitteeRoot)
dag.lcDataStore.cache.best[headPeriod] =
dag.lcDataStore.cache.bestUpdates[headPeriod] =
dag.lcDataStore.cache.pendingBest.getOrDefault(key)
else: raiseAssert "Unreachable" # `tailSlot` cannot be before Altair
@ -731,10 +731,8 @@ proc processFinalizationForLightClient*(
break
bid = bsi.bid
if bid.slot >= lowSlot:
dag.lcDataStore.cache.bootstrap[bid.slot] =
CachedLightClientBootstrap(
current_sync_committee_branch:
dag.getLightClientData(bid).current_sync_committee_branch)
dag.lcDataStore.cache.currentBranches[bid.slot] =
dag.getLightClientData(bid).current_sync_committee_branch
boundarySlot = bid.slot.nextEpochBoundarySlot
if boundarySlot < SLOTS_PER_EPOCH:
break
@ -755,19 +753,19 @@ proc processFinalizationForLightClient*(
# Prune bootstrap data that is no longer relevant
var slotsToDelete: seq[Slot]
for slot in dag.lcDataStore.cache.bootstrap.keys:
for slot in dag.lcDataStore.cache.currentBranches.keys:
if slot < targetTailSlot:
slotsToDelete.add slot
for slot in slotsToDelete:
dag.lcDataStore.cache.bootstrap.del slot
dag.lcDataStore.cache.currentBranches.del slot
# Prune best `LightClientUpdate` that are no longer relevant
var periodsToDelete: seq[SyncCommitteePeriod]
for period in dag.lcDataStore.cache.best.keys:
for period in dag.lcDataStore.cache.bestUpdates.keys:
if period < targetTailPeriod:
periodsToDelete.add period
for period in periodsToDelete:
dag.lcDataStore.cache.best.del period
dag.lcDataStore.cache.bestUpdates.del period
# Prune best `LightClientUpdate` referring to non-finalized sync committees
# that are no longer relevant, i.e., orphaned or too old
@ -798,18 +796,18 @@ proc getLightClientBootstrap*(
if slot > dag.finalizedHead.blck.slot:
debug "LC bootstrap unavailable: Not finalized", blockRoot
return err()
var cachedBootstrap = dag.lcDataStore.cache.bootstrap.getOrDefault(slot)
if cachedBootstrap.current_sync_committee_branch.isZeroMemory:
var branch = dag.lcDataStore.cache.currentBranches.getOrDefault(slot)
if branch.isZeroMemory:
if dag.lcDataStore.importMode == LightClientDataImportMode.OnDemand:
let bsi = ? dag.getExistingBlockIdAtSlot(slot)
var tmpState = assignClone(dag.headState)
dag.withUpdatedExistingState(tmpState[], bsi) do:
cachedBootstrap.current_sync_committee_branch = withState(state):
branch = withState(state):
when stateFork >= BeaconStateFork.Altair:
state.data.build_proof(altair.CURRENT_SYNC_COMMITTEE_INDEX).get
else: raiseAssert "Unreachable"
do: return err()
dag.lcDataStore.cache.bootstrap[slot] = cachedBootstrap
dag.lcDataStore.cache.currentBranches[slot] = branch
else:
debug "LC bootstrap unavailable: Data not cached", slot
return err()
@ -822,7 +820,7 @@ proc getLightClientBootstrap*(
bootstrap.current_sync_committee =
? dag.existingCurrentSyncCommitteeForPeriod(tmpState[], period)
bootstrap.current_sync_committee_branch =
cachedBootstrap.current_sync_committee_branch
branch
return ok bootstrap
else:
debug "LC bootstrap unavailable: Block before Altair", slot
@ -836,7 +834,7 @@ proc getLightClientUpdateForPeriod*(
if dag.lcDataStore.importMode == LightClientDataImportMode.OnDemand:
dag.initLightClientUpdateForPeriod(period)
result = some(dag.lcDataStore.cache.best.getOrDefault(period))
result = some(dag.lcDataStore.cache.bestUpdates.getOrDefault(period))
let numParticipants = countOnes(result.get.sync_aggregate.sync_committee_bits)
if numParticipants < MIN_SYNC_COMMITTEE_PARTICIPANTS:
result.reset()

View File

@ -149,6 +149,15 @@ type
### Modified/overloaded
FinalityBranch* =
array[log2trunc(FINALIZED_ROOT_INDEX), Eth2Digest]
CurrentSyncCommitteeBranch* =
array[log2trunc(CURRENT_SYNC_COMMITTEE_INDEX), Eth2Digest]
NextSyncCommitteeBranch* =
array[log2trunc(NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#lightclientbootstrap
LightClientBootstrap* = object
header*: BeaconBlockHeader
@ -156,9 +165,7 @@ type
current_sync_committee*: SyncCommittee
## Current sync committee corresponding to `header`
current_sync_committee_branch*:
array[log2trunc(CURRENT_SYNC_COMMITTEE_INDEX), Eth2Digest]
current_sync_committee_branch*: CurrentSyncCommitteeBranch
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#lightclientupdate
LightClientUpdate* = object
@ -168,13 +175,11 @@ type
next_sync_committee*: SyncCommittee
## Next sync committee corresponding to `attested_header`,
## if signature is from current sync committee
next_sync_committee_branch*:
array[log2trunc(NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
next_sync_committee_branch*: NextSyncCommitteeBranch
# The finalized beacon block header attested to by Merkle branch
finalized_header*: BeaconBlockHeader
finality_branch*:
array[log2trunc(FINALIZED_ROOT_INDEX), Eth2Digest]
finality_branch*: FinalityBranch
sync_aggregate*: SyncAggregate
signature_slot*: Slot
@ -187,8 +192,7 @@ type
# The finalized beacon block header attested to by Merkle branch
finalized_header*: BeaconBlockHeader
finality_branch*:
array[log2trunc(FINALIZED_ROOT_INDEX), Eth2Digest]
finality_branch*: FinalityBranch
# Sync committee aggregate signature
sync_aggregate*: SyncAggregate

View File

@ -177,16 +177,14 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
# Sync committee signing the attested_header
(sync_aggregate, signature_slot) = get_sync_aggregate(cfg, forked[])
next_sync_committee = SyncCommittee()
next_sync_committee_branch =
default(array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest])
next_sync_committee_branch = default(altair.NextSyncCommitteeBranch)
# Ensure that finality checkpoint is genesis
check state.finalized_checkpoint.epoch == 0
# Finality is unchanged
let
finality_header = BeaconBlockHeader()
finality_branch =
default(array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest])
finality_branch = default(altair.FinalityBranch)
update = altair.LightClientUpdate(
attested_header: attested_header,
@ -235,13 +233,11 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
# Sync committee signing the attested_header
(sync_aggregate, signature_slot) = get_sync_aggregate(cfg, forked[])
next_sync_committee = SyncCommittee()
next_sync_committee_branch =
default(array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest])
next_sync_committee_branch = default(altair.NextSyncCommitteeBranch)
# Finality is unchanged
finality_header = BeaconBlockHeader()
finality_branch =
default(array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest])
finality_branch = default(altair.FinalityBranch)
update = altair.LightClientUpdate(
attested_header: attested_header,
@ -297,8 +293,7 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
# Finality is unchanged
finality_header = BeaconBlockHeader()
finality_branch =
default(array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest])
finality_branch = default(altair.FinalityBranch)
update = altair.LightClientUpdate(
attested_header: attested_header,
@ -354,8 +349,7 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
# Updated sync_committee and finality
next_sync_committee = SyncCommittee()
next_sync_committee_branch =
default(array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest])
next_sync_committee_branch = default(altair.NextSyncCommitteeBranch)
finalized_block = blocks[SLOTS_PER_EPOCH - 1].altairData
finalized_header = finalized_block.toBeaconBlockHeader
check: