Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement
attestation_rewards
API (per-validator reward) (#3822)
## Issue Addressed #3661 ## Proposed Changes `/eth/v1/beacon/rewards/attestations/{epoch}` ```json { "execution_optimistic": false, "finalized": false, "data": [ { "ideal_rewards": [ { "effective_balance": "1000000000", "head": "2500", "target": "5000", "source": "5000" } ], "total_rewards": [ { "validator_index": "0", "head": "2000", "target": "2000", "source": "4000", "inclusion_delay": "2000" } ] } ] } ``` The issue contains the implementation of three per-validator reward APIs: - [`sync_committee_rewards`](#3790) - `attestation_rewards` - `block_rewards`. This PR *only* implements the `attestation_rewards`. The endpoints can be viewed in the Ethereum Beacon nodes API browser: https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Rewards ## Additional Info The implementation of [consensus client reward APIs](https://github.com/eth-protocol-fellows/cohort-three/blob/master/projects/project-ideas.md#consensus-client-reward-apis) is part of the [EPF](https://github.com/eth-protocol-fellows/cohort-three). --- - [x] `get_state` - [x] Calculate *ideal rewards* with some logic from `get_flag_index_deltas` - [x] Calculate *actual rewards* with some logic from `get_flag_index_deltas` - [x] Code cleanup - [x] Testing
- Loading branch information
1 parent
0866b73
commit 6476025
Showing
7 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; | ||
use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttestationRewards}; | ||
use eth2::lighthouse::StandardAttestationRewards; | ||
use participation_cache::ParticipationCache; | ||
use safe_arith::SafeArith; | ||
use slog::{debug, Logger}; | ||
use state_processing::{ | ||
common::altair::BaseRewardPerIncrement, | ||
per_epoch_processing::altair::{participation_cache, rewards_and_penalties::get_flag_weight}, | ||
}; | ||
use std::collections::HashMap; | ||
use store::consts::altair::{ | ||
PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, | ||
TIMELY_TARGET_FLAG_INDEX, | ||
}; | ||
use types::consts::altair::WEIGHT_DENOMINATOR; | ||
|
||
use types::{Epoch, EthSpec}; | ||
|
||
use eth2::types::ValidatorId; | ||
|
||
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()); | ||
|
||
// 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::UnableToFindTargetRoot(state_slot))?; | ||
|
||
let mut state = self | ||
.get_state(&state_root, Some(state_slot))? | ||
.ok_or(BeaconChainError::MissingBeaconState(state_root))?; | ||
|
||
// Calculate ideal_rewards | ||
let participation_cache = ParticipationCache::new(&state, spec)?; | ||
|
||
let previous_epoch = state.previous_epoch(); | ||
|
||
let mut ideal_rewards_hashmap = HashMap::new(); | ||
|
||
for flag_index in 0..PARTICIPATION_FLAG_WEIGHTS.len() { | ||
let weight = get_flag_weight(flag_index) | ||
.map_err(|_| BeaconChainError::AttestationRewardsError)?; | ||
|
||
let unslashed_participating_indices = participation_cache | ||
.get_unslashed_participating_indices(flag_index, previous_epoch)?; | ||
|
||
let unslashed_participating_balance = | ||
unslashed_participating_indices | ||
.total_balance() | ||
.map_err(|_| BeaconChainError::AttestationRewardsError)?; | ||
|
||
let unslashed_participating_increments = | ||
unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; | ||
|
||
let total_active_balance = participation_cache.current_epoch_total_active_balance(); | ||
|
||
let active_increments = | ||
total_active_balance.safe_div(spec.effective_balance_increment)?; | ||
|
||
let base_reward_per_increment = | ||
BaseRewardPerIncrement::new(total_active_balance, spec)?; | ||
|
||
for effective_balance_eth in 0..=32 { | ||
let base_reward = | ||
effective_balance_eth.safe_mul(base_reward_per_increment.as_u64())?; | ||
|
||
let penalty = -(base_reward.safe_mul(weight)?.safe_div(WEIGHT_DENOMINATOR)? as i64); | ||
|
||
let reward_numerator = base_reward | ||
.safe_mul(weight)? | ||
.safe_mul(unslashed_participating_increments)?; | ||
|
||
let ideal_reward = reward_numerator | ||
.safe_div(active_increments)? | ||
.safe_div(WEIGHT_DENOMINATOR)?; | ||
if !state.is_in_inactivity_leak(previous_epoch, spec) { | ||
ideal_rewards_hashmap | ||
.insert((flag_index, effective_balance_eth), (ideal_reward, penalty)); | ||
} else { | ||
ideal_rewards_hashmap.insert((flag_index, effective_balance_eth), (0, penalty)); | ||
} | ||
} | ||
} | ||
|
||
// Calculate total_rewards | ||
let mut total_rewards: Vec<TotalAttestationRewards> = Vec::new(); | ||
|
||
let validators = if validators.is_empty() { | ||
participation_cache.eligible_validator_indices().to_vec() | ||
} else { | ||
validators | ||
.into_iter() | ||
.filter_map(|validator| match validator { | ||
ValidatorId::Index(i) => Some(i as usize), | ||
ValidatorId::PublicKey(pubkey) => match state.get_validator_index(&pubkey) { | ||
Ok(Some(i)) => Some(i as usize), | ||
_ => None, | ||
}, | ||
}) | ||
.collect::<Vec<usize>>() | ||
}; | ||
|
||
for validator_index in &validators { | ||
let eligible = state.is_eligible_validator(previous_epoch, *validator_index)?; | ||
let mut head_reward = 0u64; | ||
let mut target_reward = 0i64; | ||
let mut source_reward = 0i64; | ||
|
||
if eligible { | ||
let effective_balance = state.get_effective_balance(*validator_index)?; | ||
|
||
let effective_balance_eth = | ||
effective_balance.safe_div(spec.effective_balance_increment)?; | ||
|
||
for flag_index in 0..PARTICIPATION_FLAG_WEIGHTS.len() { | ||
let (ideal_reward, penalty) = ideal_rewards_hashmap | ||
.get(&(flag_index, effective_balance_eth)) | ||
.ok_or(BeaconChainError::AttestationRewardsError)?; | ||
let voted_correctly = participation_cache | ||
.get_unslashed_participating_indices(flag_index, previous_epoch) | ||
.map_err(|_| BeaconChainError::AttestationRewardsError)? | ||
.contains(*validator_index) | ||
.map_err(|_| BeaconChainError::AttestationRewardsError)?; | ||
if voted_correctly { | ||
if flag_index == TIMELY_HEAD_FLAG_INDEX { | ||
head_reward += ideal_reward; | ||
} else if flag_index == TIMELY_TARGET_FLAG_INDEX { | ||
target_reward += *ideal_reward as i64; | ||
} else if flag_index == TIMELY_SOURCE_FLAG_INDEX { | ||
source_reward += *ideal_reward as i64; | ||
} | ||
} else if flag_index == TIMELY_HEAD_FLAG_INDEX { | ||
head_reward = 0; | ||
} else if flag_index == TIMELY_TARGET_FLAG_INDEX { | ||
target_reward = *penalty; | ||
} else if flag_index == TIMELY_SOURCE_FLAG_INDEX { | ||
source_reward = *penalty; | ||
} | ||
} | ||
} | ||
total_rewards.push(TotalAttestationRewards { | ||
validator_index: *validator_index as u64, | ||
head: head_reward, | ||
target: target_reward, | ||
source: source_reward, | ||
}); | ||
} | ||
|
||
// Convert hashmap to vector | ||
let mut ideal_rewards: Vec<IdealAttestationRewards> = ideal_rewards_hashmap | ||
.iter() | ||
.map( | ||
|((flag_index, effective_balance_eth), (ideal_reward, _penalty))| { | ||
(flag_index, effective_balance_eth, ideal_reward) | ||
}, | ||
) | ||
.fold( | ||
HashMap::new(), | ||
|mut acc, (flag_index, effective_balance_eth, ideal_reward)| { | ||
let entry = acc.entry(*effective_balance_eth as u32).or_insert( | ||
IdealAttestationRewards { | ||
effective_balance: *effective_balance_eth, | ||
head: 0, | ||
target: 0, | ||
source: 0, | ||
}, | ||
); | ||
match *flag_index { | ||
TIMELY_SOURCE_FLAG_INDEX => entry.source += ideal_reward, | ||
TIMELY_TARGET_FLAG_INDEX => entry.target += ideal_reward, | ||
TIMELY_HEAD_FLAG_INDEX => entry.head += ideal_reward, | ||
_ => {} | ||
} | ||
acc | ||
}, | ||
) | ||
.into_values() | ||
.collect::<Vec<IdealAttestationRewards>>(); | ||
ideal_rewards.sort_by(|a, b| a.effective_balance.cmp(&b.effective_balance)); | ||
|
||
Ok(StandardAttestationRewards { | ||
ideal_rewards, | ||
total_rewards, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
use serde::{Deserialize, Serialize}; | ||
|
||
// Details about the rewards paid for attestations | ||
// All rewards in GWei | ||
|
||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] | ||
pub struct IdealAttestationRewards { | ||
// Validator's effective balance in gwei | ||
#[serde(with = "eth2_serde_utils::quoted_u64")] | ||
pub effective_balance: u64, | ||
// Ideal attester's reward for head vote in gwei | ||
#[serde(with = "eth2_serde_utils::quoted_u64")] | ||
pub head: u64, | ||
// Ideal attester's reward for target vote in gwei | ||
#[serde(with = "eth2_serde_utils::quoted_u64")] | ||
pub target: u64, | ||
// Ideal attester's reward for source vote in gwei | ||
#[serde(with = "eth2_serde_utils::quoted_u64")] | ||
pub source: u64, | ||
} | ||
|
||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] | ||
pub struct TotalAttestationRewards { | ||
// one entry for every validator based on their attestations in the epoch | ||
#[serde(with = "eth2_serde_utils::quoted_u64")] | ||
pub validator_index: u64, | ||
// attester's reward for head vote in gwei | ||
#[serde(with = "eth2_serde_utils::quoted_u64")] | ||
pub head: u64, | ||
// attester's reward for target vote in gwei | ||
pub target: i64, | ||
// attester's reward for source vote in gwei | ||
pub source: i64, | ||
// TBD attester's inclusion_delay reward in gwei (phase0 only) | ||
// pub inclusion_delay: u64, | ||
} | ||
|
||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] | ||
pub struct StandardAttestationRewards { | ||
pub ideal_rewards: Vec<IdealAttestationRewards>, | ||
pub total_rewards: Vec<TotalAttestationRewards>, | ||
} |