diff --git a/configs/mainnet/altair.yaml b/configs/mainnet/altair.yaml index e387fc87a..a6761b142 100644 --- a/configs/mainnet/altair.yaml +++ b/configs/mainnet/altair.yaml @@ -22,6 +22,8 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 512 # --------------------------------------------------------------- # 2**2 (= 4) INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 # Signature domains diff --git a/configs/minimal/altair.yaml b/configs/minimal/altair.yaml index a66b5c7ca..f9b8401e1 100644 --- a/configs/minimal/altair.yaml +++ b/configs/minimal/altair.yaml @@ -22,6 +22,8 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8 # --------------------------------------------------------------- # 2**2 (= 4) INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 # Signature domains diff --git a/specs/altair/beacon-chain.md b/specs/altair/beacon-chain.md index 7bdedada7..35909f23f 100644 --- a/specs/altair/beacon-chain.md +++ b/specs/altair/beacon-chain.md @@ -127,6 +127,7 @@ This patch updates a few configuration values to move penalty parameters closer | Name | Value | | - | - | | `INACTIVITY_SCORE_BIAS` | `uint64(4)` | +| `INACTIVITY_SCORE_RECOVERY_RATE` | `uint64(16)` | ### Domain types @@ -417,14 +418,13 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S """ rewards = [Gwei(0) for _ in range(len(state.validators))] penalties = [Gwei(0) for _ in range(len(state.validators))] - if is_in_inactivity_leak(state): - previous_epoch = get_previous_epoch(state) - matching_target_indices = get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, previous_epoch) - for index in get_eligible_validator_indices(state): - if index not in matching_target_indices: - penalty_numerator = state.validators[index].effective_balance * state.inactivity_scores[index] - penalty_denominator = INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_ALTAIR - penalties[index] += Gwei(penalty_numerator // penalty_denominator) + previous_epoch = get_previous_epoch(state) + matching_target_indices = get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, previous_epoch) + for index in get_eligible_validator_indices(state): + if index not in matching_target_indices: + penalty_numerator = state.validators[index].effective_balance * state.inactivity_scores[index] + penalty_denominator = INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_ALTAIR + penalties[index] += Gwei(penalty_numerator // penalty_denominator) return rewards, penalties ``` @@ -624,12 +624,19 @@ def process_justification_and_finalization(state: BeaconState) -> None: ```python def process_inactivity_updates(state: BeaconState) -> None: + # Score updates based on previous epoch participation, skip genesis epoch + if get_current_epoch(state) == GENESIS_EPOCH: + return + for index in get_eligible_validator_indices(state): + # Increase inactivity score of inactive validators if index in get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state)): - if state.inactivity_scores[index] > 0: - state.inactivity_scores[index] -= 1 - elif is_in_inactivity_leak(state): + state.inactivity_scores[index] -= min(1, state.inactivity_scores[index]) + else: state.inactivity_scores[index] += INACTIVITY_SCORE_BIAS + # Decrease the score of all validators for forgiveness when not during a leak + if not is_in_inactivity_leak(state): + state.inactivity_scores[index] -= min(INACTIVITY_SCORE_RECOVERY_RATE, state.inactivity_scores[index]) ``` #### Rewards and penalties diff --git a/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_inactivity_updates.py b/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_inactivity_updates.py new file mode 100644 index 000000000..1cc8cb9ae --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_inactivity_updates.py @@ -0,0 +1,90 @@ +from random import Random + +from eth2spec.test.context import spec_state_test, with_altair_and_later +from eth2spec.test.helpers.inactivity_scores import randomize_inactivity_scores +from eth2spec.test.helpers.state import ( + next_epoch_via_block, +) +from eth2spec.test.helpers.epoch_processing import ( + run_epoch_processing_with +) +from eth2spec.test.helpers.random import ( + randomize_attestation_participation, +) + + +def set_full_participation(spec, state): + full_flags = spec.ParticipationFlags(0) + for flag_index in range(len(spec.PARTICIPATION_FLAG_WEIGHTS)): + full_flags = spec.add_flag(full_flags, flag_index) + + for index in range(len(state.validators)): + state.current_epoch_participation[index] = full_flags + state.previous_epoch_participation[index] = full_flags + + +def run_process_inactivity_updates(spec, state): + yield from run_epoch_processing_with(spec, state, 'process_inactivity_updates') + + +@with_altair_and_later +@spec_state_test +def test_genesis(spec, state): + yield from run_process_inactivity_updates(spec, state) + + +# +# Genesis epoch processing is skipped +# Thus all of following tests all go past genesis epoch to test core functionality +# + +@with_altair_and_later +@spec_state_test +def test_all_zero_inactivity_scores_empty_participation(spec, state): + next_epoch_via_block(spec, state) + state.inactivity_scores = [0] * len(state.validators) + yield from run_process_inactivity_updates(spec, state) + + +@with_altair_and_later +@spec_state_test +def test_all_zero_inactivity_scores_random_participation(spec, state): + next_epoch_via_block(spec, state) + state.inactivity_scores = [0] * len(state.validators) + randomize_attestation_participation(spec, state, rng=Random(5555)) + yield from run_process_inactivity_updates(spec, state) + + +@with_altair_and_later +@spec_state_test +def test_all_zero_inactivity_scores_full_participation(spec, state): + next_epoch_via_block(spec, state) + set_full_participation(spec, state) + state.inactivity_scores = [0] * len(state.validators) + yield from run_process_inactivity_updates(spec, state) + + +@with_altair_and_later +@spec_state_test +def test_random_inactivity_scores_empty_participation(spec, state): + next_epoch_via_block(spec, state) + randomize_inactivity_scores(spec, state, rng=Random(9999)) + yield from run_process_inactivity_updates(spec, state) + + +@with_altair_and_later +@spec_state_test +def test_random_inactivity_scores_random_participation(spec, state): + next_epoch_via_block(spec, state) + randomize_attestation_participation(spec, state, rng=Random(22222)) + randomize_inactivity_scores(spec, state, rng=Random(22222)) + yield from run_process_inactivity_updates(spec, state) + + +@with_altair_and_later +@spec_state_test +def test_random_inactivity_scores_full_participation(spec, state): + next_epoch_via_block(spec, state) + set_full_participation(spec, state) + randomize_inactivity_scores(spec, state, rng=Random(33333)) + yield from run_process_inactivity_updates(spec, state) diff --git a/tests/core/pyspec/eth2spec/test/altair/rewards/test_inactivity_scores.py b/tests/core/pyspec/eth2spec/test/altair/rewards/test_inactivity_scores.py new file mode 100644 index 000000000..9eca9a92a --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/rewards/test_inactivity_scores.py @@ -0,0 +1,118 @@ +from random import Random + +from eth2spec.test.context import ( + with_altair_and_later, + spec_test, + spec_state_test, + with_custom_state, + single_phase, + low_balances, misc_balances, +) +from eth2spec.test.helpers.inactivity_scores import randomize_inactivity_scores +from eth2spec.test.helpers.rewards import leaking +import eth2spec.test.helpers.rewards as rewards_helpers + + +@with_altair_and_later +@spec_state_test +def test_random_inactivity_scores_0(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(9999)) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(9999)) + + +@with_altair_and_later +@spec_state_test +def test_random_inactivity_scores_1(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(10000)) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(10000)) + + +@with_altair_and_later +@spec_state_test +def test_half_zero_half_random_inactivity_scores(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(10101)) + half_val_point = len(state.validators) // 2 + state.inactivity_scores = [0] * half_val_point + state.inactivity_scores[half_val_point:] + + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(10101)) + + +@with_altair_and_later +@spec_state_test +def test_random_high_inactivity_scores(spec, state): + randomize_inactivity_scores(spec, state, minimum=500000, maximum=5000000, rng=Random(9998)) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(9998)) + + +@with_altair_and_later +@with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE) +@spec_test +@single_phase +def test_random_inactivity_scores_low_balances_0(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(11111)) + yield from rewards_helpers.run_test_full_random(spec, state) + + +@with_altair_and_later +@with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE) +@spec_test +@single_phase +def test_random_inactivity_scores_low_balances_1(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(22222)) + yield from rewards_helpers.run_test_full_random(spec, state) + + +@with_altair_and_later +@with_custom_state(balances_fn=misc_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE) +@spec_test +@single_phase +def test_full_random_misc_balances(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(33333)) + yield from rewards_helpers.run_test_full_random(spec, state) + + +# +# Leaking variants +# + +@with_altair_and_later +@spec_state_test +@leaking() +def test_random_inactivity_scores_leaking_0(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(9999)) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(9999)) + + +@with_altair_and_later +@spec_state_test +@leaking() +def test_random_inactivity_scores_leaking_1(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(10000)) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(10000)) + + +@with_altair_and_later +@spec_state_test +@leaking() +def test_half_zero_half_random_inactivity_scores_leaking(spec, state): + randomize_inactivity_scores(spec, state, rng=Random(10101)) + half_val_point = len(state.validators) // 2 + state.inactivity_scores = [0] * half_val_point + state.inactivity_scores[half_val_point:] + + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(10101)) + + +@with_altair_and_later +@spec_state_test +@leaking() +def test_random_high_inactivity_scores_leaking(spec, state): + randomize_inactivity_scores(spec, state, minimum=500000, maximum=5000000, rng=Random(9998)) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(9998)) + + +@with_altair_and_later +@spec_state_test +@leaking(epochs=5) +def test_random_high_inactivity_scores_leaking_5_epochs(spec, state): + randomize_inactivity_scores(spec, state, minimum=500000, maximum=5000000, rng=Random(9998)) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(9998)) diff --git a/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py index 3c47c4895..c783692fc 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py +++ b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py @@ -9,6 +9,7 @@ def get_process_calls(spec): # or the old function will stick around. return [ 'process_justification_and_finalization', + 'process_inactivity_updates', # altair 'process_rewards_and_penalties', 'process_registry_updates', 'process_reveal_deadlines', # custody game @@ -26,7 +27,7 @@ def get_process_calls(spec): 'process_participation_flag_updates' if is_post_altair(spec) else ( 'process_participation_record_updates' ), - 'process_sync_committee_updates', + 'process_sync_committee_updates', # altair 'process_shard_epoch_increment' # sharding ] diff --git a/tests/core/pyspec/eth2spec/test/helpers/inactivity_scores.py b/tests/core/pyspec/eth2spec/test/helpers/inactivity_scores.py new file mode 100644 index 000000000..5c28bfc24 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/inactivity_scores.py @@ -0,0 +1,5 @@ +from random import Random + + +def randomize_inactivity_scores(spec, state, minimum=0, maximum=50000, rng=Random(4242)): + state.inactivity_scores = [rng.randint(minimum, maximum) 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 6bf922750..86bb70133 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -272,7 +272,6 @@ _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, 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 ae44c6640..b1af64a77 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py +++ b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py @@ -39,8 +39,16 @@ def test_full_random_3(spec, state): @with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE) @spec_test @single_phase -def test_full_random_low_balances(spec, state): - yield from rewards_helpers.run_test_full_random(spec, state) +def test_full_random_low_balances_0(spec, state): + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(5050)) + + +@with_all_phases +@with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE) +@spec_test +@single_phase +def test_full_random_low_balances_1(spec, state): + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(6060)) @with_all_phases @@ -48,4 +56,4 @@ def test_full_random_low_balances(spec, state): @spec_test @single_phase def test_full_random_misc_balances(spec, state): - yield from rewards_helpers.run_test_full_random(spec, state) + yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(7070)) diff --git a/tests/formats/epoch_processing/README.md b/tests/formats/epoch_processing/README.md index 3ac2a28c4..d9abcaf98 100644 --- a/tests/formats/epoch_processing/README.md +++ b/tests/formats/epoch_processing/README.md @@ -32,9 +32,8 @@ The provided pre-state is already transitioned to just before the specific sub-t Sub-transitions: -Sub-transitions: - - `justification_and_finalization` +- `inactivity_penalty_updates` - `rewards_and_penalties` - `registry_updates` - `slashings` @@ -44,5 +43,6 @@ Sub-transitions: - `randao_mixes_reset` - `historical_roots_update` - `participation_record_updates` +- `sync_committee_updates` The resulting state should match the expected `post` state.