Skip to content

Implement per-validator reward APIs #3661

@michaelsproul

Description

@michaelsproul

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 current block_rewards by diffing the ParticipationFlags 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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    HTTP-APIconsensusAn issue/PR that touches consensus code, such as state_processing or block verification.enhancementNew feature or requesthelp wantedExtra attention is neededrewards-apiImplementation of the rewards API

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions