diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index a98aa3415..e8cd950ce 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -1,3 +1,5 @@ +from random import Random + from eth2spec.test.helpers.attestations import prepare_state_with_full_attestations from eth2spec.utils.ssz.ssz_typing import Container, uint64, List @@ -7,6 +9,13 @@ class Deltas(Container): delta_list: List[uint64, 2**30] +def has_enough_for_reward(spec, state, index): + return ( + state.validators[index].effective_balance * spec.BASE_REWARD_FACTOR + > spec.integer_squareroot(spec.get_total_active_balance(state)) // spec.BASE_REWARDS_PER_EPOCH + ) + + def run_attestation_component_deltas(spec, state, component_delta_fn, matching_att_fn): """ Run ``component_delta_fn``, yielding: @@ -25,11 +34,7 @@ def run_attestation_component_deltas(spec, state, component_delta_fn, matching_a matching_indices = spec.get_unslashed_attesting_indices(state, matching_attestations) for index in spec.get_eligible_validator_indices(state): validator = state.validators[index] - enough_for_reward = ( - validator.effective_balance * spec.BASE_REWARD_FACTOR - > spec.integer_squareroot(spec.get_total_active_balance(state)) // spec.BASE_REWARDS_PER_EPOCH - ) - + enough_for_reward = has_enough_for_reward(spec, state, index) if index in matching_indices and not validator.slashed: if enough_for_reward: assert rewards[index] > 0 @@ -56,15 +61,29 @@ def test_full_all_correct(spec, state, runner): yield from runner(spec, state) -def test_half_full(spec, state, runner): +def test_full_but_partial_participation(spec, state, runner, rng=Random(5522)): prepare_state_with_full_attestations(spec, state) - # Remove half of attestations - state.previous_epoch_attestations = state.previous_epoch_attestations[:len(state.previous_epoch_attestations) // 2] + for a in state.previous_epoch_attestations: + a.aggregation_bits = [rng.choice([True, False]) for _ in a.aggregation_bits] yield from runner(spec, state) +def test_partial(spec, state, fraction_filled, runner): + prepare_state_with_full_attestations(spec, state) + + # Remove portion of attestations + num_attestations = int(len(state.previous_epoch_attestations) * fraction_filled) + state.previous_epoch_attestations = state.previous_epoch_attestations[:num_attestations] + + yield from runner(spec, state) + + +def test_half_full(spec, state, runner): + yield from test_partial(spec, state, 0.5, runner) + + def test_one_attestation_one_correct(spec, state, runner): prepare_state_with_full_attestations(spec, state) @@ -84,12 +103,16 @@ def test_with_slashed_validators(spec, state, runner): yield from runner(spec, state) -def test_some_zero_effective_balances_that_attested(spec, state, runner): +def test_some_very_low_effective_balances_that_attested(spec, state, runner): + state.balances prepare_state_with_full_attestations(spec, state) - # Set some balances to zero + # Set some balances to be very low (including 0) state.validators[0].effective_balance = 0 - state.validators[1].effective_balance = 0 + state.validators[1].effective_balance = 2 + state.validators[2].effective_balance = 10 + state.validators[3].effective_balance = 100 + state.validators[4].effective_balance = 1000 yield from runner(spec, state) @@ -97,9 +120,8 @@ def test_some_zero_effective_balances_that_attested(spec, state, runner): def test_some_zero_effective_balances_that_did_not_attest(spec, state, runner): prepare_state_with_full_attestations(spec, state) - # Set some balances to zero - attestation = state.previous_epoch_attestations[0] # Remove attestation + attestation = state.previous_epoch_attestations[0] state.previous_epoch_attestations = state.previous_epoch_attestations[1:] # Set removed indices effective balance to zero indices = spec.get_unslashed_attesting_indices(state, [attestation]) @@ -121,3 +143,21 @@ def test_full_fraction_incorrect(spec, state, correct_target, correct_head, frac pending_attestation.data.beacon_block_root = b'\x66' * 32 yield from runner(spec, state) + + +def test_full_random(spec, state, runner, rng=Random(8020)): + prepare_state_with_full_attestations(spec, state) + + for pending_attestation in state.previous_epoch_attestations: + # 1/3 have bad target + if rng.randint(0, 2) == 0: + pending_attestation.data.target.root = b'\x55' * 32 + # 1/3 have bad head + if rng.randint(0, 2) == 0: + pending_attestation.data.beacon_block_root = b'\x66' * 32 + # ~50% participation + pending_attestation.aggregation_bits = [rng.choice([True, False]) for _ in pending_attestation.aggregation_bits] + # Random inclusion delay + pending_attestation.inclusion_delay = rng.randint(1, spec.SLOTS_PER_EPOCH) + + yield from runner(spec, state) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_head_deltas.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_head_deltas.py index 24e3aaac5..b9aab92cb 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_head_deltas.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_head_deltas.py @@ -51,8 +51,8 @@ def test_with_slashed_validators(spec, state): @with_all_phases @spec_state_test -def test_some_zero_effective_balances_that_attested(spec, state): - yield from rewards_helpers.test_some_zero_effective_balances_that_attested(spec, state, run_get_head_deltas) +def test_some_very_low_effective_balances_that_attested(spec, state): + yield from rewards_helpers.test_some_very_low_effective_balances_that_attested(spec, state, run_get_head_deltas) @with_all_phases @@ -107,3 +107,9 @@ def test_full_half_incorrect_target_correct_head(spec, state): fraction_incorrect=0.5, runner=run_get_head_deltas ) + + +@with_all_phases +@spec_state_test +def test_full_random(spec, state): + yield from rewards_helpers.test_full_random(spec, state, run_get_head_deltas) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_inclusion_delay_deltas.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_inclusion_delay_deltas.py new file mode 100644 index 000000000..b9ae9882c --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_inclusion_delay_deltas.py @@ -0,0 +1,208 @@ +from random import Random + +from eth2spec.test.context import with_all_phases, spec_state_test +from eth2spec.test.helpers.attestations import prepare_state_with_full_attestations +from eth2spec.test.helpers.rewards import has_enough_for_reward +import eth2spec.test.helpers.rewards as rewards_helpers +from eth2spec.utils.ssz.ssz_typing import Container, uint64, List + + +# HACK to get the generators outputting correctly +class Deltas(Container): + delta_list: List[uint64, 2**30] + + +def run_get_inclusion_delay_deltas(spec, state): + """ + Run ``get_inclusion_delay_deltas``, yielding: + - pre-state ('pre') + - rewards ('rewards') + - penalties ('penalties') + """ + + yield 'pre', state + + rewards, penalties = spec.get_inclusion_delay_deltas(state) + + yield 'rewards', Deltas(delta_list=rewards) + yield 'penalties', Deltas(delta_list=penalties) + + eligible_attestations = spec.get_matching_source_attestations(state, spec.get_previous_epoch(state)) + attesting_indices = spec.get_unslashed_attesting_indices(state, eligible_attestations) + + rewarded_indices = set() + rewarded_proposer_indices = set() + # Ensure attesters with enough balance are rewarded for attestations + # Track those that are rewarded and track proposers that should be rewarded + for index in range(len(state.validators)): + if index in attesting_indices and has_enough_for_reward(spec, state, index): + assert rewards[index] > 0 + rewarded_indices.add(index) + + # Track proposer of earliest included attestation for the validator defined by index + earliest_attestation = min([ + a for a in eligible_attestations + if index in spec.get_attesting_indices(state, a.data, a.aggregation_bits) + ], key=lambda a: a.inclusion_delay) + rewarded_proposer_indices.add(earliest_attestation.proposer_index) + + # Ensure all expected proposers have been rewarded + # Track rewarde indices + proposing_indices = [a.proposer_index for a in eligible_attestations] + for index in proposing_indices: + if index in rewarded_proposer_indices: + assert rewards[index] > 0 + rewarded_indices.add(index) + + # Ensure all expected non-rewarded indices received no reward + for index in range(len(state.validators)): + assert penalties[index] == 0 + if index not in rewarded_indices: + assert rewards[index] == 0 + + +@with_all_phases +@spec_state_test +def test_empty(spec, state): + yield from rewards_helpers.test_empty(spec, state, run_get_inclusion_delay_deltas) + + +@with_all_phases +@spec_state_test +def test_full(spec, state): + yield from rewards_helpers.test_full_all_correct(spec, state, run_get_inclusion_delay_deltas) + + +@with_all_phases +@spec_state_test +def test_half_full(spec, state): + yield from rewards_helpers.test_half_full(spec, state, run_get_inclusion_delay_deltas) + + +@with_all_phases +@spec_state_test +def test_quarter_full(spec, state): + yield from rewards_helpers.test_partial(spec, state, 0.25, run_get_inclusion_delay_deltas) + + +@with_all_phases +@spec_state_test +def test_full_but_partial_participation(spec, state): + yield from rewards_helpers.test_full_but_partial_participation(spec, state, run_get_inclusion_delay_deltas) + + +@with_all_phases +@spec_state_test +def test_with_slashed_validators(spec, state): + yield from rewards_helpers.test_with_slashed_validators(spec, state, run_get_inclusion_delay_deltas) + + +@with_all_phases +@spec_state_test +def test_some_very_low_effective_balances_that_attested(spec, state): + yield from rewards_helpers.test_some_very_low_effective_balances_that_attested( + spec, + state, + run_get_inclusion_delay_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_random(spec, state): + yield from rewards_helpers.test_full_random(spec, state, run_get_inclusion_delay_deltas) + + +@with_all_phases +@spec_state_test +def test_full_delay_one_slot(spec, state): + prepare_state_with_full_attestations(spec, state) + for a in state.previous_epoch_attestations: + a.inclusion_delay += 1 + + yield from run_get_inclusion_delay_deltas(spec, state) + + +@with_all_phases +@spec_state_test +def test_full_delay_max_slots(spec, state): + prepare_state_with_full_attestations(spec, state) + for a in state.previous_epoch_attestations: + a.inclusion_delay += spec.SLOTS_PER_EPOCH + + yield from run_get_inclusion_delay_deltas(spec, state) + + +@with_all_phases +@spec_state_test +def test_full_mixed_delay(spec, state): + rng = Random(1234) + + prepare_state_with_full_attestations(spec, state) + for a in state.previous_epoch_attestations: + a.inclusion_delay = rng.randint(1, spec.SLOTS_PER_EPOCH) + + yield from run_get_inclusion_delay_deltas(spec, state) + + +@with_all_phases +@spec_state_test +def test_proposer_not_in_attestations(spec, state): + prepare_state_with_full_attestations(spec, state) + + # Get an attestation where the proposer is not in the committee + non_proposer_attestations = [] + for a in state.previous_epoch_attestations: + if a.proposer_index not in spec.get_unslashed_attesting_indices(state, [a]): + non_proposer_attestations.append(a) + + assert any(non_proposer_attestations) + state.previous_epoch_attestations = non_proposer_attestations + + yield from run_get_inclusion_delay_deltas(spec, state) + + +@with_all_phases +@spec_state_test +def test_duplicate_attestations_at_later_slots(spec, state): + prepare_state_with_full_attestations(spec, state) + + # Remove 2/3 of attestations to make it more interesting + num_attestations = int(len(state.previous_epoch_attestations) * 0.33) + state.previous_epoch_attestations = state.previous_epoch_attestations[:num_attestations] + + # Get map of the proposer at each slot to make valid-looking duplicate attestations + per_slot_proposers = { + (a.data.slot + a.inclusion_delay): a.proposer_index + for a in state.previous_epoch_attestations + } + max_slot = max([a.data.slot + a.inclusion_delay for a in state.previous_epoch_attestations]) + later_attestations = [] + for a in state.previous_epoch_attestations: + # Do not create later duplicate if goes into next epoch + if a.data.slot + a.inclusion_delay >= max_slot: + continue + later_a = a.copy() + later_a.inclusion_delay += 1 + later_a.proposer_index = per_slot_proposers[later_a.data.slot + later_a.inclusion_delay] + later_attestations.append(later_a) + + assert any(later_attestations) + + state.previous_epoch_attestations = sorted( + state.previous_epoch_attestations + later_attestations, + key=lambda a: a.data.slot + a.inclusion_delay + ) + + yield from run_get_inclusion_delay_deltas(spec, state) + + +@with_all_phases +@spec_state_test +def test_all_balances_too_low_for_reward(spec, state): + prepare_state_with_full_attestations(spec, state) + + for index in range(len(state.validators)): + state.validators[index].effective_balance = 10 + + yield from run_get_inclusion_delay_deltas(spec, state) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_source_deltas.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_source_deltas.py index da47bd204..9063abfc6 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_source_deltas.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_source_deltas.py @@ -37,6 +37,12 @@ def test_half_full(spec, state): yield from rewards_helpers.test_half_full(spec, state, run_get_source_deltas) +@with_all_phases +@spec_state_test +def test_full_but_partial_participation(spec, state): + yield from rewards_helpers.test_full_but_partial_participation(spec, state, run_get_source_deltas) + + @with_all_phases @spec_state_test def test_one_attestation_one_correct(spec, state): @@ -51,8 +57,8 @@ def test_with_slashed_validators(spec, state): @with_all_phases @spec_state_test -def test_some_zero_effective_balances_that_attested(spec, state): - yield from rewards_helpers.test_some_zero_effective_balances_that_attested(spec, state, run_get_source_deltas) +def test_some_very_low_effective_balances_that_attested(spec, state): + yield from rewards_helpers.test_some_very_low_effective_balances_that_attested(spec, state, run_get_source_deltas) @with_all_phases @@ -114,3 +120,9 @@ def test_full_half_incorrect_target_correct_head(spec, state): fraction_incorrect=0.5, runner=run_get_source_deltas ) + + +@with_all_phases +@spec_state_test +def test_full_random(spec, state): + yield from rewards_helpers.test_full_random(spec, state, run_get_source_deltas) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_target_deltas.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_target_deltas.py index 406471c67..ff20014c8 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_target_deltas.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_get_target_deltas.py @@ -37,6 +37,12 @@ def test_half_full(spec, state): yield from rewards_helpers.test_half_full(spec, state, run_get_target_deltas) +@with_all_phases +@spec_state_test +def test_full_but_partial_participation(spec, state): + yield from rewards_helpers.test_full_but_partial_participation(spec, state, run_get_target_deltas) + + @with_all_phases @spec_state_test def test_one_attestation_one_correct(spec, state): @@ -51,8 +57,8 @@ def test_with_slashed_validators(spec, state): @with_all_phases @spec_state_test -def test_some_zero_effective_balances_that_attested(spec, state): - yield from rewards_helpers.test_some_zero_effective_balances_that_attested(spec, state, run_get_target_deltas) +def test_some_very_low_effective_balances_that_attested(spec, state): + yield from rewards_helpers.test_some_very_low_effective_balances_that_attested(spec, state, run_get_target_deltas) @with_all_phases @@ -107,3 +113,9 @@ def test_full_half_incorrect_target_correct_head(spec, state): fraction_incorrect=0.5, runner=run_get_target_deltas ) + + +@with_all_phases +@spec_state_test +def test_full_random(spec, state): + yield from rewards_helpers.test_full_random(spec, state, run_get_target_deltas)