diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 37a428b50..b067f222f 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -58,8 +58,8 @@ INACTIVITY_SCORE_RECOVERY_RATE: 16 EJECTION_BALANCE: 16000000000 # 2**2 (= 4) MIN_PER_EPOCH_CHURN_LIMIT: 4 -# 2**16 (= 65,536) -CHURN_LIMIT_QUOTIENT: 65536 +# [customized] scale queue churn at much lower validator counts for testing +CHURN_LIMIT_QUOTIENT: 32 # Deposit contract diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 346cdc8f1..ef92efade 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -126,6 +126,17 @@ def default_balances(spec): return [spec.MAX_EFFECTIVE_BALANCE] * num_validators +def scaled_churn_balances(spec): + """ + Helper method to create enough validators to scale the churn limit. + (This is *firmly* over the churn limit -- thus the +2 instead of just +1) + See the second argument of ``max`` in ``get_validator_churn_limit``. + Usage: `@with_custom_state(balances_fn=scaled_churn_balances, ...)` + """ + num_validators = spec.config.CHURN_LIMIT_QUOTIENT * (2 + spec.config.MIN_PER_EPOCH_CHURN_LIMIT) + return [spec.MAX_EFFECTIVE_BALANCE] * num_validators + + with_state = with_custom_state(default_balances, default_activation_threshold) diff --git a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py index f713d1792..9e209f23e 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py +++ b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py @@ -1,4 +1,10 @@ -from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls, with_all_phases +from eth2spec.test.helpers.constants import MINIMAL +from eth2spec.test.context import ( + spec_state_test, expect_assertion_error, + always_bls, with_all_phases, with_presets, + spec_test, single_phase, + with_custom_state, scaled_churn_balances, +) from eth2spec.test.helpers.keys import pubkey_to_privkey from eth2spec.test.helpers.voluntary_exits import sign_voluntary_exit @@ -68,9 +74,7 @@ def test_invalid_signature(spec, state): yield from run_voluntary_exit_processing(spec, state, signed_voluntary_exit, False) -@with_all_phases -@spec_state_test -def test_success_exit_queue(spec, state): +def run_test_success_exit_queue(spec, state): # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH @@ -106,10 +110,29 @@ def test_success_exit_queue(spec, state): # when processing an additional exit, it results in an exit in a later epoch yield from run_voluntary_exit_processing(spec, state, signed_voluntary_exit) - assert ( - state.validators[validator_index].exit_epoch == - state.validators[initial_indices[0]].exit_epoch + 1 - ) + for index in initial_indices: + assert ( + state.validators[validator_index].exit_epoch == + state.validators[index].exit_epoch + 1 + ) + + +@with_all_phases +@spec_state_test +def test_success_exit_queue__min_churn(spec, state): + yield from run_test_success_exit_queue(spec, state) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_success_exit_queue__scaled_churn(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_success_exit_queue(spec, state) @with_all_phases diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py index 6e7784aa9..6539dc92d 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py @@ -1,6 +1,12 @@ from eth2spec.test.helpers.deposits import mock_deposit from eth2spec.test.helpers.state import next_epoch, next_slots -from eth2spec.test.context import spec_state_test, with_all_phases +from eth2spec.test.helpers.constants import MINIMAL +from eth2spec.test.context import ( + spec_test, spec_state_test, + with_all_phases, single_phase, + with_custom_state, with_presets, + scaled_churn_balances, +) from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with @@ -112,9 +118,7 @@ def test_activation_queue_sorting(spec, state): assert state.validators[churn_limit - 1].activation_epoch != spec.FAR_FUTURE_EPOCH -@with_all_phases -@spec_state_test -def test_activation_queue_efficiency(spec, state): +def run_test_activation_queue_efficiency(spec, state): churn_limit = spec.get_validator_churn_limit(state) mock_activations = churn_limit * 2 @@ -128,23 +132,45 @@ def test_activation_queue_efficiency(spec, state): state.finalized_checkpoint.epoch = epoch + 1 + # Churn limit could have changed given the active vals removed via `mock_deposit` + churn_limit_0 = spec.get_validator_churn_limit(state) + # Run first registry update. Do not yield test vectors for _ in run_process_registry_updates(spec, state): pass # Half should churn in first run of registry update for i in range(mock_activations): - if i < mock_activations // 2: + if i < churn_limit_0: assert state.validators[i].activation_epoch < spec.FAR_FUTURE_EPOCH else: assert state.validators[i].activation_epoch == spec.FAR_FUTURE_EPOCH # Second half should churn in second run of registry update + churn_limit_1 = spec.get_validator_churn_limit(state) yield from run_process_registry_updates(spec, state) - for i in range(mock_activations): + for i in range(churn_limit_0 + churn_limit_1): assert state.validators[i].activation_epoch < spec.FAR_FUTURE_EPOCH +@with_all_phases +@spec_state_test +def test_activation_queue_efficiency_min(spec, state): + assert spec.get_validator_churn_limit(state) == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_efficiency(spec, state) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_activation_queue_efficiency_scaled(spec, state): + assert spec.get_validator_churn_limit(state) > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_efficiency(spec, state) + + @with_all_phases @spec_state_test def test_ejection(spec, state): @@ -165,9 +191,7 @@ def test_ejection(spec, state): ) -@with_all_phases -@spec_state_test -def test_ejection_past_churn_limit(spec, state): +def run_test_ejection_past_churn_limit(spec, state): churn_limit = spec.get_validator_churn_limit(state) # try to eject more than per-epoch churn limit @@ -184,58 +208,137 @@ def test_ejection_past_churn_limit(spec, state): # first third ejected in normal speed if i < mock_ejections // 3: assert state.validators[i].exit_epoch == expected_ejection_epoch - # second thirdgets delayed by 1 epoch + # second third gets delayed by 1 epoch elif mock_ejections // 3 <= i < mock_ejections * 2 // 3: assert state.validators[i].exit_epoch == expected_ejection_epoch + 1 - # second thirdgets delayed by 2 epochs + # final third gets delayed by 2 epochs else: assert state.validators[i].exit_epoch == expected_ejection_epoch + 2 @with_all_phases @spec_state_test -def test_activation_queue_activation_and_ejection(spec, state): +def test_ejection_past_churn_limit_min(spec, state): + assert spec.get_validator_churn_limit(state) == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_ejection_past_churn_limit(spec, state) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_ejection_past_churn_limit_scaled(spec, state): + assert spec.get_validator_churn_limit(state) > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_ejection_past_churn_limit(spec, state) + + +def run_test_activation_queue_activation_and_ejection(spec, state, num_per_status): # move past first two irregular epochs wrt finality next_epoch(spec, state) next_epoch(spec, state) # ready for entrance into activation queue - activation_queue_index = 0 - mock_deposit(spec, state, activation_queue_index) + activation_queue_start_index = 0 + activation_queue_indices = list(range(activation_queue_start_index, activation_queue_start_index + num_per_status)) + for validator_index in activation_queue_indices: + mock_deposit(spec, state, validator_index) # ready for activation - activation_index = 1 - mock_deposit(spec, state, activation_index) state.finalized_checkpoint.epoch = spec.get_current_epoch(state) - 1 - state.validators[activation_index].activation_eligibility_epoch = state.finalized_checkpoint.epoch + activation_start_index = num_per_status + activation_indices = list(range(activation_start_index, activation_start_index + num_per_status)) + for validator_index in activation_indices: + mock_deposit(spec, state, validator_index) + state.validators[validator_index].activation_eligibility_epoch = state.finalized_checkpoint.epoch # ready for ejection - ejection_index = 2 - state.validators[ejection_index].effective_balance = spec.config.EJECTION_BALANCE + ejection_start_index = num_per_status * 2 + ejection_indices = list(range(ejection_start_index, ejection_start_index + num_per_status)) + for validator_index in ejection_indices: + state.validators[validator_index].effective_balance = spec.config.EJECTION_BALANCE + churn_limit = spec.get_validator_churn_limit(state) yield from run_process_registry_updates(spec, state) - # validator moved into activation queue - validator = state.validators[activation_queue_index] - assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH - assert validator.activation_epoch == spec.FAR_FUTURE_EPOCH - assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + # all eligible validators moved into activation queue + for validator_index in activation_queue_indices: + validator = state.validators[validator_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch == spec.FAR_FUTURE_EPOCH + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) - # validator activated for future epoch - validator = state.validators[activation_index] - assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH - assert validator.activation_epoch != spec.FAR_FUTURE_EPOCH - assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) - assert spec.is_active_validator( - validator, - spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) - ) + # up to churn limit validators get activated for future epoch from the queue + for validator_index in activation_indices[:churn_limit]: + validator = state.validators[validator_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch != spec.FAR_FUTURE_EPOCH + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert spec.is_active_validator( + validator, + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + ) - # validator ejected for future epoch - validator = state.validators[ejection_index] - assert validator.exit_epoch != spec.FAR_FUTURE_EPOCH - assert spec.is_active_validator(validator, spec.get_current_epoch(state)) - assert not spec.is_active_validator( - validator, - spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) - ) + # any remaining validators do not exit the activation queue + for validator_index in activation_indices[churn_limit:]: + validator = state.validators[validator_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch == spec.FAR_FUTURE_EPOCH + + # all ejection balance validators ejected for a future epoch + for i, validator_index in enumerate(ejection_indices): + validator = state.validators[validator_index] + assert validator.exit_epoch != spec.FAR_FUTURE_EPOCH + assert spec.is_active_validator(validator, spec.get_current_epoch(state)) + queue_offset = i // churn_limit + assert not spec.is_active_validator( + validator, + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + queue_offset + ) + + +@with_all_phases +@spec_state_test +def test_activation_queue_activation_and_ejection__1(spec, state): + yield from run_test_activation_queue_activation_and_ejection(spec, state, 1) + + +@with_all_phases +@spec_state_test +def test_activation_queue_activation_and_ejection__churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit) + + +@with_all_phases +@spec_state_test +def test_activation_queue_activation_and_ejection__exceed_churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit + 1) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_activation_queue_activation_and_ejection__scaled_churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_activation_queue_activation_and_ejection__exceed_scaled_churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit * 2)