diff --git a/specs/altair/beacon-chain.md b/specs/altair/beacon-chain.md index ab507a4f7..58e82364c 100644 --- a/specs/altair/beacon-chain.md +++ b/specs/altair/beacon-chain.md @@ -10,7 +10,7 @@ - [Custom types](#custom-types) - [Constants](#constants) - [Participation flag indices](#participation-flag-indices) - - [Participation flag fractions](#participation-flag-fractions) + - [Incentivization weights](#incentivization-weights) - [Misc](#misc) - [Configuration](#configuration) - [Updated penalty values](#updated-penalty-values) @@ -28,12 +28,13 @@ - [`Predicates`](#predicates) - [`eth2_fast_aggregate_verify`](#eth2_fast_aggregate_verify) - [Misc](#misc-2) - - [`get_flag_indices_and_numerators`](#get_flag_indices_and_numerators) + - [`get_flag_indices_and_weights`](#get_flag_indices_and_weights) - [`add_flag`](#add_flag) - [`has_flag`](#has_flag) - [Beacon state accessors](#beacon-state-accessors) - [`get_sync_committee_indices`](#get_sync_committee_indices) - [`get_sync_committee`](#get_sync_committee) + - [`get_base_reward_per_increment`](#get_base_reward_per_increment) - [`get_base_reward`](#get_base_reward) - [`get_unslashed_participating_indices`](#get_unslashed_participating_indices) - [`get_flag_index_deltas`](#get_flag_index_deltas) @@ -61,7 +62,7 @@ Altair is the first beacon chain hard fork. Its main features are: * sync committees to support light clients * incentive accounting reforms to reduce spec complexity -* penalty parameter updates to move them to their planned maximally punitive configuration +* penalty parameter updates towards their planned maximally punitive values ## Custom types @@ -79,16 +80,17 @@ Altair is the first beacon chain hard fork. Its main features are: | `TIMELY_SOURCE_FLAG_INDEX` | `1` | | `TIMELY_TARGET_FLAG_INDEX` | `2` | -### Participation flag fractions +### Incentivization weights | Name | Value | | - | - | -| `TIMELY_HEAD_FLAG_NUMERATOR` | `12` | -| `TIMELY_SOURCE_FLAG_NUMERATOR` | `12` | -| `TIMELY_TARGET_FLAG_NUMERATOR` | `32` | -| `FLAG_DENOMINATOR` | `64` | +| `TIMELY_HEAD_WEIGHT` | `12` | +| `TIMELY_SOURCE_WEIGHT` | `12` | +| `TIMELY_TARGET_WEIGHT` | `24` | +| `SYNC_REWARD_WEIGHT` | `8` | +| `WEIGHT_DENOMINATOR` | `64` | -*Note*: The sum of the participatition flag fractions (7/8) plus the proposer reward fraction (1/8) equals 1. +*Note*: The sum of the weight fractions (7/8) plus the proposer inclusion fraction (1/8) equals 1. ### Misc @@ -229,14 +231,14 @@ def eth2_fast_aggregate_verify(pubkeys: Sequence[BLSPubkey], message: Bytes32, s ### Misc -#### `get_flag_indices_and_numerators` +#### `get_flag_indices_and_weights` ```python -def get_flag_indices_and_numerators() -> Sequence[Tuple[int, int]]: +def get_flag_indices_and_weights() -> Sequence[Tuple[int, int]]: return ( - (TIMELY_HEAD_FLAG_INDEX, TIMELY_HEAD_FLAG_NUMERATOR), - (TIMELY_SOURCE_FLAG_INDEX, TIMELY_SOURCE_FLAG_NUMERATOR), - (TIMELY_TARGET_FLAG_INDEX, TIMELY_TARGET_FLAG_NUMERATOR), + (TIMELY_HEAD_FLAG_INDEX, TIMELY_HEAD_WEIGHT), + (TIMELY_SOURCE_FLAG_INDEX, TIMELY_SOURCE_WEIGHT), + (TIMELY_TARGET_FLAG_INDEX, TIMELY_TARGET_WEIGHT), ) ``` @@ -297,15 +299,21 @@ def get_sync_committee(state: BeaconState, epoch: Epoch) -> SyncCommittee: return SyncCommittee(pubkeys=pubkeys, pubkey_aggregates=pubkey_aggregates) ``` +#### `get_base_reward_per_increment` + +```python +def get_base_reward_per_increment(state: BeaconState) -> Gwei: + return Gwei(EFFECTIVE_BALANCE_INCREMENT * BASE_REWARD_FACTOR // integer_squareroot(get_total_active_balance(state))) +``` + #### `get_base_reward` *Note*: The function `get_base_reward` is modified with the removal of `BASE_REWARDS_PER_EPOCH`. ```python def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: - total_balance = get_total_active_balance(state) - effective_balance = state.validators[index].effective_balance - return Gwei(effective_balance * BASE_REWARD_FACTOR // integer_squareroot(total_balance)) + increments = state.validators[index].effective_balance // EFFECTIVE_BALANCE_INCREMENT + return Gwei(increments * get_base_reward_per_increment(state)) ``` #### `get_unslashed_participating_indices` @@ -328,9 +336,7 @@ def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epo #### `get_flag_index_deltas` ```python -def get_flag_index_deltas(state: BeaconState, - flag_index: int, - numerator: uint64) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: +def get_flag_index_deltas(state: BeaconState, flag_index: int, weight: uint64) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: """ Return the deltas for a given flag index by scanning through the participation flags. """ @@ -345,12 +351,12 @@ def get_flag_index_deltas(state: BeaconState, if index in unslashed_participating_indices: if is_in_inactivity_leak(state): # This flag reward cancels the inactivity penalty corresponding to the flag index - rewards[index] += Gwei(base_reward * numerator // FLAG_DENOMINATOR) + rewards[index] += Gwei(base_reward * weight // WEIGHT_DENOMINATOR) else: - reward_numerator = base_reward * numerator * unslashed_participating_increments - rewards[index] += Gwei(reward_numerator // (active_increments * FLAG_DENOMINATOR)) + reward_numerator = base_reward * weight * unslashed_participating_increments + rewards[index] += Gwei(reward_numerator // (active_increments * WEIGHT_DENOMINATOR)) else: - penalties[index] += Gwei(base_reward * numerator // FLAG_DENOMINATOR) + penalties[index] += Gwei(base_reward * weight // WEIGHT_DENOMINATOR) return rewards, penalties ``` @@ -370,9 +376,9 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S 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): - for (_, numerator) in get_flag_indices_and_numerators(): + for (_, weight) in get_flag_indices_and_weights(): # This inactivity penalty cancels the flag reward corresponding to the flag index - penalties[index] += Gwei(get_base_reward(state, index) * numerator // FLAG_DENOMINATOR) + penalties[index] += Gwei(get_base_reward(state, index) * weight // WEIGHT_DENOMINATOR) 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 @@ -465,13 +471,13 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: # Update epoch participation flags proposer_reward_numerator = 0 for index in get_attesting_indices(state, data, attestation.aggregation_bits): - for flag_index, flag_numerator in get_flag_indices_and_numerators(): + for flag_index, weight in get_flag_indices_and_weights(): if flag_index in participation_flag_indices and not has_flag(epoch_participation[index], flag_index): epoch_participation[index] = add_flag(epoch_participation[index], flag_index) - proposer_reward_numerator += get_base_reward(state, index) * flag_numerator + proposer_reward_numerator += get_base_reward(state, index) * weight # Reward proposer - proposer_reward = Gwei(proposer_reward_numerator // (FLAG_DENOMINATOR * PROPOSER_REWARD_QUOTIENT)) + proposer_reward = Gwei(proposer_reward_numerator // (WEIGHT_DENOMINATOR * PROPOSER_REWARD_QUOTIENT)) increase_balance(state, get_beacon_proposer_index(state), proposer_reward) ``` @@ -525,26 +531,28 @@ def process_sync_committee(state: BeaconState, aggregate: SyncAggregate) -> None # Verify sync committee aggregate signature signing over the previous slot block root previous_slot = Slot(max(int(state.slot), 1) - 1) committee_indices = get_sync_committee_indices(state, get_current_epoch(state)) - participant_indices = [index for index, bit in zip(committee_indices, aggregate.sync_committee_bits) if bit] + included_indices = [index for index, bit in zip(committee_indices, aggregate.sync_committee_bits) if bit] committee_pubkeys = state.current_sync_committee.pubkeys - participant_pubkeys = [pubkey for pubkey, bit in zip(committee_pubkeys, aggregate.sync_committee_bits) if bit] + included_pubkeys = [pubkey for pubkey, bit in zip(committee_pubkeys, aggregate.sync_committee_bits) if bit] domain = get_domain(state, DOMAIN_SYNC_COMMITTEE, compute_epoch_at_slot(previous_slot)) signing_root = compute_signing_root(get_block_root_at_slot(state, previous_slot), domain) - assert eth2_fast_aggregate_verify(participant_pubkeys, signing_root, aggregate.sync_committee_signature) + assert eth2_fast_aggregate_verify(included_pubkeys, signing_root, aggregate.sync_committee_signature) - # Reward sync committee participants - proposer_rewards = Gwei(0) - active_validator_count = uint64(len(get_active_validator_indices(state, get_current_epoch(state)))) - for participant_index in participant_indices: - proposer_reward = get_proposer_reward(state, participant_index) - proposer_rewards += proposer_reward - base_reward = get_base_reward(state, participant_index) - max_participant_reward = base_reward - proposer_reward - reward = Gwei(max_participant_reward * active_validator_count // (len(committee_indices) * SLOTS_PER_EPOCH)) - increase_balance(state, participant_index, reward) + # Compute the maximum sync rewards for the slot + total_active_increments = get_total_active_balance(state) // EFFECTIVE_BALANCE_INCREMENT + total_base_rewards = Gwei(get_base_reward_per_increment(state) * total_active_increments) + max_epoch_rewards = Gwei(total_base_rewards * SYNC_REWARD_WEIGHT // WEIGHT_DENOMINATOR) + max_slot_rewards = Gwei(max_epoch_rewards * len(included_indices) // len(committee_indices) // SLOTS_PER_EPOCH) - # Reward beacon proposer - increase_balance(state, get_beacon_proposer_index(state), proposer_rewards) + # Compute the participant and proposer sync rewards + committee_effective_balance = sum([state.validators[index].effective_balance for index in included_indices]) + committee_effective_balance = max(EFFECTIVE_BALANCE_INCREMENT, committee_effective_balance) + for included_index in included_indices: + effective_balance = state.validators[included_index].effective_balance + inclusion_reward = Gwei(max_slot_rewards * effective_balance // committee_effective_balance) + proposer_reward = Gwei(inclusion_reward // PROPOSER_REWARD_QUOTIENT) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) + increase_balance(state, included_index, inclusion_reward - proposer_reward) ``` ### Epoch processing @@ -635,7 +643,7 @@ def process_rewards_and_penalties(state: BeaconState) -> None: if get_current_epoch(state) == GENESIS_EPOCH: return - flag_indices_and_numerators = get_flag_indices_and_numerators() + flag_indices_and_numerators = get_flag_indices_and_weights() flag_deltas = [get_flag_index_deltas(state, index, numerator) for (index, numerator) in flag_indices_and_numerators] deltas = flag_deltas + [get_inactivity_penalty_deltas(state)] for (rewards, penalties) in deltas: diff --git a/tests/core/pyspec/eth2spec/test/altair/block_processing/test_process_sync_committee.py b/tests/core/pyspec/eth2spec/test/altair/block_processing/test_process_sync_committee.py index d24294272..370c44411 100644 --- a/tests/core/pyspec/eth2spec/test/altair/block_processing/test_process_sync_committee.py +++ b/tests/core/pyspec/eth2spec/test/altair/block_processing/test_process_sync_committee.py @@ -65,7 +65,8 @@ def get_committee_indices(spec, state, duplicates=False): @always_bls def test_invalid_signature_missing_participant(spec, state): committee = spec.get_sync_committee_indices(state, spec.get_current_epoch(state)) - random_participant = random.choice(committee) + rng = random.Random(2020) + random_participant = rng.choice(committee) yield 'pre', state @@ -88,7 +89,8 @@ def test_invalid_signature_missing_participant(spec, state): @always_bls def test_invalid_signature_extra_participant(spec, state): committee = spec.get_sync_committee_indices(state, spec.get_current_epoch(state)) - random_participant = random.choice(committee) + rng = random.Random(3030) + random_participant = rng.choice(committee) block = build_empty_block_for_next_slot(spec, state) # Exclude one signature even though the block claims the entire committee participated. @@ -105,11 +107,92 @@ def test_invalid_signature_extra_participant(spec, state): yield from run_sync_committee_processing(spec, state, block, expect_exception=True) -def compute_sync_committee_participant_reward(spec, state, participant_index, active_validator_count, committee_size): - base_reward = spec.get_base_reward(state, participant_index) - proposer_reward = spec.get_proposer_reward(state, participant_index) - max_participant_reward = base_reward - proposer_reward - return max_participant_reward * active_validator_count // committee_size // spec.SLOTS_PER_EPOCH +def compute_sync_committee_inclusion_reward(spec, state, participant_index, committee, committee_bits): + total_active_increments = spec.get_total_active_balance(state) // spec.EFFECTIVE_BALANCE_INCREMENT + total_base_rewards = spec.Gwei(spec.get_base_reward_per_increment(state) * total_active_increments) + max_epoch_rewards = spec.Gwei(total_base_rewards * spec.SYNC_REWARD_WEIGHT // spec.WEIGHT_DENOMINATOR) + included_indices = [index for index, bit in zip(committee, committee_bits) if bit] + max_slot_rewards = spec.Gwei(max_epoch_rewards * len(included_indices) // len(committee) // spec.SLOTS_PER_EPOCH) + + # Compute the participant and proposer sync rewards + committee_effective_balance = sum([state.validators[index].effective_balance for index in included_indices]) + committee_effective_balance = max(spec.EFFECTIVE_BALANCE_INCREMENT, committee_effective_balance) + effective_balance = state.validators[participant_index].effective_balance + return spec.Gwei(max_slot_rewards * effective_balance // committee_effective_balance) + + +def compute_sync_committee_participant_reward(spec, state, participant_index, committee, committee_bits): + included_indices = [index for index, bit in zip(committee, committee_bits) if bit] + multiplicities = Counter(included_indices) + + inclusion_reward = compute_sync_committee_inclusion_reward( + spec, state, participant_index, committee, committee_bits, + ) + proposer_reward = spec.Gwei(inclusion_reward // spec.PROPOSER_REWARD_QUOTIENT) + return spec.Gwei((inclusion_reward - proposer_reward) * multiplicities[participant_index]) + + +def compute_sync_committee_proposer_reward(spec, state, committee, committee_bits): + proposer_reward = 0 + for index, bit in zip(committee, committee_bits): + if not bit: + continue + inclusion_reward = compute_sync_committee_inclusion_reward( + spec, state, index, committee, committee_bits, + ) + proposer_reward += spec.Gwei(inclusion_reward // spec.PROPOSER_REWARD_QUOTIENT) + return proposer_reward + + +def validate_sync_committee_rewards(spec, pre_state, post_state, committee, committee_bits, proposer_index): + for index in range(len(post_state.validators)): + reward = 0 + if index in committee: + reward += compute_sync_committee_participant_reward( + spec, + pre_state, + index, + committee, + committee_bits, + ) + + if proposer_index == index: + reward += compute_sync_committee_proposer_reward( + spec, + pre_state, + committee, + committee_bits, + ) + + assert post_state.balances[index] == pre_state.balances[index] + reward + + +def run_successful_sync_committee_test(spec, state, committee, committee_bits): + yield 'pre', state + + pre_state = state.copy() + + block = build_empty_block_for_next_slot(spec, state) + block.body.sync_aggregate = spec.SyncAggregate( + sync_committee_bits=committee_bits, + sync_committee_signature=compute_aggregate_sync_committee_signature( + spec, + state, + block.slot - 1, + [index for index, bit in zip(committee, committee_bits) if bit], + ) + ) + + yield from run_sync_committee_processing(spec, state, block) + + validate_sync_committee_rewards( + spec, + pre_state, + state, + committee, + committee_bits, + block.proposer_index, + ) @with_all_phases_except([PHASE0, PHASE1]) @@ -118,45 +201,25 @@ def compute_sync_committee_participant_reward(spec, state, participant_index, ac def test_sync_committee_rewards_nonduplicate_committee(spec, state): committee = get_committee_indices(spec, state, duplicates=False) committee_size = len(committee) + committee_bits = [True] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) # Preconditions of this test case assert active_validator_count >= spec.SYNC_COMMITTEE_SIZE assert committee_size == len(set(committee)) - yield 'pre', state + yield from run_successful_sync_committee_test(spec, state, committee, committee_bits) - pre_balances = state.balances.copy() - block = build_empty_block_for_next_slot(spec, state) - block.body.sync_aggregate = spec.SyncAggregate( - sync_committee_bits=[True] * committee_size, - sync_committee_signature=compute_aggregate_sync_committee_signature( - spec, - state, - block.slot - 1, - committee, - ) - ) +@with_all_phases_except([PHASE0, PHASE1]) +@spec_state_test +@always_bls +def test_sync_committee_rewards_not_full_participants(spec, state): + committee = get_committee_indices(spec, state, duplicates=False) + rng = random.Random(1010) + committee_bits = [rng.choice([True, False]) for _ in committee] - yield from run_sync_committee_processing(spec, state, block) - - for index in range(len(state.validators)): - expected_reward = 0 - - if index == block.proposer_index: - expected_reward += sum([spec.get_proposer_reward(state, index) for index in committee]) - - if index in committee: - expected_reward += compute_sync_committee_participant_reward( - spec, - state, - index, - active_validator_count, - committee_size - ) - - assert state.balances[index] == pre_balances[index] + expected_reward + yield from run_successful_sync_committee_test(spec, state, committee, committee_bits) @with_all_phases_except([PHASE0, PHASE1]) @@ -165,44 +228,14 @@ def test_sync_committee_rewards_nonduplicate_committee(spec, state): def test_sync_committee_rewards_duplicate_committee(spec, state): committee = get_committee_indices(spec, state, duplicates=True) committee_size = len(committee) + committee_bits = [True] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) # Preconditions of this test case assert active_validator_count < spec.SYNC_COMMITTEE_SIZE assert committee_size > len(set(committee)) - pre_balances = state.balances.copy() - block = build_empty_block_for_next_slot(spec, state) - block.body.sync_aggregate = spec.SyncAggregate( - sync_committee_bits=[True] * committee_size, - sync_committee_signature=compute_aggregate_sync_committee_signature( - spec, - state, - block.slot - 1, - committee, - ) - ) - yield from run_sync_committee_processing(spec, state, block) - - multiplicities = Counter(committee) - - for index in range(len(state.validators)): - expected_reward = 0 - - if index == block.proposer_index: - expected_reward += sum([spec.get_proposer_reward(state, index) for index in committee]) - - if index in committee: - reward = compute_sync_committee_participant_reward( - spec, - state, - index, - active_validator_count, - committee_size, - ) - expected_reward += reward * multiplicities[index] - - assert state.balances[index] == pre_balances[index] + expected_reward + yield from run_successful_sync_committee_test(spec, state, committee, committee_bits) @with_all_phases_except([PHASE0, PHASE1]) diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index 9360392d4..f81c1fc2a 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -62,13 +62,13 @@ def run_deltas(spec, state): if is_post_altair(spec): def get_source_deltas(state): - return spec.get_flag_index_deltas(state, spec.TIMELY_SOURCE_FLAG_INDEX, spec.TIMELY_SOURCE_FLAG_NUMERATOR) + return spec.get_flag_index_deltas(state, spec.TIMELY_SOURCE_FLAG_INDEX, spec.TIMELY_SOURCE_WEIGHT) def get_head_deltas(state): - return spec.get_flag_index_deltas(state, spec.TIMELY_HEAD_FLAG_INDEX, spec.TIMELY_HEAD_FLAG_NUMERATOR) + return spec.get_flag_index_deltas(state, spec.TIMELY_HEAD_FLAG_INDEX, spec.TIMELY_HEAD_WEIGHT) def get_target_deltas(state): - return spec.get_flag_index_deltas(state, spec.TIMELY_TARGET_FLAG_INDEX, spec.TIMELY_TARGET_FLAG_NUMERATOR) + return spec.get_flag_index_deltas(state, spec.TIMELY_TARGET_FLAG_INDEX, spec.TIMELY_TARGET_WEIGHT) yield from run_attestation_component_deltas( spec, @@ -227,8 +227,8 @@ def run_get_inactivity_penalty_deltas(spec, state): base_penalty = cancel_base_rewards_per_epoch * base_reward - spec.get_proposer_reward(state, index) else: base_penalty = sum( - base_reward * numerator // spec.FLAG_DENOMINATOR - for (_, numerator) in spec.get_flag_indices_and_numerators() + base_reward * numerator // spec.WEIGHT_DENOMINATOR + for (_, numerator) in spec.get_flag_indices_and_weights() ) if not has_enough_for_reward(spec, state, index):