diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 142cf3b02..7bf4cd06e 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 d62fee6ce..034a79fd4 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 eff286448..52d3b3c06 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 @@ from eth2spec.test.helpers.attestations import ( 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 4e75079c0..b0f9767b2 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