nimbus-eth2/tests/consensus_spec/altair/test_fixture_sync_protocol.nim

391 lines
14 KiB
Nim

# beacon_chain
# Copyright (c) 2021-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).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.used.}
import
# Standard library
std/[algorithm, sequtils, sets],
# Status libraries
stew/bitops2,
# Beacon chain internals
../../../beacon_chain/spec/datatypes/altair,
../../../beacon_chain/spec/
[beaconstate, forks, helpers, light_client_sync, signatures,
state_transition],
# Mock helpers
../../mocking/mock_genesis,
# Test utilities
../../testutil, ../../testblockutil
# References to `vFuture` refer to the pre-release proposal of the libp2p based
# light client sync protocol. Conflicting release versions are not in use.
# https://github.com/ethereum/consensus-specs/pull/2802
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/helpers/light_client.py#L63-L93
proc compute_aggregate_sync_committee_signature(
cfg: RuntimeConfig,
forked: ForkedHashedBeaconState,
signature_slot: Slot,
participants: openArray[ValidatorIndex],
block_root: Eth2Digest): ValidatorSig =
template state(): auto = forked.altairData.data
if len(participants) == 0:
return ValidatorSig.infinity
var
aggregateSig {.noinit.}: AggregateSignature
initialized = false
for validator_index in participants:
let
privkey = MockPrivKeys[validator_index]
signature = get_sync_committee_message_signature(
state.fork,
state.genesis_validators_root,
signature_slot,
block_root,
privkey)
if not initialized:
initialized = true
aggregateSig.init(signature)
else:
aggregateSig.aggregate(signature)
aggregateSig.finish.toValidatorSig
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/helpers/light_client.py#L63-L93
proc get_sync_aggregate(
cfg: RuntimeConfig,
forked: ForkedHashedBeaconState,
num_participants = SYNC_COMMITTEE_SIZE.uint64,
signature_slot = FAR_FUTURE_SLOT): (SyncAggregate, Slot) =
template state(): auto = forked.altairData.data
let
block_root =
BeaconBlockHeader(
slot: state.latest_block_header.slot,
proposer_index: state.latest_block_header.proposer_index,
parent_root: state.latest_block_header.parent_root,
state_root: hash_tree_root(state),
body_root: state.latest_block_header.body_root
).hash_tree_root()
# By default, the sync committee signs the previous slot
sig_slot =
if signature_slot == FAR_FUTURE_SLOT:
state.slot + 1
else:
signature_slot
# Ensure correct sync committee and fork version are selected
var
signature_forked = assignClone(forked)
cache: StateCache
info: ForkedEpochInfo
template signature_state(): auto = signature_forked.altairData.data
process_slots(cfg, signature_forked[], sig_slot, cache, info, flags = {})
.expect("no failure")
# Fetch sync committee
let
all_pubkeys = signature_state.validators.mapIt(it.pubkey)
committee_indices = signature_state.current_sync_committee.pubkeys
.mapIt(all_pubkeys.find(it).ValidatorIndex)
committee_size = lenu64(committee_indices)
# By default, use full participation
doAssert committee_size == SYNC_COMMITTEE_SIZE
doAssert committee_size >= num_participants
# Compute sync aggregate
var sync_committee_bits: BitArray[SYNC_COMMITTEE_SIZE]
for i in 0 ..< num_participants:
sync_committee_bits[i] = true
let
sync_committee_signature = compute_aggregate_sync_committee_signature(
cfg,
signature_forked[],
sig_slot,
committee_indices[0 ..< num_participants],
block_root)
sync_aggregate = SyncAggregate(
sync_committee_bits: sync_committee_bits,
sync_committee_signature: sync_committee_signature
)
(sync_aggregate, sig_slot)
proc block_for_next_slot(
cfg: RuntimeConfig,
forked: var ForkedHashedBeaconState,
cache: var StateCache,
withAttestations = false): ForkedSignedBeaconBlock =
template state(): auto = forked.altairData.data
let attestations =
if withAttestations:
let block_root = withState(forked): state.latest_block_root
makeFullAttestations(forked, block_root, state.slot, cache)
else:
@[]
addTestBlock(
forked, cache, attestations = attestations, cfg = cfg)
let full_sync_committee_bits = block:
var res: BitArray[SYNC_COMMITTEE_SIZE]
res.bytes.fill(byte.high)
res
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/tests/core/pyspec/eth2spec/test/helpers/light_client.py#L6-L15
func initialize_light_client_store(state: auto): LightClientStore =
LightClientStore(
finalized_header: BeaconBlockHeader(),
current_sync_committee: state.current_sync_committee,
next_sync_committee: state.next_sync_committee,
best_valid_update: none(altair.LightClientUpdate),
optimistic_header: BeaconBlockHeader(),
previous_max_active_participants: 0,
current_max_active_participants: 0,
)
suite "EF - Altair - Unittests - Sync protocol" & preset():
let
cfg = block:
var res = defaultRuntimeConfig
res.ALTAIR_FORK_EPOCH = GENESIS_EPOCH
res
genesisState = newClone(initGenesisState(cfg = cfg))
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L25-L67
test "test_process_light_client_update_not_timeout":
let forked = assignClone(genesisState[])
template state(): auto = forked[].altairData.data
var store = initialize_light_client_store(state)
# Block at slot 1 doesn't increase sync committee period,
# so it won't update snapshot
var cache: StateCache
let
attested_block = block_for_next_slot(cfg, forked[], cache).altairData
attested_header = attested_block.toBeaconBlockHeader
# 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])
# 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])
update = altair.LightClientUpdate(
attested_header: attested_header,
next_sync_committee: next_sync_committee,
next_sync_committee_branch: next_sync_committee_branch,
finalized_header: finality_header,
finality_branch: finality_branch,
sync_aggregate: sync_aggregate,
signature_slot: signature_slot)
pre_store_finalized_header = store.finalized_header
res = process_light_client_update(
store, update, signature_slot, cfg, state.genesis_validators_root)
check:
res.isOk
store.finalized_header == pre_store_finalized_header
store.best_valid_update.get == update
store.optimistic_header == update.attested_header
store.current_max_active_participants > 0
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L70-L116
test "test_process_light_client_update_at_period_boundary":
var forked = assignClone(genesisState[])
template state(): auto = forked[].altairData.data
var store = initialize_light_client_store(state)
# Forward to slot before next sync committee period so that next block is
# final one in period
var
cache: StateCache
info: ForkedEpochInfo
process_slots(
cfg, forked[], Slot(UPDATE_TIMEOUT - 2), cache, info, flags = {}
).expect("no failure")
let
store_period = sync_committee_period(store.optimistic_header.slot)
update_period = sync_committee_period(state.slot)
check: store_period == update_period
let
attested_block = block_for_next_slot(cfg, forked[], cache).altairData
attested_header = attested_block.toBeaconBlockHeader
# 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])
# Finality is unchanged
let
finality_header = BeaconBlockHeader()
finality_branch =
default(array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest])
update = altair.LightClientUpdate(
attested_header: attested_header,
next_sync_committee: next_sync_committee,
next_sync_committee_branch: next_sync_committee_branch,
finalized_header: finality_header,
finality_branch: finality_branch,
sync_aggregate: sync_aggregate,
signature_slot: signature_slot)
pre_store_finalized_header = store.finalized_header
res = process_light_client_update(
store, update, signature_slot, cfg, state.genesis_validators_root)
check:
res.isOk
store.finalized_header == pre_store_finalized_header
store.best_valid_update.get == update
store.optimistic_header == update.attested_header
store.current_max_active_participants > 0
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L119-L166
test "process_light_client_update_timeout":
let forked = assignClone(genesisState[])
template state(): auto = forked[].altairData.data
var store = initialize_light_client_store(state)
# Forward to next sync committee period
var
cache: StateCache
info: ForkedEpochInfo
process_slots(
cfg, forked[], Slot(UPDATE_TIMEOUT), cache, info, flags = {}
).expect("no failure")
let
store_period = sync_committee_period(store.optimistic_header.slot)
update_period = sync_committee_period(state.slot)
check: store_period + 1 == update_period
let
attested_block = block_for_next_slot(cfg, forked[], cache).altairData
attested_header = attested_block.toBeaconBlockHeader
# Sync committee signing the attested_header
(sync_aggregate, signature_slot) = get_sync_aggregate(cfg, forked[])
# Sync committee is updated
template next_sync_committee(): auto = state.next_sync_committee
var next_sync_committee_branch {.noinit.}:
array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
state.build_proof(
altair.NEXT_SYNC_COMMITTEE_INDEX, next_sync_committee_branch)
# Finality is unchanged
let
finality_header = BeaconBlockHeader()
finality_branch =
default(array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest])
update = altair.LightClientUpdate(
attested_header: attested_header,
next_sync_committee: next_sync_committee,
next_sync_committee_branch: next_sync_committee_branch,
finalized_header: finality_header,
finality_branch: finality_branch,
sync_aggregate: sync_aggregate,
signature_slot: signature_slot)
pre_store_finalized_header = store.finalized_header
res = process_light_client_update(
store, update, signature_slot, cfg, state.genesis_validators_root)
check:
res.isOk
store.finalized_header == pre_store_finalized_header
store.best_valid_update.get == update
store.optimistic_header == update.attested_header
store.current_max_active_participants > 0
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L169-L223
test "process_light_client_update_finality_updated":
let forked = assignClone(genesisState[])
template state(): auto = forked[].altairData.data
var store = initialize_light_client_store(state)
# Change finality
var
cache: StateCache
info: ForkedEpochInfo
blocks = newSeq[ForkedSignedBeaconBlock]()
process_slots(
cfg, forked[], Slot(SLOTS_PER_EPOCH * 2), cache, info, flags = {}).expect("no failure")
for slot in 0 ..< 3 * SLOTS_PER_EPOCH:
blocks.add block_for_next_slot(cfg, forked[], cache,
withAttestations = true)
# Ensure that finality checkpoint has changed
check: state.finalized_checkpoint.epoch == 3
# Ensure that it's same period
let
store_period = sync_committee_period(store.optimistic_header.slot)
update_period = sync_committee_period(state.slot)
check: store_period == update_period
let
attested_block = blocks[^1].altairData
attested_header = attested_block.toBeaconBlockHeader
# Sync committee signing the attested_header
(sync_aggregate, signature_slot) = get_sync_aggregate(cfg, forked[])
# Updated sync_committee and finality
next_sync_committee = SyncCommittee()
next_sync_committee_branch =
default(array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest])
finalized_block = blocks[SLOTS_PER_EPOCH - 1].altairData
finalized_header = finalized_block.toBeaconBlockHeader
check:
finalized_header.slot == start_slot(state.finalized_checkpoint.epoch)
finalized_header.hash_tree_root() == state.finalized_checkpoint.root
var finality_branch {.noinit.}:
array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest]
state.build_proof(
altair.FINALIZED_ROOT_INDEX, finality_branch)
let
update = altair.LightClientUpdate(
attested_header: attested_header,
next_sync_committee: next_sync_committee,
next_sync_committee_branch: next_sync_committee_branch,
finalized_header: finalized_header,
finality_branch: finality_branch,
sync_aggregate: sync_aggregate,
signature_slot: signature_slot)
res = process_light_client_update(
store, update, signature_slot, cfg, state.genesis_validators_root)
check:
res.isOk
store.finalized_header == update.finalized_header
store.best_valid_update.isNone
store.optimistic_header == update.attested_header
store.current_max_active_participants > 0