Merge pull request #2845 from ethereum/discard-equivocations

Remove equivocating validators from fork choice consideration
This commit is contained in:
Danny Ryan 2022-03-08 11:41:04 -07:00 committed by GitHub
commit 825a39577e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 111 additions and 1 deletions

View File

@ -32,6 +32,7 @@
- [`on_tick`](#on_tick) - [`on_tick`](#on_tick)
- [`on_block`](#on_block) - [`on_block`](#on_block)
- [`on_attestation`](#on_attestation) - [`on_attestation`](#on_attestation)
- [`on_attester_slashing`](#on_attester_slashing)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- /TOC --> <!-- /TOC -->
@ -101,6 +102,7 @@ class Store(object):
finalized_checkpoint: Checkpoint finalized_checkpoint: Checkpoint
best_justified_checkpoint: Checkpoint best_justified_checkpoint: Checkpoint
proposer_boost_root: Root proposer_boost_root: Root
equivocating_indices: Set[ValidatorIndex]
blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
block_states: Dict[Root, BeaconState] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict)
checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict)
@ -129,6 +131,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -
finalized_checkpoint=finalized_checkpoint, finalized_checkpoint=finalized_checkpoint,
best_justified_checkpoint=justified_checkpoint, best_justified_checkpoint=justified_checkpoint,
proposer_boost_root=proposer_boost_root, proposer_boost_root=proposer_boost_root,
equivocating_indices=set(),
blocks={anchor_root: copy(anchor_block)}, blocks={anchor_root: copy(anchor_block)},
block_states={anchor_root: copy(anchor_state)}, block_states={anchor_root: copy(anchor_state)},
checkpoint_states={justified_checkpoint: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)},
@ -179,6 +182,7 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei:
attestation_score = Gwei(sum( attestation_score = Gwei(sum(
state.validators[i].effective_balance for i in active_indices state.validators[i].effective_balance for i in active_indices
if (i in store.latest_messages if (i in store.latest_messages
and i not in store.equivocating_indices
and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root)
)) ))
if store.proposer_boost_root == Root(): if store.proposer_boost_root == Root():
@ -357,7 +361,8 @@ def store_target_checkpoint_state(store: Store, target: Checkpoint) -> None:
def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation) -> None: def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation) -> None:
target = attestation.data.target target = attestation.data.target
beacon_block_root = attestation.data.beacon_block_root beacon_block_root = attestation.data.beacon_block_root
for i in attesting_indices: 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: 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) store.latest_messages[i] = LatestMessage(epoch=target.epoch, root=beacon_block_root)
``` ```
@ -459,3 +464,25 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F
# Update latest messages for attesting indices # Update latest messages for attesting indices
update_latest_messages(store, indexed_attestation.attesting_indices, attestation) update_latest_messages(store, indexed_attestation.attesting_indices, attestation)
``` ```
#### `on_attester_slashing`
*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:
"""
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
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 indices:
store.equivocating_indices.add(index)
```

View File

@ -24,6 +24,8 @@ from eth2spec.test.helpers.state import (
next_epoch, next_epoch,
state_transition_and_sign_block, 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) rng = random.Random(1001)
@ -338,3 +340,84 @@ def test_proposer_boost_correct_head(spec, state):
}) })
yield 'steps', test_steps 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