mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-19 21:04:41 +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:
@@ -3,7 +3,8 @@ use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttest
|
||||
use eth2::lighthouse::StandardAttestationRewards;
|
||||
use participation_cache::ParticipationCache;
|
||||
use safe_arith::SafeArith;
|
||||
use slog::{debug, Logger};
|
||||
use serde_utils::quoted_u64::Quoted;
|
||||
use slog::debug;
|
||||
use state_processing::{
|
||||
common::altair::BaseRewardPerIncrement,
|
||||
per_epoch_processing::altair::{participation_cache, rewards_and_penalties::get_flag_weight},
|
||||
@@ -15,32 +16,111 @@ use store::consts::altair::{
|
||||
};
|
||||
use types::consts::altair::WEIGHT_DENOMINATOR;
|
||||
|
||||
use types::{Epoch, EthSpec};
|
||||
use types::{BeaconState, Epoch, EthSpec};
|
||||
|
||||
use eth2::types::ValidatorId;
|
||||
use state_processing::common::base::get_base_reward_from_effective_balance;
|
||||
use state_processing::per_epoch_processing::base::rewards_and_penalties::{
|
||||
get_attestation_component_delta, get_attestation_deltas_all, get_attestation_deltas_subset,
|
||||
get_inactivity_penalty_delta, get_inclusion_delay_delta,
|
||||
};
|
||||
use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo;
|
||||
use state_processing::per_epoch_processing::base::{
|
||||
TotalBalances, ValidatorStatus, ValidatorStatuses,
|
||||
};
|
||||
|
||||
impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub fn compute_attestation_rewards(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
validators: Vec<ValidatorId>,
|
||||
log: Logger,
|
||||
) -> Result<StandardAttestationRewards, BeaconChainError> {
|
||||
debug!(log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len());
|
||||
debug!(self.log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len());
|
||||
|
||||
// Get state
|
||||
let spec = &self.spec;
|
||||
|
||||
let state_slot = (epoch + 1).end_slot(T::EthSpec::slots_per_epoch());
|
||||
|
||||
let state_root = self
|
||||
.state_root_at_slot(state_slot)?
|
||||
.ok_or(BeaconChainError::NoStateForSlot(state_slot))?;
|
||||
|
||||
let mut state = self
|
||||
let state = self
|
||||
.get_state(&state_root, Some(state_slot))?
|
||||
.ok_or(BeaconChainError::MissingBeaconState(state_root))?;
|
||||
|
||||
match state {
|
||||
BeaconState::Base(_) => self.compute_attestation_rewards_base(state, validators),
|
||||
BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => {
|
||||
self.compute_attestation_rewards_altair(state, validators)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_attestation_rewards_base(
|
||||
&self,
|
||||
mut state: BeaconState<T::EthSpec>,
|
||||
validators: Vec<ValidatorId>,
|
||||
) -> Result<StandardAttestationRewards, BeaconChainError> {
|
||||
let spec = &self.spec;
|
||||
let mut validator_statuses = ValidatorStatuses::new(&state, spec)?;
|
||||
validator_statuses.process_attestations(&state)?;
|
||||
|
||||
let ideal_rewards =
|
||||
self.compute_ideal_rewards_base(&state, &validator_statuses.total_balances)?;
|
||||
|
||||
let indices_to_attestation_delta = if validators.is_empty() {
|
||||
get_attestation_deltas_all(&state, &validator_statuses, spec)?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.collect()
|
||||
} else {
|
||||
let validator_indices = Self::validators_ids_to_indices(&mut state, validators)?;
|
||||
get_attestation_deltas_subset(&state, &validator_statuses, &validator_indices, spec)?
|
||||
};
|
||||
|
||||
let mut total_rewards = vec![];
|
||||
|
||||
for (index, delta) in indices_to_attestation_delta.into_iter() {
|
||||
let head_delta = delta.head_delta;
|
||||
let head = (head_delta.rewards as i64).safe_sub(head_delta.penalties as i64)?;
|
||||
|
||||
let target_delta = delta.target_delta;
|
||||
let target = (target_delta.rewards as i64).safe_sub(target_delta.penalties as i64)?;
|
||||
|
||||
let source_delta = delta.source_delta;
|
||||
let source = (source_delta.rewards as i64).safe_sub(source_delta.penalties as i64)?;
|
||||
|
||||
// No penalties associated with inclusion delay
|
||||
let inclusion_delay = delta.inclusion_delay_delta.rewards;
|
||||
let inactivity = delta.inactivity_penalty_delta.penalties.wrapping_neg() as i64;
|
||||
|
||||
let rewards = TotalAttestationRewards {
|
||||
validator_index: index as u64,
|
||||
head,
|
||||
target,
|
||||
source,
|
||||
inclusion_delay: Some(Quoted {
|
||||
value: inclusion_delay,
|
||||
}),
|
||||
inactivity,
|
||||
};
|
||||
|
||||
total_rewards.push(rewards);
|
||||
}
|
||||
|
||||
Ok(StandardAttestationRewards {
|
||||
ideal_rewards,
|
||||
total_rewards,
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_attestation_rewards_altair(
|
||||
&self,
|
||||
mut state: BeaconState<T::EthSpec>,
|
||||
validators: Vec<ValidatorId>,
|
||||
) -> Result<StandardAttestationRewards, BeaconChainError> {
|
||||
let spec = &self.spec;
|
||||
|
||||
// Calculate ideal_rewards
|
||||
let participation_cache = ParticipationCache::new(&state, spec)?;
|
||||
|
||||
@@ -71,7 +151,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let base_reward_per_increment =
|
||||
BaseRewardPerIncrement::new(total_active_balance, spec)?;
|
||||
|
||||
for effective_balance_eth in 0..=32 {
|
||||
for effective_balance_eth in 1..=self.max_effective_balance_increment_steps()? {
|
||||
let effective_balance =
|
||||
effective_balance_eth.safe_mul(spec.effective_balance_increment)?;
|
||||
let base_reward =
|
||||
@@ -101,20 +181,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let validators = if validators.is_empty() {
|
||||
participation_cache.eligible_validator_indices().to_vec()
|
||||
} else {
|
||||
validators
|
||||
.into_iter()
|
||||
.map(|validator| match validator {
|
||||
ValidatorId::Index(i) => Ok(i as usize),
|
||||
ValidatorId::PublicKey(pubkey) => state
|
||||
.get_validator_index(&pubkey)?
|
||||
.ok_or(BeaconChainError::ValidatorPubkeyUnknown(pubkey)),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
Self::validators_ids_to_indices(&mut state, validators)?
|
||||
};
|
||||
|
||||
for validator_index in &validators {
|
||||
let eligible = state.is_eligible_validator(previous_epoch, *validator_index)?;
|
||||
let mut head_reward = 0u64;
|
||||
let mut head_reward = 0i64;
|
||||
let mut target_reward = 0i64;
|
||||
let mut source_reward = 0i64;
|
||||
|
||||
@@ -132,7 +204,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.map_err(|_| BeaconChainError::AttestationRewardsError)?;
|
||||
if voted_correctly {
|
||||
if flag_index == TIMELY_HEAD_FLAG_INDEX {
|
||||
head_reward += ideal_reward;
|
||||
head_reward += *ideal_reward as i64;
|
||||
} else if flag_index == TIMELY_TARGET_FLAG_INDEX {
|
||||
target_reward += *ideal_reward as i64;
|
||||
} else if flag_index == TIMELY_SOURCE_FLAG_INDEX {
|
||||
@@ -152,6 +224,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
head: head_reward,
|
||||
target: target_reward,
|
||||
source: source_reward,
|
||||
inclusion_delay: None,
|
||||
// TODO: altair calculation logic needs to be updated to include inactivity penalty
|
||||
inactivity: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,6 +248,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
head: 0,
|
||||
target: 0,
|
||||
source: 0,
|
||||
inclusion_delay: None,
|
||||
// TODO: altair calculation logic needs to be updated to include inactivity penalty
|
||||
inactivity: 0,
|
||||
});
|
||||
match *flag_index {
|
||||
TIMELY_SOURCE_FLAG_INDEX => entry.source += ideal_reward,
|
||||
@@ -192,4 +270,126 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
total_rewards,
|
||||
})
|
||||
}
|
||||
|
||||
fn max_effective_balance_increment_steps(&self) -> Result<u64, BeaconChainError> {
|
||||
let spec = &self.spec;
|
||||
let max_steps = spec
|
||||
.max_effective_balance
|
||||
.safe_div(spec.effective_balance_increment)?;
|
||||
Ok(max_steps)
|
||||
}
|
||||
|
||||
fn validators_ids_to_indices(
|
||||
state: &mut BeaconState<T::EthSpec>,
|
||||
validators: Vec<ValidatorId>,
|
||||
) -> Result<Vec<usize>, BeaconChainError> {
|
||||
let indices = validators
|
||||
.into_iter()
|
||||
.map(|validator| match validator {
|
||||
ValidatorId::Index(i) => Ok(i as usize),
|
||||
ValidatorId::PublicKey(pubkey) => state
|
||||
.get_validator_index(&pubkey)?
|
||||
.ok_or(BeaconChainError::ValidatorPubkeyUnknown(pubkey)),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
fn compute_ideal_rewards_base(
|
||||
&self,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
total_balances: &TotalBalances,
|
||||
) -> Result<Vec<IdealAttestationRewards>, BeaconChainError> {
|
||||
let spec = &self.spec;
|
||||
let previous_epoch = state.previous_epoch();
|
||||
let finality_delay = previous_epoch
|
||||
.safe_sub(state.finalized_checkpoint().epoch)?
|
||||
.as_u64();
|
||||
|
||||
let ideal_validator_status = ValidatorStatus {
|
||||
is_previous_epoch_attester: true,
|
||||
is_slashed: false,
|
||||
inclusion_info: Some(InclusionInfo {
|
||||
delay: 1,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut ideal_attestation_rewards_list = Vec::new();
|
||||
|
||||
for effective_balance_step in 1..=self.max_effective_balance_increment_steps()? {
|
||||
let effective_balance =
|
||||
effective_balance_step.safe_mul(spec.effective_balance_increment)?;
|
||||
let base_reward = get_base_reward_from_effective_balance::<T::EthSpec>(
|
||||
effective_balance,
|
||||
total_balances.current_epoch(),
|
||||
spec,
|
||||
)?;
|
||||
|
||||
// compute ideal head rewards
|
||||
let head = get_attestation_component_delta(
|
||||
true,
|
||||
total_balances.previous_epoch_attesters(),
|
||||
total_balances,
|
||||
base_reward,
|
||||
finality_delay,
|
||||
spec,
|
||||
)?
|
||||
.rewards;
|
||||
|
||||
// compute ideal target rewards
|
||||
let target = get_attestation_component_delta(
|
||||
true,
|
||||
total_balances.previous_epoch_target_attesters(),
|
||||
total_balances,
|
||||
base_reward,
|
||||
finality_delay,
|
||||
spec,
|
||||
)?
|
||||
.rewards;
|
||||
|
||||
// compute ideal source rewards
|
||||
let source = get_attestation_component_delta(
|
||||
true,
|
||||
total_balances.previous_epoch_head_attesters(),
|
||||
total_balances,
|
||||
base_reward,
|
||||
finality_delay,
|
||||
spec,
|
||||
)?
|
||||
.rewards;
|
||||
|
||||
// compute ideal inclusion delay rewards
|
||||
let inclusion_delay =
|
||||
get_inclusion_delay_delta(&ideal_validator_status, base_reward, spec)?
|
||||
.0
|
||||
.rewards;
|
||||
|
||||
// compute inactivity penalty
|
||||
let inactivity = get_inactivity_penalty_delta(
|
||||
&ideal_validator_status,
|
||||
base_reward,
|
||||
finality_delay,
|
||||
spec,
|
||||
)?
|
||||
.penalties
|
||||
.wrapping_neg() as i64;
|
||||
|
||||
let ideal_attestation_rewards = IdealAttestationRewards {
|
||||
effective_balance,
|
||||
head,
|
||||
target,
|
||||
source,
|
||||
inclusion_delay: Some(Quoted {
|
||||
value: inclusion_delay,
|
||||
}),
|
||||
inactivity,
|
||||
};
|
||||
|
||||
ideal_attestation_rewards_list.push(ideal_attestation_rewards);
|
||||
}
|
||||
|
||||
Ok(ideal_attestation_rewards_list)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use state_processing::{
|
||||
},
|
||||
signature_sets::Error as SignatureSetError,
|
||||
state_advance::Error as StateAdvanceError,
|
||||
BlockProcessingError, BlockReplayError, SlotProcessingError,
|
||||
BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use task_executor::ShutdownReason;
|
||||
@@ -60,6 +60,7 @@ pub enum BeaconChainError {
|
||||
MissingBeaconBlock(Hash256),
|
||||
MissingBeaconState(Hash256),
|
||||
SlotProcessingError(SlotProcessingError),
|
||||
EpochProcessingError(EpochProcessingError),
|
||||
StateAdvanceError(StateAdvanceError),
|
||||
UnableToAdvanceState(String),
|
||||
NoStateForAttestation {
|
||||
@@ -217,6 +218,7 @@ pub enum BeaconChainError {
|
||||
}
|
||||
|
||||
easy_from_to!(SlotProcessingError, BeaconChainError);
|
||||
easy_from_to!(EpochProcessingError, BeaconChainError);
|
||||
easy_from_to!(AttestationValidationError, BeaconChainError);
|
||||
easy_from_to!(SyncCommitteeMessageValidationError, BeaconChainError);
|
||||
easy_from_to!(ExitValidationError, BeaconChainError);
|
||||
|
||||
Reference in New Issue
Block a user