diff --git a/specs/phase1/beacon-chain.md b/specs/phase1/beacon-chain.md index a1a940adf..5a5200c9e 100644 --- a/specs/phase1/beacon-chain.md +++ b/specs/phase1/beacon-chain.md @@ -652,11 +652,11 @@ def get_offset_slots(state: BeaconState, shard: Shard) -> Sequence[Slot]: ```python def is_on_time_attestation(state: BeaconState, - attestation: Attestation) -> bool: + attestation_data: AttestationData) -> bool: """ - Check if the given attestation is on-time. + Check if the given ``attestation_data`` is on-time. """ - return attestation.data.slot == compute_previous_slot(state.slot) + return attestation_data.slot == compute_previous_slot(state.slot) ``` #### `is_winning_attestation` @@ -667,11 +667,11 @@ def is_winning_attestation(state: BeaconState, committee_index: CommitteeIndex, winning_root: Root) -> bool: """ - Check if ``attestation`` helped contribute to the successful crosslink of - ``winning_root`` formed by ``committee_index`` committee at the current slot. + Check if on-time ``attestation`` helped contribute to the successful crosslink of + ``winning_root`` formed by ``committee_index`` committee. """ return ( - attestation.data.slot == state.slot + is_on_time_attestation(state, attestation.data) and attestation.data.index == committee_index and attestation.data.shard_transition_root == winning_root ) @@ -766,12 +766,14 @@ def validate_attestation(state: BeaconState, attestation: Attestation) -> None: assert attestation.data.source == state.previous_justified_checkpoint # Type 1: on-time attestations - if is_on_time_attestation(state, attestation): + if is_on_time_attestation(state, attestation.data): # Correct parent block root assert data.beacon_block_root == get_block_root_at_slot(state, compute_previous_slot(state.slot)) # Correct shard number shard = compute_shard_from_committee_index(state, attestation.data.index, attestation.data.slot) assert attestation.data.shard == shard + # On-time attestations should have a non-empty shard transition root + assert attestation.data.shard_transition_root != hash_tree_root(ShardTransition()) # Type 2: no shard transition else: # Ensure delayed attestation @@ -886,9 +888,6 @@ def process_crosslink_for_shard(state: BeaconState, for attestation in transition_attestations: participants = get_attesting_indices(state, attestation.data, attestation.aggregation_bits) transition_participants = transition_participants.union(participants) - assert attestation.data.shard_head_root == shard_transition.shard_data_roots[ - len(shard_transition.shard_data_roots) - 1 - ] enough_online_stake = ( get_total_balance(state, online_indices.intersection(transition_participants)) * 3 >= @@ -901,6 +900,12 @@ def process_crosslink_for_shard(state: BeaconState, # Attestation <-> shard transition consistency assert shard_transition_root == hash_tree_root(shard_transition) + # Check `shard_head_root` of the winning root + last_offset_index = len(shard_transition.shard_states) - 1 + shard_head_root = shard_transition.shard_states[last_offset_index].latest_block_root + for attestation in transition_attestations: + assert attestation.data.shard_head_root == shard_head_root + # Apply transition apply_shard_transition(state, shard, shard_transition) # Apply proposer reward and cost @@ -939,7 +944,7 @@ def process_crosslinks(state: BeaconState, # Since the attestations are validated, all `shard_attestations` satisfy `attestation.data.shard == shard` shard_attestations = [ attestation for attestation in attestations - if is_on_time_attestation(state, attestation) and attestation.data.index == committee_index + if is_on_time_attestation(state, attestation.data) and attestation.data.index == committee_index ] winning_root = process_crosslink_for_shard( state, committee_index, shard_transitions[shard], shard_attestations diff --git a/specs/phase1/validator.md b/specs/phase1/validator.md index f347f8757..fd3b7fef6 100644 --- a/specs/phase1/validator.md +++ b/specs/phase1/validator.md @@ -157,7 +157,7 @@ def get_shard_winning_roots(state: BeaconState, # All attestations in the block for this committee/shard and are "on time" shard_attestations = [ attestation for attestation in attestations - if is_on_time_attestation(state, attestation) and attestation.data.index == committee_index + if is_on_time_attestation(state, attestation.data) and attestation.data.index == committee_index ] committee = get_beacon_committee(state, on_time_attestation_slot, committee_index) diff --git a/tests/core/pyspec/eth2spec/test/fork_choice/test_on_shard_head.py b/tests/core/pyspec/eth2spec/test/fork_choice/test_on_shard_head.py index c84f073b2..f7b888a2e 100644 --- a/tests/core/pyspec/eth2spec/test/fork_choice/test_on_shard_head.py +++ b/tests/core/pyspec/eth2spec/test/fork_choice/test_on_shard_head.py @@ -72,7 +72,7 @@ def apply_shard_and_beacon(spec, state, store, shard_store, shard_blocks_buffer) shard_transitions = get_shard_transitions( spec, state, - shard_blocks={shard: shard_blocks_buffer}, + shard_block_dict={shard: shard_blocks_buffer}, ) shard_transition = shard_transitions[shard] attestation = get_valid_on_time_attestation( diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py index 0c3f012f5..3445f9ff6 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py @@ -83,14 +83,14 @@ def build_attestation_data(spec, state, slot, index, shard=None, shard_transitio attestation_data.shard = shard if shard_transition is not None: - lastest_shard_data_root_index = len(shard_transition.shard_data_roots) - 1 - attestation_data.shard_head_root = shard_transition.shard_data_roots[lastest_shard_data_root_index] + last_offset_index = len(shard_transition.shard_data_roots) - 1 + attestation_data.shard_head_root = shard_transition.shard_states[last_offset_index].latest_block_root attestation_data.shard_transition_root = shard_transition.hash_tree_root() else: if on_time: shard_transition = spec.get_shard_transition(state, shard, shard_blocks=[]) - lastest_shard_data_root_index = len(shard_transition.shard_data_roots) - 1 - attestation_data.shard_head_root = shard_transition.shard_data_roots[lastest_shard_data_root_index] + last_offset_index = len(shard_transition.shard_data_roots) - 1 + attestation_data.shard_head_root = shard_transition.shard_states[last_offset_index].latest_block_root attestation_data.shard_transition_root = shard_transition.hash_tree_root() else: attestation_data.shard_head_root = state.shard_states[shard].latest_block_root diff --git a/tests/core/pyspec/eth2spec/test/helpers/custody.py b/tests/core/pyspec/eth2spec/test/helpers/custody.py index f63a07099..d25b91a41 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/custody.py +++ b/tests/core/pyspec/eth2spec/test/helpers/custody.py @@ -172,7 +172,7 @@ def get_sample_shard_transition(spec, start_slot, block_lengths): start_slot=start_slot, shard_block_lengths=block_lengths, shard_data_roots=b, - shard_states=[spec.Root() for x in block_lengths], + shard_states=[spec.ShardState() for x in block_lengths], proposer_signature_aggregate=spec.BLSSignature(), ) return shard_transition diff --git a/tests/core/pyspec/eth2spec/test/helpers/shard_block.py b/tests/core/pyspec/eth2spec/test/helpers/shard_block.py index 6957f2283..c8d60938b 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/shard_block.py +++ b/tests/core/pyspec/eth2spec/test/helpers/shard_block.py @@ -30,7 +30,7 @@ def build_shard_block(spec, slot = shard_parent_state.slot + 1 if body is None: - body = b'\x56' * 128 + body = get_sample_shard_block_body(spec) beacon_state, beacon_parent_root = get_state_and_beacon_parent_root_at_slot(spec, beacon_state, slot) proposer_index = spec.get_shard_proposer_index(beacon_state, slot, shard) @@ -52,10 +52,10 @@ def build_shard_block(spec, return signed_block -def get_shard_transitions(spec, parent_beacon_state, shard_blocks): +def get_shard_transitions(spec, parent_beacon_state, shard_block_dict): shard_transitions = [spec.ShardTransition()] * spec.MAX_SHARDS on_time_slot = parent_beacon_state.slot + 1 - for shard, blocks in shard_blocks.items(): + for shard, blocks in shard_block_dict.items(): shard_transition = spec.get_shard_transition(parent_beacon_state, shard, blocks) offset_slots = spec.compute_offset_slots( spec.get_latest_slot_for_shard(parent_beacon_state, shard), @@ -81,3 +81,8 @@ def get_committee_index_of_shard(spec, state, slot, shard): # Optional[Committe if (start_shard + committee_index) % active_shard_count == shard: return committee_index return None + + +def get_sample_shard_block_body(spec, is_max=False): + size = spec.MAX_SHARD_BLOCK_SIZE if is_max else 128 + return b'\x56' * size diff --git a/tests/core/pyspec/eth2spec/test/phase1/block_processing/test_process_shard_transition.py b/tests/core/pyspec/eth2spec/test/phase1/block_processing/test_process_shard_transition.py index 6d8878177..b14b836c0 100644 --- a/tests/core/pyspec/eth2spec/test/phase1/block_processing/test_process_shard_transition.py +++ b/tests/core/pyspec/eth2spec/test/phase1/block_processing/test_process_shard_transition.py @@ -3,69 +3,208 @@ from eth2spec.test.context import ( with_all_phases_except, spec_state_test, ) -from eth2spec.test.helpers.attestations import get_valid_on_time_attestation +from eth2spec.test.helpers.attestations import ( + get_valid_attestation, + get_valid_on_time_attestation, + run_attestation_processing, +) from eth2spec.test.helpers.shard_transitions import run_shard_transitions_processing from eth2spec.test.helpers.shard_block import ( build_shard_block, get_shard_transitions, + get_sample_shard_block_body, + get_committee_index_of_shard, ) from eth2spec.test.helpers.state import transition_to, transition_to_valid_shard_slot, next_slot -def run_basic_crosslink_tests(spec, state, target_len_offset_slot, valid=True): +def get_initial_env(spec, state, target_len_offset_slot): state = transition_to_valid_shard_slot(spec, state) committee_index = spec.CommitteeIndex(0) + target_shard_slot = state.slot + target_len_offset_slot - 1 + shard = spec.compute_shard_from_committee_index(state, committee_index, target_shard_slot) + assert state.shard_states[shard].slot == state.slot - 1 + return state, shard, target_shard_slot + + +def get_attestations_and_shard_transitions(spec, state, shard_block_dict): + shard_transitions = get_shard_transitions(spec, state, shard_block_dict) + attestations = [ + get_valid_on_time_attestation( + spec, state, + index=get_committee_index_of_shard(spec, state, state.slot, shard), + shard_transition=shard_transition, + signed=True, + ) + for shard, shard_transition in enumerate(shard_transitions) + if shard_transition != spec.ShardTransition() + ] + return attestations, shard_transitions + + +def is_full_crosslink(spec, state): + epoch = spec.compute_epoch_at_slot(state.slot) + return spec.get_committee_count_per_slot(state, epoch) >= spec.get_active_shard_count(state) + + +def run_successful_crosslink_tests(spec, state, target_len_offset_slot): + state, shard, target_shard_slot = get_initial_env(spec, state, target_len_offset_slot) init_slot = state.slot - shard_slot = state.slot + target_len_offset_slot - 1 - shard = spec.compute_shard_from_committee_index(state, committee_index, shard_slot) - assert state.shard_states[shard].slot == init_slot - 1 - # Create SignedShardBlock - body = b'\x56' * spec.MAX_SHARD_BLOCK_SIZE - shard_block = build_shard_block(spec, state, shard, body=body, slot=state.slot, signed=True) - shard_blocks = [shard_block] + # Create SignedShardBlock at init_slot + shard_block = build_shard_block( + spec, state, shard, + slot=init_slot, body=get_sample_shard_block_body(spec, is_max=True), signed=True + ) + + # Transition state to target shard slot + transition_to(spec, state, target_shard_slot) + + # Create a shard_transitions that would be included at beacon block `target_shard_slot + 1` + shard_block_dict = {shard: [shard_block]} + attestations, shard_transitions = get_attestations_and_shard_transitions(spec, state, shard_block_dict) - # Transition state latest shard slot - transition_to(spec, state, shard_slot) - # Create a shard_transitions that would be included at beacon block `state.slot + target_len_offset_slot` - shard_transitions = get_shard_transitions( - spec, - state, - shard_blocks={shard: shard_blocks}, - ) - shard_transition = shard_transitions[shard] - attestation = get_valid_on_time_attestation( - spec, - state, - index=committee_index, - shard_transition=shard_transition, - signed=False, - ) next_slot(spec, state) + + for attestation in attestations: + _, _, _ = run_attestation_processing(spec, state, attestation) + + _, winning_roots = spec.get_shard_winning_roots(state, attestations) + assert len(winning_roots) == 1 + shard_transition = shard_transitions[shard] + assert winning_roots[0] == shard_transition.hash_tree_root() + pre_gasprice = state.shard_states[shard].gasprice + pre_shard_states = state.shard_states.copy() + yield from run_shard_transitions_processing(spec, state, shard_transitions, attestations) - transition_to(spec, state, init_slot + target_len_offset_slot) - pre_shard_state = state.shard_states[shard] - yield from run_shard_transitions_processing(spec, state, shard_transitions, [attestation], valid=valid) + for index, shard_state in enumerate(state.shard_states): + if index == shard: + assert shard_state != pre_shard_states[index] + assert shard_state == shard_transition.shard_states[len(shard_transition.shard_states) - 1] + assert shard_state.latest_block_root == shard_block.message.hash_tree_root() + if target_len_offset_slot == 1: + assert shard_state.gasprice > pre_gasprice + else: + assert shard_state == pre_shard_states[index] - if valid: - shard_state = state.shard_states[shard] - assert shard_state != pre_shard_state - assert shard_state == shard_transition.shard_states[len(shard_transition.shard_states) - 1] - assert shard_state.latest_block_root == shard_block.message.hash_tree_root() - if target_len_offset_slot == 1: - assert shard_state.gasprice > pre_gasprice + for pending_attestation in state.current_epoch_attestations: + assert bool(pending_attestation.crosslink_success) is True @with_all_phases_except([PHASE0]) @spec_state_test def test_basic_crosslinks(spec, state): - # NOTE: this test is only for full crosslink (minimal config), not for mainnet - yield from run_basic_crosslink_tests(spec, state, target_len_offset_slot=1, valid=True) + if not is_full_crosslink(spec, state): + # Skip this test + return + + yield from run_successful_crosslink_tests(spec, state, target_len_offset_slot=1) @with_all_phases_except([PHASE0]) @spec_state_test def test_multiple_offset_slots(spec, state): - # NOTE: this test is only for full crosslink (minimal config), not for mainnet - yield from run_basic_crosslink_tests(spec, state, target_len_offset_slot=2, valid=True) + if not is_full_crosslink(spec, state): + # Skip this test + return + + yield from run_successful_crosslink_tests(spec, state, target_len_offset_slot=2) + + +@with_all_phases_except([PHASE0]) +@spec_state_test +def test_no_winning_root(spec, state): + if not is_full_crosslink(spec, state): + # Skip this test + return + + state, shard, target_shard_slot = get_initial_env(spec, state, target_len_offset_slot=1) + init_slot = state.slot + + # Create SignedShardBlock at init_slot + shard_block = build_shard_block( + spec, state, shard, + slot=init_slot, body=get_sample_shard_block_body(spec, is_max=True), signed=True + ) + + # Transition state to target shard slot + transition_to(spec, state, target_shard_slot) + + # Create a shard_transitions that would be included at beacon block `target_shard_slot + 1` + shard_transitions = get_shard_transitions(spec, state, {shard: [shard_block]}) + shard_transition = shard_transitions[shard] + committee_index = get_committee_index_of_shard(spec, state, state.slot, shard) + attestation = get_valid_attestation( + spec, state, + index=committee_index, + shard_transition=shard_transition, + # Decrease attested participants to 1/3 committee + filter_participant_set=lambda committee: set(list(committee)[:len(committee) // 3]), + signed=True, + on_time=True, + ) + + next_slot(spec, state) + + _, _, _ = run_attestation_processing(spec, state, attestation) + + _, winning_roots = spec.get_shard_winning_roots(state, [attestation]) + assert len(winning_roots) == 0 + + # No winning root, shard_transitions[shard] is empty + shard_transitions = [spec.ShardTransition()] * spec.MAX_SHARDS + pre_shard_states = state.shard_states.copy() + yield from run_shard_transitions_processing(spec, state, shard_transitions, [attestation]) + + for pending_attestation in state.current_epoch_attestations: + assert bool(pending_attestation.crosslink_success) is False + + assert state.shard_states == pre_shard_states + + +@with_all_phases_except([PHASE0]) +@spec_state_test +def test_wrong_shard_transition_root(spec, state): + if not is_full_crosslink(spec, state): + # Skip this test + return + + state, shard, target_shard_slot = get_initial_env(spec, state, target_len_offset_slot=1) + init_slot = state.slot + + # Create SignedShardBlock at init_slot + shard_block = build_shard_block( + spec, state, shard, + slot=init_slot, body=get_sample_shard_block_body(spec, is_max=True), signed=True + ) + + # Transition state to target shard slot + transition_to(spec, state, target_shard_slot) + + # Create a shard_transitions that would be included at beacon block `target_shard_slot + 1` + shard_transitions = get_shard_transitions(spec, state, {shard: [shard_block]}) + shard_transition = shard_transitions[shard] + wrong_shard_transition = shard_transition.copy() + wrong_shard_transition.shard_states[shard].gasprice = shard_transition.shard_states[shard].gasprice + 1 + committee_index = get_committee_index_of_shard(spec, state, state.slot, shard) + attestation = get_valid_attestation( + spec, state, + index=committee_index, + shard_transition=wrong_shard_transition, + signed=True, + on_time=True, + ) + attestations = [attestation] + + next_slot(spec, state) + + run_attestation_processing(spec, state, attestation) + + # Check if winning root != shard_transition.hash_tree_root() + _, winning_roots = spec.get_shard_winning_roots(state, attestations) + assert len(winning_roots) == 1 + shard_transition = shard_transitions[shard] + assert winning_roots[0] != shard_transition.hash_tree_root() + + yield from run_shard_transitions_processing(spec, state, shard_transitions, attestations, valid=False) diff --git a/tests/core/pyspec/eth2spec/test/phase1/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase1/sanity/test_blocks.py index 33b0beac7..aa24bfc1e 100644 --- a/tests/core/pyspec/eth2spec/test/phase1/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/phase1/sanity/test_blocks.py @@ -19,9 +19,9 @@ def run_beacon_block_with_shard_blocks(spec, state, target_len_offset_slot, comm body = b'\x56' * spec.MAX_SHARD_BLOCK_SIZE shard_block = build_shard_block(spec, state, shard, body=body, slot=state.slot, signed=True) - shard_blocks: Dict[spec.Shard, Sequence[spec.SignedShardBlock]] = {shard: [shard_block]} + shard_block_dict: Dict[spec.Shard, Sequence[spec.SignedShardBlock]] = {shard: [shard_block]} - shard_transitions = get_shard_transitions(spec, state, shard_blocks) + shard_transitions = get_shard_transitions(spec, state, shard_block_dict) attestations = [ get_valid_on_time_attestation( spec, @@ -30,7 +30,7 @@ def run_beacon_block_with_shard_blocks(spec, state, target_len_offset_slot, comm shard_transition=shard_transitions[shard], signed=True, ) - for shard in shard_blocks.keys() + for shard in shard_block_dict.keys() ] beacon_block = build_empty_block(spec, state, slot=state.slot + 1) @@ -50,16 +50,16 @@ def run_beacon_block_with_shard_blocks(spec, state, target_len_offset_slot, comm for shard in range(spec.get_active_shard_count(state)): post_shard_state = state.shard_states[shard] - if shard in shard_blocks: + if shard in shard_block_dict: # Shard state has been changed to state_transition result assert post_shard_state == shard_transitions[shard].shard_states[ len(shard_transitions[shard].shard_states) - 1 ] assert post_shard_state.slot == state.slot - 1 - if len(shard_blocks[shard]) == 0: + if len((shard_block_dict[shard])) == 0: # `latest_block_root` is the same assert post_shard_state.latest_block_root == pre_shard_states[shard].latest_block_root - if target_len_offset_slot == 1 and len(shard_blocks) > 0: + if target_len_offset_slot == 1 and len(shard_block_dict[shard]) > 0: assert post_shard_state.gasprice > pre_gasprice