diff --git a/configs/mainnet/lightclient_patch.yaml b/configs/mainnet/lightclient_patch.yaml index a9ddc16f6..ce4b509c4 100644 --- a/configs/mainnet/lightclient_patch.yaml +++ b/configs/mainnet/lightclient_patch.yaml @@ -29,3 +29,9 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256 # Signature domains # --------------------------------------------------------------- DOMAIN_SYNC_COMMITTEE: 0x07000000 + + +# Sync protocol +# --------------------------------------------------------------- +# 2**13 (=8192) +LIGHT_CLIENT_UPDATE_TIMEOUT: 8192 diff --git a/configs/minimal/lightclient_patch.yaml b/configs/minimal/lightclient_patch.yaml index 56ce34591..4e8cf3fd3 100644 --- a/configs/minimal/lightclient_patch.yaml +++ b/configs/minimal/lightclient_patch.yaml @@ -29,3 +29,9 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8 # Signature domains # --------------------------------------------------------------- DOMAIN_SYNC_COMMITTEE: 0x07000000 + + +# Sync protocol +# --------------------------------------------------------------- +# [customized] +LIGHT_CLIENT_UPDATE_TIMEOUT: 32 diff --git a/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py b/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py index b7b2381e3..da85fad60 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py +++ b/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py @@ -5,17 +5,18 @@ from eth2spec.test.helpers.block import ( from eth2spec.utils import bls -def compute_sync_committee_signature(spec, state, slot, privkey): +def compute_sync_committee_signature(spec, state, slot, privkey, block_root=None): domain = spec.get_domain(state, spec.DOMAIN_SYNC_COMMITTEE, spec.compute_epoch_at_slot(slot)) - if slot == state.slot: - block_root = build_empty_block_for_next_slot(spec, state).parent_root - else: - block_root = spec.get_block_root_at_slot(state, slot) + if block_root is None: + if slot == state.slot: + block_root = build_empty_block_for_next_slot(spec, state).parent_root + else: + block_root = spec.get_block_root_at_slot(state, slot) signing_root = spec.compute_signing_root(block_root, domain) return bls.Sign(privkey, signing_root) -def compute_aggregate_sync_committee_signature(spec, state, slot, participants): +def compute_aggregate_sync_committee_signature(spec, state, slot, participants, block_root=None): if len(participants) == 0: return spec.G2_POINT_AT_INFINITY @@ -28,6 +29,7 @@ def compute_aggregate_sync_committee_signature(spec, state, slot, participants): state, slot, privkey, + block_root=block_root, ) ) return bls.Aggregate(signatures) diff --git a/tests/core/pyspec/eth2spec/test/lightclient_patch/unittests/test_helpers.py b/tests/core/pyspec/eth2spec/test/lightclient_patch/unittests/test_helpers.py index 33e749380..48054d088 100644 --- a/tests/core/pyspec/eth2spec/test/lightclient_patch/unittests/test_helpers.py +++ b/tests/core/pyspec/eth2spec/test/lightclient_patch/unittests/test_helpers.py @@ -9,7 +9,7 @@ from eth2spec.test.helpers.merkle import build_proof @with_phases([LIGHTCLIENT_PATCH]) @spec_state_test def test_next_sync_committee_tree(spec, state): - state.next_sync_committee = spec.SyncCommittee( + state.next_sync_committee: object = spec.SyncCommittee( pubkeys=[state.validators[i]for i in range(spec.SYNC_COMMITTEE_SIZE)] ) next_sync_committee_branch = build_proof(state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX) diff --git a/tests/core/pyspec/eth2spec/test/lightclient_patch/unittests/test_sync_protocol.py b/tests/core/pyspec/eth2spec/test/lightclient_patch/unittests/test_sync_protocol.py new file mode 100644 index 000000000..b9fdb66ba --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/lightclient_patch/unittests/test_sync_protocol.py @@ -0,0 +1,212 @@ +from eth2spec.test.context import ( + LIGHTCLIENT_PATCH, + spec_state_test, + with_phases, +) +from eth2spec.test.helpers.attestations import next_epoch_with_attestations +from eth2spec.test.helpers.block import ( + build_empty_block, + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.state import ( + next_slots, + state_transition_and_sign_block, +) +from eth2spec.test.helpers.sync_committee import ( + compute_aggregate_sync_committee_signature, +) +from eth2spec.test.helpers.merkle import build_proof + + +@with_phases([LIGHTCLIENT_PATCH]) +@spec_state_test +def test_process_light_client_update_not_updated(spec, state): + pre_snapshot = spec.LightClientSnapshot( + header=spec.BeaconBlockHeader(), + current_sync_committee=state.current_sync_committee, + next_sync_committee=state.next_sync_committee, + ) + store = spec.LightClientStore( + snapshot=pre_snapshot, + valid_updates=[] + ) + + # Block at slot 1 doesn't increase sync committee period, so it won't update snapshot + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_header = spec.BeaconBlockHeader( + slot=signed_block.message.slot, + proposer_index=signed_block.message.proposer_index, + parent_root=signed_block.message.parent_root, + state_root=signed_block.message.state_root, + body_root=signed_block.message.body.hash_tree_root(), + ) + # Sync committee signing the header + committee = spec.get_sync_committee_indices(state, spec.get_current_epoch(state)) + sync_committee_bits = [True] * len(committee) + sync_committee_signature = compute_aggregate_sync_committee_signature( + spec, + state, + block.slot, + committee, + ) + next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))] + + # Ensure that finality checkpoint is genesis + assert state.finalized_checkpoint.epoch == 0 + # Finality is unchanged + finality_header = spec.BeaconBlockHeader() + finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))] + + update = spec.LightClientUpdate( + header=block_header, + next_sync_committee=state.next_sync_committee, + next_sync_committee_branch=next_sync_committee_branch, + finality_header=finality_header, + finality_branch=finality_branch, + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + fork_version=state.fork.current_version, + ) + + spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) + + assert len(store.valid_updates) == 1 + assert store.valid_updates[0] == update + assert store.snapshot == pre_snapshot + + +@with_phases([LIGHTCLIENT_PATCH]) +@spec_state_test +def test_process_light_client_update_timeout(spec, state): + pre_snapshot = spec.LightClientSnapshot( + header=spec.BeaconBlockHeader(), + current_sync_committee=state.current_sync_committee, + next_sync_committee=state.next_sync_committee, + ) + store = spec.LightClientStore( + snapshot=pre_snapshot, + valid_updates=[] + ) + + # Forward to next sync committee period + next_slots(spec, state, spec.SLOTS_PER_EPOCH * (spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD)) + snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + assert snapshot_period + 1 == update_period + + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_header = spec.BeaconBlockHeader( + slot=signed_block.message.slot, + proposer_index=signed_block.message.proposer_index, + parent_root=signed_block.message.parent_root, + state_root=signed_block.message.state_root, + body_root=signed_block.message.body.hash_tree_root(), + ) + + # Sync committee is updated + next_sync_committee_branch = build_proof(state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX) + # Finality is unchanged + finality_header = spec.BeaconBlockHeader() + finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))] + + # Sync committee signing the finalized_block_header + committee = spec.get_sync_committee_indices(state, spec.get_current_epoch(state)) + sync_committee_bits = [True] * len(committee) + sync_committee_signature = compute_aggregate_sync_committee_signature( + spec, + state, + block_header.slot, + committee, + block_root=spec.Root(block_header.hash_tree_root()), + ) + + update = spec.LightClientUpdate( + header=block_header, + next_sync_committee=state.next_sync_committee, + next_sync_committee_branch=next_sync_committee_branch, + finality_header=finality_header, + finality_branch=finality_branch, + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + fork_version=state.fork.current_version, + ) + + spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) + + # snapshot has been updated + assert len(store.valid_updates) == 0 + assert store.snapshot.header == update.header + + +@with_phases([LIGHTCLIENT_PATCH]) +@spec_state_test +def test_process_light_client_update_finality_updated(spec, state): + pre_snapshot = spec.LightClientSnapshot( + header=spec.BeaconBlockHeader(), + current_sync_committee=state.current_sync_committee, + next_sync_committee=state.next_sync_committee, + ) + store = spec.LightClientStore( + snapshot=pre_snapshot, + valid_updates=[] + ) + + # Change finality + blocks = [] + next_slots(spec, state, spec.SLOTS_PER_EPOCH * 2) + for epoch in range(3): + prev_state, new_blocks, state = next_epoch_with_attestations(spec, state, True, True) + blocks += new_blocks + # Ensure that finality checkpoint has changed + assert state.finalized_checkpoint.epoch == 3 + # Ensure that it's same period + snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + assert snapshot_period == update_period + + # Updated sync_committee and finality + next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))] + finalized_block_header = blocks[spec.SLOTS_PER_EPOCH - 1].message + assert finalized_block_header.slot == spec.compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) + assert finalized_block_header.hash_tree_root() == state.finalized_checkpoint.root + finality_branch = build_proof(state.get_backing(), spec.FINALIZED_ROOT_INDEX) + + # Sync committee signing the finalized_block_header + committee = spec.get_sync_committee_indices(state, spec.get_current_epoch(state)) + sync_committee_bits = [True] * len(committee) + sync_committee_signature = compute_aggregate_sync_committee_signature( + spec, + state, + finalized_block_header.slot, + committee, + block_root=spec.Root(finalized_block_header.hash_tree_root()), + ) + + # Build block header + block = build_empty_block(spec, state) + block_header = spec.BeaconBlockHeader( + slot=block.slot, + proposer_index=block.proposer_index, + parent_root=block.parent_root, + state_root=state.hash_tree_root(), + body_root=block.body.hash_tree_root(), + ) + + update = spec.LightClientUpdate( + header=finalized_block_header, + next_sync_committee=state.next_sync_committee, + next_sync_committee_branch=next_sync_committee_branch, + finality_header=block_header, + finality_branch=finality_branch, + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + fork_version=state.fork.current_version, + ) + + spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) + + # snapshot has been updated + assert len(store.valid_updates) == 0 + assert store.snapshot.header == update.header