Skip to content

Commit fc7f1ba

Browse files
committed
Phase 0 attestation rewards via Beacon API (sigp#4474)
## Issue Addressed Addresses sigp#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>
1 parent 4435a22 commit fc7f1ba

File tree

12 files changed

+514
-70
lines changed

12 files changed

+514
-70
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beacon_node/beacon_chain/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ operation_pool = { path = "../operation_pool" }
2727
rayon = "1.4.1"
2828
serde = "1.0.116"
2929
serde_derive = "1.0.116"
30+
ethereum_serde_utils = "0.5.0"
3031
slog = { version = "2.5.2", features = ["max_level_trace"] }
3132
sloggers = { version = "2.1.1", features = ["json"] }
3233
slot_clock = { path = "../../common/slot_clock" }

beacon_node/beacon_chain/src/attestation_rewards.rs

Lines changed: 219 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttest
33
use eth2::lighthouse::StandardAttestationRewards;
44
use participation_cache::ParticipationCache;
55
use safe_arith::SafeArith;
6-
use slog::{debug, Logger};
6+
use serde_utils::quoted_u64::Quoted;
7+
use slog::debug;
78
use state_processing::{
89
common::altair::BaseRewardPerIncrement,
910
per_epoch_processing::altair::{participation_cache, rewards_and_penalties::get_flag_weight},
@@ -15,32 +16,111 @@ use store::consts::altair::{
1516
};
1617
use types::consts::altair::WEIGHT_DENOMINATOR;
1718

18-
use types::{Epoch, EthSpec};
19+
use types::{BeaconState, Epoch, EthSpec};
1920

2021
use eth2::types::ValidatorId;
22+
use state_processing::common::base::get_base_reward_from_effective_balance;
23+
use state_processing::per_epoch_processing::base::rewards_and_penalties::{
24+
get_attestation_component_delta, get_attestation_deltas_all, get_attestation_deltas_subset,
25+
get_inactivity_penalty_delta, get_inclusion_delay_delta,
26+
};
27+
use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo;
28+
use state_processing::per_epoch_processing::base::{
29+
TotalBalances, ValidatorStatus, ValidatorStatuses,
30+
};
2131

2232
impl<T: BeaconChainTypes> BeaconChain<T> {
2333
pub fn compute_attestation_rewards(
2434
&self,
2535
epoch: Epoch,
2636
validators: Vec<ValidatorId>,
27-
log: Logger,
2837
) -> Result<StandardAttestationRewards, BeaconChainError> {
29-
debug!(log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len());
38+
debug!(self.log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len());
3039

3140
// Get state
32-
let spec = &self.spec;
33-
3441
let state_slot = (epoch + 1).end_slot(T::EthSpec::slots_per_epoch());
3542

3643
let state_root = self
3744
.state_root_at_slot(state_slot)?
3845
.ok_or(BeaconChainError::NoStateForSlot(state_slot))?;
3946

40-
let mut state = self
47+
let state = self
4148
.get_state(&state_root, Some(state_slot))?
4249
.ok_or(BeaconChainError::MissingBeaconState(state_root))?;
4350

51+
match state {
52+
BeaconState::Base(_) => self.compute_attestation_rewards_base(state, validators),
53+
BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => {
54+
self.compute_attestation_rewards_altair(state, validators)
55+
}
56+
}
57+
}
58+
59+
fn compute_attestation_rewards_base(
60+
&self,
61+
mut state: BeaconState<T::EthSpec>,
62+
validators: Vec<ValidatorId>,
63+
) -> Result<StandardAttestationRewards, BeaconChainError> {
64+
let spec = &self.spec;
65+
let mut validator_statuses = ValidatorStatuses::new(&state, spec)?;
66+
validator_statuses.process_attestations(&state)?;
67+
68+
let ideal_rewards =
69+
self.compute_ideal_rewards_base(&state, &validator_statuses.total_balances)?;
70+
71+
let indices_to_attestation_delta = if validators.is_empty() {
72+
get_attestation_deltas_all(&state, &validator_statuses, spec)?
73+
.into_iter()
74+
.enumerate()
75+
.collect()
76+
} else {
77+
let validator_indices = Self::validators_ids_to_indices(&mut state, validators)?;
78+
get_attestation_deltas_subset(&state, &validator_statuses, &validator_indices, spec)?
79+
};
80+
81+
let mut total_rewards = vec![];
82+
83+
for (index, delta) in indices_to_attestation_delta.into_iter() {
84+
let head_delta = delta.head_delta;
85+
let head = (head_delta.rewards as i64).safe_sub(head_delta.penalties as i64)?;
86+
87+
let target_delta = delta.target_delta;
88+
let target = (target_delta.rewards as i64).safe_sub(target_delta.penalties as i64)?;
89+
90+
let source_delta = delta.source_delta;
91+
let source = (source_delta.rewards as i64).safe_sub(source_delta.penalties as i64)?;
92+
93+
// No penalties associated with inclusion delay
94+
let inclusion_delay = delta.inclusion_delay_delta.rewards;
95+
let inactivity = delta.inactivity_penalty_delta.penalties.wrapping_neg() as i64;
96+
97+
let rewards = TotalAttestationRewards {
98+
validator_index: index as u64,
99+
head,
100+
target,
101+
source,
102+
inclusion_delay: Some(Quoted {
103+
value: inclusion_delay,
104+
}),
105+
inactivity,
106+
};
107+
108+
total_rewards.push(rewards);
109+
}
110+
111+
Ok(StandardAttestationRewards {
112+
ideal_rewards,
113+
total_rewards,
114+
})
115+
}
116+
117+
fn compute_attestation_rewards_altair(
118+
&self,
119+
mut state: BeaconState<T::EthSpec>,
120+
validators: Vec<ValidatorId>,
121+
) -> Result<StandardAttestationRewards, BeaconChainError> {
122+
let spec = &self.spec;
123+
44124
// Calculate ideal_rewards
45125
let participation_cache = ParticipationCache::new(&state, spec)?;
46126

@@ -71,7 +151,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
71151
let base_reward_per_increment =
72152
BaseRewardPerIncrement::new(total_active_balance, spec)?;
73153

74-
for effective_balance_eth in 0..=32 {
154+
for effective_balance_eth in 1..=self.max_effective_balance_increment_steps()? {
75155
let effective_balance =
76156
effective_balance_eth.safe_mul(spec.effective_balance_increment)?;
77157
let base_reward =
@@ -101,20 +181,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
101181
let validators = if validators.is_empty() {
102182
participation_cache.eligible_validator_indices().to_vec()
103183
} else {
104-
validators
105-
.into_iter()
106-
.map(|validator| match validator {
107-
ValidatorId::Index(i) => Ok(i as usize),
108-
ValidatorId::PublicKey(pubkey) => state
109-
.get_validator_index(&pubkey)?
110-
.ok_or(BeaconChainError::ValidatorPubkeyUnknown(pubkey)),
111-
})
112-
.collect::<Result<Vec<_>, _>>()?
184+
Self::validators_ids_to_indices(&mut state, validators)?
113185
};
114186

115187
for validator_index in &validators {
116188
let eligible = state.is_eligible_validator(previous_epoch, *validator_index)?;
117-
let mut head_reward = 0u64;
189+
let mut head_reward = 0i64;
118190
let mut target_reward = 0i64;
119191
let mut source_reward = 0i64;
120192

@@ -132,7 +204,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
132204
.map_err(|_| BeaconChainError::AttestationRewardsError)?;
133205
if voted_correctly {
134206
if flag_index == TIMELY_HEAD_FLAG_INDEX {
135-
head_reward += ideal_reward;
207+
head_reward += *ideal_reward as i64;
136208
} else if flag_index == TIMELY_TARGET_FLAG_INDEX {
137209
target_reward += *ideal_reward as i64;
138210
} else if flag_index == TIMELY_SOURCE_FLAG_INDEX {
@@ -152,6 +224,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
152224
head: head_reward,
153225
target: target_reward,
154226
source: source_reward,
227+
inclusion_delay: None,
228+
// TODO: altair calculation logic needs to be updated to include inactivity penalty
229+
inactivity: 0,
155230
});
156231
}
157232

@@ -173,6 +248,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
173248
head: 0,
174249
target: 0,
175250
source: 0,
251+
inclusion_delay: None,
252+
// TODO: altair calculation logic needs to be updated to include inactivity penalty
253+
inactivity: 0,
176254
});
177255
match *flag_index {
178256
TIMELY_SOURCE_FLAG_INDEX => entry.source += ideal_reward,
@@ -192,4 +270,126 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
192270
total_rewards,
193271
})
194272
}
273+
274+
fn max_effective_balance_increment_steps(&self) -> Result<u64, BeaconChainError> {
275+
let spec = &self.spec;
276+
let max_steps = spec
277+
.max_effective_balance
278+
.safe_div(spec.effective_balance_increment)?;
279+
Ok(max_steps)
280+
}
281+
282+
fn validators_ids_to_indices(
283+
state: &mut BeaconState<T::EthSpec>,
284+
validators: Vec<ValidatorId>,
285+
) -> Result<Vec<usize>, BeaconChainError> {
286+
let indices = validators
287+
.into_iter()
288+
.map(|validator| match validator {
289+
ValidatorId::Index(i) => Ok(i as usize),
290+
ValidatorId::PublicKey(pubkey) => state
291+
.get_validator_index(&pubkey)?
292+
.ok_or(BeaconChainError::ValidatorPubkeyUnknown(pubkey)),
293+
})
294+
.collect::<Result<Vec<_>, _>>()?;
295+
Ok(indices)
296+
}
297+
298+
fn compute_ideal_rewards_base(
299+
&self,
300+
state: &BeaconState<T::EthSpec>,
301+
total_balances: &TotalBalances,
302+
) -> Result<Vec<IdealAttestationRewards>, BeaconChainError> {
303+
let spec = &self.spec;
304+
let previous_epoch = state.previous_epoch();
305+
let finality_delay = previous_epoch
306+
.safe_sub(state.finalized_checkpoint().epoch)?
307+
.as_u64();
308+
309+
let ideal_validator_status = ValidatorStatus {
310+
is_previous_epoch_attester: true,
311+
is_slashed: false,
312+
inclusion_info: Some(InclusionInfo {
313+
delay: 1,
314+
..Default::default()
315+
}),
316+
..Default::default()
317+
};
318+
319+
let mut ideal_attestation_rewards_list = Vec::new();
320+
321+
for effective_balance_step in 1..=self.max_effective_balance_increment_steps()? {
322+
let effective_balance =
323+
effective_balance_step.safe_mul(spec.effective_balance_increment)?;
324+
let base_reward = get_base_reward_from_effective_balance::<T::EthSpec>(
325+
effective_balance,
326+
total_balances.current_epoch(),
327+
spec,
328+
)?;
329+
330+
// compute ideal head rewards
331+
let head = get_attestation_component_delta(
332+
true,
333+
total_balances.previous_epoch_attesters(),
334+
total_balances,
335+
base_reward,
336+
finality_delay,
337+
spec,
338+
)?
339+
.rewards;
340+
341+
// compute ideal target rewards
342+
let target = get_attestation_component_delta(
343+
true,
344+
total_balances.previous_epoch_target_attesters(),
345+
total_balances,
346+
base_reward,
347+
finality_delay,
348+
spec,
349+
)?
350+
.rewards;
351+
352+
// compute ideal source rewards
353+
let source = get_attestation_component_delta(
354+
true,
355+
total_balances.previous_epoch_head_attesters(),
356+
total_balances,
357+
base_reward,
358+
finality_delay,
359+
spec,
360+
)?
361+
.rewards;
362+
363+
// compute ideal inclusion delay rewards
364+
let inclusion_delay =
365+
get_inclusion_delay_delta(&ideal_validator_status, base_reward, spec)?
366+
.0
367+
.rewards;
368+
369+
// compute inactivity penalty
370+
let inactivity = get_inactivity_penalty_delta(
371+
&ideal_validator_status,
372+
base_reward,
373+
finality_delay,
374+
spec,
375+
)?
376+
.penalties
377+
.wrapping_neg() as i64;
378+
379+
let ideal_attestation_rewards = IdealAttestationRewards {
380+
effective_balance,
381+
head,
382+
target,
383+
source,
384+
inclusion_delay: Some(Quoted {
385+
value: inclusion_delay,
386+
}),
387+
inactivity,
388+
};
389+
390+
ideal_attestation_rewards_list.push(ideal_attestation_rewards);
391+
}
392+
393+
Ok(ideal_attestation_rewards_list)
394+
}
195395
}

beacon_node/beacon_chain/src/errors.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use state_processing::{
2424
},
2525
signature_sets::Error as SignatureSetError,
2626
state_advance::Error as StateAdvanceError,
27-
BlockProcessingError, BlockReplayError, SlotProcessingError,
27+
BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError,
2828
};
2929
use std::time::Duration;
3030
use task_executor::ShutdownReason;
@@ -60,6 +60,7 @@ pub enum BeaconChainError {
6060
MissingBeaconBlock(Hash256),
6161
MissingBeaconState(Hash256),
6262
SlotProcessingError(SlotProcessingError),
63+
EpochProcessingError(EpochProcessingError),
6364
StateAdvanceError(StateAdvanceError),
6465
UnableToAdvanceState(String),
6566
NoStateForAttestation {
@@ -217,6 +218,7 @@ pub enum BeaconChainError {
217218
}
218219

219220
easy_from_to!(SlotProcessingError, BeaconChainError);
221+
easy_from_to!(EpochProcessingError, BeaconChainError);
220222
easy_from_to!(AttestationValidationError, BeaconChainError);
221223
easy_from_to!(SyncCommitteeMessageValidationError, BeaconChainError);
222224
easy_from_to!(ExitValidationError, BeaconChainError);

0 commit comments

Comments
 (0)