diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index f40f0096b..95f06f125 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -110,6 +110,10 @@ - [Helper functions](#helper-functions-1) - [Justification and finalization](#justification-and-finalization) - [Rewards and penalties](#rewards-and-penalties-1) + - [Helpers](#helpers) + - [Components of attestation deltas](#components-of-attestation-deltas) + - [`get_attestation_deltas`](#get_attestation_deltas) + - [`process_rewards_and_penalties`](#process_rewards_and_penalties) - [Registry updates](#registry-updates) - [Slashings](#slashings) - [Final updates](#final-updates) @@ -1403,7 +1407,7 @@ def get_head_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]] """ Return attester micro-rewards/penalties for head-vote for each validator. """ - matching_head_attestations = get_matching_source_attestations(state, get_previous_epoch(state)) + matching_head_attestations = get_matching_head_attestations(state, get_previous_epoch(state)) return compute_attestation_component_deltas(state, matching_head_attestations) ``` diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py new file mode 100644 index 000000000..41497fe60 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -0,0 +1,117 @@ +from eth2spec.test.helpers.attestations import prepare_state_with_full_attestations + + +def run_attestation_component_deltas(spec, state, component_delta_fn, matching_att_fn): + """ + Run ``component_delta_fn``, yielding: + - pre-state ('pre') + - rewards ('rewards') + - penalties ('penalties') + """ + yield 'pre', state + + rewards, penalties = component_delta_fn(state) + + yield 'rewards', rewards + yield 'penalties', penalties + + matching_attestations = matching_att_fn(state, spec.get_previous_epoch(state)) + 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 + ) + + if index in matching_indices and not validator.slashed: + if enough_for_reward: + assert rewards[index] > 0 + else: + assert rewards[index] == 0 + assert penalties[index] == 0 + else: + assert rewards[index] == 0 + if enough_for_reward: + assert penalties[index] > 0 + else: + assert penalties[index] == 0 + + +def test_empty(spec, state, runner): + # Do not add any attestations to state + + yield from runner(spec, state) + + +def test_full_all_correct(spec, state, runner): + prepare_state_with_full_attestations(spec, state) + + yield from runner(spec, state) + + +def test_half_full(spec, state, runner): + 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] + + yield from runner(spec, state) + + +def test_one_attestation_one_correct(spec, state, runner): + prepare_state_with_full_attestations(spec, state) + + # Remove half of attestations + state.previous_epoch_attestations = state.previous_epoch_attestations[:1] + + yield from runner(spec, state) + + +def test_with_slashed_validators(spec, state, runner): + prepare_state_with_full_attestations(spec, state) + + # Slash half of validators + for validator in state.validators[:len(state.validators) // 2]: + validator.slashed = True + + yield from runner(spec, state) + + +def test_some_zero_effective_balances_that_attested(spec, state, runner): + prepare_state_with_full_attestations(spec, state) + + # Set some balances to zero + state.validators[0].effective_balance = 0 + state.validators[1].effective_balance = 0 + + yield from runner(spec, state) + + +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 + state.previous_epoch_attestations = state.previous_epoch_attestations[1:] + # Set removed indices effective balance to zero + indices = spec.get_unslashed_attesting_indices(state, [attestation]) + for index in indices: + state.validators[index].effective_balance = 0 + + yield from runner(spec, state) + + +def test_full_fraction_incorrect(spec, state, correct_target, correct_head, fraction_incorrect, runner): + prepare_state_with_full_attestations(spec, state) + + # Make fraction_incorrect of pending attestations have bad target/head as specified + num_incorrect = int(fraction_incorrect * len(state.previous_epoch_attestations)) + for pending_attestation in state.previous_epoch_attestations[:num_incorrect]: + if not correct_target: + pending_attestation.data.target.root = b'\x55' * 32 + if not correct_head: + pending_attestation.data.beacon_block_root = b'\x66' * 32 + + yield from runner(spec, state) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_head_deltas.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_head_deltas.py new file mode 100644 index 000000000..24e3aaac5 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_head_deltas.py @@ -0,0 +1,109 @@ +from eth2spec.test.context import with_all_phases, spec_state_test +from eth2spec.test.helpers.rewards import run_attestation_component_deltas +import eth2spec.test.helpers.rewards as rewards_helpers + + +def run_get_head_deltas(spec, state): + """ + Run ``get_head_deltas``, yielding: + - pre-state ('pre') + - rewards ('rewards') + - penalties ('penalties') + """ + + yield from run_attestation_component_deltas( + spec, + state, + spec.get_head_deltas, + spec.get_matching_head_attestations, + ) + + +@with_all_phases +@spec_state_test +def test_empty(spec, state): + yield from rewards_helpers.test_empty(spec, state, run_get_head_deltas) + + +@with_all_phases +@spec_state_test +def test_full_all_correct(spec, state): + yield from rewards_helpers.test_full_all_correct(spec, state, run_get_head_deltas) + + +@with_all_phases +@spec_state_test +def test_half_full(spec, state): + yield from rewards_helpers.test_half_full(spec, state, run_get_head_deltas) + + +@with_all_phases +@spec_state_test +def test_one_attestation_one_correct(spec, state): + yield from rewards_helpers.test_one_attestation_one_correct(spec, state, run_get_head_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_head_deltas) + + +@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) + + +@with_all_phases +@spec_state_test +def test_some_zero_effective_balances_that_did_not_attest(spec, state): + yield from rewards_helpers.test_some_zero_effective_balances_that_did_not_attest(spec, state, run_get_head_deltas) + + +@with_all_phases +@spec_state_test +def test_full_half_correct_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=True, + correct_head=False, + fraction_incorrect=0.5, + runner=run_get_head_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_correct_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=True, + correct_head=False, + fraction_incorrect=1.0, + runner=run_get_head_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_half_incorrect_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=False, + correct_head=False, + fraction_incorrect=0.5, + runner=run_get_head_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_half_incorrect_target_correct_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=False, + correct_head=True, + fraction_incorrect=0.5, + runner=run_get_head_deltas + ) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_source_deltas.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_source_deltas.py new file mode 100644 index 000000000..da47bd204 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_source_deltas.py @@ -0,0 +1,116 @@ +from eth2spec.test.context import with_all_phases, spec_state_test +from eth2spec.test.helpers.rewards import run_attestation_component_deltas +import eth2spec.test.helpers.rewards as rewards_helpers + + +def run_get_source_deltas(spec, state): + """ + Run ``get_source_deltas``, yielding: + - pre-state ('pre') + - rewards ('rewards') + - penalties ('penalties') + """ + + yield from run_attestation_component_deltas( + spec, + state, + spec.get_source_deltas, + spec.get_matching_source_attestations, + ) + + +@with_all_phases +@spec_state_test +def test_empty(spec, state): + yield from rewards_helpers.test_empty(spec, state, run_get_source_deltas) + + +@with_all_phases +@spec_state_test +def test_full_all_correct(spec, state): + yield from rewards_helpers.test_full_all_correct(spec, state, run_get_source_deltas) + + +@with_all_phases +@spec_state_test +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_one_attestation_one_correct(spec, state): + yield from rewards_helpers.test_one_attestation_one_correct(spec, state, run_get_source_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_source_deltas) + + +@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) + + +@with_all_phases +@spec_state_test +def test_some_zero_effective_balances_that_did_not_attest(spec, state): + yield from rewards_helpers.test_some_zero_effective_balances_that_did_not_attest(spec, state, run_get_source_deltas) + + +# +# NOTE: No source incorrect tests +# All PendingAttestations in state have source validated +# We choose to keep this invariant in these tests to not force clients to test with degenerate states +# + + +@with_all_phases +@spec_state_test +def test_full_half_correct_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=True, + correct_head=False, + fraction_incorrect=0.5, + runner=run_get_source_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_correct_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=True, + correct_head=False, + fraction_incorrect=1.0, + runner=run_get_source_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_half_incorrect_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=False, + correct_head=False, + fraction_incorrect=0.5, + runner=run_get_source_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_half_incorrect_target_correct_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=False, + correct_head=True, + fraction_incorrect=0.5, + runner=run_get_source_deltas + ) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_target_deltas.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_target_deltas.py index c30865cf0..406471c67 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_target_deltas.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/rewards/test_get_target_deltas.py @@ -1,92 +1,109 @@ 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 run_attestation_component_deltas +import eth2spec.test.helpers.rewards as rewards_helpers def run_get_target_deltas(spec, state): """ - Run ``process_block_header``, yielding: + Run ``get_target_deltas``, yielding: - pre-state ('pre') - rewards ('rewards') - penalties ('penalties') """ - yield 'pre', state - rewards, penalties = spec.get_target_deltas(state) - - yield 'rewards', rewards - yield 'penalties', penalties - - matching_target_attestations = spec.get_matching_target_attestations(state, spec.get_previous_epoch(state)) - matching_target_indices = spec.get_unslashed_attesting_indices(state, matching_target_attestations) - for index in spec.get_eligible_validator_indices(state): - if index in matching_target_indices and not state.validators[index].slashed: - assert rewards[index] > 0 - assert penalties[index] == 0 - else: - assert rewards[index] == 0 - assert penalties[index] > 0 + yield from run_attestation_component_deltas( + spec, + state, + spec.get_target_deltas, + spec.get_matching_target_attestations, + ) @with_all_phases @spec_state_test def test_empty(spec, state): - # Do not add any attestations to state - - yield from run_get_target_deltas(spec, state) + yield from rewards_helpers.test_empty(spec, state, run_get_target_deltas) @with_all_phases @spec_state_test def test_full_all_correct(spec, state): - prepare_state_with_full_attestations(spec, state) - - yield from run_get_target_deltas(spec, state) - - -@with_all_phases -@spec_state_test -def test_full_half_correct(spec, state): - prepare_state_with_full_attestations(spec, state) - - # Make half of pending attestations have bad target - for pending_attestation in state.previous_epoch_attestations[:len(state.previous_epoch_attestations) // 2]: - pending_attestation.data.target.root = b'\x66'*32 - - yield from run_get_target_deltas(spec, state) + yield from rewards_helpers.test_full_all_correct(spec, state, run_get_target_deltas) @with_all_phases @spec_state_test def test_half_full(spec, state): - 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] - - yield from run_get_target_deltas(spec, state) + yield from rewards_helpers.test_half_full(spec, state, run_get_target_deltas) @with_all_phases @spec_state_test -def test_one_correct(spec, state): - prepare_state_with_full_attestations(spec, state) - - # Remove half of attestations - state.previous_epoch_attestations = state.previous_epoch_attestations[:1] - - yield from run_get_target_deltas(spec, state) +def test_one_attestation_one_correct(spec, state): + yield from rewards_helpers.test_one_attestation_one_correct(spec, state, run_get_target_deltas) @with_all_phases @spec_state_test def test_with_slashed_validators(spec, state): - prepare_state_with_full_attestations(spec, state) + yield from rewards_helpers.test_with_slashed_validators(spec, state, run_get_target_deltas) - # Slash half of validators - for validator in state.validators: - validator.slashed = True - yield from run_get_target_deltas(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_zero_balances(spec, state): +@with_all_phases +@spec_state_test +def test_some_zero_effective_balances_that_did_not_attest(spec, state): + yield from rewards_helpers.test_some_zero_effective_balances_that_did_not_attest(spec, state, run_get_target_deltas) + + +@with_all_phases +@spec_state_test +def test_full_half_correct_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=True, + correct_head=False, + fraction_incorrect=0.5, + runner=run_get_target_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_correct_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=True, + correct_head=False, + fraction_incorrect=1.0, + runner=run_get_target_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_half_incorrect_target_incorrect_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=False, + correct_head=False, + fraction_incorrect=0.5, + runner=run_get_target_deltas + ) + + +@with_all_phases +@spec_state_test +def test_full_half_incorrect_target_correct_head(spec, state): + yield from rewards_helpers.test_full_fraction_incorrect( + spec, state, + correct_target=False, + correct_head=True, + fraction_incorrect=0.5, + runner=run_get_target_deltas + )