Skip to content

Commit e70daaa

Browse files
committed
Implement API for block rewards (#2628)
## Proposed Changes Add an API endpoint for retrieving detailed information about block rewards. For information on usage see [the docs](https://github.com/sigp/lighthouse/blob/block-rewards-api/book/src/api-lighthouse.md#lighthouseblock_rewards), and the source.
1 parent 013a3cc commit e70daaa

File tree

14 files changed

+366
-16
lines changed

14 files changed

+366
-16
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
2+
use eth2::lighthouse::{AttestationRewards, BlockReward, BlockRewardMeta};
3+
use operation_pool::{AttMaxCover, MaxCover};
4+
use state_processing::per_block_processing::altair::sync_committee::compute_sync_aggregate_rewards;
5+
use types::{BeaconBlockRef, BeaconState, EthSpec, Hash256, RelativeEpoch};
6+
7+
impl<T: BeaconChainTypes> BeaconChain<T> {
8+
pub fn compute_block_reward(
9+
&self,
10+
block: BeaconBlockRef<'_, T::EthSpec>,
11+
block_root: Hash256,
12+
state: &BeaconState<T::EthSpec>,
13+
) -> Result<BlockReward, BeaconChainError> {
14+
if block.slot() != state.slot() {
15+
return Err(BeaconChainError::BlockRewardSlotError);
16+
}
17+
18+
let active_indices = state.get_cached_active_validator_indices(RelativeEpoch::Current)?;
19+
let total_active_balance = state.get_total_balance(active_indices, &self.spec)?;
20+
let mut per_attestation_rewards = block
21+
.body()
22+
.attestations()
23+
.iter()
24+
.map(|att| {
25+
AttMaxCover::new(att, state, total_active_balance, &self.spec)
26+
.ok_or(BeaconChainError::BlockRewardAttestationError)
27+
})
28+
.collect::<Result<Vec<_>, _>>()?;
29+
30+
// Update the attestation rewards for each previous attestation included.
31+
// This is O(n^2) in the number of attestations n.
32+
for i in 0..per_attestation_rewards.len() {
33+
let (updated, to_update) = per_attestation_rewards.split_at_mut(i + 1);
34+
let latest_att = &updated[i];
35+
36+
for att in to_update {
37+
att.update_covering_set(latest_att.object(), latest_att.covering_set());
38+
}
39+
}
40+
41+
let mut prev_epoch_total = 0;
42+
let mut curr_epoch_total = 0;
43+
44+
for cover in &per_attestation_rewards {
45+
for &reward in cover.fresh_validators_rewards.values() {
46+
if cover.att.data.slot.epoch(T::EthSpec::slots_per_epoch()) == state.current_epoch()
47+
{
48+
curr_epoch_total += reward;
49+
} else {
50+
prev_epoch_total += reward;
51+
}
52+
}
53+
}
54+
55+
let attestation_total = prev_epoch_total + curr_epoch_total;
56+
57+
// Drop the covers.
58+
let per_attestation_rewards = per_attestation_rewards
59+
.into_iter()
60+
.map(|cover| cover.fresh_validators_rewards)
61+
.collect();
62+
63+
let attestation_rewards = AttestationRewards {
64+
total: attestation_total,
65+
prev_epoch_total,
66+
curr_epoch_total,
67+
per_attestation_rewards,
68+
};
69+
70+
// Sync committee rewards.
71+
let sync_committee_rewards = if let Ok(sync_aggregate) = block.body().sync_aggregate() {
72+
let (_, proposer_reward_per_bit) = compute_sync_aggregate_rewards(state, &self.spec)
73+
.map_err(|_| BeaconChainError::BlockRewardSyncError)?;
74+
sync_aggregate.sync_committee_bits.num_set_bits() as u64 * proposer_reward_per_bit
75+
} else {
76+
0
77+
};
78+
79+
// Total, metadata
80+
let total = attestation_total + sync_committee_rewards;
81+
82+
let meta = BlockRewardMeta {
83+
slot: block.slot(),
84+
parent_slot: state.latest_block_header().slot,
85+
proposer_index: block.proposer_index(),
86+
graffiti: block.body().graffiti().as_utf8_lossy(),
87+
};
88+
89+
Ok(BlockReward {
90+
total,
91+
block_root,
92+
meta,
93+
attestation_rewards,
94+
sync_committee_rewards,
95+
})
96+
}
97+
}

beacon_node/beacon_chain/src/block_verification.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use crate::{
5353
},
5454
metrics, BeaconChain, BeaconChainError, BeaconChainTypes,
5555
};
56+
use eth2::types::EventKind;
5657
use fork_choice::{ForkChoice, ForkChoiceStore, PayloadVerificationStatus};
5758
use parking_lot::RwLockReadGuard;
5859
use proto_array::Block as ProtoBlock;
@@ -1165,6 +1166,18 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
11651166

11661167
metrics::stop_timer(committee_timer);
11671168

1169+
/*
1170+
* If we have block reward listeners, compute the block reward and push it to the
1171+
* event handler.
1172+
*/
1173+
if let Some(ref event_handler) = chain.event_handler {
1174+
if event_handler.has_block_reward_subscribers() {
1175+
let block_reward =
1176+
chain.compute_block_reward(block.message(), block_root, &state)?;
1177+
event_handler.register(EventKind::BlockReward(block_reward));
1178+
}
1179+
}
1180+
11681181
/*
11691182
* Perform `per_block_processing` on the block and state, returning early if the block is
11701183
* invalid.

beacon_node/beacon_chain/src/errors.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ pub enum BeaconChainError {
137137
AltairForkDisabled,
138138
ExecutionLayerMissing,
139139
ExecutionForkChoiceUpdateFailed(execution_layer::Error),
140+
BlockRewardSlotError,
141+
BlockRewardAttestationError,
142+
BlockRewardSyncError,
140143
HeadMissingFromForkChoice(Hash256),
141144
FinalizedBlockMissingFromForkChoice(Hash256),
142145
InvalidFinalizedPayloadShutdownError(TrySendError<ShutdownReason>),

beacon_node/beacon_chain/src/events.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct ServerSentEventHandler<T: EthSpec> {
1515
chain_reorg_tx: Sender<EventKind<T>>,
1616
contribution_tx: Sender<EventKind<T>>,
1717
late_head: Sender<EventKind<T>>,
18+
block_reward_tx: Sender<EventKind<T>>,
1819
log: Logger,
1920
}
2021

@@ -32,6 +33,7 @@ impl<T: EthSpec> ServerSentEventHandler<T> {
3233
let (chain_reorg_tx, _) = broadcast::channel(capacity);
3334
let (contribution_tx, _) = broadcast::channel(capacity);
3435
let (late_head, _) = broadcast::channel(capacity);
36+
let (block_reward_tx, _) = broadcast::channel(capacity);
3537

3638
Self {
3739
attestation_tx,
@@ -42,6 +44,7 @@ impl<T: EthSpec> ServerSentEventHandler<T> {
4244
chain_reorg_tx,
4345
contribution_tx,
4446
late_head,
47+
block_reward_tx,
4548
log,
4649
}
4750
}
@@ -67,6 +70,8 @@ impl<T: EthSpec> ServerSentEventHandler<T> {
6770
.map(|count| trace!(self.log, "Registering server-sent contribution and proof event"; "receiver_count" => count)),
6871
EventKind::LateHead(late_head) => self.late_head.send(EventKind::LateHead(late_head))
6972
.map(|count| trace!(self.log, "Registering server-sent late head event"; "receiver_count" => count)),
73+
EventKind::BlockReward(block_reward) => self.block_reward_tx.send(EventKind::BlockReward(block_reward))
74+
.map(|count| trace!(self.log, "Registering server-sent contribution and proof event"; "receiver_count" => count)),
7075
};
7176
if let Err(SendError(event)) = result {
7277
trace!(self.log, "No receivers registered to listen for event"; "event" => ?event);
@@ -105,6 +110,10 @@ impl<T: EthSpec> ServerSentEventHandler<T> {
105110
self.late_head.subscribe()
106111
}
107112

113+
pub fn subscribe_block_reward(&self) -> Receiver<EventKind<T>> {
114+
self.block_reward_tx.subscribe()
115+
}
116+
108117
pub fn has_attestation_subscribers(&self) -> bool {
109118
self.attestation_tx.receiver_count() > 0
110119
}
@@ -136,4 +145,8 @@ impl<T: EthSpec> ServerSentEventHandler<T> {
136145
pub fn has_late_head_subscribers(&self) -> bool {
137146
self.late_head.receiver_count() > 0
138147
}
148+
149+
pub fn has_block_reward_subscribers(&self) -> bool {
150+
self.block_reward_tx.receiver_count() > 0
151+
}
139152
}

beacon_node/beacon_chain/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod beacon_chain;
55
mod beacon_fork_choice_store;
66
mod beacon_proposer_cache;
77
mod beacon_snapshot;
8+
pub mod block_reward;
89
mod block_times_cache;
910
mod block_verification;
1011
pub mod builder;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped};
2+
use eth2::lighthouse::{BlockReward, BlockRewardsQuery};
3+
use slog::{warn, Logger};
4+
use state_processing::BlockReplayer;
5+
use std::sync::Arc;
6+
use warp_utils::reject::{beacon_chain_error, beacon_state_error, custom_bad_request};
7+
8+
pub fn get_block_rewards<T: BeaconChainTypes>(
9+
query: BlockRewardsQuery,
10+
chain: Arc<BeaconChain<T>>,
11+
log: Logger,
12+
) -> Result<Vec<BlockReward>, warp::Rejection> {
13+
let start_slot = query.start_slot;
14+
let end_slot = query.end_slot;
15+
let prior_slot = start_slot - 1;
16+
17+
if start_slot > end_slot || start_slot == 0 {
18+
return Err(custom_bad_request(format!(
19+
"invalid start and end: {}, {}",
20+
start_slot, end_slot
21+
)));
22+
}
23+
24+
let end_block_root = chain
25+
.block_root_at_slot(end_slot, WhenSlotSkipped::Prev)
26+
.map_err(beacon_chain_error)?
27+
.ok_or_else(|| custom_bad_request(format!("block at end slot {} unknown", end_slot)))?;
28+
29+
let blocks = chain
30+
.store
31+
.load_blocks_to_replay(start_slot, end_slot, end_block_root)
32+
.map_err(|e| beacon_chain_error(e.into()))?;
33+
34+
let state_root = chain
35+
.state_root_at_slot(prior_slot)
36+
.map_err(beacon_chain_error)?
37+
.ok_or_else(|| custom_bad_request(format!("prior state at slot {} unknown", prior_slot)))?;
38+
39+
let mut state = chain
40+
.get_state(&state_root, Some(prior_slot))
41+
.and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root)))
42+
.map_err(beacon_chain_error)?;
43+
44+
state
45+
.build_all_caches(&chain.spec)
46+
.map_err(beacon_state_error)?;
47+
48+
let mut block_rewards = Vec::with_capacity(blocks.len());
49+
50+
let block_replayer = BlockReplayer::new(state, &chain.spec)
51+
.pre_block_hook(Box::new(|state, block| {
52+
// Compute block reward.
53+
let block_reward =
54+
chain.compute_block_reward(block.message(), block.canonical_root(), state)?;
55+
block_rewards.push(block_reward);
56+
Ok(())
57+
}))
58+
.state_root_iter(
59+
chain
60+
.forwards_iter_state_roots_until(prior_slot, end_slot)
61+
.map_err(beacon_chain_error)?,
62+
)
63+
.no_signature_verification()
64+
.minimal_block_root_verification()
65+
.apply_blocks(blocks, None)
66+
.map_err(beacon_chain_error)?;
67+
68+
if block_replayer.state_root_miss() {
69+
warn!(
70+
log,
71+
"Block reward state root miss";
72+
"start_slot" => start_slot,
73+
"end_slot" => end_slot,
74+
);
75+
}
76+
77+
drop(block_replayer);
78+
79+
Ok(block_rewards)
80+
}

beacon_node/http_api/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
88
mod attester_duties;
99
mod block_id;
10+
mod block_rewards;
1011
mod database;
1112
mod metrics;
1213
mod proposer_duties;
@@ -2540,6 +2541,16 @@ pub fn serve<T: BeaconChainTypes>(
25402541
},
25412542
);
25422543

2544+
let get_lighthouse_block_rewards = warp::path("lighthouse")
2545+
.and(warp::path("block_rewards"))
2546+
.and(warp::query::<eth2::lighthouse::BlockRewardsQuery>())
2547+
.and(warp::path::end())
2548+
.and(chain_filter.clone())
2549+
.and(log_filter.clone())
2550+
.and_then(|query, chain, log| {
2551+
blocking_json_task(move || block_rewards::get_block_rewards(query, chain, log))
2552+
});
2553+
25432554
let get_events = eth1_v1
25442555
.and(warp::path("events"))
25452556
.and(warp::path::end())
@@ -2576,6 +2587,9 @@ pub fn serve<T: BeaconChainTypes>(
25762587
api_types::EventTopic::LateHead => {
25772588
event_handler.subscribe_late_head()
25782589
}
2590+
api_types::EventTopic::BlockReward => {
2591+
event_handler.subscribe_block_reward()
2592+
}
25792593
};
25802594

25812595
receivers.push(BroadcastStream::new(receiver).map(|msg| {
@@ -2661,6 +2675,7 @@ pub fn serve<T: BeaconChainTypes>(
26612675
.or(get_lighthouse_beacon_states_ssz.boxed())
26622676
.or(get_lighthouse_staking.boxed())
26632677
.or(get_lighthouse_database_info.boxed())
2678+
.or(get_lighthouse_block_rewards.boxed())
26642679
.or(get_events.boxed()),
26652680
)
26662681
.or(warp::post().and(

beacon_node/operation_pool/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ mod metrics;
66
mod persistence;
77
mod sync_aggregate_id;
88

9+
pub use attestation::AttMaxCover;
10+
pub use max_cover::MaxCover;
911
pub use persistence::{
1012
PersistedOperationPool, PersistedOperationPoolAltair, PersistedOperationPoolBase,
1113
};
1214

1315
use crate::sync_aggregate_id::SyncAggregateId;
14-
use attestation::AttMaxCover;
1516
use attestation_id::AttestationId;
1617
use attester_slashing::AttesterSlashingMaxCover;
17-
use max_cover::{maximum_cover, MaxCover};
18+
use max_cover::maximum_cover;
1819
use parking_lot::RwLock;
1920
use state_processing::per_block_processing::errors::AttestationValidationError;
2021
use state_processing::per_block_processing::{

book/src/api-lighthouse.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,4 +407,44 @@ The endpoint will return immediately. See the beacon node logs for an indication
407407
### `/lighthouse/database/historical_blocks`
408408

409409
Manually provide `SignedBeaconBlock`s to backfill the database. This is intended
410-
for use by Lighthouse developers during testing only.
410+
for use by Lighthouse developers during testing only.
411+
412+
### `/lighthouse/block_rewards`
413+
414+
Fetch information about the block rewards paid to proposers for a range of consecutive blocks.
415+
416+
Two query parameters are required:
417+
418+
* `start_slot` (inclusive): the slot of the first block to compute rewards for.
419+
* `end_slot` (inclusive): the slot of the last block to compute rewards for.
420+
421+
Example:
422+
423+
```bash
424+
curl "http://localhost:5052/lighthouse/block_rewards?start_slot=1&end_slot=32" | jq
425+
```
426+
427+
```json
428+
[
429+
{
430+
"block_root": "0x51576c2fcf0ab68d7d93c65e6828e620efbb391730511ffa35584d6c30e51410",
431+
"attestation_rewards": {
432+
"total": 4941156,
433+
},
434+
..
435+
},
436+
..
437+
]
438+
```
439+
440+
Caveats:
441+
442+
* Presently only attestation rewards are computed.
443+
* The output format is verbose and subject to change. Please see [`BlockReward`][block_reward_src]
444+
in the source.
445+
* For maximum efficiency the `start_slot` should satisfy `start_slot % slots_per_restore_point == 1`.
446+
This is because the state _prior_ to the `start_slot` needs to be loaded from the database, and
447+
loading a state on a boundary is most efficient.
448+
449+
[block_reward_src]:
450+
https://github.com/sigp/lighthouse/tree/unstable/common/eth2/src/lighthouse/block_reward.rs

0 commit comments

Comments
 (0)