From 85e78223dd361bbdf6e3e567bdf6860820b008ce Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 19 May 2020 16:51:46 -0600 Subject: [PATCH 1/3] ensure when performing optimally that you don't lose money during a leak --- specs/phase0/beacon-chain.md | 31 ++++++++--- .../pyspec/eth2spec/test/helpers/rewards.py | 34 +++++++++++++ .../test_process_rewards_and_penalties.py | 51 +++++++++++++++++++ .../test/phase_0/rewards/test_leak.py | 36 +------------ 4 files changed, 110 insertions(+), 42 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 142cf3b025..7bf4cd06e5 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -1354,6 +1354,19 @@ def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: return Gwei(effective_balance * BASE_REWARD_FACTOR // integer_squareroot(total_balance) // BASE_REWARDS_PER_EPOCH) ``` + +```python +def get_finality_delay(state: BeaconState) -> uint64: + return get_previous_epoch(state) - state.finalized_checkpoint.epoch +``` + + +```python +def in_inactivity_leak(state: BeaconState) -> bool: + return get_finality_delay(state) > MIN_EPOCHS_TO_INACTIVITY_PENALTY +``` + + ```python def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]: previous_epoch = get_previous_epoch(state) @@ -1378,8 +1391,11 @@ def get_attestation_component_deltas(state: BeaconState, for index in get_eligible_validator_indices(state): if index in unslashed_attesting_indices: increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow - reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) - rewards[index] += reward_numerator // (total_balance // increment) + if in_inactivity_leak(state): + rewards[index] += get_base_reward(state, index) + else: + reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) + rewards[index] += reward_numerator // (total_balance // increment) else: penalties[index] += get_base_reward(state, index) return rewards, penalties @@ -1428,7 +1444,10 @@ def get_inclusion_delay_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequ ], key=lambda a: a.inclusion_delay) proposer_reward = Gwei(get_base_reward(state, index) // PROPOSER_REWARD_QUOTIENT) rewards[attestation.proposer_index] += proposer_reward - max_attester_reward = get_base_reward(state, index) - proposer_reward + if in_inactivity_leak(state): + max_attester_reward = get_base_reward(state, index) + else: + max_attester_reward = get_base_reward(state, index) - proposer_reward rewards[index] += Gwei(max_attester_reward // attestation.inclusion_delay) # No penalties associated with inclusion delay @@ -1442,16 +1461,14 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S Return inactivity reward/penalty deltas for each validator. """ penalties = [Gwei(0) for _ in range(len(state.validators))] - finality_delay = get_previous_epoch(state) - state.finalized_checkpoint.epoch - - if finality_delay > MIN_EPOCHS_TO_INACTIVITY_PENALTY: + if in_inactivity_leak(state): matching_target_attestations = get_matching_target_attestations(state, get_previous_epoch(state)) matching_target_attesting_indices = get_unslashed_attesting_indices(state, matching_target_attestations) for index in get_eligible_validator_indices(state): penalties[index] += Gwei(BASE_REWARDS_PER_EPOCH * get_base_reward(state, index)) if index not in matching_target_attesting_indices: effective_balance = state.validators[index].effective_balance - penalties[index] += Gwei(effective_balance * finality_delay // INACTIVITY_PENALTY_QUOTIENT) + penalties[index] += Gwei(effective_balance * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT) # No rewards associated with inactivity penalties rewards = [Gwei(0) for _ in range(len(state.validators))] diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index d62fee6ce9..034a79fd4c 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -1,4 +1,5 @@ from random import Random +from lru import LRU from eth2spec.phase0 import spec as spec_phase0 from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations @@ -170,6 +171,39 @@ def run_get_inactivity_penalty_deltas(spec, state): assert penalties[index] == 0 +def transition_state_to_leak(spec, state, epochs=None): + if epochs is None: + epochs = spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY + assert epochs >= spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY + + for _ in range(epochs): + next_epoch(spec, state) + + +_cache_dict = LRU(size=10) + + +def leaking(epochs=None): + + def deco(fn): + def entry(*args, spec, state, **kw): + # If the pre-state is not already known in the LRU, then take it, + # transition it to leak, and put it in the LRU. + # The input state is likely already cached, so the hash-tree-root does not affect speed. + key = (state.hash_tree_root(), spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY, spec.SLOTS_PER_EPOCH, epochs) + global _cache_dict + if key not in _cache_dict: + transition_state_to_leak(spec, state, epochs=epochs) + _cache_dict[key] = state.get_backing() # cache the tree structure, not the view wrapping it. + + # Take an entry out of the LRU. + # No copy is necessary, as we wrap the immutable backing with a new view. + state = spec.BeaconState(backing=_cache_dict[key]) + return fn(*args, spec=spec, state=state, **kw) + return entry + return deco + + def set_some_new_deposits(spec, state, rng): num_validators = len(state.validators) # Set ~1/10 to just recently deposited diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py index eff2864484..52d3b3c069 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py @@ -14,6 +14,7 @@ get_valid_attestation, prepare_state_with_attestations, ) +from eth2spec.test.helpers.rewards import leaking from eth2spec.test.helpers.attester_slashings import get_indexed_attestation_participants from eth2spec.test.phase_0.epoch_processing.run_epoch_process_base import run_epoch_processing_with from random import Random @@ -80,6 +81,56 @@ def test_full_attestations(spec, state): assert state.balances[index] < pre_state.balances[index] +@with_all_phases +@spec_state_test +@leaking() +def test_full_attestations_with_leak(spec, state): + attestations = prepare_state_with_attestations(spec, state) + + proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations] + pre_state = state.copy() + + yield from run_process_rewards_and_penalties(spec, state) + + attesting_indices = spec.get_unslashed_attesting_indices(state, attestations) + assert len(attesting_indices) == len(pre_state.validators) + for index in range(len(pre_state.validators)): + # Proposers can still make money during a leak + if index in proposer_indices: + assert state.balances[index] > pre_state.balances[index] + # If not proposer but participated optimally, should have exactly neutral balance + elif index in attesting_indices: + assert state.balances[index] == pre_state.balances[index] + else: + assert state.balances[index] < pre_state.balances[index] + + +@with_all_phases +@spec_state_test +@leaking() +def test_partial_attestations_with_leak(spec, state): + attestations = prepare_state_with_attestations(spec, state) + + attestations = attestations[:len(attestations) // 2] + state.previous_epoch_attestations = state.previous_epoch_attestations[:len(attestations)] + proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations] + pre_state = state.copy() + + yield from run_process_rewards_and_penalties(spec, state) + + attesting_indices = spec.get_unslashed_attesting_indices(state, attestations) + assert len(attesting_indices) < len(pre_state.validators) + for index in range(len(pre_state.validators)): + # Proposers can still make money during a leak + if index in proposer_indices and index in attesting_indices: + assert state.balances[index] > pre_state.balances[index] + # If not proposer but participated optimally, should have exactly neutral balance + elif index in attesting_indices: + assert state.balances[index] == pre_state.balances[index] + else: + assert state.balances[index] < pre_state.balances[index] + + @with_all_phases @spec_state_test def test_full_attestations_random_incorrect_fields(spec, state): diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py index 4e75079c05..b0f9767b23 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py @@ -1,40 +1,6 @@ from eth2spec.test.context import with_all_phases, spec_state_test -from eth2spec.test.helpers.state import next_epoch +from eth2spec.test.helpers.rewards import leaking import eth2spec.test.helpers.rewards as rewards_helpers -from lru import LRU - - -def transition_state_to_leak(spec, state, epochs=None): - if epochs is None: - epochs = spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY - assert epochs >= spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY - - for _ in range(epochs): - next_epoch(spec, state) - - -_cache_dict = LRU(size=10) - - -def leaking(epochs=None): - - def deco(fn): - def entry(*args, spec, state, **kw): - # If the pre-state is not already known in the LRU, then take it, - # transition it to leak, and put it in the LRU. - # The input state is likely already cached, so the hash-tree-root does not affect speed. - key = (state.hash_tree_root(), spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY, spec.SLOTS_PER_EPOCH, epochs) - global _cache_dict - if key not in _cache_dict: - transition_state_to_leak(spec, state, epochs=epochs) - _cache_dict[key] = state.get_backing() # cache the tree structure, not the view wrapping it. - - # Take an entry out of the LRU. - # No copy is necessary, as we wrap the immutable backing with a new view. - state = spec.BeaconState(backing=_cache_dict[key]) - return fn(*args, spec=spec, state=state, **kw) - return entry - return deco @with_all_phases From 95c3295eeba373e94b9340b8b0fe8c804858ee08 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 19 May 2020 17:12:53 -0600 Subject: [PATCH 2/3] move proposer negation to inactivity_penalty deltas --- specs/phase0/beacon-chain.md | 19 ++++++++++++------- .../pyspec/eth2spec/test/helpers/rewards.py | 3 ++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 7bf4cd06e5..b0a9724fa7 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -1355,6 +1355,12 @@ def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: ``` +```python +def get_proposer_reward(state: BeaconState, attesting_index: ValidatorIndex) -> Gwei: + return Gwei(get_base_reward(state, attesting_index) // PROPOSER_REWARD_QUOTIENT) +``` + + ```python def get_finality_delay(state: BeaconState) -> uint64: return get_previous_epoch(state) - state.finalized_checkpoint.epoch @@ -1392,6 +1398,7 @@ def get_attestation_component_deltas(state: BeaconState, if index in unslashed_attesting_indices: increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow if in_inactivity_leak(state): + # Full base reward will be cancelled out by inactivity penalty deltas rewards[index] += get_base_reward(state, index) else: reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) @@ -1442,12 +1449,8 @@ def get_inclusion_delay_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequ a for a in matching_source_attestations if index in get_attesting_indices(state, a.data, a.aggregation_bits) ], key=lambda a: a.inclusion_delay) - proposer_reward = Gwei(get_base_reward(state, index) // PROPOSER_REWARD_QUOTIENT) - rewards[attestation.proposer_index] += proposer_reward - if in_inactivity_leak(state): - max_attester_reward = get_base_reward(state, index) - else: - max_attester_reward = get_base_reward(state, index) - proposer_reward + rewards[attestation.proposer_index] += get_proposer_reward(state, index) + max_attester_reward = get_base_reward(state, index) - get_proposer_reward(state, index) rewards[index] += Gwei(max_attester_reward // attestation.inclusion_delay) # No penalties associated with inclusion delay @@ -1465,7 +1468,9 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S matching_target_attestations = get_matching_target_attestations(state, get_previous_epoch(state)) matching_target_attesting_indices = get_unslashed_attesting_indices(state, matching_target_attestations) for index in get_eligible_validator_indices(state): - penalties[index] += Gwei(BASE_REWARDS_PER_EPOCH * get_base_reward(state, index)) + # If validator is performing optimally this cancels all rewards for a neutral balance + base_reward = get_base_reward(state, index) + penalties[index] += Gwei(BASE_REWARDS_PER_EPOCH * base_reward - get_proposer_reward(state, index)) if index not in matching_target_attesting_indices: effective_balance = state.validators[index].effective_balance penalties[index] += Gwei(effective_balance * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT) diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index 034a79fd4c..801434e798 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -160,7 +160,8 @@ def run_get_inactivity_penalty_deltas(spec, state): continue if finality_delay > spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY: - base_penalty = spec.BASE_REWARDS_PER_EPOCH * spec.get_base_reward(state, index) + base_reward = spec.get_base_reward(state, index) + base_penalty = spec.BASE_REWARDS_PER_EPOCH * base_reward - spec.get_proposer_reward(state, index) if not has_enough_for_reward(spec, state, index): assert penalties[index] == 0 elif index in matching_attesting_indices: From 943e51aef18bb025fa34b2caeb7810f9d77e9890 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 20 May 2020 10:11:47 -0600 Subject: [PATCH 3/3] hww feedback for finality rewards fix --- specs/phase0/beacon-chain.md | 9 +- .../pyspec/eth2spec/test/helpers/rewards.py | 3 +- .../test_process_rewards_and_penalties.py | 116 +++++++----------- 3 files changed, 51 insertions(+), 77 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index b0a9724fa7..839af5f7b5 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -1368,7 +1368,7 @@ def get_finality_delay(state: BeaconState) -> uint64: ```python -def in_inactivity_leak(state: BeaconState) -> bool: +def is_in_inactivity_leak(state: BeaconState) -> bool: return get_finality_delay(state) > MIN_EPOCHS_TO_INACTIVITY_PENALTY ``` @@ -1397,8 +1397,9 @@ def get_attestation_component_deltas(state: BeaconState, for index in get_eligible_validator_indices(state): if index in unslashed_attesting_indices: increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow - if in_inactivity_leak(state): - # Full base reward will be cancelled out by inactivity penalty deltas + if is_in_inactivity_leak(state): + # Since full base reward will be canceled out by inactivity penalty deltas, + # optimal participation receives full base reward compensation here. rewards[index] += get_base_reward(state, index) else: reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) @@ -1464,7 +1465,7 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S Return inactivity reward/penalty deltas for each validator. """ penalties = [Gwei(0) for _ in range(len(state.validators))] - if in_inactivity_leak(state): + if is_in_inactivity_leak(state): matching_target_attestations = get_matching_target_attestations(state, get_previous_epoch(state)) matching_target_attesting_indices = get_unslashed_attesting_indices(state, matching_target_attestations) for index in get_eligible_validator_indices(state): diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index 801434e798..c11ba1ec1d 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -151,7 +151,6 @@ def run_get_inactivity_penalty_deltas(spec, state): matching_attestations = spec.get_matching_target_attestations(state, spec.get_previous_epoch(state)) matching_attesting_indices = spec.get_unslashed_attesting_indices(state, matching_attestations) - finality_delay = spec.get_previous_epoch(state) - state.finalized_checkpoint.epoch eligible_indices = spec.get_eligible_validator_indices(state) for index in range(len(state.validators)): assert rewards[index] == 0 @@ -159,7 +158,7 @@ def run_get_inactivity_penalty_deltas(spec, state): assert penalties[index] == 0 continue - if finality_delay > spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY: + if spec.is_in_inactivity_leak(state): base_reward = spec.get_base_reward(state, index) base_penalty = spec.BASE_REWARDS_PER_EPOCH * base_reward - spec.get_proposer_reward(state, index) if not has_enough_for_reward(spec, state, index): diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py index 52d3b3c069..bafefcad63 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py @@ -63,74 +63,6 @@ def test_genesis_epoch_full_attestations_no_rewards(spec, state): assert state.balances[index] == pre_state.balances[index] -@with_all_phases -@spec_state_test -def test_full_attestations(spec, state): - attestations = prepare_state_with_attestations(spec, state) - - pre_state = state.copy() - - yield from run_process_rewards_and_penalties(spec, state) - - attesting_indices = spec.get_unslashed_attesting_indices(state, attestations) - assert len(attesting_indices) == len(pre_state.validators) - for index in range(len(pre_state.validators)): - if index in attesting_indices: - assert state.balances[index] > pre_state.balances[index] - else: - assert state.balances[index] < pre_state.balances[index] - - -@with_all_phases -@spec_state_test -@leaking() -def test_full_attestations_with_leak(spec, state): - attestations = prepare_state_with_attestations(spec, state) - - proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations] - pre_state = state.copy() - - yield from run_process_rewards_and_penalties(spec, state) - - attesting_indices = spec.get_unslashed_attesting_indices(state, attestations) - assert len(attesting_indices) == len(pre_state.validators) - for index in range(len(pre_state.validators)): - # Proposers can still make money during a leak - if index in proposer_indices: - assert state.balances[index] > pre_state.balances[index] - # If not proposer but participated optimally, should have exactly neutral balance - elif index in attesting_indices: - assert state.balances[index] == pre_state.balances[index] - else: - assert state.balances[index] < pre_state.balances[index] - - -@with_all_phases -@spec_state_test -@leaking() -def test_partial_attestations_with_leak(spec, state): - attestations = prepare_state_with_attestations(spec, state) - - attestations = attestations[:len(attestations) // 2] - state.previous_epoch_attestations = state.previous_epoch_attestations[:len(attestations)] - proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations] - pre_state = state.copy() - - yield from run_process_rewards_and_penalties(spec, state) - - attesting_indices = spec.get_unslashed_attesting_indices(state, attestations) - assert len(attesting_indices) < len(pre_state.validators) - for index in range(len(pre_state.validators)): - # Proposers can still make money during a leak - if index in proposer_indices and index in attesting_indices: - assert state.balances[index] > pre_state.balances[index] - # If not proposer but participated optimally, should have exactly neutral balance - elif index in attesting_indices: - assert state.balances[index] == pre_state.balances[index] - else: - assert state.balances[index] < pre_state.balances[index] - - @with_all_phases @spec_state_test def test_full_attestations_random_incorrect_fields(spec, state): @@ -224,6 +156,7 @@ def participation_tracker(slot, comm_index, comm): return att_participants attestations = prepare_state_with_attestations(spec, state, participation_fn=participation_tracker) + proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations] pre_state = state.copy() @@ -233,10 +166,20 @@ def participation_tracker(slot, comm_index, comm): assert len(attesting_indices) == len(participated) for index in range(len(pre_state.validators)): - if index in participated: - assert state.balances[index] > pre_state.balances[index] + if spec.is_in_inactivity_leak(state): + # Proposers can still make money during a leak + if index in proposer_indices and index in participated: + assert state.balances[index] > pre_state.balances[index] + # If not proposer but participated optimally, should have exactly neutral balance + elif index in attesting_indices: + assert state.balances[index] == pre_state.balances[index] + else: + assert state.balances[index] < pre_state.balances[index] else: - assert state.balances[index] < pre_state.balances[index] + if index in participated: + assert state.balances[index] > pre_state.balances[index] + else: + assert state.balances[index] < pre_state.balances[index] @with_all_phases @@ -246,6 +189,14 @@ def test_almost_empty_attestations(spec, state): yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1)) +@with_all_phases +@spec_state_test +@leaking() +def test_almost_empty_attestations_with_leak(spec, state): + rng = Random(1234) + yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1)) + + @with_all_phases @spec_state_test def test_random_fill_attestations(spec, state): @@ -253,6 +204,14 @@ def test_random_fill_attestations(spec, state): yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3)) +@with_all_phases +@spec_state_test +@leaking() +def test_random_fill_attestations_with_leak(spec, state): + rng = Random(4567) + yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3)) + + @with_all_phases @spec_state_test def test_almost_full_attestations(spec, state): @@ -260,12 +219,27 @@ def test_almost_full_attestations(spec, state): yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1)) +@with_all_phases +@spec_state_test +@leaking() +def test_almost_full_attestations_with_leak(spec, state): + rng = Random(8901) + yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1)) + + @with_all_phases @spec_state_test def test_full_attestation_participation(spec, state): yield from run_with_participation(spec, state, lambda slot, comm_index, comm: comm) +@with_all_phases +@spec_state_test +@leaking() +def test_full_attestation_participation_with_leak(spec, state): + yield from run_with_participation(spec, state, lambda slot, comm_index, comm: comm) + + @with_all_phases @spec_state_test def test_duplicate_attestation(spec, state):