From e341f4e1f858ac678ab9f9c052b01588d67612cc Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 31 Aug 2021 12:45:47 -0700 Subject: [PATCH 01/14] refactor "leaking patch" helper --- .../pyspec/eth2spec/test/helpers/random.py | 30 +++++++++++++++++++ .../test/utils/randomized_block_tests.py | 19 ++---------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/random.py b/tests/core/pyspec/eth2spec/test/helpers/random.py index 8f095aebb..0bb2a6672 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/random.py +++ b/tests/core/pyspec/eth2spec/test/helpers/random.py @@ -128,3 +128,33 @@ def randomize_state(spec, state, rng=Random(8020), exit_fraction=None, slash_fra exit_random_validators(spec, state, rng, fraction=exit_fraction) slash_random_validators(spec, state, rng, fraction=slash_fraction) randomize_attestation_participation(spec, state, rng) + + +def patch_state_to_non_leaking(spec, state): + """ + This function performs an irregular state transition so that: + 1. the current justified checkpoint references the previous epoch + 2. the previous justified checkpoint references the epoch before previous + 3. the finalized checkpoint matches the previous justified checkpoint + + The effects of this function are intended to offset randomization side effects + performed by other functionality in this module so that if the ``state`` was leaking, + then the ``state`` is not leaking after. + """ + state.justification_bits = (True, True, True, True) + previous_epoch = spec.get_previous_epoch(state) + previous_root = spec.get_block_root(state, previous_epoch) + previous_previous_epoch = max(spec.GENESIS_EPOCH, spec.Epoch(previous_epoch - 1)) + previous_previous_root = spec.get_block_root(state, previous_previous_epoch) + state.previous_justified_checkpoint = spec.Checkpoint( + epoch=previous_previous_epoch, + root=previous_previous_root, + ) + state.current_justified_checkpoint = spec.Checkpoint( + epoch=previous_epoch, + root=previous_root, + ) + state.finalized_checkpoint = spec.Checkpoint( + epoch=previous_previous_epoch, + root=previous_previous_root, + ) diff --git a/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py index ce99c1053..02a5464f7 100644 --- a/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py +++ b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py @@ -17,6 +17,7 @@ from eth2spec.test.helpers.inactivity_scores import ( ) from eth2spec.test.helpers.random import ( randomize_state as randomize_state_helper, + patch_state_to_non_leaking, ) from eth2spec.test.helpers.state import ( next_slot, @@ -274,23 +275,7 @@ def _randomized_scenario_setup(state_randomizer): may not reflect this condition with prior (arbitrary) mutations, so this mutator addresses that fact. """ - state.justification_bits = (True, True, True, True) - previous_epoch = spec.get_previous_epoch(state) - previous_root = spec.get_block_root(state, previous_epoch) - previous_previous_epoch = max(spec.GENESIS_EPOCH, spec.Epoch(previous_epoch - 1)) - previous_previous_root = spec.get_block_root(state, previous_previous_epoch) - state.previous_justified_checkpoint = spec.Checkpoint( - epoch=previous_previous_epoch, - root=previous_previous_root, - ) - state.current_justified_checkpoint = spec.Checkpoint( - epoch=previous_epoch, - root=previous_root, - ) - state.finalized_checkpoint = spec.Checkpoint( - epoch=previous_previous_epoch, - root=previous_previous_root, - ) + patch_state_to_non_leaking(spec, state) return ( # NOTE: the block randomization function assumes at least 1 shard committee period From 7cb5901ee62d2ec99ff05537c079db2d3f055402 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 31 Aug 2021 12:46:00 -0700 Subject: [PATCH 02/14] add spec test case for rewards with exited validators and _no_ leak --- tests/core/pyspec/eth2spec/test/helpers/rewards.py | 14 +++++++++++++- .../eth2spec/test/phase0/rewards/test_random.py | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index 1867db08f..ec617bda9 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -255,7 +255,19 @@ def run_get_inactivity_penalty_deltas(spec, state): else: assert penalties[index] > base_penalty else: - assert penalties[index] == 0 + if not is_post_altair(spec): + assert penalties[index] == 0 + continue + else: + # post altair, this penalty is derived from the inactivity score + # regardless if the state is leaking or not... + if index in matching_attesting_indices: + assert penalties[index] == 0 + else: + # copied from spec: + penalty_numerator = state.validators[index].effective_balance * state.inactivity_scores[index] + penalty_denominator = spec.config.INACTIVITY_SCORE_BIAS * spec.INACTIVITY_PENALTY_QUOTIENT_ALTAIR + assert penalties[index] == penalty_numerator // penalty_denominator def transition_state_to_leak(spec, state, epochs=None): diff --git a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py index 1184a6617..78c6846ae 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py +++ b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py @@ -9,6 +9,7 @@ from eth2spec.test.context import ( low_balances, misc_balances, ) import eth2spec.test.helpers.rewards as rewards_helpers +from eth2spec.test.helpers.random import randomize_state, patch_state_to_non_leaking @with_all_phases @@ -57,3 +58,14 @@ def test_full_random_low_balances_1(spec, state): @single_phase def test_full_random_misc_balances(spec, state): yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(7070)) + + +@with_all_phases +@spec_state_test +def test_full_random_without_leak_0(spec, state): + rng = Random(1010) + randomize_state(spec, state, rng) + assert spec.is_in_inactivity_leak(state) + patch_state_to_non_leaking(spec, state) + assert not spec.is_in_inactivity_leak(state) + yield from rewards_helpers.run_deltas(spec, state) From cf23cd00ab2c5bf4fe5addfed5df1be06402bf2a Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 1 Sep 2021 14:38:02 -0700 Subject: [PATCH 03/14] ensure the test covers exited, unslashed validators --- tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py | 7 +++++++ .../pyspec/eth2spec/test/phase0/rewards/test_random.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py index 73d4598b3..55ea0b5b0 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py +++ b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py @@ -34,6 +34,13 @@ def get_exited_validators(spec, state): return [index for (index, validator) in enumerate(state.validators) if validator.exit_epoch <= current_epoch] +def get_unslashed_exited_validators(spec, state): + return [ + index for index in get_exited_validators(spec, state) + if not state.validators[index].slashed + ] + + def exit_validators(spec, state, validator_count, rng=None): if rng is None: rng = Random(1337) diff --git a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py index 78c6846ae..f158f3cf8 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py +++ b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py @@ -10,6 +10,7 @@ from eth2spec.test.context import ( ) import eth2spec.test.helpers.rewards as rewards_helpers from eth2spec.test.helpers.random import randomize_state, patch_state_to_non_leaking +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators @with_all_phases @@ -68,4 +69,6 @@ def test_full_random_without_leak_0(spec, state): assert spec.is_in_inactivity_leak(state) patch_state_to_non_leaking(spec, state) assert not spec.is_in_inactivity_leak(state) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 yield from rewards_helpers.run_deltas(spec, state) From 0cc5f9cd59369ead4088b07580817fb66a5fc6e0 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 1 Sep 2021 14:43:11 -0700 Subject: [PATCH 04/14] modify helper for more precision on exited validators --- tests/core/pyspec/eth2spec/test/helpers/state.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py index b4c9e1d67..d8fda3754 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/state.py +++ b/tests/core/pyspec/eth2spec/test/helpers/state.py @@ -1,6 +1,6 @@ from eth2spec.test.context import expect_assertion_error, is_post_altair from eth2spec.test.helpers.block import apply_empty_block, sign_block, transition_unsigned_block -from eth2spec.test.helpers.voluntary_exits import get_exited_validators +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators def get_balance(state, index): @@ -142,7 +142,7 @@ def ensure_state_has_validators_across_lifecycle(spec, state): for each of the following lifecycle states: 1. Pending / deposited 2. Active - 3. Exited + 3. Exited (but not slashed) 4. Slashed """ has_pending = any(filter(spec.is_eligible_for_activation_queue, state.validators)) @@ -150,7 +150,7 @@ def ensure_state_has_validators_across_lifecycle(spec, state): current_epoch = spec.get_current_epoch(state) has_active = any(filter(lambda v: spec.is_active_validator(v, current_epoch), state.validators)) - has_exited = any(get_exited_validators(spec, state)) + has_exited = any(get_unslashed_exited_validators(spec, state)) has_slashed = any(filter(lambda v: v.slashed, state.validators)) From 58c0da9059844e879593f2aff5ee081d01587c01 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 1 Sep 2021 14:44:06 -0700 Subject: [PATCH 05/14] ensure rewards spec test with exited validators --- .../eth2spec/test/phase0/rewards/test_random.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py index f158f3cf8..68a6a3279 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py +++ b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py @@ -37,6 +37,20 @@ def test_full_random_3(spec, state): yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(4040)) +@with_all_phases +@spec_state_test +def test_full_random_4(spec, state): + """ + Ensure a rewards test with some exited (but not slashed) validators. + """ + rng = Random(5050) + randomize_state(spec, state, rng) + assert spec.is_in_inactivity_leak(state) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + yield from rewards_helpers.run_deltas(spec, state) + + @with_all_phases @with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) @spec_test From df8976377736c2d9121cb63454345cdb6011eaa0 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 1 Sep 2021 15:46:40 -0700 Subject: [PATCH 06/14] ensure balance differential as a sanity check --- tests/core/pyspec/eth2spec/test/helpers/state.py | 12 ++++++++++++ .../eth2spec/test/phase0/rewards/test_random.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py index d8fda3754..327bebaf8 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/state.py +++ b/tests/core/pyspec/eth2spec/test/helpers/state.py @@ -155,3 +155,15 @@ def ensure_state_has_validators_across_lifecycle(spec, state): has_slashed = any(filter(lambda v: v.slashed, state.validators)) return has_pending and has_active and has_exited and has_slashed + + +def has_active_balance_differential(spec, state): + """ + Ensure there is a difference between the total balance of + all _active_ validators and _all_ validators. + """ + epoch = spec.get_current_epoch(state) + active_indices = spec.get_active_validator_indices(state, epoch) + active_balance = spec.get_total_balance(state, set(active_indices)) + total_balance = spec.get_total_balance(state, set(range(len(state.validators)))) + return active_balance // spec.EFFECTIVE_BALANCE_INCREMENT != total_balance // spec.EFFECTIVE_BALANCE_INCREMENT diff --git a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py index 68a6a3279..44f22270b 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py +++ b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py @@ -10,6 +10,7 @@ from eth2spec.test.context import ( ) import eth2spec.test.helpers.rewards as rewards_helpers from eth2spec.test.helpers.random import randomize_state, patch_state_to_non_leaking +from eth2spec.test.helpers.state import has_active_balance_differential from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators @@ -48,6 +49,7 @@ def test_full_random_4(spec, state): assert spec.is_in_inactivity_leak(state) target_validators = get_unslashed_exited_validators(spec, state) assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) yield from rewards_helpers.run_deltas(spec, state) @@ -85,4 +87,5 @@ def test_full_random_without_leak_0(spec, state): assert not spec.is_in_inactivity_leak(state) target_validators = get_unslashed_exited_validators(spec, state) assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) yield from rewards_helpers.run_deltas(spec, state) From ad076697f424a84c0e9b07e1d9316c3cd5906ee6 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 1 Sep 2021 15:47:05 -0700 Subject: [PATCH 07/14] add test case for active/exited difference for sync rewards processing --- .../test_process_sync_aggregate.py | 21 +++---- .../test_process_sync_aggregate_random.py | 62 ++++++++++++++++--- .../eth2spec/test/helpers/sync_committee.py | 26 ++------ 3 files changed, 67 insertions(+), 42 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py index bd52ce727..0eaddbb88 100644 --- a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py +++ b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py @@ -12,7 +12,6 @@ from eth2spec.test.helpers.constants import ( from eth2spec.test.helpers.sync_committee import ( compute_aggregate_sync_committee_signature, compute_committee_indices, - get_committee_indices, run_sync_committee_processing, run_successful_sync_committee_test, ) @@ -28,7 +27,7 @@ from eth2spec.test.context import ( @spec_state_test @always_bls def test_invalid_signature_bad_domain(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) block = build_empty_block_for_next_slot(spec, state) block.body.sync_aggregate = spec.SyncAggregate( @@ -48,7 +47,7 @@ def test_invalid_signature_bad_domain(spec, state): @spec_state_test @always_bls def test_invalid_signature_missing_participant(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) rng = random.Random(2020) random_participant = rng.choice(committee_indices) @@ -111,7 +110,7 @@ def test_invalid_signature_infinite_signature_with_single_participant(spec, stat @spec_state_test @always_bls def test_invalid_signature_extra_participant(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) rng = random.Random(3030) random_participant = rng.choice(committee_indices) @@ -134,7 +133,7 @@ def test_invalid_signature_extra_participant(spec, state): @with_presets([MINIMAL], reason="to create nonduplicate committee") @spec_state_test def test_sync_committee_rewards_nonduplicate_committee(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=False) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [True] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) @@ -150,7 +149,7 @@ def test_sync_committee_rewards_nonduplicate_committee(spec, state): @with_presets([MAINNET], reason="to create duplicate committee") @spec_state_test def test_sync_committee_rewards_duplicate_committee_no_participation(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=True) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [False] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) @@ -166,7 +165,7 @@ def test_sync_committee_rewards_duplicate_committee_no_participation(spec, state @with_presets([MAINNET], reason="to create duplicate committee") @spec_state_test def test_sync_committee_rewards_duplicate_committee_half_participation(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=True) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [True] * (committee_size // 2) + [False] * (committee_size // 2) assert len(committee_bits) == committee_size @@ -183,7 +182,7 @@ def test_sync_committee_rewards_duplicate_committee_half_participation(spec, sta @with_presets([MAINNET], reason="to create duplicate committee") @spec_state_test def test_sync_committee_rewards_duplicate_committee_full_participation(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=True) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [True] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) @@ -199,7 +198,7 @@ def test_sync_committee_rewards_duplicate_committee_full_participation(spec, sta @spec_state_test @always_bls def test_sync_committee_rewards_not_full_participants(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) rng = random.Random(1010) committee_bits = [rng.choice([True, False]) for _ in committee_indices] @@ -210,7 +209,7 @@ def test_sync_committee_rewards_not_full_participants(spec, state): @spec_state_test @always_bls def test_sync_committee_rewards_empty_participants(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) committee_bits = [False for _ in committee_indices] yield from run_successful_sync_committee_test(spec, state, committee_indices, committee_bits) @@ -220,7 +219,7 @@ def test_sync_committee_rewards_empty_participants(spec, state): @spec_state_test @always_bls def test_invalid_signature_past_block(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) for _ in range(2): # NOTE: need to transition twice to move beyond the degenerate case at genesis diff --git a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py index 75845e060..436e4d04b 100644 --- a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py +++ b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py @@ -2,10 +2,19 @@ import random from eth2spec.test.helpers.constants import ( MAINNET, MINIMAL, ) +from eth2spec.test.helpers.random import ( + randomize_state +) +from eth2spec.test.helpers.state import ( + has_active_balance_differential, +) from eth2spec.test.helpers.sync_committee import ( - get_committee_indices, + compute_committee_indices, run_successful_sync_committee_test, ) +from eth2spec.test.helpers.voluntary_exits import ( + get_unslashed_exited_validators, +) from eth2spec.test.context import ( with_altair_and_later, spec_state_test, @@ -18,8 +27,8 @@ from eth2spec.test.context import ( ) -def _test_harness_for_randomized_test_case(spec, state, duplicates=False, participation_fn=None): - committee_indices = get_committee_indices(spec, state, duplicates=duplicates) +def _test_harness_for_randomized_test_case(spec, state, expect_duplicates=False, participation_fn=None): + committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) if participation_fn: participating_indices = participation_fn(committee_indices) @@ -28,7 +37,7 @@ def _test_harness_for_randomized_test_case(spec, state, duplicates=False, partic committee_bits = [index in participating_indices for index in committee_indices] committee_size = len(committee_indices) - if duplicates: + if expect_duplicates: assert committee_size > len(set(committee_indices)) else: assert committee_size == len(set(committee_indices)) @@ -44,7 +53,7 @@ def test_random_only_one_participant_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: [rng.choice(comm)], ) @@ -57,7 +66,7 @@ def test_random_low_participation_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, int(len(comm) * 0.25)), ) @@ -70,7 +79,7 @@ def test_random_high_participation_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, int(len(comm) * 0.75)), ) @@ -83,7 +92,7 @@ def test_random_all_but_one_participating_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, len(comm) - 1), ) @@ -98,7 +107,25 @@ def test_random_misc_balances_and_half_participation_with_duplicates(spec, state yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, + participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), + ) + + +@with_altair_and_later +@with_presets([MAINNET], reason="to create duplicate committee") +@spec_state_test +@single_phase +def test_random_with_exits_with_duplicates(spec, state): + rng = random.Random(1402) + randomize_state(spec, state, rng=rng, exit_fraction=0.1, slash_fraction=0.0) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + yield from _test_harness_for_randomized_test_case( + spec, + state, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), ) @@ -163,3 +190,20 @@ def test_random_misc_balances_and_half_participation_without_duplicates(spec, st state, participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), ) + + +@with_altair_and_later +@with_presets([MINIMAL], reason="to create nonduplicate committee") +@spec_state_test +@single_phase +def test_random_with_exits_without_duplicates(spec, state): + rng = random.Random(1502) + randomize_state(spec, state, rng=rng, exit_fraction=0.1, slash_fraction=0.0) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + yield from _test_harness_for_randomized_test_case( + spec, + state, + participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), + ) diff --git a/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py b/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py index e59f679e1..417802ece 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py +++ b/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py @@ -9,7 +9,6 @@ from eth2spec.test.helpers.block import ( ) from eth2spec.test.helpers.block_processing import run_block_processing_to from eth2spec.utils import bls -from eth2spec.utils.hash_function import hash def compute_sync_committee_signature(spec, state, slot, privkey, block_root=None, domain_type=None): @@ -75,10 +74,12 @@ def compute_sync_committee_proposer_reward(spec, state, committee_indices, commi return spec.Gwei(participant_reward * participant_number) -def compute_committee_indices(spec, state, committee): +def compute_committee_indices(spec, state, committee=None): """ Given a ``committee``, calculate and return the related indices """ + if committee is None: + committee = state.current_sync_committee all_pubkeys = [v.pubkey for v in state.validators] return [all_pubkeys.index(pubkey) for pubkey in committee.pubkeys] @@ -153,6 +154,7 @@ def _build_block_for_next_slot_with_sync_participation(spec, state, committee_in state, block.slot - 1, [index for index, bit in zip(committee_indices, committee_bits) if bit], + block_root=block.parent_root, ) ) return block @@ -161,23 +163,3 @@ def _build_block_for_next_slot_with_sync_participation(spec, state, committee_in def run_successful_sync_committee_test(spec, state, committee_indices, committee_bits): block = _build_block_for_next_slot_with_sync_participation(spec, state, committee_indices, committee_bits) yield from run_sync_committee_processing(spec, state, block) - - -def get_committee_indices(spec, state, duplicates=False): - """ - This utility function allows the caller to ensure there are or are not - duplicate validator indices in the returned committee based on - the boolean ``duplicates``. - """ - state = state.copy() - current_epoch = spec.get_current_epoch(state) - randao_index = (current_epoch + 1) % spec.EPOCHS_PER_HISTORICAL_VECTOR - while True: - committee = spec.get_next_sync_committee_indices(state) - if duplicates: - if len(committee) != len(set(committee)): - return committee - else: - if len(committee) == len(set(committee)): - return committee - state.randao_mixes[randao_index] = hash(state.randao_mixes[randao_index]) From bd38587a1e4491f66174a7bc7314e17f34c53e8e Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 1 Sep 2021 15:53:08 -0700 Subject: [PATCH 08/14] add active/exited balances test for `process_slashings` --- .../test_process_slashings.py | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py index e336ebef7..1b977640d 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py @@ -1,7 +1,11 @@ +from random import Random from eth2spec.test.context import spec_state_test, with_all_phases, is_post_altair from eth2spec.test.helpers.epoch_processing import ( run_epoch_processing_with, run_epoch_processing_to ) +from eth2spec.test.helpers.random import randomize_state +from eth2spec.test.helpers.state import has_active_balance_differential +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators from eth2spec.test.helpers.state import next_epoch @@ -22,6 +26,9 @@ def slash_validators(spec, state, indices, out_epochs): spec.get_current_epoch(state) % spec.EPOCHS_PER_SLASHINGS_VECTOR ] = total_slashed_balance + # verify some slashings happened... + assert total_slashed_balance != 0 + def get_slashing_multiplier(spec): if is_post_altair(spec): @@ -30,9 +37,7 @@ def get_slashing_multiplier(spec): return spec.PROPORTIONAL_SLASHING_MULTIPLIER -@with_all_phases -@spec_state_test -def test_max_penalties(spec, state): +def _setup_process_slashings_test(spec, state, not_slashable_set=set()): # Slashed count to ensure that enough validators are slashed to induce maximum penalties slashed_count = min( (len(state.validators) // get_slashing_multiplier(spec)) + 1, @@ -41,14 +46,23 @@ def test_max_penalties(spec, state): ) out_epoch = spec.get_current_epoch(state) + (spec.EPOCHS_PER_SLASHINGS_VECTOR // 2) - slashed_indices = list(range(slashed_count)) - slash_validators(spec, state, slashed_indices, [out_epoch] * slashed_count) + eligible_indices = set(range(slashed_count)) + slashed_indices = eligible_indices.difference(not_slashable_set) + slash_validators(spec, state, sorted(slashed_indices), [out_epoch] * slashed_count) total_balance = spec.get_total_active_balance(state) total_penalties = sum(state.slashings) assert total_balance // get_slashing_multiplier(spec) <= total_penalties + return slashed_indices + + +@with_all_phases +@spec_state_test +def test_max_penalties(spec, state): + slashed_indices = _setup_process_slashings_test(spec, state) + yield from run_process_slashings(spec, state) for i in slashed_indices: @@ -171,3 +185,28 @@ def test_scaled_penalties(spec, state): * spec.EFFECTIVE_BALANCE_INCREMENT ) assert state.balances[i] == pre_slash_balances[i] - expected_penalty + + +@with_all_phases +@spec_state_test +def test_slashings_with_random_state(spec, state): + rng = Random(9998) + randomize_state(spec, state, rng) + + pre_balances = state.balances.copy() + + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + + slashed_indices = _setup_process_slashings_test(spec, state, not_slashable_set=target_validators) + + # ensure no accidental slashings of protected set... + current_target_validators = get_unslashed_exited_validators(spec, state) + assert len(current_target_validators) != 0 + assert current_target_validators == target_validators + + yield from run_process_slashings(spec, state) + + for i in slashed_indices: + assert state.balances[i] < pre_balances[i] From d834b6e800eacea1961682e5abad701c5fb17c34 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 2 Sep 2021 12:37:11 -0700 Subject: [PATCH 09/14] add active/exited balances test for justification --- ..._process_justification_and_finalization.py | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py index 9db6076f8..1dfc07188 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py @@ -1,8 +1,10 @@ +from random import Random from eth2spec.test.context import is_post_altair, spec_state_test, with_all_phases from eth2spec.test.helpers.epoch_processing import ( run_epoch_processing_with, ) -from eth2spec.test.helpers.state import transition_to +from eth2spec.test.helpers.state import transition_to, next_epoch_via_block, next_slot +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators def run_process_just_and_fin(spec, state): @@ -300,3 +302,75 @@ def test_12_ok_support_messed_target(spec, state): @spec_state_test def test_12_poor_support(spec, state): yield from finalize_on_12(spec, state, 3, False, False) + + +@with_all_phases +@spec_state_test +def test_balance_threshold_with_exited_validators(spec, state): + """ + This test exercises a very specific failure mode where + exited validators are incorrectly included in the total active balance + when weighing justification. + """ + rng = Random(133333) + # move past genesis conditions + for _ in range(3): + next_epoch_via_block(spec, state) + + # mock attestation helper requires last slot of epoch + for _ in range(spec.SLOTS_PER_EPOCH - 1): + next_slot(spec, state) + + # Step 1: Exit ~1/2 vals in current epoch + epoch = spec.get_current_epoch(state) + for index in spec.get_active_validator_indices(state, epoch): + if rng.choice([True, False]): + continue + + validator = state.validators[index] + validator.exit_epoch = epoch + validator.withdrawable_epoch = epoch + 1 + + exited_validators = get_unslashed_exited_validators(spec, state) + assert len(exited_validators) != 0 + + source = state.current_justified_checkpoint + target = spec.Checkpoint( + epoch=epoch, + root=spec.get_block_root(state, epoch) + ) + add_mock_attestations( + spec, + state, + epoch, + source, + target, + sufficient_support=False, + ) + + if not is_post_altair(spec): + current_attestations = spec.get_matching_target_attestations(state, epoch) + total_active_balance = spec.get_total_active_balance(state) + current_target_balance = spec.get_attesting_balance(state, current_attestations) + # Check we will not justify the current checkpoint + does_justify = current_target_balance * 3 >= total_active_balance * 2 + assert not does_justify + # Ensure we would have justified the current checkpoint w/ the exited validators + current_exited_balance = spec.get_total_balance(state, exited_validators) + does_justify = (current_target_balance + current_exited_balance) * 3 >= total_active_balance * 2 + assert does_justify + else: + current_indices = spec.get_unslashed_participating_indices(state, spec.TIMELY_TARGET_FLAG_INDEX, epoch) + total_active_balance = spec.get_total_active_balance(state) + current_target_balance = spec.get_total_balance(state, current_indices) + # Check we will not justify the current checkpoint + does_justify = current_target_balance * 3 >= total_active_balance * 2 + assert not does_justify + # Ensure we would have justified the current checkpoint w/ the exited validators + current_exited_balance = spec.get_total_balance(state, exited_validators) + does_justify = (current_target_balance + current_exited_balance) * 3 >= total_active_balance * 2 + assert does_justify + + yield from run_process_just_and_fin(spec, state) + + assert state.current_justified_checkpoint.epoch != epoch From 30596fb8a1c47b8d14c8619ae3e1c4ccb068b313 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 2 Sep 2021 15:35:15 -0600 Subject: [PATCH 10/14] Update tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py --- .../sync_aggregate/test_process_sync_aggregate_random.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py index 436e4d04b..d38c3fadf 100644 --- a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py +++ b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py @@ -28,7 +28,7 @@ from eth2spec.test.context import ( def _test_harness_for_randomized_test_case(spec, state, expect_duplicates=False, participation_fn=None): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) if participation_fn: participating_indices = participation_fn(committee_indices) From 4168943ecf0f3465fdea74f3d9bdc2d21f567153 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 2 Sep 2021 15:38:52 -0600 Subject: [PATCH 11/14] Update tests/core/pyspec/eth2spec/test/helpers/random.py --- tests/core/pyspec/eth2spec/test/helpers/random.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/random.py b/tests/core/pyspec/eth2spec/test/helpers/random.py index 0bb2a6672..b24d3c8c7 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/random.py +++ b/tests/core/pyspec/eth2spec/test/helpers/random.py @@ -141,7 +141,8 @@ def patch_state_to_non_leaking(spec, state): performed by other functionality in this module so that if the ``state`` was leaking, then the ``state`` is not leaking after. """ - state.justification_bits = (True, True, True, True) + state.justification_bits[0] = True + state.justification_bits[1] = True previous_epoch = spec.get_previous_epoch(state) previous_root = spec.get_block_root(state, previous_epoch) previous_previous_epoch = max(spec.GENESIS_EPOCH, spec.Epoch(previous_epoch - 1)) From fb4a4f669460fba812715501dfdd0be40fac4f81 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Sep 2021 13:09:23 -0600 Subject: [PATCH 12/14] Update tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py Co-authored-by: Danny Ryan --- .../sync_aggregate/test_process_sync_aggregate_random.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py index d38c3fadf..903df4081 100644 --- a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py +++ b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py @@ -3,7 +3,7 @@ from eth2spec.test.helpers.constants import ( MAINNET, MINIMAL, ) from eth2spec.test.helpers.random import ( - randomize_state + randomize_state, ) from eth2spec.test.helpers.state import ( has_active_balance_differential, From 064b489d18504e796adc4661cd4d641456dfd0a8 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Sep 2021 12:23:57 -0700 Subject: [PATCH 13/14] Use spec function for total active balance --- tests/core/pyspec/eth2spec/test/helpers/state.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py index 327bebaf8..6f1923e54 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/state.py +++ b/tests/core/pyspec/eth2spec/test/helpers/state.py @@ -162,8 +162,6 @@ def has_active_balance_differential(spec, state): Ensure there is a difference between the total balance of all _active_ validators and _all_ validators. """ - epoch = spec.get_current_epoch(state) - active_indices = spec.get_active_validator_indices(state, epoch) - active_balance = spec.get_total_balance(state, set(active_indices)) + active_balance = spec.get_total_active_balance(state) total_balance = spec.get_total_balance(state, set(range(len(state.validators)))) return active_balance // spec.EFFECTIVE_BALANCE_INCREMENT != total_balance // spec.EFFECTIVE_BALANCE_INCREMENT From 14f71ffb4bb49f436dff5262cebed2720e0bdef7 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Sep 2021 12:25:09 -0700 Subject: [PATCH 14/14] Use realistic `withdrawable_epoch` in spec test --- .../test_process_justification_and_finalization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py index 1dfc07188..1d3197ba6 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py @@ -330,6 +330,7 @@ def test_balance_threshold_with_exited_validators(spec, state): validator = state.validators[index] validator.exit_epoch = epoch validator.withdrawable_epoch = epoch + 1 + validator.withdrawable_epoch = validator.exit_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY exited_validators = get_unslashed_exited_validators(spec, state) assert len(exited_validators) != 0