nimbus-eth2/tests/test_light_client.nim
Etan Kissling ea97e93e74
accelerate getShufflingRef (#4911)
When an uncached `ShufflingRef` is requested, we currently replay state
which can take several seconds. Acceleration is possible by:

1. Start from any state with locked-in `get_active_validator_indices`.
   Any blocks / slots applied to such a state can only affect that
   result for future epochs, so are viable for querying target epoch.
   `compute_activation_exit_epoch(state.slot.epoch) > target.epoch`

2. Determine highest common ancestor among `state` and `target.blck`.
   At the ancestor slot, same rules re `get_active_validator_indices`.
   `compute_activation_exit_epoch(ancestorSlot.epoch) > target.epoch`

3. We now have a `state` that shares history with `target.blck` up
   through a common ancestor slot. Any blocks / slots that the `state`
   contains, which are not part of the `target.blck` history, affect
   `get_active_validator_indices` at epochs _after_ `target.epoch`.

4. Select `state.randao_mixes[N]` that is closest to common ancestor.
   Either direction is fine (above / below ancestor).

5. From that RANDAO mix, mix in / out all RANDAO reveals from blocks
   in-between. This is just an XOR operation, so fully reversible.
   `mix = mix xor SHA256(blck.message.body.randao_reveal)`

6. Compute the attester dependent slot from `target.epoch`.
   `if epoch >= 2: (target.epoch - 1).start_slot - 1 else: GENESIS_SLOT`

7. Trace back from `target.blck` to the attester dependent slot.
   We now have the destination for which we want to obtain RANDAO.

8. Mix in all RANDAO reveals from blocks up through the `dependentBlck`.
   Same method, no special handling necessary for epoch transitions.

9. Combine `get_active_validator_indices` from `state` at `target.epoch`
   with the recovered RANDAO value at `dependentBlck` to obtain the
   requested shuffling, and construct the `ShufflingRef` without replay.

* more tests and simplify logic

* test with different number of deposits per branch

* Update beacon_chain/consensus_object_pools/blockchain_dag.nim

Co-authored-by: Jacek Sieka <jacek@status.im>

* `commonAncestor` tests

* lint

---------

Co-authored-by: Jacek Sieka <jacek@status.im>
2023-05-12 19:36:59 +02:00

256 lines
10 KiB
Nim

# beacon_chain
# Copyright (c) 2021-2023 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.
{.used.}
import
# Status libraries
eth/keys, taskpools,
# Beacon chain internals
../beacon_chain/consensus_object_pools/
[block_clearance, block_quarantine, blockchain_dag],
../beacon_chain/spec/[forks, helpers, light_client_sync, state_transition],
# Test utilities
./testutil, ./testdbutil
suite "Light client" & preset():
const # Test config, should be long enough to cover interesting transitions
headPeriod = 3.SyncCommitteePeriod
let
cfg = block: # Fork schedule so that each `LightClientDataFork` is covered
static: doAssert ConsensusFork.high == ConsensusFork.Deneb
var res = defaultRuntimeConfig
res.ALTAIR_FORK_EPOCH = 1.Epoch
res.BELLATRIX_FORK_EPOCH = 2.Epoch
res.CAPELLA_FORK_EPOCH = (EPOCHS_PER_SYNC_COMMITTEE_PERIOD * 1).Epoch
res.DENEB_FORK_EPOCH = (EPOCHS_PER_SYNC_COMMITTEE_PERIOD * 2).Epoch
res
altairStartSlot = cfg.ALTAIR_FORK_EPOCH.start_slot
proc advanceToSlot(
dag: ChainDAGRef,
targetSlot: Slot,
verifier: var BatchVerifier,
quarantine: var Quarantine,
attested = true,
syncCommitteeRatio = 0.82) =
var cache: StateCache
const maxAttestedSlotsPerPeriod = 3 * SLOTS_PER_EPOCH
while true:
var slot = getStateField(dag.headState, slot)
doAssert targetSlot >= slot
if targetSlot == slot: break
# When there is a large jump, skip to the end of the current period,
# create blocks for a few epochs to finalize it, then proceed
let
nextPeriod = slot.sync_committee_period + 1
periodEpoch = nextPeriod.start_epoch
periodSlot = periodEpoch.start_slot
checkpointSlot = periodSlot - maxAttestedSlotsPerPeriod
if targetSlot > checkpointSlot and checkpointSlot > dag.head.slot:
var info: ForkedEpochInfo
doAssert process_slots(cfg, dag.headState, checkpointSlot,
cache, info, flags = {}).isOk()
slot = checkpointSlot
# Create blocks for final few epochs
let blocks = min(targetSlot - slot, maxAttestedSlotsPerPeriod)
for blck in makeTestBlocks(
dag.headState, cache, blocks.int, attested = attested,
syncCommitteeRatio = syncCommitteeRatio, cfg = cfg):
let added =
case blck.kind
of ConsensusFork.Phase0:
const nilCallback = OnPhase0BlockAdded(nil)
dag.addHeadBlock(verifier, blck.phase0Data, nilCallback)
of ConsensusFork.Altair:
const nilCallback = OnAltairBlockAdded(nil)
dag.addHeadBlock(verifier, blck.altairData, nilCallback)
of ConsensusFork.Bellatrix:
const nilCallback = OnBellatrixBlockAdded(nil)
dag.addHeadBlock(verifier, blck.bellatrixData, nilCallback)
of ConsensusFork.Capella:
const nilCallback = OnCapellaBlockAdded(nil)
dag.addHeadBlock(verifier, blck.capellaData, nilCallback)
of ConsensusFork.Deneb:
const nilCallback = OnDenebBlockAdded(nil)
dag.addHeadBlock(verifier, blck.denebData, nilCallback)
check: added.isOk()
dag.updateHead(added[], quarantine, [])
setup:
const num_validators = SLOTS_PER_EPOCH
let
validatorMonitor = newClone(ValidatorMonitor.init())
dag = ChainDAGRef.init(
cfg, makeTestDB(num_validators, cfg = cfg), validatorMonitor, {},
lcDataConfig = LightClientDataConfig(
serve: true,
importMode: LightClientDataImportMode.OnlyNew))
quarantine = newClone(Quarantine.init())
taskpool = Taskpool.new()
var verifier = BatchVerifier(rng: keys.newRng(), taskpool: taskpool)
test "Pre-Altair":
# Genesis
block:
let
update = dag.getLightClientUpdateForPeriod(0.SyncCommitteePeriod)
finalityUpdate = dag.getLightClientFinalityUpdate
optimisticUpdate = dag.getLightClientOptimisticUpdate
check:
dag.headState.kind == ConsensusFork.Phase0
update.kind == LightClientDataFork.None
finalityUpdate.kind == LightClientDataFork.None
optimisticUpdate.kind == LightClientDataFork.None
# Advance to last slot before Altair
dag.advanceToSlot(altairStartSlot - 1, verifier, quarantine[])
block:
let
update = dag.getLightClientUpdateForPeriod(0.SyncCommitteePeriod)
finalityUpdate = dag.getLightClientFinalityUpdate
optimisticUpdate = dag.getLightClientOptimisticUpdate
check:
dag.headState.kind == ConsensusFork.Phase0
update.kind == LightClientDataFork.None
finalityUpdate.kind == LightClientDataFork.None
optimisticUpdate.kind == LightClientDataFork.None
# Advance to Altair
dag.advanceToSlot(altairStartSlot, verifier, quarantine[])
block:
let
update = dag.getLightClientUpdateForPeriod(0.SyncCommitteePeriod)
finalityUpdate = dag.getLightClientFinalityUpdate
optimisticUpdate = dag.getLightClientOptimisticUpdate
check:
dag.headState.kind == ConsensusFork.Altair
update.kind == LightClientDataFork.None
finalityUpdate.kind == LightClientDataFork.None
optimisticUpdate.kind == LightClientDataFork.None
test "Light client sync":
# Advance to Altair
dag.advanceToSlot(altairStartSlot, verifier, quarantine[])
# Track trusted checkpoint for light client
let
genesis_validators_root = dag.genesis_validators_root
trusted_block_root = dag.head.root
# Advance to target slot
const
periodEpoch = headPeriod.start_epoch
headSlot = (periodEpoch + 2).start_slot + 5
dag.advanceToSlot(headSlot, verifier, quarantine[])
let currentSlot = getStateField(dag.headState, slot)
# Initialize light client store
var bootstrap = dag.getLightClientBootstrap(trusted_block_root)
check bootstrap.kind > LightClientDataFork.None
var store {.noinit.}: ForkedLightClientStore
withForkyBootstrap(bootstrap):
when lcDataFork > LightClientDataFork.None:
var storeRes = initialize_light_client_store(
trusted_block_root, forkyBootstrap, cfg)
check storeRes.isOk
store = ForkedLightClientStore(kind: lcDataFork)
store.forky(lcDataFork) = storeRes.get
# Sync to latest sync committee period
var numIterations = 0
while true:
let storePeriod = withForkyStore(store):
when lcDataFork > LightClientDataFork.None:
forkyStore.finalized_header.beacon.slot.sync_committee_period
else:
GENESIS_SLOT.SyncCommitteePeriod
if storePeriod + 1 >= headPeriod:
break
let
period = withForkyStore(store):
when lcDataFork > LightClientDataFork.None:
if forkyStore.is_next_sync_committee_known:
storePeriod + 1
else:
storePeriod
else:
storePeriod
update = dag.getLightClientUpdateForPeriod(period)
check update.kind > LightClientDataFork.None
if update.kind > store.kind:
withForkyUpdate(update):
when lcDataFork > LightClientDataFork.None:
store.migrateToDataFork(lcDataFork)
withForkyStore(store):
when lcDataFork > LightClientDataFork.None:
bootstrap.migrateToDataFork(lcDataFork)
template forkyBootstrap: untyped = bootstrap.forky(lcDataFork)
let upgradedUpdate = update.migratingToDataFork(lcDataFork)
template forkyUpdate: untyped = upgradedUpdate.forky(lcDataFork)
let res = process_light_client_update(
forkyStore, forkyUpdate, currentSlot, cfg, genesis_validators_root)
check:
forkyUpdate.finalized_header.beacon.slot.sync_committee_period ==
period
res.isOk
if forkyUpdate.finalized_header.beacon.slot >
forkyBootstrap.header.beacon.slot:
forkyStore.finalized_header == forkyUpdate.finalized_header
else:
forkyStore.finalized_header == forkyBootstrap.header
inc numIterations
if numIterations > 20: doAssert false # Avoid endless loop on test failure
# Sync to latest update
let finalityUpdate = dag.getLightClientFinalityUpdate
check finalityUpdate.kind > LightClientDataFork.None
if finalityUpdate.kind > store.kind:
withForkyFinalityUpdate(finalityUpdate):
when lcDataFork > LightClientDataFork.None:
store.migrateToDataFork(lcDataFork)
withForkyStore(store):
when lcDataFork > LightClientDataFork.None:
let upgradedUpdate = finalityUpdate.migratingToDataFork(lcDataFork)
template forkyUpdate: untyped = upgradedUpdate.forky(lcDataFork)
let res = process_light_client_update(
forkyStore, forkyUpdate, currentSlot, cfg, genesis_validators_root)
check:
forkyUpdate.attested_header.beacon.slot == dag.head.parent.slot
res.isOk
forkyStore.finalized_header == forkyUpdate.finalized_header
forkyStore.optimistic_header == forkyUpdate.attested_header
test "Init from checkpoint":
# Fetch genesis state
let genesisState = assignClone dag.headState
# Advance to target slot for checkpoint
let finalizedSlot =
((altairStartSlot.sync_committee_period + 1).start_epoch + 2).start_slot
dag.advanceToSlot(finalizedSlot, verifier, quarantine[])
# Initialize new DAG from checkpoint
let cpDb = BeaconChainDB.new("", cfg = cfg, inMemory = true)
ChainDAGRef.preInit(cpDb, genesisState[])
ChainDAGRef.preInit(cpDb, dag.headState) # dag.getForkedBlock(dag.head.bid).get)
let cpDag = ChainDAGRef.init(
cfg, cpDb, validatorMonitor, {},
lcDataConfig = LightClientDataConfig(
serve: true,
importMode: LightClientDataImportMode.Full))
# Advance by a couple epochs
for i in 1'u64 .. 10:
let headSlot = (finalizedSlot.epoch + i).start_slot
cpDag.advanceToSlot(headSlot, verifier, quarantine[])
check true