diff --git a/specs/lightclient/beacon-chain.md b/specs/lightclient/beacon-chain.md index 4fff3a4f1..afcb3a36d 100644 --- a/specs/lightclient/beacon-chain.md +++ b/specs/lightclient/beacon-chain.md @@ -49,7 +49,8 @@ This is a patch implementing the first hard fork to the beacon chain, tentatively named HF1 pending a permanent name. It has three main features: * Light client support via sync committees -* Incentive accounting reforms, reducing spec complexity and reducing the cost of processing chains that have very little or zero participation for a long span of epochs +* Incentive accounting reforms, reducing spec complexity + and [TODO] reducing the cost of processing chains that have very little or zero participation for a long span of epochs * Fork choice rule changes to address weaknesses recently discovered in the existing fork choice ## Constants @@ -78,9 +79,7 @@ The reward fractions add up to 7/8, leaving the remaining 1/8 for proposer rewar | Name | Value | | - | - | | `PARTICIPATION_FLAGS_LENGTH` | `8` | -| `FLAGS_AND_NUMERATORS` | `((TIMELY_HEAD_FLAG, TIMELY_HEAD_NUMERATOR), (TIMELY_SOURCE_FLAG, TIMELY_SOURCE_NUMERATOR), (TIMELY_TARGET_FLAG, TIMELY_TARGET_NUMERATOR))` | | `G2_POINT_AT_INFINITY` | `BLSSignature(b'\xc0' + b'\x00' * 95)` | -| `GWEI_PER_ETH` | `10**9` | ## Configuration @@ -155,8 +154,6 @@ class BeaconState(Container): # Light client sync committees current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee - # Denominator to real-time-updated balances (NOT effective balances!) - balance_denominator: uint64 ``` ### New containers @@ -173,23 +170,6 @@ class SyncCommittee(Container): ### `Predicates` -#### `get_base_reward` - -*Note*: The function `get_base_reward` is modified with the removal of `BASE_REWARDS_PER_EPOCH`. Additionally, it is split into `get_base_reward_per_eth` to allow penalties to be computed to the denominator. - -```python -def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: - return get_base_reward_per_eth(state) * state.validators[index].effective_balance // GWEI_PER_ETH -``` - -#### `get_base_reward_per_eth` - -```python -def get_base_reward_per_eth(state: BeaconState) -> Gwei: - total_balance = get_total_active_balance(state) - return Gwei(GWEI_PER_ETH * BASE_REWARD_FACTOR // integer_squareroot(total_balance)) -``` - #### `eth2_fast_aggregate_verify` ```python @@ -202,6 +182,21 @@ def eth2_fast_aggregate_verify(pubkeys: Sequence[BLSPubkey], message: Bytes32, s return bls.FastAggregateVerify(pubkeys, message, signature) ``` +### Misc + +#### `flags_and_numerators` + +```python +def get_flags_and_numerators() -> Sequence[Tuple[int, int]]: + return ( + (TIMELY_HEAD_FLAG, TIMELY_HEAD_NUMERATOR), + (TIMELY_SOURCE_FLAG, TIMELY_SOURCE_NUMERATOR), + (TIMELY_TARGET_FLAG, TIMELY_TARGET_NUMERATOR) + ) +``` + + + ### Beacon state accessors #### `get_sync_committee_indices` @@ -246,6 +241,17 @@ def get_sync_committee(state: BeaconState, epoch: Epoch) -> SyncCommittee: return SyncCommittee(pubkeys=pubkeys, pubkey_aggregates=aggregates) ``` +#### `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)) +``` + #### `get_unslashed_participating_indices` ```python @@ -255,20 +261,24 @@ def get_unslashed_participating_indices(state: BeaconState, flag: uint8, epoch: epoch_participation = state.current_epoch_participation else: epoch_participation = state.previous_epoch_participation - participating_indices = [index for index in get_active_validator_indices(state, epoch) - if epoch_participation[index][flag]] + participating_indices = [ + index for index in get_active_validator_indices(state, epoch) + if epoch_participation[index][flag] + ] return set(filter(lambda index: not state.validators[index].slashed, participating_indices)) ``` #### `get_flag_deltas` ```python -def get_flag_deltas(state: BeaconState, flag: uint8, numerator: uint64) -> Tuple[Sequence[Gwei], Gwei]: +def get_flag_deltas(state: BeaconState, flag: uint8, numerator: uint64) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: """ Computes the rewards and penalties associated with a particular duty, by scanning through the participation flags to determine who participated and who did not and assigning them the appropriate rewards and penalties. """ rewards = [Gwei(0)] * len(state.validators) + penalties = [Gwei(0)] * len(state.validators) + unslashed_participating_indices = get_unslashed_participating_indices(state, flag, get_previous_epoch(state)) increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balances to avoid uint64 overflow unslashed_participating_increments = get_total_balance(state, unslashed_participating_indices) // increment @@ -281,10 +291,12 @@ def get_flag_deltas(state: BeaconState, flag: uint8, numerator: uint64) -> Tuple rewards[index] = base_reward * numerator // REWARD_DENOMINATOR else: rewards[index] = ( - (base_reward * unslashed_participating_increments // active_increments + base_reward) - * numerator // REWARD_DENOMINATOR + (base_reward * numerator * unslashed_participating_increments) + // (active_increments * REWARD_DENOMINATOR) ) - return rewards, get_base_reward_per_eth(state) * numerator // REWARD_DENOMINATOR + else: + penalties[index] = base_reward * numerator // REWARD_DENOMINATOR + return rewards, penalties ``` ##### `get_inactivity_penalty_deltas` @@ -298,17 +310,21 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S flags to determine who participated and who did not, applying the leak penalty globally and applying compensatory rewards to participants. """ - rewards = [Gwei(0) for _ in range(len(state.validators))] + penalties = [Gwei(0) for _ in range(len(state.validators))] if is_in_inactivity_leak(state): - reward_numerator_sum = sum(numerator for (_, numerator) in FLAGS_AND_NUMERATORS) + reward_numerator_sum = sum(numerator for (_, numerator) in get_flags_and_numerators()) matching_target_attesting_indices = get_unslashed_participating_indices( state, TIMELY_TARGET_FLAG, get_previous_epoch(state) ) for index in get_eligible_validator_indices(state): - if index in matching_target_attesting_indices: + # If validator is performing optimally this cancels all attestation rewards for a neutral balance + penalties[index] += Gwei(get_base_reward(state, index) * reward_numerator_sum // REWARD_DENOMINATOR) + if index not in matching_target_attesting_indices: effective_balance = state.validators[index].effective_balance - rewards[index] += Gwei(effective_balance * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT) - return rewards, Gwei(GWEI_PER_ETH * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT) + penalties[index] += Gwei(effective_balance * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT) + + rewards = [Gwei(0) for _ in range(len(state.validators))] + return rewards, penalties ``` ### Block processing @@ -334,19 +350,26 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: assert data.target.epoch == compute_epoch_at_slot(data.slot) assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot <= data.slot + SLOTS_PER_EPOCH assert data.index < get_committee_count_per_slot(state, data.target.epoch) + committee = get_beacon_committee(state, data.slot, data.index) assert len(attestation.aggregation_bits) == len(committee) + if data.target.epoch == get_current_epoch(state): epoch_participation = state.current_epoch_participation justified_checkpoint = state.current_justified_checkpoint else: epoch_participation = state.previous_epoch_participation justified_checkpoint = state.previous_justified_checkpoint + # Matching roots is_matching_head = data.beacon_block_root == get_block_root_at_slot(state, data.slot) is_matching_source = data.source == justified_checkpoint is_matching_target = data.target.root == get_block_root(state, data.target.epoch) assert is_matching_source + + # Verify signature + assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) + # Participation flags participation_flags = [] if is_matching_head and state.slot <= data.slot + MIN_ATTESTATION_INCLUSION_DELAY: @@ -355,18 +378,18 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: participation_flags.append(TIMELY_SOURCE_FLAG) if is_matching_target and state.slot <= data.slot + SLOTS_PER_EPOCH: participation_flags.append(TIMELY_TARGET_FLAG) + # Update epoch participation flags proposer_reward_numerator = 0 for index in get_attesting_indices(state, data, attestation.aggregation_bits): - for flag, numerator in FLAGS_AND_NUMERATORS: + for flag, numerator in get_flags_and_numerators(): if flag in participation_flags and not epoch_participation[index][flag]: epoch_participation[index][flag] = True proposer_reward_numerator += get_base_reward(state, index) * numerator + # Reward proposer proposer_reward = Gwei(proposer_reward_numerator // (REWARD_DENOMINATOR * PROPOSER_REWARD_QUOTIENT)) increase_balance(state, get_beacon_proposer_index(state), proposer_reward) - # Verify signature - assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) ``` @@ -406,6 +429,7 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None: # Add validator and balance entries state.validators.append(get_validator_from_deposit(state, deposit)) state.balances.append(amount) + # [Added in hf-1] Initialize empty participation flags for new validator state.previous_epoch_participation.append(Bitvector[PARTICIPATION_FLAGS_LENGTH]()) state.current_epoch_participation.append(Bitvector[PARTICIPATION_FLAGS_LENGTH]()) else: @@ -458,6 +482,7 @@ def process_justification_and_finalization(state: BeaconState) -> None: current_epoch = get_current_epoch(state) old_previous_justified_checkpoint = state.previous_justified_checkpoint old_current_justified_checkpoint = state.current_justified_checkpoint + # Process justifications state.previous_justified_checkpoint = state.current_justified_checkpoint state.justification_bits[1:] = state.justification_bits[:JUSTIFICATION_BITS_LENGTH - 1] @@ -472,6 +497,7 @@ def process_justification_and_finalization(state: BeaconState) -> None: state.current_justified_checkpoint = Checkpoint(epoch=current_epoch, root=get_block_root(state, current_epoch)) state.justification_bits[0] = 0b1 + # Process finalizations bits = state.justification_bits # The 2nd/3rd/4th most recent epochs are justified, the 2nd using the 4th as source @@ -497,22 +523,17 @@ def process_rewards_and_penalties(state: BeaconState) -> None: # No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch if get_current_epoch(state) == GENESIS_EPOCH: return - flag_deltas = [get_flag_deltas(state, flag, numerator) for (flag, numerator) in FLAGS_AND_NUMERATORS] + flag_deltas = [get_flag_deltas(state, flag, numerator) for (flag, numerator) in get_flags_and_numerators()] deltas = flag_deltas + [get_inactivity_penalty_deltas(state)] - for (rewards, penalty) in deltas: + for (rewards, penalties) in deltas: for index in range(len(state.validators)): increase_balance(state, ValidatorIndex(index), rewards[index]) - # Bounds-friendly expansion for `denom *= (1 + penalty // GWEI_PER_ETH)` - state.balance_denominator = ( - state.balance_denominator + - penalty + - (state.balance_denominator - GWEI_PER_ETH) * penalty // GWEI_PER_ETH - ) + decrease_balance(state, ValidatorIndex(index), penalties[index]) ``` #### Final updates -*Note*: The function `process_final_updates` is modified to handle sync committee updates, replacement of `PendingAttestation`s with participation flags, and a sliding-denominator-aware hysteresis mechanism. +*Note*: The function `process_final_updates` is modified to handle sync committee updates and with the replacement of `PendingAttestation`s with participation flags. ```python def process_final_updates(state: BeaconState) -> None: @@ -521,23 +542,21 @@ def process_final_updates(state: BeaconState) -> None: # Reset eth1 data votes if next_epoch % EPOCHS_PER_ETH1_VOTING_PERIOD == 0: state.eth1_data_votes = [] - # Update sync committees + # [Added in hf-1] Update sync committees if next_epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD == 0: state.current_sync_committee = state.next_sync_committee state.next_sync_committee = get_sync_committee(state, next_epoch + EPOCHS_PER_SYNC_COMMITTEE_PERIOD) # Update effective balances with hysteresis for index, validator in enumerate(state.validators): - balance_increments = state.balances[index] * HYSTERESIS_QUOTIENT // state.balance_denominator - effective_increments = validator.effective_balance // GWEI_PER_ETH * HYSTERESIS_QUOTIENT + balance = state.balances[index] + HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) + DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER + UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER if ( - balance_increments + HYSTERESIS_DOWNWARD_MULTIPLIER < effective_increments - or effective_increments + HYSTERESIS_UPWARD_MULTIPLIER < balance_increments - or get_current_epoch(state) == validator.withdrawable_epoch + balance + DOWNWARD_THRESHOLD < validator.effective_balance + or validator.effective_balance + UPWARD_THRESHOLD < balance ): - validator.effective_balance = (state.balances[index] // state.balance_denominator) * GWEI_PER_ETH - if state.balance_denominator >= 2 * GWEI_PER_ETH: - state.balances = [x // 2 for x in state.balances] - state.balance_denominator //= 2 + validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) # Reset slashings state.slashings[next_epoch % EPOCHS_PER_SLASHINGS_VECTOR] = Gwei(0) # Set randao mix @@ -546,6 +565,8 @@ def process_final_updates(state: BeaconState) -> None: if next_epoch % (SLOTS_PER_HISTORICAL_ROOT // SLOTS_PER_EPOCH) == 0: historical_batch = HistoricalBatch(block_roots=state.block_roots, state_roots=state.state_roots) state.historical_roots.append(hash_tree_root(historical_batch)) - # Rotate current/previous epoch participation flags + # [Added in hf-1] Rotate current/previous epoch participation flags state.previous_epoch_participation = state.current_epoch_participation state.current_epoch_participation = [Bitvector[PARTICIPATION_FLAGS_LENGTH]() for _ in range(len(state.validators))] + # [Removed in hf-1] Rotate current/previous epoch attestations +``` diff --git a/specs/lightclient/lightclient-fork.md b/specs/lightclient/lightclient-fork.md index 8e2791d4b..a10e8c5f6 100644 --- a/specs/lightclient/lightclient-fork.md +++ b/specs/lightclient/lightclient-fork.md @@ -74,7 +74,6 @@ def upgrade_to_lightclient_patch(pre: phase0.BeaconState) -> BeaconState: previous_justified_checkpoint=pre.previous_justified_checkpoint, current_justified_checkpoint=pre.current_justified_checkpoint, finalized_checkpoint=pre.finalized_checkpoint, - balance_denominator=GWEI_PER_ETH, ) # Fill in sync committees post.current_sync_committee = get_sync_committee(post, get_current_epoch(post))