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:
Jimmy Chen 2023-07-18 01:48:40 +00:00
parent 4435a22221
commit fc7f1ba6b9
12 changed files with 513 additions and 69 deletions

1
Cargo.lock generated
View File

@ -630,6 +630,7 @@ dependencies = [
"eth1", "eth1",
"eth2", "eth2",
"ethereum_hashing", "ethereum_hashing",
"ethereum_serde_utils",
"ethereum_ssz", "ethereum_ssz",
"ethereum_ssz_derive", "ethereum_ssz_derive",
"execution_layer", "execution_layer",

View File

@ -27,6 +27,7 @@ operation_pool = { path = "../operation_pool" }
rayon = "1.4.1" rayon = "1.4.1"
serde = "1.0.116" serde = "1.0.116"
serde_derive = "1.0.116" serde_derive = "1.0.116"
ethereum_serde_utils = "0.5.0"
slog = { version = "2.5.2", features = ["max_level_trace"] } slog = { version = "2.5.2", features = ["max_level_trace"] }
sloggers = { version = "2.1.1", features = ["json"] } sloggers = { version = "2.1.1", features = ["json"] }
slot_clock = { path = "../../common/slot_clock" } slot_clock = { path = "../../common/slot_clock" }

View File

@ -3,7 +3,8 @@ use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttest
use eth2::lighthouse::StandardAttestationRewards; use eth2::lighthouse::StandardAttestationRewards;
use participation_cache::ParticipationCache; use participation_cache::ParticipationCache;
use safe_arith::SafeArith; use safe_arith::SafeArith;
use slog::{debug, Logger}; use serde_utils::quoted_u64::Quoted;
use slog::debug;
use state_processing::{ use state_processing::{
common::altair::BaseRewardPerIncrement, common::altair::BaseRewardPerIncrement,
per_epoch_processing::altair::{participation_cache, rewards_and_penalties::get_flag_weight}, 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::consts::altair::WEIGHT_DENOMINATOR;
use types::{Epoch, EthSpec}; use types::{BeaconState, Epoch, EthSpec};
use eth2::types::ValidatorId; 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> { impl<T: BeaconChainTypes> BeaconChain<T> {
pub fn compute_attestation_rewards( pub fn compute_attestation_rewards(
&self, &self,
epoch: Epoch, epoch: Epoch,
validators: Vec<ValidatorId>, validators: Vec<ValidatorId>,
log: Logger,
) -> Result<StandardAttestationRewards, BeaconChainError> { ) -> 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 // Get state
let spec = &self.spec;
let state_slot = (epoch + 1).end_slot(T::EthSpec::slots_per_epoch()); let state_slot = (epoch + 1).end_slot(T::EthSpec::slots_per_epoch());
let state_root = self let state_root = self
.state_root_at_slot(state_slot)? .state_root_at_slot(state_slot)?
.ok_or(BeaconChainError::NoStateForSlot(state_slot))?; .ok_or(BeaconChainError::NoStateForSlot(state_slot))?;
let mut state = self let state = self
.get_state(&state_root, Some(state_slot))? .get_state(&state_root, Some(state_slot))?
.ok_or(BeaconChainError::MissingBeaconState(state_root))?; .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 // Calculate ideal_rewards
let participation_cache = ParticipationCache::new(&state, spec)?; let participation_cache = ParticipationCache::new(&state, spec)?;
@ -71,7 +151,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let base_reward_per_increment = let base_reward_per_increment =
BaseRewardPerIncrement::new(total_active_balance, spec)?; 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 = let effective_balance =
effective_balance_eth.safe_mul(spec.effective_balance_increment)?; effective_balance_eth.safe_mul(spec.effective_balance_increment)?;
let base_reward = let base_reward =
@ -101,20 +181,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let validators = if validators.is_empty() { let validators = if validators.is_empty() {
participation_cache.eligible_validator_indices().to_vec() participation_cache.eligible_validator_indices().to_vec()
} else { } else {
validators Self::validators_ids_to_indices(&mut state, 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<_>, _>>()?
}; };
for validator_index in &validators { for validator_index in &validators {
let eligible = state.is_eligible_validator(previous_epoch, *validator_index)?; 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 target_reward = 0i64;
let mut source_reward = 0i64; let mut source_reward = 0i64;
@ -132,7 +204,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.map_err(|_| BeaconChainError::AttestationRewardsError)?; .map_err(|_| BeaconChainError::AttestationRewardsError)?;
if voted_correctly { if voted_correctly {
if flag_index == TIMELY_HEAD_FLAG_INDEX { 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 { } else if flag_index == TIMELY_TARGET_FLAG_INDEX {
target_reward += *ideal_reward as i64; target_reward += *ideal_reward as i64;
} else if flag_index == TIMELY_SOURCE_FLAG_INDEX { } else if flag_index == TIMELY_SOURCE_FLAG_INDEX {
@ -152,6 +224,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
head: head_reward, head: head_reward,
target: target_reward, target: target_reward,
source: source_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, head: 0,
target: 0, target: 0,
source: 0, source: 0,
inclusion_delay: None,
// TODO: altair calculation logic needs to be updated to include inactivity penalty
inactivity: 0,
}); });
match *flag_index { match *flag_index {
TIMELY_SOURCE_FLAG_INDEX => entry.source += ideal_reward, TIMELY_SOURCE_FLAG_INDEX => entry.source += ideal_reward,
@ -192,4 +270,126 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
total_rewards, 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)
}
} }

View File

@ -24,7 +24,7 @@ use state_processing::{
}, },
signature_sets::Error as SignatureSetError, signature_sets::Error as SignatureSetError,
state_advance::Error as StateAdvanceError, state_advance::Error as StateAdvanceError,
BlockProcessingError, BlockReplayError, SlotProcessingError, BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError,
}; };
use std::time::Duration; use std::time::Duration;
use task_executor::ShutdownReason; use task_executor::ShutdownReason;
@ -60,6 +60,7 @@ pub enum BeaconChainError {
MissingBeaconBlock(Hash256), MissingBeaconBlock(Hash256),
MissingBeaconState(Hash256), MissingBeaconState(Hash256),
SlotProcessingError(SlotProcessingError), SlotProcessingError(SlotProcessingError),
EpochProcessingError(EpochProcessingError),
StateAdvanceError(StateAdvanceError), StateAdvanceError(StateAdvanceError),
UnableToAdvanceState(String), UnableToAdvanceState(String),
NoStateForAttestation { NoStateForAttestation {
@ -217,6 +218,7 @@ pub enum BeaconChainError {
} }
easy_from_to!(SlotProcessingError, BeaconChainError); easy_from_to!(SlotProcessingError, BeaconChainError);
easy_from_to!(EpochProcessingError, BeaconChainError);
easy_from_to!(AttestationValidationError, BeaconChainError); easy_from_to!(AttestationValidationError, BeaconChainError);
easy_from_to!(SyncCommitteeMessageValidationError, BeaconChainError); easy_from_to!(SyncCommitteeMessageValidationError, BeaconChainError);
easy_from_to!(ExitValidationError, BeaconChainError); easy_from_to!(ExitValidationError, BeaconChainError);

View File

@ -9,19 +9,22 @@ use beacon_chain::{
test_utils::{AttestationStrategy, BlockStrategy, RelativeSyncCommittee}, test_utils::{AttestationStrategy, BlockStrategy, RelativeSyncCommittee},
types::{Epoch, EthSpec, Keypair, MinimalEthSpec}, types::{Epoch, EthSpec, Keypair, MinimalEthSpec},
}; };
use eth2::lighthouse::attestation_rewards::TotalAttestationRewards;
use eth2::lighthouse::StandardAttestationRewards;
use eth2::types::ValidatorId;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use types::beacon_state::Error as BeaconStateError;
use types::{BeaconState, ChainSpec};
pub const VALIDATOR_COUNT: usize = 64; pub const VALIDATOR_COUNT: usize = 64;
type E = MinimalEthSpec;
lazy_static! { lazy_static! {
static ref KEYPAIRS: Vec<Keypair> = generate_deterministic_keypairs(VALIDATOR_COUNT); static ref KEYPAIRS: Vec<Keypair> = generate_deterministic_keypairs(VALIDATOR_COUNT);
} }
fn get_harness<E: EthSpec>() -> BeaconChainHarness<EphemeralHarnessType<E>> { fn get_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> {
let mut spec = E::default_spec();
spec.altair_fork_epoch = Some(Epoch::new(0)); // We use altair for all tests
let harness = BeaconChainHarness::builder(E::default()) let harness = BeaconChainHarness::builder(E::default())
.spec(spec) .spec(spec)
.keypairs(KEYPAIRS.to_vec()) .keypairs(KEYPAIRS.to_vec())
@ -35,8 +38,11 @@ fn get_harness<E: EthSpec>() -> BeaconChainHarness<EphemeralHarnessType<E>> {
#[tokio::test] #[tokio::test]
async fn test_sync_committee_rewards() { async fn test_sync_committee_rewards() {
let num_block_produced = MinimalEthSpec::slots_per_epoch(); let mut spec = E::default_spec();
let harness = get_harness::<MinimalEthSpec>(); spec.altair_fork_epoch = Some(Epoch::new(0));
let harness = get_harness(spec);
let num_block_produced = E::slots_per_epoch();
let latest_block_root = harness let latest_block_root = harness
.extend_chain( .extend_chain(
@ -119,3 +125,175 @@ async fn test_sync_committee_rewards() {
mismatches.join(",") mismatches.join(",")
); );
} }
#[tokio::test]
async fn test_verify_attestation_rewards_base() {
let harness = get_harness(E::default_spec());
// epoch 0 (N), only two thirds of validators vote.
let two_thirds = (VALIDATOR_COUNT / 3) * 2;
let two_thirds_validators: Vec<usize> = (0..two_thirds).collect();
harness
.extend_chain(
E::slots_per_epoch() as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::SomeValidators(two_thirds_validators),
)
.await;
let initial_balances: Vec<u64> = harness.get_current_state().balances().clone().into();
// extend slots to beginning of epoch N + 2
harness.extend_slots(E::slots_per_epoch() as usize).await;
// compute reward deltas for all validators in epoch N
let StandardAttestationRewards {
ideal_rewards,
total_rewards,
} = harness
.chain
.compute_attestation_rewards(Epoch::new(0), vec![])
.unwrap();
// assert no inactivity penalty for both ideal rewards and individual validators
assert!(ideal_rewards.iter().all(|reward| reward.inactivity == 0));
assert!(total_rewards.iter().all(|reward| reward.inactivity == 0));
// apply attestation rewards to initial balances
let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards);
// verify expected balances against actual balances
let balances: Vec<u64> = harness.get_current_state().balances().clone().into();
assert_eq!(expected_balances, balances);
}
#[tokio::test]
async fn test_verify_attestation_rewards_base_inactivity_leak() {
let spec = E::default_spec();
let harness = get_harness(spec.clone());
let half = VALIDATOR_COUNT / 2;
let half_validators: Vec<usize> = (0..half).collect();
// target epoch is the epoch where the chain enters inactivity leak
let target_epoch = &spec.min_epochs_to_inactivity_penalty + 1;
// advance until beginning of epoch N + 1 and get balances
harness
.extend_chain(
(E::slots_per_epoch() * (target_epoch + 1)) as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::SomeValidators(half_validators.clone()),
)
.await;
let initial_balances: Vec<u64> = harness.get_current_state().balances().clone().into();
// extend slots to beginning of epoch N + 2
harness.advance_slot();
harness
.extend_chain(
E::slots_per_epoch() as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::SomeValidators(half_validators),
)
.await;
let _slot = harness.get_current_slot();
// compute reward deltas for all validators in epoch N
let StandardAttestationRewards {
ideal_rewards,
total_rewards,
} = harness
.chain
.compute_attestation_rewards(Epoch::new(target_epoch), vec![])
.unwrap();
// assert inactivity penalty for both ideal rewards and individual validators
assert!(ideal_rewards.iter().all(|reward| reward.inactivity < 0));
assert!(total_rewards.iter().all(|reward| reward.inactivity < 0));
// apply attestation rewards to initial balances
let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards);
// verify expected balances against actual balances
let balances: Vec<u64> = harness.get_current_state().balances().clone().into();
assert_eq!(expected_balances, balances);
}
#[tokio::test]
async fn test_verify_attestation_rewards_base_subset_only() {
let harness = get_harness(E::default_spec());
// epoch 0 (N), only two thirds of validators vote.
let two_thirds = (VALIDATOR_COUNT / 3) * 2;
let two_thirds_validators: Vec<usize> = (0..two_thirds).collect();
harness
.extend_chain(
E::slots_per_epoch() as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::SomeValidators(two_thirds_validators),
)
.await;
// a small subset of validators to compute attestation rewards for
let validators_subset = [0, VALIDATOR_COUNT / 2, VALIDATOR_COUNT - 1];
// capture balances before transitioning to N + 2
let initial_balances = get_validator_balances(harness.get_current_state(), &validators_subset);
// extend slots to beginning of epoch N + 2
harness.extend_slots(E::slots_per_epoch() as usize).await;
let validators_subset_ids: Vec<ValidatorId> = validators_subset
.into_iter()
.map(|idx| ValidatorId::Index(idx as u64))
.collect();
// compute reward deltas for the subset of validators in epoch N
let StandardAttestationRewards {
ideal_rewards: _,
total_rewards,
} = harness
.chain
.compute_attestation_rewards(Epoch::new(0), validators_subset_ids)
.unwrap();
// apply attestation rewards to initial balances
let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards);
// verify expected balances against actual balances
let balances = get_validator_balances(harness.get_current_state(), &validators_subset);
assert_eq!(expected_balances, balances);
}
/// Apply a vec of `TotalAttestationRewards` to initial balances, and return
fn apply_attestation_rewards(
initial_balances: &[u64],
attestation_rewards: Vec<TotalAttestationRewards>,
) -> Vec<u64> {
initial_balances
.iter()
.zip(attestation_rewards)
.map(|(&initial_balance, rewards)| {
let expected_balance = initial_balance as i64
+ rewards.head
+ rewards.source
+ rewards.target
+ rewards.inclusion_delay.map(|q| q.value).unwrap_or(0) as i64
+ rewards.inactivity;
expected_balance as u64
})
.collect::<Vec<u64>>()
}
fn get_validator_balances(state: BeaconState<E>, validators: &[usize]) -> Vec<u64> {
validators
.iter()
.flat_map(|&id| {
state
.balances()
.get(id)
.cloned()
.ok_or(BeaconStateError::BalancesOutOfBounds(id))
})
.collect()
}

View File

@ -2034,15 +2034,11 @@ pub fn serve<T: BeaconChainTypes>(
.and(warp::path::param::<Epoch>()) .and(warp::path::param::<Epoch>())
.and(warp::path::end()) .and(warp::path::end())
.and(warp::body::json()) .and(warp::body::json())
.and(log_filter.clone())
.and_then( .and_then(
|chain: Arc<BeaconChain<T>>, |chain: Arc<BeaconChain<T>>, epoch: Epoch, validators: Vec<ValidatorId>| {
epoch: Epoch,
validators: Vec<ValidatorId>,
log: Logger| {
blocking_json_task(move || { blocking_json_task(move || {
let attestation_rewards = chain let attestation_rewards = chain
.compute_attestation_rewards(epoch, validators, log) .compute_attestation_rewards(epoch, validators)
.map_err(|e| match e { .map_err(|e| match e {
BeaconChainError::MissingBeaconState(root) => { BeaconChainError::MissingBeaconState(root) => {
warp_utils::reject::custom_not_found(format!( warp_utils::reject::custom_not_found(format!(

View File

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_utils::quoted_u64::Quoted;
// Details about the rewards paid for attestations // Details about the rewards paid for attestations
// All rewards in GWei // All rewards in GWei
@ -17,6 +18,12 @@ pub struct IdealAttestationRewards {
// Ideal attester's reward for source vote in gwei // Ideal attester's reward for source vote in gwei
#[serde(with = "serde_utils::quoted_u64")] #[serde(with = "serde_utils::quoted_u64")]
pub source: u64, pub source: u64,
// Ideal attester's inclusion_delay reward in gwei (phase0 only)
#[serde(skip_serializing_if = "Option::is_none")]
pub inclusion_delay: Option<Quoted<u64>>,
// Ideal attester's inactivity penalty in gwei
#[serde(with = "serde_utils::quoted_i64")]
pub inactivity: i64,
} }
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
@ -25,16 +32,20 @@ pub struct TotalAttestationRewards {
#[serde(with = "serde_utils::quoted_u64")] #[serde(with = "serde_utils::quoted_u64")]
pub validator_index: u64, pub validator_index: u64,
// attester's reward for head vote in gwei // attester's reward for head vote in gwei
#[serde(with = "serde_utils::quoted_u64")] #[serde(with = "serde_utils::quoted_i64")]
pub head: u64, pub head: i64,
// attester's reward for target vote in gwei // attester's reward for target vote in gwei
#[serde(with = "serde_utils::quoted_i64")] #[serde(with = "serde_utils::quoted_i64")]
pub target: i64, pub target: i64,
// attester's reward for source vote in gwei // attester's reward for source vote in gwei
#[serde(with = "serde_utils::quoted_i64")] #[serde(with = "serde_utils::quoted_i64")]
pub source: i64, pub source: i64,
// TBD attester's inclusion_delay reward in gwei (phase0 only) // attester's inclusion_delay reward in gwei (phase0 only)
// pub inclusion_delay: u64, #[serde(skip_serializing_if = "Option::is_none")]
pub inclusion_delay: Option<Quoted<u64>>,
// attester's inactivity penalty in gwei
#[serde(with = "serde_utils::quoted_i64")]
pub inactivity: i64,
} }
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]

View File

@ -17,3 +17,15 @@ pub fn get_base_reward<T: EthSpec>(
.safe_div(spec.base_rewards_per_epoch) .safe_div(spec.base_rewards_per_epoch)
.map_err(Into::into) .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)
}

View File

@ -36,7 +36,7 @@ pub fn process_epoch<T: EthSpec>(
justification_and_finalization_state.apply_changes_to_state(state); justification_and_finalization_state.apply_changes_to_state(state);
// Rewards and Penalties. // Rewards and Penalties.
process_rewards_and_penalties(state, &mut validator_statuses, spec)?; process_rewards_and_penalties(state, &validator_statuses, spec)?;
// Registry Updates. // Registry Updates.
process_registry_updates(state, spec)?; process_registry_updates(state, spec)?;

View File

@ -45,7 +45,7 @@ impl AttestationDelta {
/// Apply attester and proposer rewards. /// Apply attester and proposer rewards.
pub fn process_rewards_and_penalties<T: EthSpec>( pub fn process_rewards_and_penalties<T: EthSpec>(
state: &mut BeaconState<T>, state: &mut BeaconState<T>,
validator_statuses: &mut ValidatorStatuses, validator_statuses: &ValidatorStatuses,
spec: &ChainSpec, spec: &ChainSpec,
) -> Result<(), Error> { ) -> Result<(), Error> {
if state.current_epoch() == T::genesis_epoch() { if state.current_epoch() == T::genesis_epoch() {
@ -59,7 +59,7 @@ pub fn process_rewards_and_penalties<T: EthSpec>(
return Err(Error::ValidatorStatusesInconsistent); 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 // Apply the deltas, erroring on overflow above but not on overflow below (saturating at 0
// instead). // instead).
@ -73,10 +73,41 @@ pub fn process_rewards_and_penalties<T: EthSpec>(
} }
/// Apply rewards for participation in attestations during the previous epoch. /// 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>, state: &BeaconState<T>,
validator_statuses: &ValidatorStatuses, validator_statuses: &ValidatorStatuses,
spec: &ChainSpec, 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> { ) -> Result<Vec<AttestationDelta>, Error> {
let previous_epoch = state.previous_epoch(); let previous_epoch = state.previous_epoch();
let finality_delay = state let finality_delay = state
@ -88,6 +119,13 @@ pub fn get_attestation_deltas<T: EthSpec>(
let total_balances = &validator_statuses.total_balances; 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() { for (index, validator) in validator_statuses.statuses.iter().enumerate() {
// Ignore ineligible validators. All sub-functions of the spec do this except for // 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 // `get_inclusion_delay_deltas`. It's safe to do so here because any validator that is in
@ -99,14 +137,16 @@ pub fn get_attestation_deltas<T: EthSpec>(
let base_reward = get_base_reward(state, index, total_balances.current_epoch(), spec)?; let base_reward = get_base_reward(state, index, total_balances.current_epoch(), spec)?;
let (inclusion_delay_delta, proposer_delta) =
get_inclusion_delay_delta(validator, base_reward, spec)?;
if include_validator_delta(index) {
let source_delta = let source_delta =
get_source_delta(validator, base_reward, total_balances, finality_delay, spec)?; get_source_delta(validator, base_reward, total_balances, finality_delay, spec)?;
let target_delta = let target_delta =
get_target_delta(validator, base_reward, total_balances, finality_delay, spec)?; get_target_delta(validator, base_reward, total_balances, finality_delay, spec)?;
let head_delta = let head_delta =
get_head_delta(validator, base_reward, total_balances, finality_delay, spec)?; 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 = let inactivity_penalty_delta =
get_inactivity_penalty_delta(validator, base_reward, finality_delay, spec)?; get_inactivity_penalty_delta(validator, base_reward, finality_delay, spec)?;
@ -120,8 +160,10 @@ pub fn get_attestation_deltas<T: EthSpec>(
delta delta
.inactivity_penalty_delta .inactivity_penalty_delta
.combine(inactivity_penalty_delta)?; .combine(inactivity_penalty_delta)?;
}
if let Some((proposer_index, proposer_delta)) = proposer_delta { if let Some((proposer_index, proposer_delta)) = proposer_delta {
if include_validator_delta(proposer_index) {
deltas deltas
.get_mut(proposer_index) .get_mut(proposer_index)
.ok_or(Error::ValidatorStatusesInconsistent)? .ok_or(Error::ValidatorStatusesInconsistent)?
@ -129,11 +171,12 @@ pub fn get_attestation_deltas<T: EthSpec>(
.combine(proposer_delta)?; .combine(proposer_delta)?;
} }
} }
}
Ok(deltas) Ok(deltas)
} }
fn get_attestation_component_delta( pub fn get_attestation_component_delta(
index_in_unslashed_attesting_indices: bool, index_in_unslashed_attesting_indices: bool,
attesting_balance: u64, attesting_balance: u64,
total_balances: &TotalBalances, total_balances: &TotalBalances,
@ -216,7 +259,7 @@ fn get_head_delta(
) )
} }
fn get_inclusion_delay_delta( pub fn get_inclusion_delay_delta(
validator: &ValidatorStatus, validator: &ValidatorStatus,
base_reward: u64, base_reward: u64,
spec: &ChainSpec, spec: &ChainSpec,
@ -242,7 +285,7 @@ fn get_inclusion_delay_delta(
} }
} }
fn get_inactivity_penalty_delta( pub fn get_inactivity_penalty_delta(
validator: &ValidatorStatus, validator: &ValidatorStatus,
base_reward: u64, base_reward: u64,
finality_delay: u64, finality_delay: u64,

View File

@ -120,7 +120,7 @@ impl<E: EthSpec> EpochTransition<E> for RewardsAndPenalties {
BeaconState::Base(_) => { BeaconState::Base(_) => {
let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?;
validator_statuses.process_attestations(state)?; validator_statuses.process_attestations(state)?;
base::process_rewards_and_penalties(state, &mut validator_statuses, spec) base::process_rewards_and_penalties(state, &validator_statuses, spec)
} }
BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => {
altair::process_rewards_and_penalties( altair::process_rewards_and_penalties(

View File

@ -118,7 +118,7 @@ impl<E: EthSpec> Case for RewardsTest<E> {
let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; let mut validator_statuses = ValidatorStatuses::new(&state, spec)?;
validator_statuses.process_attestations(&state)?; validator_statuses.process_attestations(&state)?;
let deltas = base::rewards_and_penalties::get_attestation_deltas( let deltas = base::rewards_and_penalties::get_attestation_deltas_all(
&state, &state,
&validator_statuses, &validator_statuses,
spec, spec,