Skip to content

Commit 37d344a

Browse files
committed
add option to produce light client data
Light clients require full nodes to serve additional data so that they can stay in sync with the network. This patch adds a new launch option `--serve-light-client-data` to enable collection ot light client data. Note that data is only produced locally, a separate patch is needed to actually make the data availble over the network.
1 parent c2ce51e commit 37d344a

File tree

10 files changed

+932
-16
lines changed

10 files changed

+932
-16
lines changed

AllTests-mainnet.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ OK: 3/3 Fail: 0/3 Skip: 0/3
223223
+ [SCRYPT] Network Keystore encryption OK
224224
```
225225
OK: 9/9 Fail: 0/9 Skip: 0/9
226+
## Light client [Preset: mainnet]
227+
```diff
228+
+ Light client sync OK
229+
+ Pre-Altair OK
230+
```
231+
OK: 2/2 Fail: 0/2 Skip: 0/2
226232
## ListKeys requests [Preset: mainnet]
227233
```diff
228234
+ Correct token provided [Preset: mainnet] OK
@@ -450,4 +456,4 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
450456
OK: 1/1 Fail: 0/1 Skip: 0/1
451457

452458
---TOTAL---
453-
OK: 248/252 Fail: 0/252 Skip: 4/252
459+
OK: 250/254 Fail: 0/254 Skip: 4/254

beacon_chain/conf.nim

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# beacon_chain
2-
# Copyright (c) 2018-2021 Status Research & Development GmbH
2+
# Copyright (c) 2018-2022 Status Research & Development GmbH
33
# Licensed and distributed under either of
44
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
55
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
@@ -360,6 +360,11 @@ type
360360
desc: "A file specifying the authorizition token required for accessing the keymanager API"
361361
name: "keymanager-token-file" }: Option[InputFile]
362362

363+
serveLightClientData* {.
364+
desc: "Serve data to allow light clients to sync"
365+
defaultValue: false
366+
name: "serve-light-client-data"}: bool
367+
363368
inProcessValidators* {.
364369
desc: "Disable the push model (the beacon node tells a signing process with the private keys of the validators what to sign and when) and load the validators in the beacon node itself"
365370
defaultValue: true # the use of the nimbus_signing_process binary by default will be delayed until async I/O over stdin/stdout is developed for the child process.

beacon_chain/consensus_object_pools/block_clearance.nim

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# beacon_chain
2-
# Copyright (c) 2018-2021 Status Research & Development GmbH
2+
# Copyright (c) 2018-2022 Status Research & Development GmbH
33
# Licensed and distributed under either of
44
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
55
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
@@ -11,7 +11,7 @@ import
1111
chronicles,
1212
stew/[assign2, results],
1313
../spec/[forks, signatures, signatures_batch, state_transition],
14-
"."/[block_dag, blockchain_dag]
14+
"."/[block_dag, blockchain_dag, blockchain_dag_light_client]
1515

1616
export results, signatures_batch, block_dag, blockchain_dag
1717

@@ -65,10 +65,12 @@ proc addResolvedHeadBlock(
6565
# been applied but the `blck` field was still set to the parent
6666
state.blck = blockRef
6767

68+
# Enable light clients to stay in sync with the network.
69+
dag.processNewBlockForLightClient(state, trustedBlock, parent)
70+
6871
# Regardless of the chain we're on, the deposits come in the same order so
6972
# as soon as we import a block, we'll also update the shared public key
7073
# cache
71-
7274
dag.updateValidatorKeys(getStateField(state.data, validators).asSeq())
7375

7476
# Getting epochRef with the state will potentially create a new EpochRef

beacon_chain/consensus_object_pools/block_dag.nim

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ type
5757
## Slot time for this BlockSlot which may differ from blck.slot when time
5858
## has advanced without blocks
5959

60+
func hash*(bid: BlockId): Hash =
61+
var h: Hash = 0
62+
h = h !& hash(bid.root)
63+
h = h !& hash(bid.slot)
64+
!$h
65+
6066
template root*(blck: BlockRef): Eth2Digest = blck.bid.root
6167
template slot*(blck: BlockRef): Slot = blck.bid.slot
6268

beacon_chain/consensus_object_pools/block_pools_types.nim

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import
1111
# Standard library
1212
std/[options, sets, tables, hashes],
1313
# Status libraries
14-
chronicles,
14+
stew/[bitops2, endians2], chronicles,
1515
# Internals
1616
../spec/[signatures_batch, forks, helpers],
1717
../spec/datatypes/[phase0, altair, bellatrix],
@@ -52,6 +52,8 @@ type
5252
proc(data: ReorgInfoObject) {.gcsafe, raises: [Defect].}
5353
OnFinalizedCallback* =
5454
proc(data: FinalizationInfoObject) {.gcsafe, raises: [Defect].}
55+
OnOptimisticLightClientUpdateCallback* =
56+
proc(data: OptimisticLightClientUpdate) {.gcsafe, raises: [Defect].}
5557

5658
FetchRecord* = object
5759
root*: Eth2Digest
@@ -63,6 +65,24 @@ type
6365
# unnecessary overhead.
6466
data*: BlockRef
6567

68+
CachedSyncCommittee* = object
69+
## Cached sync committee. Note that the next sync committee is not finalized
70+
## until the first block of the sync committee period is finalized. Forks at
71+
## the beginning of a new sync committee period may refer to different next
72+
## sync committees.
73+
sync_committee*: SyncCommittee
74+
ref_count*: uint64
75+
76+
CachedLightClientData* = object
77+
## Cached data from historical states to improve speed when creating
78+
## future `LightClientUpdate` instances.
79+
next_sync_committee_root*: Eth2Digest
80+
next_sync_committee_branch*:
81+
array[log2trunc(NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
82+
83+
finalized_bid*: BlockId
84+
finality_branch*: array[log2trunc(FINALIZED_ROOT_INDEX), Eth2Digest]
85+
6686
ChainDAGRef* = ref object
6787
## Pool of blocks responsible for keeping a DAG of resolved blocks.
6888
##
@@ -151,6 +171,9 @@ type
151171

152172
cfg*: RuntimeConfig
153173

174+
createLightClientData*: bool
175+
## Whether or not `LightClientUpdate` should be produced.
176+
154177
epochRefs*: array[32, EpochRef]
155178
## Cached information about a particular epoch ending with the given
156179
## block - we limit the number of held EpochRefs to put a cap on
@@ -162,6 +185,41 @@ type
162185
## value with other components which don't have access to the
163186
## full ChainDAG.
164187

188+
# -----------------------------------
189+
# Cached data to enable light clients to stay in sync with the network
190+
191+
cachedSyncCommittees*: Table[Eth2Digest, CachedSyncCommittee]
192+
## Cached sync committees for creating future `LightClientUpdate`
193+
## instances. Key is `hash_tree_root(sync_committee)`.
194+
## Count is reference count, from `cachedLightClientData`.
195+
196+
cachedLightClientData*: Table[BlockId, CachedLightClientData]
197+
## Cached data for creating future `LightClientUpdate` instances.
198+
## Key is the block ID of which the post state was used to get the data.
199+
200+
lightClientCheckpoints*: array[4, Checkpoint]
201+
## Keeps track of the latest four `finalized_checkpoint` references
202+
## leading to `finalizedHead`. Used to prune `cachedLightClientData`.
203+
## Non-finalized states may only refer to these checkpoints.
204+
205+
lastLightClientCheckpointIndex*: int
206+
## Last index that was modified in `lightClientCheckpoints`.
207+
208+
bestLightClientUpdates*: Table[SyncCommitteePeriod, LightClientUpdate]
209+
## Stores the `LightClientUpdate` with the most `sync_committee_bits` per
210+
## `SyncCommitteePeriod`. Updates with a finality proof have precedence.
211+
212+
latestLightClientUpdate*: LightClientUpdate
213+
## Tracks the `LightClientUpdate` for the latest slot. This may be older
214+
## than head for empty slots or if not signed by sync committee.
215+
216+
optimisticLightClientUpdate*: OptimisticLightClientUpdate
217+
## Tracks the `OptimisticLightClientUpdate` for the latest slot. This may
218+
## be older than head for empty slots or if not signed by sync committee.
219+
220+
# -----------------------------------
221+
# Callbacks
222+
165223
onBlockAdded*: OnBlockCallback
166224
## On block added callback
167225
onHeadChanged*: OnHeadCallback
@@ -170,6 +228,8 @@ type
170228
## On beacon chain reorganization
171229
onFinHappened*: OnFinalizedCallback
172230
## On finalization callback
231+
onOptimisticLightClientUpdate*: OnOptimisticLightClientUpdateCallback
232+
## On `OptimisticLightClientUpdate` updated callback
173233

174234
headSyncCommittees*: SyncCommitteeCache
175235
## A cache of the sync committees, as they appear in the head state -

beacon_chain/consensus_object_pools/blockchain_dag.nim

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
{.push raises: [Defect].}
99

1010
import
11-
std/[options, sequtils, tables, sets],
12-
stew/[assign2, byteutils, results],
11+
std/[algorithm, options, sequtils, tables, sets],
12+
stew/[assign2, bitops2, byteutils, objects, results],
1313
metrics, snappy, chronicles,
1414
../spec/[beaconstate, eth2_merkleization, eth2_ssz_serialization, helpers,
1515
state_transition, validator],
16-
../spec/datatypes/[phase0, altair],
16+
../spec/datatypes/[phase0, altair, bellatrix],
1717
".."/beacon_chain_db,
1818
"."/[block_pools_types, block_quarantine]
1919

@@ -53,7 +53,8 @@ const
5353
EPOCHS_PER_STATE_SNAPSHOT = 32
5454

5555
proc putBlock*(
56-
dag: ChainDAGRef, signedBlock: ForkyTrustedSignedBeaconBlock) =
56+
dag: ChainDAGRef,
57+
signedBlock: ForkyTrustedSignedBeaconBlock) =
5758
dag.db.putBlock(signedBlock)
5859

5960
proc updateStateData*(
@@ -401,11 +402,24 @@ proc getForkedBlock*(
401402
dag.getForkedBlock(blck.bid).expect(
402403
"BlockRef block should always load, database corrupt?")
403404

405+
import blockchain_dag_light_client
406+
407+
export
408+
blockchain_dag_light_client.getBestLightClientUpdateForPeriod,
409+
blockchain_dag_light_client.getLatestLightClientUpdate,
410+
blockchain_dag_light_client.getOptimisticLightClientUpdate,
411+
blockchain_dag_light_client.getLightClientBootstrap
412+
404413
proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
405414
validatorMonitor: ref ValidatorMonitor, updateFlags: UpdateFlags,
406415
onBlockCb: OnBlockCallback = nil, onHeadCb: OnHeadCallback = nil,
407416
onReorgCb: OnReorgCallback = nil,
408-
onFinCb: OnFinalizedCallback = nil): ChainDAGRef =
417+
onFinCb: OnFinalizedCallback = nil,
418+
onOptimisticLCUpdateCb: OnOptimisticLightClientUpdateCallback = nil,
419+
createLightClientData = false): ChainDAGRef =
420+
if onOptimisticLCUpdateCb != nil:
421+
doAssert createLightClientData
422+
409423
# TODO we require that the db contains both a head and a tail block -
410424
# asserting here doesn't seem like the right way to go about it however..
411425

@@ -543,6 +557,7 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
543557
# allow skipping some validation.
544558
updateFlags: {verifyFinalization} * updateFlags,
545559
cfg: cfg,
560+
createLightClientData: createLightClientData,
546561

547562
forkDigests: newClone ForkDigests.init(
548563
cfg,
@@ -551,7 +566,8 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
551566
onBlockAdded: onBlockCb,
552567
onHeadChanged: onHeadCb,
553568
onReorgHappened: onReorgCb,
554-
onFinHappened: onFinCb
569+
onFinHappened: onFinCb,
570+
onOptimisticLightClientUpdate: onOptimisticLCUpdateCb
555571
)
556572

557573
let forkVersions =
@@ -618,6 +634,84 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
618634
forkBlocks = dag.forkBlocks.len(),
619635
backfill = (dag.backfill.slot, shortLog(dag.backfill.parent_root))
620636

637+
# Initialize cached light client data (finalized head + non-finalized blocks).
638+
if dag.createLightClientData:
639+
let altairStartSlot = dag.cfg.ALTAIR_FORK_EPOCH.start_slot
640+
if headRef.slot >= altairStartSlot:
641+
let
642+
finalizedSlot = dag.finalizedHead.blck.slot
643+
finalizedPeriod = finalizedSlot.sync_committee_period
644+
dag.initializeBestLightClientUpdateForPeriod(
645+
finalizedPeriod, prune = false)
646+
647+
debug "Initializing cached light client data"
648+
let lightClientStartTick = Moment.now()
649+
650+
# Build lists of block to process.
651+
# As it is slow to load states in descending order,
652+
# first build a todo list, then process them in ascending order.
653+
let earliestSlot = max(finalizedSlot, altairStartSlot)
654+
var
655+
blocksBetween = newSeqOfCap[BlockRef](headRef.slot - earliestSlot + 1)
656+
blockRef = headRef
657+
while blockRef.slot > earliestSlot:
658+
blocksBetween.add blockRef
659+
blockRef = blockRef.parent
660+
blocksBetween.add blockRef
661+
662+
# Process blocks.
663+
let lowSlot = max(altairStartSlot, dag.tail.slot)
664+
var
665+
oldCheckpoint: Checkpoint
666+
checkpointIndex = 0
667+
for i in countdown(blocksBetween.high, blocksBetween.low):
668+
blockRef = blocksBetween[i]
669+
dag.withUpdatedState(dag.headState, blockRef.atSlot(blockRef.slot)) do:
670+
withStateAndBlck(stateData.data, dag.getForkedBlock(blck)):
671+
when stateFork >= BeaconStateFork.Altair:
672+
# Cache data for `LightClientUpdate` of descendant blocks.
673+
dag.cacheLightClientData(state, blck, isNew = false)
674+
675+
# Cache data for the block's `finalized_checkpoint`.
676+
# The `finalized_checkpoint` may refer to:
677+
# 1. `finalizedHead.blck -> finalized_checkpoint`
678+
# This may happen when there were skipped slots.
679+
# 2. `finalizedHead -> finalized_checkpoint`
680+
# 3. One epoch boundary that got justified then finalized
681+
# between `finalizedHead -> finalized_checkpoint`
682+
# and `finalizedHead`
683+
# 4. `finalizedHead`
684+
let checkpoint = state.data.finalized_checkpoint
685+
if checkpoint != oldCheckpoint:
686+
oldCheckpoint = checkpoint
687+
doAssert checkpointIndex < dag.lightClientCheckpoints.len
688+
dag.lightClientCheckpoints[checkpointIndex] = checkpoint
689+
dag.lastLightClientCheckpointIndex = checkpointIndex
690+
inc checkpointIndex
691+
if checkpoint.root != dag.finalizedHead.blck.root:
692+
let cpRef =
693+
dag.getBlockAtSlot(checkpoint.epoch.start_slot).blck
694+
if cpRef != nil and cpRef.slot >= lowSlot:
695+
assert cpRef.bid.root == checkpoint.root
696+
dag.withUpdatedState(tmpState[],
697+
cpRef.atSlot(cpRef.slot)) do:
698+
withStateAndBlck(
699+
stateData.data, dag.getForkedBlock(blck)):
700+
when stateFork >= BeaconStateFork.Altair:
701+
dag.cacheLightClientData(state, blck, isNew = false)
702+
else: raiseAssert "Unreachable"
703+
do: raiseAssert "Unreachable"
704+
705+
# Create `LightClientUpdate` for non-finalized blocks.
706+
if blockRef.slot > earliestSlot:
707+
dag.createLightClientUpdates(state, blck, blockRef.parent)
708+
else: raiseAssert "Unreachable"
709+
do: raiseAssert "Unreachable"
710+
711+
let lightClientEndTick = Moment.now()
712+
debug "Initialized cached light client data",
713+
initDur = lightClientEndTick - lightClientStartTick
714+
621715
dag
622716

623717
template genesisValidatorsRoot*(dag: ChainDAGRef): Eth2Digest =
@@ -1127,6 +1221,9 @@ proc pruneBlocksDAG(dag: ChainDAGRef) =
11271221

11281222
var cur = head.atSlot()
11291223
while not cur.blck.isAncestorOf(dag.finalizedHead.blck):
1224+
if dag.createLightClientData:
1225+
dag.deleteLightClientData(cur.blck.bid)
1226+
11301227
dag.delState(cur) # TODO: should we move that disk I/O to `onSlotEnd`
11311228

11321229
if cur.isProposed():
@@ -1447,6 +1544,8 @@ proc updateHead*(
14471544
# in order to clear out blocks that are no longer viable and should
14481545
# therefore no longer be considered as part of the chain we're following
14491546
dag.pruneBlocksDAG()
1547+
if dag.createLightClientData:
1548+
dag.pruneLightClientData()
14501549

14511550
# Send notification about new finalization point via callback.
14521551
if not(isNil(dag.onFinHappened)):

0 commit comments

Comments
 (0)