From 4fbe1626550d5a20f5e1cc596725ca4547f0ff37 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Tue, 1 Mar 2022 11:42:49 -0800 Subject: [PATCH] Add on_attester_slashing() and related test --- specs/phase0/fork-choice.md | 27 +++++- .../test/phase0/fork_choice/test_get_head.py | 83 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index de0a2e785..920746da2 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -101,6 +101,7 @@ class Store(object): finalized_checkpoint: Checkpoint best_justified_checkpoint: Checkpoint proposer_boost_root: Root + has_equivocated: Set[ValidatorIndex] blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) @@ -129,6 +130,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - finalized_checkpoint=finalized_checkpoint, best_justified_checkpoint=justified_checkpoint, proposer_boost_root=proposer_boost_root, + has_equivocated=set(), blocks={anchor_root: copy(anchor_block)}, block_states={anchor_root: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, @@ -179,6 +181,7 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: attestation_score = Gwei(sum( state.validators[i].effective_balance for i in active_indices if (i in store.latest_messages + and not i in store.has_equivocated and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) if store.proposer_boost_root == Root(): @@ -358,8 +361,9 @@ def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIn target = attestation.data.target beacon_block_root = attestation.data.beacon_block_root for i in attesting_indices: - if i not in store.latest_messages or target.epoch > store.latest_messages[i].epoch: - store.latest_messages[i] = LatestMessage(epoch=target.epoch, root=beacon_block_root) + if i not in store.has_equivocated: + if i not in store.latest_messages or target.epoch > store.latest_messages[i].epoch: + store.latest_messages[i] = LatestMessage(epoch=target.epoch, root=beacon_block_root) ``` @@ -459,3 +463,22 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F # Update latest messages for attesting indices update_latest_messages(store, indexed_attestation.attesting_indices, attestation) ``` + +#### `on_attester_slashing` + +```python +def on_attester_slashing(store: Store, attester_slashing: AttesterSlashing) -> None: + """ + Run ``on_attester_slashing`` upon receiving a new ``AttesterSlashing`` from either within a block or directly on the wire. + """ + attestation_1 = attester_slashing.attestation_1 + attestation_2 = attester_slashing.attestation_2 + assert is_slashable_attestation_data(attestation_1.data, attestation_2.data) + state = store.block_states[store.justified_checkpoint.root] + assert is_valid_indexed_attestation(state, attestation_1) + assert is_valid_indexed_attestation(state, attestation_2) + + indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices) + for index in sorted(indices): + store.has_equivocated.add(index) +``` \ No newline at end of file diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py index 5e4d247e7..469c8e9d4 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -24,6 +24,8 @@ from eth2spec.test.helpers.state import ( next_epoch, state_transition_and_sign_block, ) +from tests.core.pyspec.eth2spec.test.helpers.block import apply_empty_block +from tests.core.pyspec.eth2spec.test.helpers.fork_choice import run_on_attestation rng = random.Random(1001) @@ -338,3 +340,84 @@ def test_proposer_boost_correct_head(spec, state): }) yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_discard_equivocations(spec, state): + test_steps = [] + genesis_state = state.copy() + + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + anchor_root = get_anchor_root(spec, state) + assert spec.get_head(store) == anchor_root + test_steps.append({ + 'checks': { + 'head': get_formatted_head_output(spec, store), + } + }) + + # Build block that serves as head before discarding equivocations + state_1 = genesis_state.copy() + next_slots(spec, state_1, 3) + block_1 = build_empty_block_for_next_slot(spec, state_1) + signed_block_1 = state_transition_and_sign_block(spec, state_1, block_1) + + # Build equivocating attestations to feed to store + state_eqv = state_1.copy() + block_eqv = apply_empty_block(spec, state_eqv, state_eqv.slot+1) + attestation_eqv = get_valid_attestation(spec, state_eqv, slot=block_eqv.slot, signed=True) + + next_slots(spec, state_1, 1) + attestation = get_valid_attestation(spec, state_1, slot=block_eqv.slot, signed=True) + assert spec.is_slashable_attestation_data(attestation.data, attestation_eqv.data) + + indexed_attestation = spec.get_indexed_attestation(state_1, attestation) + indexed_attestation_eqv = spec.get_indexed_attestation(state_eqv, attestation_eqv) + attester_slashing = spec.AttesterSlashing(attestation_1=indexed_attestation, attestation_2=indexed_attestation_eqv) + + # Build block that serves as head after discarding equivocations + state_2 = genesis_state.copy() + next_slots(spec, state_2, 2) + block_2 = build_empty_block_for_next_slot(spec, state_2) + signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2): + block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64)) + signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + assert spec.hash_tree_root(block_1) < spec.hash_tree_root(block_2) + + # Tick to (block_eqv.slot + 2) slot time + time = store.genesis_time + (block_eqv.slot + 2) * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, time, test_steps) + + # Process block_2 + yield from add_block(spec, store, signed_block_2, test_steps) + assert store.proposer_boost_root == spec.Root() + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + # Process block_1 + # The head should remain block_2 + yield from add_block(spec, store, signed_block_1, test_steps) + assert store.proposer_boost_root == spec.Root() + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + # Process attestation + # The head should change to block_1 + run_on_attestation(spec, store, attestation) + assert spec.get_head(store) == spec.hash_tree_root(block_1) + + # Process attester_slashing + # The head should revert to block_2 + spec.on_attester_slashing(store, attester_slashing) + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + test_steps.append({ + 'checks': { + 'head': get_formatted_head_output(spec, store), + } + }) + + yield 'steps', test_steps \ No newline at end of file