-
Notifications
You must be signed in to change notification settings - Fork 924
Description
Description
For the protocol fellowship I've proposed a project to add reward calculation APIs to the beacon-APIs standard (eth-protocol-fellows/cohort-three#51). This issue contemplates the design and implementation on the Lighthouse side.
Present Behaviour
Currently we have a few APIs for doing things with rewards, but none are fully satisfactory:
- There's
/lighthouse/validator_inclusion/{epoch}/{validator_id}
, which includes whether or not the target/head votes matched, but nothing about the source vote, nor the rewards. - There's the undocumented
/lighthouse/analysis/attestation_performance/{validator_id}
endpoint that we created for beacon.watch (a quasi block explorer, see #3362). It has all relevant attestation information including source matching, but lacks inclusion distance and reward information. - There's the
/lighthouse/analysis/block_rewards
API for block rewards that is used by blockprint. However it has several limitations: it considers only attestations and sync committee rewards (no slashings) and it is liable to return slightly incorrect results when validators make slashable attestations.
Design Questions
Some main design questions (DQs) that I think we should iterate on are:
DQ1: Bulk or single block/epoch
Should the new reward APIs operate on a single block/epoch or a range? Most of our existing APIs operate on a range because that's substantially more efficient with Lighthouse's database design. However most of the standard beacon-APIs operate only on a single block or epoch at a time, so perhaps we should keep things consistent.
Another point in favour of the single block/single epoch APIs is that we can likely optimise behind the scenes to make them efficient. The ongoing tree-states
work will help with this by drastically reducing the size of archive nodes (4TB -> 250GB) and enabling caching of more states in memory (see: #3206).
DQ2: Reward granularity
There's a trade-off between implementation complexity and detail when it comes to the granularity of reward data:
- Should attestation rewards include a breakdown by head/target/source (probably yes)
- Should block rewards include a breakdown by included attestations, sync aggregate, slashings (maybe?)
We should also be mindful of the fact that rewards for attestations and sync committee messages can be negative. We should specify the rewards as signed integers.
Implementation Considerations
All of the reward APIs should probably use the BlockReplayer
, like the existing reward APIs do.
Block Rewards
One way to calculate a coarse block reward is:
- Replay the relevant block(s)
- Record the balance diff for the proposer index using
BlockReplayer
hooks - Subtract any attestation or sync committee reward paid to the proposer in the same slot from the balance diff.
Alternatively we can calculate fine-grained rewards:
- Replay the relevant block(s)
- Compute the fine-grained rewards for each attestation/sync aggregate/slashing processed. The current approach for sync aggregates used by the
block_rewards
API should suffice. For attestations we could improve on the currentblock_rewards
by diffing theParticipationFlags
to accurately track included attestations (see below). Slashings require more thought.
Attestation Rewards
I think attestation rewards are a little easier than block rewards. We can compute them for a given epoch by diffing the current_epoch_participation
which contains a ParticipationFlags
bitfield for each validator. For each bit set (or not set) we can then apply the appropriate reward/penalty based on get_base_reward
and the inactivity penalty. See:
lighthouse/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs
Lines 75 to 90 in fcfd02a
for &index in participation_cache.eligible_validator_indices() { | |
let base_reward = get_base_reward(state, index, base_reward_per_increment, spec)?; | |
let mut delta = Delta::default(); | |
if unslashed_participating_indices.contains(index as usize)? { | |
if !state.is_in_inactivity_leak(previous_epoch, spec) { | |
let reward_numerator = base_reward | |
.safe_mul(weight)? | |
.safe_mul(unslashed_participating_increments)?; | |
delta.reward( | |
reward_numerator.safe_div(active_increments.safe_mul(WEIGHT_DENOMINATOR)?)?, | |
)?; | |
} | |
} else if flag_index != TIMELY_HEAD_FLAG_INDEX { | |
delta.penalize(base_reward.safe_mul(weight)?.safe_div(WEIGHT_DENOMINATOR)?)?; | |
} |
We may not even want a block replayer for this, we might just want to load the state at the start of epoch n + 1
and diff it against the state at epoch n
.
Sync Committee Rewards
Require more thought, TODO.