update to pre-release light client sync protocol (#3465)

This adopts the spec sections of the pre-release proposal of the libp2p
based light client sync protocol, and also adds a test runner for the
new accompanying tests. While the release version of the light client
sync protocol contains conflicting definitions, it is currently unused,
and the code specific to the pre-release proposal is marked as such.
See https://github.com/ethereum/consensus-specs/pull/2802
This commit is contained in:
Etan Kissling 2022-03-08 13:21:56 +01:00 committed by GitHub
parent aaa5a5ad40
commit 5a3ba5d968
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 529 additions and 70 deletions

View File

@ -799,13 +799,19 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
+ Testing VoluntaryExit OK
```
OK: 35/35 Fail: 0/35 Skip: 0/35
## EF - Altair - Sync protocol - Light client [Preset: mainnet]
```diff
All tests Skip
```
OK: 0/1 Fail: 0/1 Skip: 1/1
## EF - Altair - Unittests - Sync protocol [Preset: mainnet]
```diff
+ process_light_client_update_finality_updated OK
+ process_light_client_update_timeout OK
+ test_process_light_client_update_at_period_boundary OK
+ test_process_light_client_update_not_timeout OK
```
OK: 3/3 Fail: 0/3 Skip: 0/3
OK: 4/4 Fail: 0/4 Skip: 0/4
## EF - Bellatrix - Epoch Processing - Effective balance updates [Preset: mainnet]
```diff
+ Effective balance updates - effective_balance_hysteresis [Preset: mainnet] OK
@ -1209,4 +1215,4 @@ OK: 44/44 Fail: 0/44 Skip: 0/44
OK: 27/27 Fail: 0/27 Skip: 0/27
---TOTAL---
OK: 1034/1035 Fail: 0/1035 Skip: 1/1035
OK: 1035/1037 Fail: 0/1037 Skip: 2/1037

View File

@ -840,13 +840,19 @@ OK: 5/5 Fail: 0/5 Skip: 0/5
+ Testing VoluntaryExit OK
```
OK: 35/35 Fail: 0/35 Skip: 0/35
## EF - Altair - Sync protocol - Light client [Preset: minimal]
```diff
All tests Skip
```
OK: 0/1 Fail: 0/1 Skip: 1/1
## EF - Altair - Unittests - Sync protocol [Preset: minimal]
```diff
+ process_light_client_update_finality_updated OK
+ process_light_client_update_timeout OK
+ test_process_light_client_update_at_period_boundary OK
+ test_process_light_client_update_not_timeout OK
```
OK: 3/3 Fail: 0/3 Skip: 0/3
OK: 4/4 Fail: 0/4 Skip: 0/4
## EF - Bellatrix - Epoch Processing - Effective balance updates [Preset: minimal]
```diff
+ Effective balance updates - effective_balance_hysteresis [Preset: minimal] OK
@ -1286,4 +1292,4 @@ OK: 48/48 Fail: 0/48 Skip: 0/48
OK: 30/30 Fail: 0/30 Skip: 0/30
---TOTAL---
OK: 1084/1104 Fail: 0/1104 Skip: 20/1104
OK: 1085/1106 Fail: 0/1106 Skip: 21/1106

View File

@ -24,6 +24,10 @@
{.push raises: [Defect].}
# 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
import
std/[typetraits, sets, hashes],
chronicles,
@ -51,13 +55,14 @@ const
TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE* = 16
SYNC_COMMITTEE_SUBNET_COUNT* = 4
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#constants
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#constants
# All of these indices are rooted in `BeaconState`.
# The first member (`genesis_time`) is 32, subsequent members +1 each.
# If there are ever more than 32 members in `BeaconState`, indices change!
# `FINALIZED_ROOT_INDEX` is one layer deeper, i.e., `52 * 2 + 1`.
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/ssz/merkle-proofs.md
FINALIZED_ROOT_INDEX* = 105.GeneralizedIndex # `finalized_checkpoint` > `root`
CURRENT_SYNC_COMMITTEE_INDEX* = 54.GeneralizedIndex # `current_sync_committee`
NEXT_SYNC_COMMITTEE_INDEX* = 55.GeneralizedIndex # `next_sync_committee`
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/beacon-chain.md#participation-flag-indices
@ -157,12 +162,23 @@ type
### Modified/overloaded
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#lightclientupdate
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#lightclientbootstrap
LightClientBootstrap* = object
header*: BeaconBlockHeader ##\
## The requested beacon block header
# Current sync committee corresponding to the requested header
current_sync_committee*: SyncCommittee
current_sync_committee_branch*:
array[log2trunc(CURRENT_SYNC_COMMITTEE_INDEX), Eth2Digest]
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#lightclientupdate
LightClientUpdate* = object
attested_header*: BeaconBlockHeader ##\
## The beacon block header that is attested to by the sync committee
# Next sync committee corresponding to the active header
# Next sync committee corresponding to the active header,
# if signature is from current sync committee
next_sync_committee*: SyncCommittee
next_sync_committee_branch*:
array[log2trunc(NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
@ -177,6 +193,20 @@ type
fork_version*: Version ##\
## Fork version for the aggregate signature
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#optimisticlightclientupdate
OptimisticLightClientUpdate* = object
attested_header*: BeaconBlockHeader ##\
## The beacon block header that is attested to by the sync committee
sync_aggregate*: SyncAggregate ##\
## Sync committee aggregate signature
fork_version*: Version ##\
## Fork version for the aggregate signature
is_signed_by_next_sync_committee*: bool ##\
## Whether the signature was produced by `attested_header`'s next sync committee
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#lightclientstore
LightClientStore* = object
finalized_header*: BeaconBlockHeader ##\

View File

@ -7,6 +7,10 @@
{.push raises: [Defect].}
# 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
import
stew/[bitops2, objects],
datatypes/altair,
@ -59,34 +63,89 @@ func get_safety_threshold(store: LightClientStore): uint64 =
store.current_max_active_participants
) div 2
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#validate_light_client_update
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#initialize_light_client_store
func initialize_light_client_store*(
trusted_block_root: Eth2Digest,
bootstrap: altair.LightClientBootstrap
): Opt[LightClientStore] =
if hash_tree_root(bootstrap.header) != trusted_block_root:
return err()
if not is_valid_merkle_branch(
hash_tree_root(bootstrap.current_sync_committee),
bootstrap.current_sync_committee_branch,
log2trunc(altair.CURRENT_SYNC_COMMITTEE_INDEX),
get_subtree_index(altair.CURRENT_SYNC_COMMITTEE_INDEX),
bootstrap.header.state_root):
return err()
return ok(LightClientStore(
finalized_header: bootstrap.header,
current_sync_committee: bootstrap.current_sync_committee,
optimistic_header: bootstrap.header))
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#validate_light_client_update
proc validate_light_client_update*(
store: LightClientStore,
update: altair.LightClientUpdate,
current_slot: Slot,
cfg: RuntimeConfig,
genesis_validators_root: Eth2Digest): bool =
# Verify update slot is larger than slot of current best finalized header
# Verify sync committee has sufficient participants
template sync_aggregate(): auto = update.sync_aggregate
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
let num_active_participants = countOnes(sync_committee_bits).uint64
if num_active_participants < MIN_SYNC_COMMITTEE_PARTICIPANTS:
return false
# Determine update header
template attested_header(): auto = update.attested_header
if current_slot < attested_header.slot:
return false
let active_header = get_active_header(update)
if not (current_slot >= active_header.slot and
active_header.slot > store.finalized_header.slot):
if attested_header.slot < active_header.slot:
return false
# Verify update is relevant
let is_next_sync_committee_known = not store.next_sync_committee.isZeroMemory
if is_next_sync_committee_known:
if active_header.slot < store.finalized_header.slot:
return false
if active_header.slot == store.finalized_header.slot:
if attested_header.slot <= store.optimistic_header.slot:
return false
# Verify update does not skip a sync committee period
let
finalized_period = sync_committee_period(store.finalized_header.slot)
update_period = sync_committee_period(active_header.slot)
finalized_period = store.finalized_header.slot.sync_committee_period
update_period = active_header.slot.sync_committee_period
if update_period notin [finalized_period, finalized_period + 1]:
return false
let
is_signed_by_next_sync_committee =
update.next_sync_committee.isZeroMemory
signature_period =
if is_signed_by_next_sync_committee:
update_period + 1
else:
update_period
current_period = current_slot.sync_committee_period
if current_period < signature_period:
return false
if is_next_sync_committee_known:
if signature_period notin [finalized_period, finalized_period + 1]:
return false
else:
if signature_period != finalized_period:
return false
# Verify fork version is acceptable
let fork_version = update.fork_version
if not cfg.period_contains_fork_version(update_period, fork_version):
if not cfg.period_contains_fork_version(signature_period, fork_version):
return false
# Verify that the `finalized_header`, if present, actually is the finalized
# header saved in the state of the `attested header`
# header saved in the state of the `attested_header`
if not update.is_finality_update:
if not update.finality_branch.isZeroMemory:
return false
@ -99,13 +158,15 @@ proc validate_light_client_update*(
update.attested_header.state_root):
return false
# Verify update next sync committee if the update period incremented
# TODO: Use a view type instead of `unsafeAddr`
let sync_committee = if update_period == finalized_period:
# Verify that the `next_sync_committee`, if present, actually is the
# next sync committee saved in the state of the `active_header`
if is_signed_by_next_sync_committee:
if not update.next_sync_committee_branch.isZeroMemory:
return false
unsafeAddr store.current_sync_committee
else:
if update_period == finalized_period and is_next_sync_committee_known:
if update.next_sync_committee != store.next_sync_committee:
return false
if not is_valid_merkle_branch(
hash_tree_root(update.next_sync_committee),
update.next_sync_committee_branch,
@ -113,25 +174,22 @@ proc validate_light_client_update*(
get_subtree_index(altair.NEXT_SYNC_COMMITTEE_INDEX),
active_header.state_root):
return false
unsafeAddr store.next_sync_committee
template sync_aggregate(): auto = update.sync_aggregate
let sync_committee_participants_count = countOnes(sync_aggregate.sync_committee_bits)
# Verify sync committee has sufficient participants
if sync_committee_participants_count < MIN_SYNC_COMMITTEE_PARTICIPANTS:
return false
# Verify sync committee aggregate signature
# participant_pubkeys = [pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) if bit]
var participant_pubkeys = newSeqOfCap[ValidatorPubKey](sync_committee_participants_count)
let sync_committee =
if signature_period == finalized_period:
unsafeAddr store.current_sync_committee
else:
unsafeAddr store.next_sync_committee
var participant_pubkeys =
newSeqOfCap[ValidatorPubKey](num_active_participants)
for idx, bit in sync_aggregate.sync_committee_bits:
if bit:
participant_pubkeys.add(sync_committee.pubkeys[idx])
let
domain = compute_domain(
DOMAIN_SYNC_COMMITTEE, fork_version, genesis_validators_root)
signing_root = compute_signing_root(update.attested_header, domain)
signing_root = compute_signing_root(attested_header, domain)
if not blsFastAggregateVerify(
participant_pubkeys, signing_root.data,
sync_aggregate.sync_committee_signature):
@ -139,59 +197,186 @@ proc validate_light_client_update*(
true
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#apply_light_client_update
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#validate_optimistic_light_client_update
proc validate_optimistic_light_client_update*(
store: LightClientStore,
optimistic_update: OptimisticLightClientUpdate,
current_slot: Slot,
cfg: RuntimeConfig,
genesis_validators_root: Eth2Digest): bool =
# Verify sync committee has sufficient participants
template sync_aggregate(): auto = optimistic_update.sync_aggregate
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
let num_active_participants = countOnes(sync_committee_bits).uint64
if num_active_participants < MIN_SYNC_COMMITTEE_PARTICIPANTS:
return false
# Determine update header
template attested_header(): auto = optimistic_update.attested_header
if current_slot < attested_header.slot:
return false
template active_header(): auto = attested_header
# Verify update is relevant
if attested_header.slot <= store.optimistic_header.slot:
return false
# Verify update does not skip a sync committee period
let
finalized_period = store.finalized_header.slot.sync_committee_period
update_period = active_header.slot.sync_committee_period
if update_period notin [finalized_period, finalized_period + 1]:
return false
let
is_signed_by_next_sync_committee =
optimistic_update.is_signed_by_next_sync_committee
signature_period =
if is_signed_by_next_sync_committee:
update_period + 1
else:
update_period
current_period = current_slot.sync_committee_period
if current_period < signature_period:
return false
let is_next_sync_committee_known = not store.next_sync_committee.isZeroMemory
if is_next_sync_committee_known:
if signature_period notin [finalized_period, finalized_period + 1]:
return false
else:
if signature_period != finalized_period:
return false
# Verify fork version is acceptable
let fork_version = optimistic_update.fork_version
if not cfg.period_contains_fork_version(signature_period, fork_version):
return false
# Verify sync committee aggregate signature
let sync_committee =
if signature_period == finalized_period:
unsafeAddr store.current_sync_committee
else:
unsafeAddr store.next_sync_committee
var participant_pubkeys =
newSeqOfCap[ValidatorPubKey](num_active_participants)
for idx, bit in sync_aggregate.sync_committee_bits:
if bit:
participant_pubkeys.add(sync_committee.pubkeys[idx])
let
domain = compute_domain(
DOMAIN_SYNC_COMMITTEE, fork_version, genesis_validators_root)
signing_root = compute_signing_root(attested_header, domain)
if not blsFastAggregateVerify(
participant_pubkeys, signing_root.data,
sync_aggregate.sync_committee_signature):
return false
true
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#apply_light_client_update
func apply_light_client_update(
store: var LightClientStore,
update: altair.LightClientUpdate) =
let
active_header = get_active_header(update)
finalized_period = sync_committee_period(store.finalized_header.slot)
update_period = sync_committee_period(active_header.slot)
if update_period == finalized_period + 1:
finalized_period = store.finalized_header.slot.sync_committee_period
update_period = active_header.slot.sync_committee_period
if store.next_sync_committee.isZeroMemory:
assert update_period == finalized_period
store.next_sync_committee = update.next_sync_committee
elif update_period == finalized_period + 1:
store.previous_max_active_participants =
store.current_max_active_participants
store.current_max_active_participants = 0
store.current_sync_committee = store.next_sync_committee
store.next_sync_committee = update.next_sync_committee
assert not store.next_sync_committee.isZeroMemory
if active_header.slot > store.finalized_header.slot:
store.finalized_header = active_header
if store.finalized_header.slot > store.optimistic_header.slot:
store.optimistic_header = store.finalized_header
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#process_light_client_update
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#apply_optimistic_light_client_header
func apply_optimistic_light_client_header(
store: var LightClientStore,
attested_header: BeaconBlockHeader,
num_active_participants: uint64) =
if store.current_max_active_participants < num_active_participants:
store.current_max_active_participants = num_active_participants
if num_active_participants > get_safety_threshold(store) and
attested_header.slot > store.optimistic_header.slot:
store.optimistic_header = attested_header
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#process_slot_for_light_client_store
func process_slot_for_light_client_store*(
store: var LightClientStore,
current_slot: Slot) =
if store.best_valid_update.isSome and
current_slot > store.finalized_header.slot + UPDATE_TIMEOUT:
apply_light_client_update(store, store.best_valid_update.get)
store.best_valid_update = none(altair.LightClientUpdate)
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#process_light_client_update
proc process_light_client_update*(
store: var LightClientStore,
update: altair.LightClientUpdate,
current_slot: Slot,
cfg: RuntimeConfig,
genesis_validators_root: Eth2Digest): bool =
genesis_validators_root: Eth2Digest,
allowForceUpdate = true): bool =
if not validate_light_client_update(
store, update, current_slot, cfg, genesis_validators_root):
return false
let
sync_committee_bits = update.sync_aggregate.sync_committee_bits
sum_sync_committee_bits = countOnes(sync_committee_bits)
# Update the best update in case we have to force-update to it if the
# timeout elapses
if store.best_valid_update.isNone or
sum_sync_committee_bits > countOnes(
store.best_valid_update.get.sync_aggregate.sync_committee_bits):
store.best_valid_update = some(update)
# Track the maximum number of active participants in the committee signatures
store.current_max_active_participants = max(
store.current_max_active_participants,
sum_sync_committee_bits.uint64,
)
template sync_aggregate(): auto = update.sync_aggregate
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
let num_active_participants = countOnes(sync_committee_bits).uint64
# Update the optimistic header
if sum_sync_committee_bits.uint64 > get_safety_threshold(store) and
update.attested_header.slot > store.optimistic_header.slot:
store.optimistic_header = update.attested_header
apply_optimistic_light_client_header(
store, update.attested_header, num_active_participants)
# Update the best update in case we have to force-update to it
# if the timeout elapses
let best_active_participants =
if store.best_valid_update.isNone:
0.uint64
else:
template best_sync_aggregate(): auto =
store.best_valid_update.get.sync_aggregate
countOnes(best_sync_aggregate.sync_committee_bits).uint64
if num_active_participants > best_active_participants:
store.best_valid_update = some(update)
# Update finalized header
if sum_sync_committee_bits * 3 >= len(sync_committee_bits) * 2 and
if num_active_participants * 3 >= static(sync_committee_bits.len * 2) and
update.is_finality_update:
# Normal update through 2/3 threshold
apply_light_client_update(store, update)
store.best_valid_update = none(altair.LightClientUpdate)
else:
if allowForceUpdate:
# Force-update to best update if the timeout elapsed
process_slot_for_light_client_store(store, current_slot)
true
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#process_light_client_update
proc process_optimistic_light_client_update*(
store: var LightClientStore,
optimistic_update: OptimisticLightClientUpdate,
current_slot: Slot,
cfg: RuntimeConfig,
genesis_validators_root: Eth2Digest): bool =
if not validate_optimistic_light_client_update(
store, optimistic_update, current_slot, cfg, genesis_validators_root):
return false
template sync_aggregate(): auto = optimistic_update.sync_aggregate
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
let num_active_participants = countOnes(sync_committee_bits).uint64
# Update the optimistic header
apply_optimistic_light_client_header(
store, optimistic_update.attested_header, num_active_participants)
true

View File

@ -18,5 +18,6 @@ import
./test_fixture_sanity_slots,
./test_fixture_ssz_consensus_objects,
./test_fixture_state_transition_epoch,
./test_fixture_sync_protocol_light_client_sync,
./test_fixture_sync_protocol,
./test_fixture_transition

View File

@ -22,6 +22,10 @@ import
# 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/v1.1.10/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py#L27-L44
proc compute_aggregate_sync_committee_signature(
forked: ForkedHashedBeaconState,
@ -94,7 +98,7 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
res
genesisState = newClone(initGenesisState(cfg = cfg))
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L27-L69
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L27-L77
test "test_process_light_client_update_not_timeout":
let forked = assignClone(genesisState[])
template state: untyped {.inject.} = forked[].altairData.data
@ -120,8 +124,10 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
sync_committee_signature: sync_committee_signature)
template next_sync_committee(): auto = state.next_sync_committee
var next_sync_committee_branch:
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)
# Ensure that finality checkpoint is genesis
check: state.finalized_checkpoint.epoch == 0
@ -151,7 +157,70 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
store.finalized_header == pre_store_finalized_header
store.best_valid_update.get == update
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L72-L121
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L80-L136
test "test_process_light_client_update_at_period_boundary":
var forked = assignClone(genesisState[])
template state: untyped {.inject.} = 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
snapshot_period = sync_committee_period(store.optimistic_header.slot)
update_period = sync_committee_period(state.slot)
check: snapshot_period == update_period
let
signed_block = block_for_next_slot(cfg, forked[], cache).altairData
block_header = signed_block.toBeaconBlockHeader
# Sync committee signing the block_header
signature_slot = block_header.slot + 1
all_pubkeys = state.validators.mapIt(it.pubkey)
committee = state.next_sync_committee.pubkeys
.mapIt(all_pubkeys.find(it).ValidatorIndex)
sync_committee_bits = full_sync_committee_bits
sync_committee_signature = compute_aggregate_sync_committee_signature(
forked[], signature_slot, committee, block_header.hash_tree_root())
sync_aggregate = SyncAggregate(
sync_committee_bits: sync_committee_bits,
sync_committee_signature: sync_committee_signature)
# Sync committee is omitted (signed by next sync committee)
next_sync_committee = SyncCommittee()
var next_sync_committee_branch:
array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
# Finality is unchanged
let
finality_header = BeaconBlockHeader()
pre_store_finalized_header = store.finalized_header
var finality_branch:
array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest]
let
update = altair.LightClientUpdate(
attested_header: block_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,
fork_version: state.fork.current_version)
res = process_light_client_update(
store, update, signature_slot, cfg, state.genesis_validators_root)
check:
res
store.current_max_active_participants > 0
store.optimistic_header == update.attested_header
store.finalized_header == pre_store_finalized_header
store.best_valid_update.get == update
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L139-L193
test "process_light_client_update_timeout":
let forked = assignClone(genesisState[])
template state: untyped {.inject.} = forked[].altairData.data
@ -196,7 +265,6 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
array[log2trunc(altair.FINALIZED_ROOT_INDEX), Eth2Digest]
let
pre_store_finalized_header = store.finalized_header
update = altair.LightClientUpdate(
attested_header: block_header,
next_sync_committee: next_sync_committee,
@ -210,12 +278,12 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
check:
res
store.current_max_active_participants > 0
store.previous_max_active_participants > 0
store.optimistic_header == update.attested_header
store.finalized_header == pre_store_finalized_header
store.best_valid_update.get == update
store.finalized_header == update.attested_header
store.best_valid_update.isNone
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L124-L179
# https://github.com/ethereum/consensus-specs/blob/vFuture/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py#L196-L260
test "process_light_client_update_finality_updated":
let forked = assignClone(genesisState[])
template state: untyped {.inject.} = forked[].altairData.data
@ -251,8 +319,10 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
# Updated sync_committee and finality
template next_sync_committee(): auto = finalized_state.next_sync_committee
var next_sync_committee_branch:
var next_sync_committee_branch {.noinit.}:
array[log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX), Eth2Digest]
finalized_state.build_proof(
altair.NEXT_SYNC_COMMITTEE_INDEX, next_sync_committee_branch)
let
finalized_block = blocks[SLOTS_PER_EPOCH - 1].altairData
finalized_block_header = finalized_block.toBeaconBlockHeader

View File

@ -0,0 +1,161 @@
# beacon_chain
# Copyright (c) 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.}
# This implements the pre-release proposal of the libp2p based light client sync
# protocol. See https://github.com/ethereum/consensus-specs/pull/2802
import
# Standard library
std/[json, os, streams],
# Status libraries
stew/bitops2,
# Third-party
yaml,
# Beacon chain internals
../../../beacon_chain/spec/light_client_sync,
../../../beacon_chain/spec/datatypes/altair,
# Test utilities
../../testutil,
../fixtures_utils
const TestsDir =
SszTestsDir/const_preset/"altair"/"sync_protocol"/"light_client_sync"/"pyspec_tests"
type
TestMeta = object
genesis_validators_root: string
trusted_block_root: string
TestStepKind {.pure.} = enum
ProcessSlot
ProcessUpdate
ProcessOptimisticUpdate
TestStep = object
case kind: TestStepKind
of TestStepKind.ProcessSlot:
discard
of TestStepKind.ProcessUpdate:
update: altair.LightClientUpdate
of TestStepKind.ProcessOptimisticUpdate:
optimistic_update: OptimisticLightClientUpdate
current_slot: Slot
proc loadSteps(path: string): seq[TestStep] =
let stepsYAML = readFile(path/"steps.yaml")
let steps = yaml.loadToJson(stepsYAML)
result = @[]
for step in steps[0]:
if step.hasKey"process_slot":
let s = step["process_slot"]
result.add TestStep(kind: TestStepKind.ProcessSlot,
current_slot: s["current_slot"].getInt().Slot)
elif step.hasKey"process_update":
let
s = step["process_update"]
filename = s["update"].getStr()
update = parseTest(path/filename & ".ssz_snappy", SSZ,
altair.LightClientUpdate)
result.add TestStep(kind: TestStepKind.ProcessUpdate,
update: update,
current_slot: s["current_slot"].getInt().Slot)
elif step.hasKey"process_optimistic_update":
let
s = step["process_optimistic_update"]
filename = s["optimistic_update"].getStr()
optimistic_update = parseTest(path/filename & ".ssz_snappy", SSZ,
OptimisticLightClientUpdate)
result.add TestStep(kind: TestStepKind.ProcessOptimisticUpdate,
optimistic_update: optimistic_update,
current_slot: s["current_slot"].getInt().Slot)
else:
doAssert false, "Unreachable: " & $step
proc runTest(identifier: string) =
let testDir = TestsDir / identifier
proc `testImpl _ sync_protocol_light_client_sync _ identifier`() =
test identifier:
let
meta = block:
var s = openFileStream(testDir/"meta.yaml")
defer: close(s)
var res: TestMeta
yaml.load(s, res)
res
genesis_validators_root =
Eth2Digest.fromHex(meta.genesis_validators_root)
trusted_block_root =
Eth2Digest.fromHex(meta.trusted_block_root)
bootstrap = parseTest(testDir/"bootstrap.ssz_snappy", SSZ,
altair.LightClientBootstrap)
steps = loadSteps(testDir)
expected_finalized_header =
parseTest(testDir/"expected_finalized_header.ssz_snappy", SSZ,
BeaconBlockHeader)
expected_optimistic_header =
parseTest(testDir/"expected_optimistic_header.ssz_snappy", SSZ,
BeaconBlockHeader)
var cfg = defaultRuntimeConfig
cfg.ALTAIR_FORK_EPOCH = GENESIS_EPOCH
var store =
initialize_light_client_store(trusted_block_root, bootstrap).get
for step in steps:
case step.kind
of TestStepKind.ProcessSlot:
process_slot_for_light_client_store(
store, step.current_slot)
of TestStepKind.ProcessUpdate:
let res = process_light_client_update(
store, step.update, step.current_slot,
cfg, genesis_validators_root)
check res
of TestStepKind.ProcessOptimisticUpdate:
let res = process_optimistic_light_client_update(
store, step.optimistic_update, step.current_slot,
cfg, genesis_validators_root)
check res
check:
store.finalized_header == expected_finalized_header
store.optimistic_header == expected_optimistic_header
`testImpl _ sync_protocol_light_client_sync _ identifier`()
suite "EF - Altair - Sync protocol - Light client" & preset():
try:
for kind, path in walkDir(TestsDir, relative = true, checkDir = true):
runTest(path)
except OSError:
# These tests are for the pre-release proposal of the libp2p based light
# client sync protocol. Corresponding test vectors need manual integration.
# https://github.com/ethereum/consensus-specs/pull/2802
#
# To locally integrate the test vectors, clone the pre-release spec repo
# at latest commit of https://github.com/ethereum/consensus-specs/pull/2802
# and place it next to the `nimbus-eth2` repo, so that `nimbus-eth2` and
# `consensus-specs` are in the same directory.
#
# To generate the additional test vectors, from `consensus-specs`:
# $ rm -rf ../consensus-spec-tests && \
# doctoc specs && make lint && make gen_sync_protocol
#
# To integrate the additional test vectors into `nimbus-eth2`, first run
# `make test` from `nimbus-eth2` to ensure that the regular test vectors
# have been downloaded and extracted, then proceed from `nimbus-eth2` with:
# $ rsync -r ../consensus-spec-tests/tests/ \
# ../nimbus-eth2/vendor/nim-eth2-scenarios/tests-v1.1.10/
test "All tests":
skip()