Skip to content

Commit

Permalink
add option to produce light client data
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
etan-status committed Jan 27, 2022
1 parent c2ce51e commit 37d344a
Show file tree
Hide file tree
Showing 10 changed files with 932 additions and 16 deletions.
8 changes: 7 additions & 1 deletion AllTests-mainnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ OK: 3/3 Fail: 0/3 Skip: 0/3
+ [SCRYPT] Network Keystore encryption OK
```
OK: 9/9 Fail: 0/9 Skip: 0/9
## Light client [Preset: mainnet]
```diff
+ Light client sync OK
+ Pre-Altair OK
```
OK: 2/2 Fail: 0/2 Skip: 0/2
## ListKeys requests [Preset: mainnet]
```diff
+ Correct token provided [Preset: mainnet] OK
Expand Down Expand Up @@ -450,4 +456,4 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
OK: 1/1 Fail: 0/1 Skip: 0/1

---TOTAL---
OK: 248/252 Fail: 0/252 Skip: 4/252
OK: 250/254 Fail: 0/254 Skip: 4/254
7 changes: 6 additions & 1 deletion beacon_chain/conf.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2021 Status Research & Development GmbH
# Copyright (c) 2018-2022 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).
Expand Down Expand Up @@ -360,6 +360,11 @@ type
desc: "A file specifying the authorizition token required for accessing the keymanager API"
name: "keymanager-token-file" }: Option[InputFile]

serveLightClientData* {.
desc: "Serve data to allow light clients to sync"
defaultValue: false
name: "serve-light-client-data"}: bool

inProcessValidators* {.
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"
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.
Expand Down
8 changes: 5 additions & 3 deletions beacon_chain/consensus_object_pools/block_clearance.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2021 Status Research & Development GmbH
# Copyright (c) 2018-2022 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).
Expand All @@ -11,7 +11,7 @@ import
chronicles,
stew/[assign2, results],
../spec/[forks, signatures, signatures_batch, state_transition],
"."/[block_dag, blockchain_dag]
"."/[block_dag, blockchain_dag, blockchain_dag_light_client]

export results, signatures_batch, block_dag, blockchain_dag

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

# Enable light clients to stay in sync with the network.
dag.processNewBlockForLightClient(state, trustedBlock, parent)

# Regardless of the chain we're on, the deposits come in the same order so
# as soon as we import a block, we'll also update the shared public key
# cache

dag.updateValidatorKeys(getStateField(state.data, validators).asSeq())

# Getting epochRef with the state will potentially create a new EpochRef
Expand Down
6 changes: 6 additions & 0 deletions beacon_chain/consensus_object_pools/block_dag.nim
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ type
## Slot time for this BlockSlot which may differ from blck.slot when time
## has advanced without blocks

func hash*(bid: BlockId): Hash =
var h: Hash = 0
h = h !& hash(bid.root)
h = h !& hash(bid.slot)
!$h

template root*(blck: BlockRef): Eth2Digest = blck.bid.root
template slot*(blck: BlockRef): Slot = blck.bid.slot

Expand Down
62 changes: 61 additions & 1 deletion beacon_chain/consensus_object_pools/block_pools_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import
# Standard library
std/[options, sets, tables, hashes],
# Status libraries
chronicles,
stew/[bitops2, endians2], chronicles,
# Internals
../spec/[signatures_batch, forks, helpers],
../spec/datatypes/[phase0, altair, bellatrix],
Expand Down Expand Up @@ -52,6 +52,8 @@ type
proc(data: ReorgInfoObject) {.gcsafe, raises: [Defect].}
OnFinalizedCallback* =
proc(data: FinalizationInfoObject) {.gcsafe, raises: [Defect].}
OnOptimisticLightClientUpdateCallback* =
proc(data: OptimisticLightClientUpdate) {.gcsafe, raises: [Defect].}

FetchRecord* = object
root*: Eth2Digest
Expand All @@ -63,6 +65,24 @@ type
# unnecessary overhead.
data*: BlockRef

CachedSyncCommittee* = object
## Cached sync committee. Note that the next sync committee is not finalized
## until the first block of the sync committee period is finalized. Forks at
## the beginning of a new sync committee period may refer to different next
## sync committees.
sync_committee*: SyncCommittee
ref_count*: uint64

CachedLightClientData* = object
## Cached data from historical states to improve speed when creating
## future `LightClientUpdate` instances.
next_sync_committee_root*: Eth2Digest
next_sync_committee_branch*:
array[log2trunc(NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]

finalized_bid*: BlockId
finality_branch*: array[log2trunc(FINALIZED_ROOT_INDEX), Eth2Digest]

ChainDAGRef* = ref object
## Pool of blocks responsible for keeping a DAG of resolved blocks.
##
Expand Down Expand Up @@ -151,6 +171,9 @@ type

cfg*: RuntimeConfig

createLightClientData*: bool
## Whether or not `LightClientUpdate` should be produced.

epochRefs*: array[32, EpochRef]
## Cached information about a particular epoch ending with the given
## block - we limit the number of held EpochRefs to put a cap on
Expand All @@ -162,6 +185,41 @@ type
## value with other components which don't have access to the
## full ChainDAG.

# -----------------------------------
# Cached data to enable light clients to stay in sync with the network

cachedSyncCommittees*: Table[Eth2Digest, CachedSyncCommittee]
## Cached sync committees for creating future `LightClientUpdate`
## instances. Key is `hash_tree_root(sync_committee)`.
## Count is reference count, from `cachedLightClientData`.

cachedLightClientData*: Table[BlockId, CachedLightClientData]
## Cached data for creating future `LightClientUpdate` instances.
## Key is the block ID of which the post state was used to get the data.

lightClientCheckpoints*: array[4, Checkpoint]
## Keeps track of the latest four `finalized_checkpoint` references
## leading to `finalizedHead`. Used to prune `cachedLightClientData`.
## Non-finalized states may only refer to these checkpoints.

lastLightClientCheckpointIndex*: int
## Last index that was modified in `lightClientCheckpoints`.

bestLightClientUpdates*: Table[SyncCommitteePeriod, LightClientUpdate]
## Stores the `LightClientUpdate` with the most `sync_committee_bits` per
## `SyncCommitteePeriod`. Updates with a finality proof have precedence.

latestLightClientUpdate*: LightClientUpdate
## Tracks the `LightClientUpdate` for the latest slot. This may be older
## than head for empty slots or if not signed by sync committee.

optimisticLightClientUpdate*: OptimisticLightClientUpdate
## Tracks the `OptimisticLightClientUpdate` for the latest slot. This may
## be older than head for empty slots or if not signed by sync committee.

# -----------------------------------
# Callbacks

onBlockAdded*: OnBlockCallback
## On block added callback
onHeadChanged*: OnHeadCallback
Expand All @@ -170,6 +228,8 @@ type
## On beacon chain reorganization
onFinHappened*: OnFinalizedCallback
## On finalization callback
onOptimisticLightClientUpdate*: OnOptimisticLightClientUpdateCallback
## On `OptimisticLightClientUpdate` updated callback

headSyncCommittees*: SyncCommitteeCache
## A cache of the sync committees, as they appear in the head state -
Expand Down
111 changes: 105 additions & 6 deletions beacon_chain/consensus_object_pools/blockchain_dag.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
{.push raises: [Defect].}

import
std/[options, sequtils, tables, sets],
stew/[assign2, byteutils, results],
std/[algorithm, options, sequtils, tables, sets],
stew/[assign2, bitops2, byteutils, objects, results],
metrics, snappy, chronicles,
../spec/[beaconstate, eth2_merkleization, eth2_ssz_serialization, helpers,
state_transition, validator],
../spec/datatypes/[phase0, altair],
../spec/datatypes/[phase0, altair, bellatrix],
".."/beacon_chain_db,
"."/[block_pools_types, block_quarantine]

Expand Down Expand Up @@ -53,7 +53,8 @@ const
EPOCHS_PER_STATE_SNAPSHOT = 32

proc putBlock*(
dag: ChainDAGRef, signedBlock: ForkyTrustedSignedBeaconBlock) =
dag: ChainDAGRef,
signedBlock: ForkyTrustedSignedBeaconBlock) =
dag.db.putBlock(signedBlock)

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

import blockchain_dag_light_client

export
blockchain_dag_light_client.getBestLightClientUpdateForPeriod,
blockchain_dag_light_client.getLatestLightClientUpdate,
blockchain_dag_light_client.getOptimisticLightClientUpdate,
blockchain_dag_light_client.getLightClientBootstrap

proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
validatorMonitor: ref ValidatorMonitor, updateFlags: UpdateFlags,
onBlockCb: OnBlockCallback = nil, onHeadCb: OnHeadCallback = nil,
onReorgCb: OnReorgCallback = nil,
onFinCb: OnFinalizedCallback = nil): ChainDAGRef =
onFinCb: OnFinalizedCallback = nil,
onOptimisticLCUpdateCb: OnOptimisticLightClientUpdateCallback = nil,
createLightClientData = false): ChainDAGRef =
if onOptimisticLCUpdateCb != nil:
doAssert createLightClientData

# TODO we require that the db contains both a head and a tail block -
# asserting here doesn't seem like the right way to go about it however..

Expand Down Expand Up @@ -543,6 +557,7 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
# allow skipping some validation.
updateFlags: {verifyFinalization} * updateFlags,
cfg: cfg,
createLightClientData: createLightClientData,

forkDigests: newClone ForkDigests.init(
cfg,
Expand All @@ -551,7 +566,8 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
onBlockAdded: onBlockCb,
onHeadChanged: onHeadCb,
onReorgHappened: onReorgCb,
onFinHappened: onFinCb
onFinHappened: onFinCb,
onOptimisticLightClientUpdate: onOptimisticLCUpdateCb
)

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

# Initialize cached light client data (finalized head + non-finalized blocks).
if dag.createLightClientData:
let altairStartSlot = dag.cfg.ALTAIR_FORK_EPOCH.start_slot
if headRef.slot >= altairStartSlot:
let
finalizedSlot = dag.finalizedHead.blck.slot
finalizedPeriod = finalizedSlot.sync_committee_period
dag.initializeBestLightClientUpdateForPeriod(
finalizedPeriod, prune = false)

debug "Initializing cached light client data"
let lightClientStartTick = Moment.now()

# Build lists of block to process.
# As it is slow to load states in descending order,
# first build a todo list, then process them in ascending order.
let earliestSlot = max(finalizedSlot, altairStartSlot)
var
blocksBetween = newSeqOfCap[BlockRef](headRef.slot - earliestSlot + 1)
blockRef = headRef
while blockRef.slot > earliestSlot:
blocksBetween.add blockRef
blockRef = blockRef.parent
blocksBetween.add blockRef

# Process blocks.
let lowSlot = max(altairStartSlot, dag.tail.slot)
var
oldCheckpoint: Checkpoint
checkpointIndex = 0
for i in countdown(blocksBetween.high, blocksBetween.low):
blockRef = blocksBetween[i]
dag.withUpdatedState(dag.headState, blockRef.atSlot(blockRef.slot)) do:
withStateAndBlck(stateData.data, dag.getForkedBlock(blck)):
when stateFork >= BeaconStateFork.Altair:
# Cache data for `LightClientUpdate` of descendant blocks.
dag.cacheLightClientData(state, blck, isNew = false)

# Cache data for the block's `finalized_checkpoint`.
# The `finalized_checkpoint` may refer to:
# 1. `finalizedHead.blck -> finalized_checkpoint`
# This may happen when there were skipped slots.
# 2. `finalizedHead -> finalized_checkpoint`
# 3. One epoch boundary that got justified then finalized
# between `finalizedHead -> finalized_checkpoint`
# and `finalizedHead`
# 4. `finalizedHead`
let checkpoint = state.data.finalized_checkpoint
if checkpoint != oldCheckpoint:
oldCheckpoint = checkpoint
doAssert checkpointIndex < dag.lightClientCheckpoints.len
dag.lightClientCheckpoints[checkpointIndex] = checkpoint
dag.lastLightClientCheckpointIndex = checkpointIndex
inc checkpointIndex
if checkpoint.root != dag.finalizedHead.blck.root:
let cpRef =
dag.getBlockAtSlot(checkpoint.epoch.start_slot).blck
if cpRef != nil and cpRef.slot >= lowSlot:
assert cpRef.bid.root == checkpoint.root
dag.withUpdatedState(tmpState[],
cpRef.atSlot(cpRef.slot)) do:
withStateAndBlck(
stateData.data, dag.getForkedBlock(blck)):
when stateFork >= BeaconStateFork.Altair:
dag.cacheLightClientData(state, blck, isNew = false)
else: raiseAssert "Unreachable"
do: raiseAssert "Unreachable"

# Create `LightClientUpdate` for non-finalized blocks.
if blockRef.slot > earliestSlot:
dag.createLightClientUpdates(state, blck, blockRef.parent)
else: raiseAssert "Unreachable"
do: raiseAssert "Unreachable"

let lightClientEndTick = Moment.now()
debug "Initialized cached light client data",
initDur = lightClientEndTick - lightClientStartTick

dag

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

var cur = head.atSlot()
while not cur.blck.isAncestorOf(dag.finalizedHead.blck):
if dag.createLightClientData:
dag.deleteLightClientData(cur.blck.bid)

dag.delState(cur) # TODO: should we move that disk I/O to `onSlotEnd`

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

# Send notification about new finalization point via callback.
if not(isNil(dag.onFinHappened)):
Expand Down
Loading

0 comments on commit 37d344a

Please sign in to comment.