diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 1ccc0823e5..eaceec6ac1 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -76,8 +76,7 @@ - [`generate_seed`](#generate_seed) - [`get_beacon_proposer_index`](#get_beacon_proposer_index) - [`verify_merkle_branch`](#verify_merkle_branch) - - [`get_crosslink_committee_for_attestation`](#get_crosslink_committee_for_attestation) - - [`get_attestation_participants`](#get_attestation_participants) + - [`get_attesting_indices`](#get_attesting_indices) - [`int_to_bytes1`, `int_to_bytes2`, ...](#int_to_bytes1-int_to_bytes2-) - [`bytes_to_int`](#bytes_to_int) - [`get_effective_balance`](#get_effective_balance) @@ -176,7 +175,7 @@ These configurations are updated for releases, but may be out of sync during `de | - | - | | `SHARD_COUNT` | `2**10` (= 1,024) | | `TARGET_COMMITTEE_SIZE` | `2**7` (= 128) | -| `MAX_ATTESTATION_PARTICIPANTS` | `2**12` (= 4,096) | +| `MAX_INDICES_PER_ATTESTATION` | `2**12` (= 4,096) | | `MIN_PER_EPOCH_CHURN_LIMIT` | `2**2` (= 4) | | `CHURN_LIMIT_QUOTIENT` | `2**16` (= 65,536) | | `SHUFFLE_ROUND_COUNT` | 90 | @@ -299,6 +298,8 @@ The types are defined topologically to aid in facilitating an executable version { # Epoch number 'epoch': 'uint64', + # Root of the previous crosslink + 'previous_crosslink_root': 'bytes32', # Shard data since the previous crosslink 'crosslink_data_root': 'bytes32', } @@ -332,7 +333,7 @@ The types are defined topologically to aid in facilitating an executable version # Crosslink vote 'shard': 'uint64', - 'previous_crosslink': Crosslink, + 'previous_crosslink_root': 'bytes32', 'crosslink_data_root': 'bytes32', } ``` @@ -584,7 +585,8 @@ The types are defined topologically to aid in facilitating an executable version 'finalized_root': 'bytes32', # Recent state - 'latest_crosslinks': [Crosslink, SHARD_COUNT], + 'current_crosslinks': [Crosslink, SHARD_COUNT], + 'previous_crosslinks': [Crosslink, SHARD_COUNT], 'latest_block_roots': ['bytes32', SLOTS_PER_HISTORICAL_ROOT], 'latest_state_roots': ['bytes32', SLOTS_PER_HISTORICAL_ROOT], 'latest_active_index_roots': ['bytes32', LATEST_ACTIVE_INDEX_ROOTS_LENGTH], @@ -998,33 +1000,17 @@ def verify_merkle_branch(leaf: Bytes32, proof: List[Bytes32], depth: int, index: return value == root ``` -### `get_crosslink_committee_for_attestation` +### `get_attesting_indices` ```python -def get_crosslink_committee_for_attestation(state: BeaconState, - attestation_data: AttestationData) -> List[ValidatorIndex]: +def get_attesting_indices(state: BeaconState, + attestation_data: AttestationData, + bitfield: bytes) -> List[ValidatorIndex]: """ - Return the crosslink committee corresponding to ``attestation_data``. + Return the sorted attesting indices corresponding to ``attestation_data`` and ``bitfield``. """ crosslink_committees = get_crosslink_committees_at_slot(state, attestation_data.slot) - - # Find the committee in the list with the desired shard - assert attestation_data.shard in [shard for _, shard in crosslink_committees] crosslink_committee = [committee for committee, shard in crosslink_committees if shard == attestation_data.shard][0] - - return crosslink_committee -``` - -### `get_attestation_participants` - -```python -def get_attestation_participants(state: BeaconState, - attestation_data: AttestationData, - bitfield: bytes) -> List[ValidatorIndex]: - """ - Return the sorted participant indices corresponding to ``attestation_data`` and ``bitfield``. - """ - crosslink_committee = get_crosslink_committee_for_attestation(state, attestation_data) assert verify_bitfield(bitfield, len(crosslink_committee)) return sorted([index for i, index in enumerate(crosslink_committee) if get_bitfield_bit(bitfield, i) == 0b1]) ``` @@ -1121,8 +1107,8 @@ def convert_to_indexed(state: BeaconState, attestation: Attestation) -> IndexedA """ Convert ``attestation`` to (almost) indexed-verifiable form. """ - attesting_indices = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) - custody_bit_1_indices = get_attestation_participants(state, attestation.data, attestation.custody_bitfield) + attesting_indices = get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield) + custody_bit_1_indices = get_attesting_indices(state, attestation.data, attestation.custody_bitfield) custody_bit_0_indices = [index for index in attesting_indices if index not in custody_bit_1_indices] return IndexedAttestation( @@ -1143,14 +1129,13 @@ def verify_indexed_attestation(state: BeaconState, indexed_attestation: IndexedA custody_bit_0_indices = indexed_attestation.custody_bit_0_indices custody_bit_1_indices = indexed_attestation.custody_bit_1_indices - # ensure no duplicate indices across custody bits + # Ensure no duplicate indices across custody bits assert len(set(custody_bit_0_indices).intersection(set(custody_bit_1_indices))) == 0 if len(custody_bit_1_indices) > 0: # [TO BE REMOVED IN PHASE 1] return False - total_attesting_indices = len(custody_bit_0_indices) + len(custody_bit_1_indices) - if not (1 <= total_attesting_indices <= MAX_ATTESTATION_PARTICIPANTS): + if not (1 <= len(custody_bit_0_indices) + len(custody_bit_1_indices) <= MAX_INDICES_PER_ATTESTATION): return False if custody_bit_0_indices != sorted(custody_bit_0_indices): @@ -1440,7 +1425,8 @@ def get_genesis_beacon_state(genesis_validator_deposits: List[Deposit], finalized_root=ZERO_HASH, # Recent state - latest_crosslinks=Vector([Crosslink(epoch=GENESIS_EPOCH, crosslink_data_root=ZERO_HASH) for _ in range(SHARD_COUNT)]), + current_crosslinks=Vector([Crosslink(epoch=GENESIS_EPOCH, previous_crosslink_root=ZERO_HASH, crosslink_data_root=ZERO_HASH) for _ in range(SHARD_COUNT)]), + previous_crosslinks=Vector([Crosslink(epoch=GENESIS_EPOCH, previous_crosslink_root=ZERO_HASH, crosslink_data_root=ZERO_HASH) for _ in range(SHARD_COUNT)]), latest_block_roots=Vector([ZERO_HASH for _ in range(SLOTS_PER_HISTORICAL_ROOT)]), latest_state_roots=Vector([ZERO_HASH for _ in range(SLOTS_PER_HISTORICAL_ROOT)]), latest_active_index_roots=Vector([ZERO_HASH for _ in range(LATEST_ACTIVE_INDEX_ROOTS_LENGTH)]), @@ -1606,7 +1592,7 @@ def get_previous_total_balance(state: BeaconState) -> Gwei: def get_unslashed_attesting_indices(state: BeaconState, attestations: List[PendingAttestation]) -> List[ValidatorIndex]: output = set() for a in attestations: - output = output.union(get_attestation_participants(state, a.data, a.aggregation_bitfield)) + output = output.union(get_attesting_indices(state, a.data, a.aggregation_bitfield)) return sorted(filter(lambda index: not state.validator_registry[index].slashed, list(output))) ``` @@ -1642,31 +1628,40 @@ def get_previous_epoch_matching_head_attestations(state: BeaconState) -> List[Pe **Note**: Total balances computed for the previous epoch might be marginally different than the actual total balances during the previous epoch transition. Due to the tight bound on validator churn each epoch and small per-epoch rewards/penalties, the potential balance difference is very low and only marginally affects consensus safety. ```python -def get_winning_root_and_participants(state: BeaconState, shard: Shard) -> Tuple[Bytes32, List[ValidatorIndex]]: - all_attestations = state.current_epoch_attestations + state.previous_epoch_attestations - valid_attestations = [ - a for a in all_attestations if a.data.previous_crosslink == state.latest_crosslinks[shard] - ] - all_roots = [a.data.crosslink_data_root for a in valid_attestations] - - # handle when no attestations for shard available - if len(all_roots) == 0: - return ZERO_HASH, [] +def get_crosslink_from_attestation_data(state: BeaconState, data: AttestationData) -> Crosslink: + return Crosslink( + epoch=min(slot_to_epoch(data.slot), state.current_crosslinks[data.shard].epoch + MAX_CROSSLINK_EPOCHS), + previous_crosslink_root=data.previous_crosslink_root, + crosslink_data_root=data.crosslink_data_root, + ) +``` - def get_attestations_for(root) -> List[PendingAttestation]: - return [a for a in valid_attestations if a.data.crosslink_data_root == root] +```python +def get_winning_crosslink_and_attesting_indices(state: BeaconState, epoch: Epoch, shard: Shard) -> Tuple[Crosslink, List[ValidatorIndex]]: + pending_attestations = state.current_epoch_attestations if epoch == get_current_epoch(state) else state.previous_epoch_attestations + shard_attestations = [a for a in pending_attestations if a.data.shard == shard] + shard_crosslinks = [get_crosslink_from_attestation_data(state, a.data) for a in shard_attestations] + candidate_crosslinks = [ + c for c in shard_crosslinks + if hash_tree_root(state.current_crosslinks[shard]) in (c.previous_crosslink_root, hash_tree_root(c)) + ] + if len(candidate_crosslinks) == 0: + return Crosslink(epoch=GENESIS_EPOCH, previous_crosslink_root=ZERO_HASH, crosslink_data_root=ZERO_HASH), [] - # Winning crosslink root is the root with the most votes for it, ties broken in favor of - # lexicographically higher hash - winning_root = max(all_roots, key=lambda r: (get_attesting_balance(state, get_attestations_for(r)), r)) + def get_attestations_for(crosslink: Crosslink) -> List[PendingAttestation]: + return [a for a in shard_attestations if get_crosslink_from_attestation_data(state, a.data) == crosslink] + # Winning crosslink has the crosslink data root with the most balance voting for it (ties broken lexicographically) + winning_crosslink = max(candidate_crosslinks, key=lambda crosslink: ( + get_attesting_balance(state, get_attestations_for(crosslink)), crosslink.crosslink_data_root + )) - return winning_root, get_unslashed_attesting_indices(state, get_attestations_for(winning_root)) + return winning_crosslink, get_unslashed_attesting_indices(state, get_attestations_for(winning_crosslink)) ``` ```python def get_earliest_attestation(state: BeaconState, attestations: List[PendingAttestation], index: ValidatorIndex) -> PendingAttestation: return min([ - a for a in attestations if index in get_attestation_participants(state, a.data, a.aggregation_bitfield) + a for a in attestations if index in get_attesting_indices(state, a.data, a.aggregation_bitfield) ], key=lambda a: a.inclusion_slot) ``` @@ -1724,18 +1719,14 @@ Run the following function: ```python def process_crosslinks(state: BeaconState) -> None: + state.previous_crosslinks = [c for c in state.current_crosslinks] previous_epoch = get_previous_epoch(state) next_epoch = get_current_epoch(state) + 1 for slot in range(get_epoch_start_slot(previous_epoch), get_epoch_start_slot(next_epoch)): for crosslink_committee, shard in get_crosslink_committees_at_slot(state, slot): - winning_root, participants = get_winning_root_and_participants(state, shard) - participating_balance = get_total_balance(state, participants) - total_balance = get_total_balance(state, crosslink_committee) - if 3 * participating_balance >= 2 * total_balance: - state.latest_crosslinks[shard] = Crosslink( - epoch=min(slot_to_epoch(slot), state.latest_crosslinks[shard].epoch + MAX_CROSSLINK_EPOCHS), - crosslink_data_root=winning_root - ) + winning_crosslink, attesting_indices = get_winning_crosslink_and_attesting_indices(state, slot_to_epoch(slot), shard) + if 3 * get_total_balance(state, attesting_indices) >= 2 * get_total_balance(state, crosslink_committee): + state.current_crosslinks[shard] = winning_crosslink ``` #### Rewards and penalties @@ -1817,16 +1808,14 @@ def get_justification_and_finalization_deltas(state: BeaconState) -> Tuple[List[ def get_crosslink_deltas(state: BeaconState) -> Tuple[List[Gwei], List[Gwei]]: rewards = [0 for index in range(len(state.validator_registry))] penalties = [0 for index in range(len(state.validator_registry))] - previous_epoch_start_slot = get_epoch_start_slot(get_previous_epoch(state)) - current_epoch_start_slot = get_epoch_start_slot(get_current_epoch(state)) - for slot in range(previous_epoch_start_slot, current_epoch_start_slot): + for slot in range(get_epoch_start_slot(get_previous_epoch(state)), get_epoch_start_slot(get_current_epoch(state))): for crosslink_committee, shard in get_crosslink_committees_at_slot(state, slot): - winning_root, participants = get_winning_root_and_participants(state, shard) - participating_balance = get_total_balance(state, participants) - total_balance = get_total_balance(state, crosslink_committee) + winning_crosslink, attesting_indices = get_winning_crosslink_and_attesting_indices(state, slot_to_epoch(slot), shard) + attesting_balance = get_total_balance(state, attesting_indices) + committee_balance = get_total_balance(state, crosslink_committee) for index in crosslink_committee: - if index in participants: - rewards[index] += get_base_reward(state, index) * participating_balance // total_balance + if index in attesting_indices: + rewards[index] += get_base_reward(state, index) * attesting_balance // committee_balance else: penalties[index] += get_base_reward(state, index) return [rewards, penalties] @@ -2077,32 +2066,26 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: Process ``Attestation`` operation. Note that this function mutates ``state``. """ - assert attestation.data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot <= attestation.data.slot + SLOTS_PER_EPOCH + data = attestation.data + min_slot = state.slot - SLOTS_PER_EPOCH if get_current_epoch(state) > GENESIS_EPOCH else GENESIS_SLOT + assert min_slot <= data.slot <= state.slot - MIN_ATTESTATION_INCLUSION_DELAY - # Check target epoch, source epoch, and source root - target_epoch = slot_to_epoch(attestation.data.slot) - assert (target_epoch, attestation.data.source_epoch, attestation.data.source_root) in { - (get_current_epoch(state), state.current_justified_epoch, state.current_justified_root), - (get_previous_epoch(state), state.previous_justified_epoch, state.previous_justified_root), + # Check target epoch, source epoch, source root, and source crosslink + target_epoch = slot_to_epoch(data.slot) + assert (target_epoch, data.source_epoch, data.source_root, data.previous_crosslink_root) in { + (get_current_epoch(state), state.current_justified_epoch, state.current_justified_root, hash_tree_root(state.current_crosslinks[data.shard])), + (get_previous_epoch(state), state.previous_justified_epoch, state.previous_justified_root, hash_tree_root(state.previous_crosslinks[data.shard])), } - # Check crosslink data - assert attestation.data.crosslink_data_root == ZERO_HASH # [to be removed in phase 1] - assert state.latest_crosslinks[attestation.data.shard] in { - attestation.data.previous_crosslink, # Case 1: latest crosslink matches previous crosslink - Crosslink( # Case 2: latest crosslink matches current crosslink - crosslink_data_root=attestation.data.crosslink_data_root, - epoch=min(slot_to_epoch(attestation.data.slot), - attestation.data.previous_crosslink.epoch + MAX_CROSSLINK_EPOCHS) - ), - } + # Check crosslink data root + assert data.crosslink_data_root == ZERO_HASH # [to be removed in phase 1] # Check signature and bitfields assert verify_indexed_attestation(state, convert_to_indexed(state, attestation)) # Cache pending attestation pending_attestation = PendingAttestation( - data=attestation.data, + data=data, aggregation_bitfield=attestation.aggregation_bitfield, inclusion_slot=state.slot ) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 138e69feec..74b086219d 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -309,7 +309,7 @@ def process_chunk_challenge(state: BeaconState, responder = state.validator_registry[challenge.responder_index] assert responder.exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY # Verify the responder participated in the attestation - attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) + attesters = get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield) assert challenge.responder_index in attesters # Verify the challenge is not a duplicate for record in state.custody_chunk_challenge_records: @@ -359,9 +359,9 @@ def process_bit_challenge(state: BeaconState, # Verify the attestation is eligible for challenging responder = state.validator_registry[challenge.responder_index] min_challengeable_epoch = responder.exit_epoch - EPOCHS_PER_CUSTODY_PERIOD * (1 + responder.max_reveal_lateness) - assert min_challengeable_epoch <= slot_to_epoch(challenge.attestation.data.slot) + assert min_challengeable_epoch <= slot_to_epoch(challenge.attestation.data.slot) # Verify the responder participated in the attestation - attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) + attesters = get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield) assert challenge.responder_index in attesters # A validator can be the challenger or responder for at most one challenge at a time for record in state.custody_bit_challenge_records: diff --git a/specs/validator/0_beacon-chain-validator.md b/specs/validator/0_beacon-chain-validator.md index f29e233903..632bf2b620 100644 --- a/specs/validator/0_beacon-chain-validator.md +++ b/specs/validator/0_beacon-chain-validator.md @@ -38,13 +38,13 @@ __NOTICE__: This document is a work-in-progress for researchers and implementers - [Attestations](#attestations-1) - [Attestation data](#attestation-data) - [Slot](#slot-1) - - [Shard](#shard) - [Beacon block root](#beacon-block-root) - - [Target root](#target-root) - - [Crosslink data root](#crosslink-data-root) - - [Latest crosslink](#latest-crosslink) - [Source epoch](#source-epoch) - [Source root](#source-root) + - [Target root](#target-root) + - [Shard](#shard) + - [Previous crosslink root](#previous-crosslink-root) + - [Crosslink data root](#crosslink-data-root) - [Construct attestation](#construct-attestation) - [Data](#data) - [Aggregation bitfield](#aggregation-bitfield) @@ -250,14 +250,18 @@ First the validator should construct `attestation_data`, an [`AttestationData`]( Set `attestation_data.slot = head_state.slot`. -##### Shard - -Set `attestation_data.shard = shard` where `shard` is the shard associated with the validator's committee defined by `get_crosslink_committees_at_slot`. - ##### Beacon block root Set `attestation_data.beacon_block_root = signing_root(head_block)`. +##### Source epoch + +Set `attestation_data.source_epoch = head_state.justified_epoch`. + +##### Source root + +Set `attestation_data.source_root = head_state.current_justified_root`. + ##### Target root Set `attestation_data.target_root = signing_root(epoch_boundary)` where `epoch_boundary` is the block at the most recent epoch boundary. @@ -266,23 +270,19 @@ _Note:_ This can be looked up in the state using: * Let `epoch_start_slot = get_epoch_start_slot(get_current_epoch(head_state))`. * Set `epoch_boundary = head if epoch_start_slot == head_state.slot else get_block_root(state, epoch_start_slot)`. -##### Crosslink data root - -Set `attestation_data.crosslink_data_root = ZERO_HASH`. - -_Note:_ This is a stub for phase 0. +##### Shard -##### Latest crosslink +Set `attestation_data.shard = shard` where `shard` is the shard associated with the validator's committee defined by `get_crosslink_committees_at_slot`. -Set `attestation_data.previous_crosslink = head_state.latest_crosslinks[shard]`. +##### Previous crosslink root -##### Source epoch +Set `attestation_data.previous_crosslink_root = hash_tree_root(head_state.current_crosslinks[shard])`. -Set `attestation_data.source_epoch = head_state.justified_epoch`. +##### Crosslink data root -##### Source root +Set `attestation_data.crosslink_data_root = ZERO_HASH`. -Set `attestation_data.source_root = head_state.current_justified_root`. +_Note:_ This is a stub for phase 0. #### Construct attestation @@ -299,7 +299,7 @@ Set `attestation.data = attestation_data` where `attestation_data` is the `Attes * Set `aggregation_bitfield[index_into_committee // 8] |= 2 ** (index_into_committee % 8)`. * Set `attestation.aggregation_bitfield = aggregation_bitfield`. -_Note_: Calling `get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield)` should return a list of length equal to 1, containing `validator_index`. +_Note_: Calling `get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield)` should return a list of length equal to 1, containing `validator_index`. ##### Custody bitfield diff --git a/test_libs/pyspec/tests/block_processing/test_process_attestation.py b/test_libs/pyspec/tests/block_processing/test_process_attestation.py index d66434c2c0..1be60c860b 100644 --- a/test_libs/pyspec/tests/block_processing/test_process_attestation.py +++ b/test_libs/pyspec/tests/block_processing/test_process_attestation.py @@ -14,6 +14,8 @@ from tests.helpers import ( build_empty_block_for_next_slot, get_valid_attestation, + next_epoch, + next_slot, ) @@ -120,10 +122,12 @@ def test_non_zero_crosslink_data_root(state): def test_bad_previous_crosslink(state): + next_epoch(state) attestation = get_valid_attestation(state) - state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): + next_slot(state) - state.latest_crosslinks[attestation.data.shard].epoch += 10 + state.current_crosslinks[attestation.data.shard].epoch += 10 pre_state, post_state = run_attestation_processing(state, attestation, False) diff --git a/test_libs/pyspec/tests/block_processing/test_process_proposer_slashing.py b/test_libs/pyspec/tests/block_processing/test_process_proposer_slashing.py index 6f9dee262c..6d5f3045d4 100644 --- a/test_libs/pyspec/tests/block_processing/test_process_proposer_slashing.py +++ b/test_libs/pyspec/tests/block_processing/test_process_proposer_slashing.py @@ -11,7 +11,7 @@ get_valid_proposer_slashing, ) -# mark entire file as 'header' +# mark entire file as 'proposer_slashings' pytestmark = pytest.mark.proposer_slashings diff --git a/test_libs/pyspec/tests/conftest.py b/test_libs/pyspec/tests/conftest.py index 21f5e18e64..bf9b1009b0 100644 --- a/test_libs/pyspec/tests/conftest.py +++ b/test_libs/pyspec/tests/conftest.py @@ -27,7 +27,8 @@ def overwrite_spec_config(config): if field == "LATEST_RANDAO_MIXES_LENGTH": spec.BeaconState.fields['latest_randao_mixes'][1] = config[field] elif field == "SHARD_COUNT": - spec.BeaconState.fields['latest_crosslinks'][1] = config[field] + spec.BeaconState.fields['current_crosslinks'][1] = config[field] + spec.BeaconState.fields['previous_crosslinks'][1] = config[field] elif field == "SLOTS_PER_HISTORICAL_ROOT": spec.BeaconState.fields['latest_block_roots'][1] = config[field] spec.BeaconState.fields['latest_state_roots'][1] = config[field] diff --git a/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py b/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py new file mode 100644 index 0000000000..fe694724a6 --- /dev/null +++ b/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py @@ -0,0 +1,136 @@ +from copy import deepcopy +import pytest + +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.state_transition import ( + state_transition, +) +from eth2spec.phase0.spec import ( + cache_state, + get_crosslink_deltas, + process_crosslinks, +) +from tests.helpers import ( + add_attestation_to_state, + build_empty_block_for_next_slot, + fill_aggregate_attestation, + get_crosslink_committee_for_attestation, + get_valid_attestation, + next_epoch, + next_slot, + set_bitfield_bit, +) + + +# mark entire file as 'crosslinks' +pytestmark = pytest.mark.crosslinks + + +def run_process_crosslinks(state, valid=True): + # transition state to slot before state transition + slot = state.slot + (spec.SLOTS_PER_EPOCH - state.slot % spec.SLOTS_PER_EPOCH) - 1 + block = build_empty_block_for_next_slot(state) + block.slot = slot + state_transition(state, block) + + # cache state before epoch transition + cache_state(state) + + post_state = deepcopy(state) + process_crosslinks(post_state) + + return state, post_state + + +def test_no_attestations(state): + pre_state, post_state = run_process_crosslinks(state) + + for shard in range(spec.SHARD_COUNT): + assert post_state.previous_crosslinks[shard] == post_state.current_crosslinks[shard] + + return pre_state, post_state + + +def test_single_crosslink_update_from_current_epoch(state): + next_epoch(state) + + attestation = get_valid_attestation(state) + + fill_aggregate_attestation(state, attestation) + add_attestation_to_state(state, attestation, state.slot + spec.MIN_ATTESTATION_INCLUSION_DELAY) + + assert len(state.current_epoch_attestations) == 1 + + pre_state, post_state = run_process_crosslinks(state) + + shard = attestation.data.shard + assert post_state.previous_crosslinks[shard] != post_state.current_crosslinks[shard] + assert pre_state.current_crosslinks[shard] != post_state.current_crosslinks[shard] + + return pre_state, post_state + + +def test_single_crosslink_update_from_previous_epoch(state): + next_epoch(state) + + attestation = get_valid_attestation(state) + + fill_aggregate_attestation(state, attestation) + add_attestation_to_state(state, attestation, state.slot + spec.SLOTS_PER_EPOCH) + + assert len(state.previous_epoch_attestations) == 1 + + pre_state, post_state = run_process_crosslinks(state) + crosslink_deltas = get_crosslink_deltas(state) + + shard = attestation.data.shard + assert post_state.previous_crosslinks[shard] != post_state.current_crosslinks[shard] + assert pre_state.current_crosslinks[shard] != post_state.current_crosslinks[shard] + # ensure rewarded + for index in get_crosslink_committee_for_attestation(state, attestation.data): + assert crosslink_deltas[0][index] > 0 + assert crosslink_deltas[1][index] == 0 + + return pre_state, post_state + + +def test_double_late_crosslink(state): + next_epoch(state) + state.slot += 4 + + attestation_1 = get_valid_attestation(state) + fill_aggregate_attestation(state, attestation_1) + + # add attestation_1 in the next epoch + next_epoch(state) + add_attestation_to_state(state, attestation_1, state.slot + 1) + + for slot in range(spec.SLOTS_PER_EPOCH): + attestation_2 = get_valid_attestation(state) + if attestation_2.data.shard == attestation_1.data.shard: + break + next_slot(state) + fill_aggregate_attestation(state, attestation_2) + + # add attestation_2 in the next epoch after attestation_1 has + # already updated the relevant crosslink + next_epoch(state) + add_attestation_to_state(state, attestation_2, state.slot + 1) + + assert len(state.previous_epoch_attestations) == 1 + assert len(state.current_epoch_attestations) == 0 + + pre_state, post_state = run_process_crosslinks(state) + crosslink_deltas = get_crosslink_deltas(state) + + shard = attestation_2.data.shard + + # ensure that the current crosslinks were not updated by the second attestation + assert post_state.previous_crosslinks[shard] == post_state.current_crosslinks[shard] + # ensure no reward, only penalties for the failed crosslink + for index in get_crosslink_committee_for_attestation(state, attestation_2.data): + assert crosslink_deltas[0][index] == 0 + assert crosslink_deltas[1][index] > 0 + + return pre_state, post_state diff --git a/test_libs/pyspec/tests/helpers.py b/test_libs/pyspec/tests/helpers.py index c44934950e..44d2dcb4d6 100644 --- a/test_libs/pyspec/tests/helpers.py +++ b/test_libs/pyspec/tests/helpers.py @@ -25,9 +25,9 @@ # functions convert_to_indexed, get_active_validator_indices, - get_attestation_participants, + get_attesting_indices, get_block_root, - get_crosslink_committee_for_attestation, + get_crosslink_committees_at_slot, get_current_epoch, get_domain, get_empty_block, @@ -35,6 +35,7 @@ get_genesis_beacon_state, get_previous_epoch, get_shard_delta, + hash_tree_root, slot_to_epoch, verify_merkle_branch, hash, @@ -154,6 +155,7 @@ def build_attestation_data(state, slot, shard): current_epoch_start_slot = get_epoch_start_slot(get_current_epoch(state)) if slot < current_epoch_start_slot: + print(slot) epoch_boundary_root = get_block_root(state, get_epoch_start_slot(get_previous_epoch(state))) elif slot == current_epoch_start_slot: epoch_boundary_root = block_root @@ -167,6 +169,7 @@ def build_attestation_data(state, slot, shard): justified_epoch = state.current_justified_epoch justified_block_root = state.current_justified_root + crosslinks = state.current_crosslinks if slot_to_epoch(slot) == get_current_epoch(state) else state.previous_crosslinks return AttestationData( slot=slot, shard=shard, @@ -175,7 +178,7 @@ def build_attestation_data(state, slot, shard): source_root=justified_block_root, target_root=epoch_boundary_root, crosslink_data_root=spec.ZERO_HASH, - previous_crosslink=deepcopy(state.latest_crosslinks[shard]), + previous_crosslink_root=hash_tree_root(crosslinks[shard]), ) @@ -273,6 +276,14 @@ def get_valid_attester_slashing(state): ) +def get_crosslink_committee_for_attestation(state, attestation_data): + """ + Return the crosslink committee corresponding to ``attestation_data``. + """ + crosslink_committees = get_crosslink_committees_at_slot(state, attestation_data.slot) + return [committee for committee, shard in crosslink_committees if shard == attestation_data.shard][0] + + def get_valid_attestation(state, slot=None): if slot is None: slot = state.slot @@ -297,7 +308,7 @@ def get_valid_attestation(state, slot=None): custody_bitfield=custody_bitfield, aggregate_signature=EMPTY_SIGNATURE, ) - participants = get_attestation_participants( + participants = get_attesting_indices( state, attestation.data, attestation.aggregation_bitfield, @@ -342,6 +353,13 @@ def fill_aggregate_attestation(state, attestation): attestation.aggregation_bitfield = set_bitfield_bit(attestation.aggregation_bitfield, i) +def add_attestation_to_state(state, attestation, slot): + block = build_empty_block_for_next_slot(state) + block.slot = slot + block.body.attestations.append(attestation) + state_transition(state, block) + + def next_slot(state): block = build_empty_block_for_next_slot(state) state_transition(state, block) diff --git a/test_libs/pyspec/tests/test_sanity.py b/test_libs/pyspec/tests/test_sanity.py index 236b409ace..29333a7ada 100644 --- a/test_libs/pyspec/tests/test_sanity.py +++ b/test_libs/pyspec/tests/test_sanity.py @@ -25,6 +25,7 @@ advance_slot, cache_state, set_balance, + slot_to_epoch, verify_merkle_branch, hash, ) @@ -382,33 +383,6 @@ def test_voluntary_exit(state): return pre_state, [initiate_exit_block, exit_block], post_state -def test_no_exit_churn_too_long_since_change(state): - pre_state = deepcopy(state) - validator_index = get_active_validator_indices( - pre_state, - get_current_epoch(pre_state) - )[-1] - - # - # setup pre_state - # - # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit - pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH - - post_state = deepcopy(pre_state) - - # - # Process registry change but ensure no exit - # - block = build_empty_block_for_next_slot(post_state) - block.slot += spec.SLOTS_PER_EPOCH - state_transition(post_state, block) - - assert post_state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH - - return pre_state, [block], post_state - - def test_transfer(state): pre_state = deepcopy(state) current_epoch = get_current_epoch(pre_state)