mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-09 03:31:45 +00:00
Phase 0 attestation rewards via Beacon API (#4474)
## Issue Addressed Addresses #4026. Beacon-API spec [here](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getAttestationsRewards). Endpoint: `POST /eth/v1/beacon/rewards/attestations/{epoch}` This endpoint already supports post-Altair epochs. This PR adds support for phase 0 rewards calculation. ## Proposed Changes - [x] Attestation rewards API to support phase 0 rewards calculation, re-using logic from `state_processing`. Refactored `get_attestation_deltas` slightly to support computing deltas for a subset of validators. - [x] Add `inclusion_delay` to `ideal_rewards` (`beacon-API` spec update to follow) - [x] Add `inactivity` penalties to both `ideal_rewards` and `total_rewards` (`beacon-API` spec update to follow) - [x] Add tests to compute attestation rewards and compare results with beacon states ## Additional Notes - The extra penalty for missing attestations or being slashed during an inactivity leak is currently not included in the API response (for both phase 0 and Altair) in the spec. - I went with adding `inactivity` as a separate component rather than combining them with the 4 rewards, because this is how it was grouped in [the phase 0 spec](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_attestation_deltas). During inactivity leak, all rewards include the optimal reward, and inactivity penalties are calculated separately (see below code snippet from the spec), so it would be quite confusing if we merge them. This would also work better with Altair, because there's no "cancelling" of rewards and inactivity penalties are more separate. - Altair calculation logic (to include inactivity penalties) to be updated in a follow-up PR. ```python def get_attestation_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: """ Return attestation reward/penalty deltas for each validator. """ source_rewards, source_penalties = get_source_deltas(state) target_rewards, target_penalties = get_target_deltas(state) head_rewards, head_penalties = get_head_deltas(state) inclusion_delay_rewards, _ = get_inclusion_delay_deltas(state) _, inactivity_penalties = get_inactivity_penalty_deltas(state) rewards = [ source_rewards[i] + target_rewards[i] + head_rewards[i] + inclusion_delay_rewards[i] for i in range(len(state.validators)) ] penalties = [ source_penalties[i] + target_penalties[i] + head_penalties[i] + inactivity_penalties[i] for i in range(len(state.validators)) ] return rewards, penalties ``` ## Example API Response <details> <summary>Click me</summary> ```json { "ideal_rewards": [ { "effective_balance": "1000000000", "head": "6638", "target": "6638", "source": "6638", "inclusion_delay": "9783", "inactivity": "0" }, { "effective_balance": "2000000000", "head": "13276", "target": "13276", "source": "13276", "inclusion_delay": "19565", "inactivity": "0" }, { "effective_balance": "3000000000", "head": "19914", "target": "19914", "source": "19914", "inclusion_delay": "29349", "inactivity": "0" }, { "effective_balance": "4000000000", "head": "26553", "target": "26553", "source": "26553", "inclusion_delay": "39131", "inactivity": "0" }, { "effective_balance": "5000000000", "head": "33191", "target": "33191", "source": "33191", "inclusion_delay": "48914", "inactivity": "0" }, { "effective_balance": "6000000000", "head": "39829", "target": "39829", "source": "39829", "inclusion_delay": "58697", "inactivity": "0" }, { "effective_balance": "7000000000", "head": "46468", "target": "46468", "source": "46468", "inclusion_delay": "68480", "inactivity": "0" }, { "effective_balance": "8000000000", "head": "53106", "target": "53106", "source": "53106", "inclusion_delay": "78262", "inactivity": "0" }, { "effective_balance": "9000000000", "head": "59744", "target": "59744", "source": "59744", "inclusion_delay": "88046", "inactivity": "0" }, { "effective_balance": "10000000000", "head": "66383", "target": "66383", "source": "66383", "inclusion_delay": "97828", "inactivity": "0" }, { "effective_balance": "11000000000", "head": "73021", "target": "73021", "source": "73021", "inclusion_delay": "107611", "inactivity": "0" }, { "effective_balance": "12000000000", "head": "79659", "target": "79659", "source": "79659", "inclusion_delay": "117394", "inactivity": "0" }, { "effective_balance": "13000000000", "head": "86298", "target": "86298", "source": "86298", "inclusion_delay": "127176", "inactivity": "0" }, { "effective_balance": "14000000000", "head": "92936", "target": "92936", "source": "92936", "inclusion_delay": "136959", "inactivity": "0" }, { "effective_balance": "15000000000", "head": "99574", "target": "99574", "source": "99574", "inclusion_delay": "146742", "inactivity": "0" }, { "effective_balance": "16000000000", "head": "106212", "target": "106212", "source": "106212", "inclusion_delay": "156525", "inactivity": "0" }, { "effective_balance": "17000000000", "head": "112851", "target": "112851", "source": "112851", "inclusion_delay": "166307", "inactivity": "0" }, { "effective_balance": "18000000000", "head": "119489", "target": "119489", "source": "119489", "inclusion_delay": "176091", "inactivity": "0" }, { "effective_balance": "19000000000", "head": "126127", "target": "126127", "source": "126127", "inclusion_delay": "185873", "inactivity": "0" }, { "effective_balance": "20000000000", "head": "132766", "target": "132766", "source": "132766", "inclusion_delay": "195656", "inactivity": "0" }, { "effective_balance": "21000000000", "head": "139404", "target": "139404", "source": "139404", "inclusion_delay": "205439", "inactivity": "0" }, { "effective_balance": "22000000000", "head": "146042", "target": "146042", "source": "146042", "inclusion_delay": "215222", "inactivity": "0" }, { "effective_balance": "23000000000", "head": "152681", "target": "152681", "source": "152681", "inclusion_delay": "225004", "inactivity": "0" }, { "effective_balance": "24000000000", "head": "159319", "target": "159319", "source": "159319", "inclusion_delay": "234787", "inactivity": "0" }, { "effective_balance": "25000000000", "head": "165957", "target": "165957", "source": "165957", "inclusion_delay": "244570", "inactivity": "0" }, { "effective_balance": "26000000000", "head": "172596", "target": "172596", "source": "172596", "inclusion_delay": "254352", "inactivity": "0" }, { "effective_balance": "27000000000", "head": "179234", "target": "179234", "source": "179234", "inclusion_delay": "264136", "inactivity": "0" }, { "effective_balance": "28000000000", "head": "185872", "target": "185872", "source": "185872", "inclusion_delay": "273918", "inactivity": "0" }, { "effective_balance": "29000000000", "head": "192510", "target": "192510", "source": "192510", "inclusion_delay": "283701", "inactivity": "0" }, { "effective_balance": "30000000000", "head": "199149", "target": "199149", "source": "199149", "inclusion_delay": "293484", "inactivity": "0" }, { "effective_balance": "31000000000", "head": "205787", "target": "205787", "source": "205787", "inclusion_delay": "303267", "inactivity": "0" }, { "effective_balance": "32000000000", "head": "212426", "target": "212426", "source": "212426", "inclusion_delay": "313050", "inactivity": "0" } ], "total_rewards": [ { "validator_index": "0", "head": "212426", "target": "212426", "source": "212426", "inclusion_delay": "313050", "inactivity": "0" }, { "validator_index": "32", "head": "212426", "target": "212426", "source": "212426", "inclusion_delay": "313050", "inactivity": "0" }, { "validator_index": "63", "head": "-357771", "target": "-357771", "source": "-357771", "inclusion_delay": "0", "inactivity": "0" } ] } ``` </details>
This commit is contained in:
@@ -17,3 +17,15 @@ pub fn get_base_reward<T: EthSpec>(
|
||||
.safe_div(spec.base_rewards_per_epoch)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_base_reward_from_effective_balance<T: EthSpec>(
|
||||
effective_balance: u64,
|
||||
total_active_balance: u64,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<u64, BeaconStateError> {
|
||||
effective_balance
|
||||
.safe_mul(spec.base_reward_factor)?
|
||||
.safe_div(total_active_balance.integer_sqrt())?
|
||||
.safe_div(spec.base_rewards_per_epoch)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn process_epoch<T: EthSpec>(
|
||||
justification_and_finalization_state.apply_changes_to_state(state);
|
||||
|
||||
// Rewards and Penalties.
|
||||
process_rewards_and_penalties(state, &mut validator_statuses, spec)?;
|
||||
process_rewards_and_penalties(state, &validator_statuses, spec)?;
|
||||
|
||||
// Registry Updates.
|
||||
process_registry_updates(state, spec)?;
|
||||
|
||||
@@ -45,7 +45,7 @@ impl AttestationDelta {
|
||||
/// Apply attester and proposer rewards.
|
||||
pub fn process_rewards_and_penalties<T: EthSpec>(
|
||||
state: &mut BeaconState<T>,
|
||||
validator_statuses: &mut ValidatorStatuses,
|
||||
validator_statuses: &ValidatorStatuses,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), Error> {
|
||||
if state.current_epoch() == T::genesis_epoch() {
|
||||
@@ -59,7 +59,7 @@ pub fn process_rewards_and_penalties<T: EthSpec>(
|
||||
return Err(Error::ValidatorStatusesInconsistent);
|
||||
}
|
||||
|
||||
let deltas = get_attestation_deltas(state, validator_statuses, spec)?;
|
||||
let deltas = get_attestation_deltas_all(state, validator_statuses, spec)?;
|
||||
|
||||
// Apply the deltas, erroring on overflow above but not on overflow below (saturating at 0
|
||||
// instead).
|
||||
@@ -73,10 +73,41 @@ pub fn process_rewards_and_penalties<T: EthSpec>(
|
||||
}
|
||||
|
||||
/// Apply rewards for participation in attestations during the previous epoch.
|
||||
pub fn get_attestation_deltas<T: EthSpec>(
|
||||
pub fn get_attestation_deltas_all<T: EthSpec>(
|
||||
state: &BeaconState<T>,
|
||||
validator_statuses: &ValidatorStatuses,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Vec<AttestationDelta>, Error> {
|
||||
get_attestation_deltas(state, validator_statuses, None, spec)
|
||||
}
|
||||
|
||||
/// Apply rewards for participation in attestations during the previous epoch, and only compute
|
||||
/// rewards for a subset of validators.
|
||||
pub fn get_attestation_deltas_subset<T: EthSpec>(
|
||||
state: &BeaconState<T>,
|
||||
validator_statuses: &ValidatorStatuses,
|
||||
validators_subset: &Vec<usize>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Vec<(usize, AttestationDelta)>, Error> {
|
||||
get_attestation_deltas(state, validator_statuses, Some(validators_subset), spec).map(|deltas| {
|
||||
deltas
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| validators_subset.contains(index))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply rewards for participation in attestations during the previous epoch.
|
||||
/// If `maybe_validators_subset` specified, only the deltas for the specified validator subset is
|
||||
/// returned, otherwise deltas for all validators are returned.
|
||||
///
|
||||
/// Returns a vec of validator indices to `AttestationDelta`.
|
||||
fn get_attestation_deltas<T: EthSpec>(
|
||||
state: &BeaconState<T>,
|
||||
validator_statuses: &ValidatorStatuses,
|
||||
maybe_validators_subset: Option<&Vec<usize>>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Vec<AttestationDelta>, Error> {
|
||||
let previous_epoch = state.previous_epoch();
|
||||
let finality_delay = state
|
||||
@@ -88,6 +119,13 @@ pub fn get_attestation_deltas<T: EthSpec>(
|
||||
|
||||
let total_balances = &validator_statuses.total_balances;
|
||||
|
||||
// Ignore validator if a subset is specified and validator is not in the subset
|
||||
let include_validator_delta = |idx| match maybe_validators_subset.as_ref() {
|
||||
None => true,
|
||||
Some(validators_subset) if validators_subset.contains(&idx) => true,
|
||||
Some(_) => false,
|
||||
};
|
||||
|
||||
for (index, validator) in validator_statuses.statuses.iter().enumerate() {
|
||||
// Ignore ineligible validators. All sub-functions of the spec do this except for
|
||||
// `get_inclusion_delay_deltas`. It's safe to do so here because any validator that is in
|
||||
@@ -99,41 +137,46 @@ pub fn get_attestation_deltas<T: EthSpec>(
|
||||
|
||||
let base_reward = get_base_reward(state, index, total_balances.current_epoch(), spec)?;
|
||||
|
||||
let source_delta =
|
||||
get_source_delta(validator, base_reward, total_balances, finality_delay, spec)?;
|
||||
let target_delta =
|
||||
get_target_delta(validator, base_reward, total_balances, finality_delay, spec)?;
|
||||
let head_delta =
|
||||
get_head_delta(validator, base_reward, total_balances, finality_delay, spec)?;
|
||||
let (inclusion_delay_delta, proposer_delta) =
|
||||
get_inclusion_delay_delta(validator, base_reward, spec)?;
|
||||
let inactivity_penalty_delta =
|
||||
get_inactivity_penalty_delta(validator, base_reward, finality_delay, spec)?;
|
||||
|
||||
let delta = deltas
|
||||
.get_mut(index)
|
||||
.ok_or(Error::DeltaOutOfBounds(index))?;
|
||||
delta.source_delta.combine(source_delta)?;
|
||||
delta.target_delta.combine(target_delta)?;
|
||||
delta.head_delta.combine(head_delta)?;
|
||||
delta.inclusion_delay_delta.combine(inclusion_delay_delta)?;
|
||||
delta
|
||||
.inactivity_penalty_delta
|
||||
.combine(inactivity_penalty_delta)?;
|
||||
if include_validator_delta(index) {
|
||||
let source_delta =
|
||||
get_source_delta(validator, base_reward, total_balances, finality_delay, spec)?;
|
||||
let target_delta =
|
||||
get_target_delta(validator, base_reward, total_balances, finality_delay, spec)?;
|
||||
let head_delta =
|
||||
get_head_delta(validator, base_reward, total_balances, finality_delay, spec)?;
|
||||
let inactivity_penalty_delta =
|
||||
get_inactivity_penalty_delta(validator, base_reward, finality_delay, spec)?;
|
||||
|
||||
let delta = deltas
|
||||
.get_mut(index)
|
||||
.ok_or(Error::DeltaOutOfBounds(index))?;
|
||||
delta.source_delta.combine(source_delta)?;
|
||||
delta.target_delta.combine(target_delta)?;
|
||||
delta.head_delta.combine(head_delta)?;
|
||||
delta.inclusion_delay_delta.combine(inclusion_delay_delta)?;
|
||||
delta
|
||||
.inactivity_penalty_delta
|
||||
.combine(inactivity_penalty_delta)?;
|
||||
}
|
||||
|
||||
if let Some((proposer_index, proposer_delta)) = proposer_delta {
|
||||
deltas
|
||||
.get_mut(proposer_index)
|
||||
.ok_or(Error::ValidatorStatusesInconsistent)?
|
||||
.inclusion_delay_delta
|
||||
.combine(proposer_delta)?;
|
||||
if include_validator_delta(proposer_index) {
|
||||
deltas
|
||||
.get_mut(proposer_index)
|
||||
.ok_or(Error::ValidatorStatusesInconsistent)?
|
||||
.inclusion_delay_delta
|
||||
.combine(proposer_delta)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(deltas)
|
||||
}
|
||||
|
||||
fn get_attestation_component_delta(
|
||||
pub fn get_attestation_component_delta(
|
||||
index_in_unslashed_attesting_indices: bool,
|
||||
attesting_balance: u64,
|
||||
total_balances: &TotalBalances,
|
||||
@@ -216,7 +259,7 @@ fn get_head_delta(
|
||||
)
|
||||
}
|
||||
|
||||
fn get_inclusion_delay_delta(
|
||||
pub fn get_inclusion_delay_delta(
|
||||
validator: &ValidatorStatus,
|
||||
base_reward: u64,
|
||||
spec: &ChainSpec,
|
||||
@@ -242,7 +285,7 @@ fn get_inclusion_delay_delta(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_inactivity_penalty_delta(
|
||||
pub fn get_inactivity_penalty_delta(
|
||||
validator: &ValidatorStatus,
|
||||
base_reward: u64,
|
||||
finality_delay: u64,
|
||||
|
||||
Reference in New Issue
Block a user