From 4fbe1626550d5a20f5e1cc596725ca4547f0ff37 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Tue, 1 Mar 2022 11:42:49 -0800 Subject: [PATCH 1/6] 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 From 7f31c80b8f0ab406f5e2f7500e06c5d5b77ef662 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Tue, 1 Mar 2022 11:47:45 -0800 Subject: [PATCH 2/6] Fix lint & CI --- specs/phase0/fork-choice.md | 6 ++++-- .../eth2spec/test/phase0/fork_choice/test_get_head.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 920746da2..4d1c967ef 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -32,6 +32,7 @@ - [`on_tick`](#on_tick) - [`on_block`](#on_block) - [`on_attestation`](#on_attestation) + - [`on_attester_slashing`](#on_attester_slashing) @@ -181,7 +182,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 i not in store.has_equivocated and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) if store.proposer_boost_root == Root(): @@ -469,7 +470,8 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F ```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. + 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 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 469c8e9d4..1f37625a8 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 @@ -368,7 +368,7 @@ def test_discard_equivocations(spec, state): # Build equivocating attestations to feed to store state_eqv = state_1.copy() - block_eqv = apply_empty_block(spec, state_eqv, state_eqv.slot+1) + 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) @@ -420,4 +420,4 @@ def test_discard_equivocations(spec, state): } }) - yield 'steps', test_steps \ No newline at end of file + yield 'steps', test_steps From 37b8a89bb13a895c8cb5df81cabfe87aa6235064 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Thu, 3 Mar 2022 11:43:52 -0800 Subject: [PATCH 3/6] Apply code review from @djrtwo --- specs/phase0/fork-choice.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 4d1c967ef..bc611b74b 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -102,7 +102,7 @@ class Store(object): finalized_checkpoint: Checkpoint best_justified_checkpoint: Checkpoint proposer_boost_root: Root - has_equivocated: Set[ValidatorIndex] + equivocating_indices: 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) @@ -131,7 +131,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(), + equivocating_indices=set(), blocks={anchor_root: copy(anchor_block)}, block_states={anchor_root: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, @@ -182,7 +182,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 i not in store.has_equivocated + and i not in store.equivocating_indices and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) if store.proposer_boost_root == Root(): @@ -361,10 +361,10 @@ def store_target_checkpoint_state(store: Store, target: Checkpoint) -> None: def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation) -> None: target = attestation.data.target beacon_block_root = attestation.data.beacon_block_root - for i in attesting_indices: - 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) + non_equivocating_attesting_indices = [i for i in attesting_indices if i not in store.equivocating_indices] + for i in non_equivocating_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) ``` @@ -481,6 +481,6 @@ def on_attester_slashing(store: Store, attester_slashing: AttesterSlashing) -> N 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) + for index in indices: + store.equivocating_indices.add(index) ``` \ No newline at end of file From 15a90407ef9eee23b7fa9b752592767ff55e6590 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Thu, 3 Mar 2022 13:50:05 -0700 Subject: [PATCH 4/6] minor comment change --- specs/phase0/fork-choice.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index bc611b74b..43dac92d1 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -470,8 +470,8 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F ```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. + Run ``on_attester_slashing`` immediately 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 From a0ba6b2a1a9c7c1f22cb3637573b56fc66e5cc39 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Tue, 8 Mar 2022 06:52:03 -0800 Subject: [PATCH 5/6] Add note about syncing --- specs/phase0/fork-choice.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 43dac92d1..66a87021c 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -467,6 +467,8 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F #### `on_attester_slashing` +*Note*: `on_attester_slashing` should be called while syncing and a node MUST maintain the equivocation set of `AttesterSlashing`s from at least the last finalized checkpoint + ```python def on_attester_slashing(store: Store, attester_slashing: AttesterSlashing) -> None: """ From bd6d2ad4ce6f37999189bdd09cf3aa8f6c767b1f Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 8 Mar 2022 11:33:59 -0700 Subject: [PATCH 6/6] minor copy edit --- specs/phase0/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 66a87021c..9b302b5fd 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -467,7 +467,7 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F #### `on_attester_slashing` -*Note*: `on_attester_slashing` should be called while syncing and a node MUST maintain the equivocation set of `AttesterSlashing`s from at least the last finalized checkpoint +*Note*: `on_attester_slashing` should be called while syncing and a client MUST maintain the equivocation set of `AttesterSlashing`s from at least the latest finalized checkpoint. ```python def on_attester_slashing(store: Store, attester_slashing: AttesterSlashing) -> None: