Skip to content

Commit 0344f68

Browse files
Update attestation rewards API for Electra (#6819)
Closes: - #6818 Use `MAX_EFFECTIVE_BALANCE_ELECTRA` (2048) for attestation reward calculations involving Electra. Add a new `InteropGenesisBuilder` that tries to provide a more flexible way to build genesis states. Unfortunately due to lifetime jank, it is quite unergonomic at present. We may want to refactor this builder in future to make it easier to use.
1 parent 6032f15 commit 0344f68

File tree

6 files changed

+341
-134
lines changed

6 files changed

+341
-134
lines changed

beacon_node/beacon_chain/src/attestation_rewards.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
175175
let base_reward_per_increment =
176176
BaseRewardPerIncrement::new(total_active_balance, spec)?;
177177

178-
for effective_balance_eth in 1..=self.max_effective_balance_increment_steps()? {
178+
for effective_balance_eth in
179+
1..=self.max_effective_balance_increment_steps(previous_epoch)?
180+
{
179181
let effective_balance =
180182
effective_balance_eth.safe_mul(spec.effective_balance_increment)?;
181183
let base_reward =
@@ -321,11 +323,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
321323
})
322324
}
323325

324-
fn max_effective_balance_increment_steps(&self) -> Result<u64, BeaconChainError> {
326+
fn max_effective_balance_increment_steps(
327+
&self,
328+
rewards_epoch: Epoch,
329+
) -> Result<u64, BeaconChainError> {
325330
let spec = &self.spec;
326-
let max_steps = spec
327-
.max_effective_balance
328-
.safe_div(spec.effective_balance_increment)?;
331+
let fork_name = spec.fork_name_at_epoch(rewards_epoch);
332+
let max_effective_balance = spec.max_effective_balance_for_fork(fork_name);
333+
let max_steps = max_effective_balance.safe_div(spec.effective_balance_increment)?;
329334
Ok(max_steps)
330335
}
331336

@@ -386,7 +391,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
386391

387392
let mut ideal_attestation_rewards_list = Vec::new();
388393
let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_balances.current_epoch());
389-
for effective_balance_step in 1..=self.max_effective_balance_increment_steps()? {
394+
for effective_balance_step in
395+
1..=self.max_effective_balance_increment_steps(previous_epoch)?
396+
{
390397
let effective_balance =
391398
effective_balance_step.safe_mul(spec.effective_balance_increment)?;
392399
let base_reward =

beacon_node/beacon_chain/src/test_utils.rs

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use execution_layer::{
3131
ExecutionLayer,
3232
};
3333
use futures::channel::mpsc::Receiver;
34-
pub use genesis::{interop_genesis_state_with_eth1, DEFAULT_ETH1_BLOCK_HASH};
34+
pub use genesis::{InteropGenesisBuilder, DEFAULT_ETH1_BLOCK_HASH};
3535
use int_to_bytes::int_to_bytes32;
3636
use kzg::trusted_setup::get_trusted_setup;
3737
use kzg::{Kzg, TrustedSetup};
@@ -232,6 +232,7 @@ pub struct Builder<T: BeaconChainTypes> {
232232
mock_execution_layer: Option<MockExecutionLayer<T::EthSpec>>,
233233
testing_slot_clock: Option<TestingSlotClock>,
234234
validator_monitor_config: Option<ValidatorMonitorConfig>,
235+
genesis_state_builder: Option<InteropGenesisBuilder<T::EthSpec>>,
235236
import_all_data_columns: bool,
236237
runtime: TestRuntime,
237238
log: Logger,
@@ -253,16 +254,22 @@ impl<E: EthSpec> Builder<EphemeralHarnessType<E>> {
253254
)
254255
.unwrap(),
255256
);
257+
let genesis_state_builder = self.genesis_state_builder.take().unwrap_or_else(|| {
258+
// Set alternating withdrawal credentials if no builder is specified.
259+
InteropGenesisBuilder::default().set_alternating_eth1_withdrawal_credentials()
260+
});
261+
256262
let mutator = move |builder: BeaconChainBuilder<_>| {
257263
let header = generate_genesis_header::<E>(builder.get_spec(), false);
258-
let genesis_state = interop_genesis_state_with_eth1::<E>(
259-
&validator_keypairs,
260-
HARNESS_GENESIS_TIME,
261-
Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
262-
header,
263-
builder.get_spec(),
264-
)
265-
.expect("should generate interop state");
264+
let genesis_state = genesis_state_builder
265+
.set_opt_execution_payload_header(header)
266+
.build_genesis_state(
267+
&validator_keypairs,
268+
HARNESS_GENESIS_TIME,
269+
Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
270+
builder.get_spec(),
271+
)
272+
.expect("should generate interop state");
266273
builder
267274
.genesis_state(genesis_state)
268275
.expect("should build state using recent genesis")
@@ -318,16 +325,22 @@ impl<E: EthSpec> Builder<DiskHarnessType<E>> {
318325
.clone()
319326
.expect("cannot build without validator keypairs");
320327

328+
let genesis_state_builder = self.genesis_state_builder.take().unwrap_or_else(|| {
329+
// Set alternating withdrawal credentials if no builder is specified.
330+
InteropGenesisBuilder::default().set_alternating_eth1_withdrawal_credentials()
331+
});
332+
321333
let mutator = move |builder: BeaconChainBuilder<_>| {
322334
let header = generate_genesis_header::<E>(builder.get_spec(), false);
323-
let genesis_state = interop_genesis_state_with_eth1::<E>(
324-
&validator_keypairs,
325-
HARNESS_GENESIS_TIME,
326-
Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
327-
header,
328-
builder.get_spec(),
329-
)
330-
.expect("should generate interop state");
335+
let genesis_state = genesis_state_builder
336+
.set_opt_execution_payload_header(header)
337+
.build_genesis_state(
338+
&validator_keypairs,
339+
HARNESS_GENESIS_TIME,
340+
Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
341+
builder.get_spec(),
342+
)
343+
.expect("should generate interop state");
331344
builder
332345
.genesis_state(genesis_state)
333346
.expect("should build state using recent genesis")
@@ -375,6 +388,7 @@ where
375388
mock_execution_layer: None,
376389
testing_slot_clock: None,
377390
validator_monitor_config: None,
391+
genesis_state_builder: None,
378392
import_all_data_columns: false,
379393
runtime,
380394
log,
@@ -560,6 +574,15 @@ where
560574
self
561575
}
562576

577+
pub fn with_genesis_state_builder(
578+
mut self,
579+
f: impl FnOnce(InteropGenesisBuilder<E>) -> InteropGenesisBuilder<E>,
580+
) -> Self {
581+
let builder = self.genesis_state_builder.take().unwrap_or_default();
582+
self.genesis_state_builder = Some(f(builder));
583+
self
584+
}
585+
563586
pub fn build(self) -> BeaconChainHarness<BaseHarnessType<E, Hot, Cold>> {
564587
let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1);
565588

beacon_node/beacon_chain/tests/rewards.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,38 @@ fn get_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> {
3636
.keypairs(KEYPAIRS.to_vec())
3737
.fresh_ephemeral_store()
3838
.chain_config(chain_config)
39+
.mock_execution_layer()
40+
.build();
41+
42+
harness.advance_slot();
43+
44+
harness
45+
}
46+
47+
fn get_electra_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> {
48+
let chain_config = ChainConfig {
49+
reconstruct_historic_states: true,
50+
..Default::default()
51+
};
52+
53+
let spec = Arc::new(spec);
54+
55+
let harness = BeaconChainHarness::builder(E::default())
56+
.spec(spec.clone())
57+
.keypairs(KEYPAIRS.to_vec())
58+
.with_genesis_state_builder(|builder| {
59+
builder.set_initial_balance_fn(Box::new(move |i| {
60+
// Use a variety of balances between min activation balance and max effective balance.
61+
let balance = spec.max_effective_balance_electra
62+
/ (i as u64 + 1)
63+
/ spec.effective_balance_increment
64+
* spec.effective_balance_increment;
65+
balance.max(spec.min_activation_balance)
66+
}))
67+
})
68+
.fresh_ephemeral_store()
69+
.chain_config(chain_config)
70+
.mock_execution_layer()
3971
.build();
4072

4173
harness.advance_slot();
@@ -560,6 +592,83 @@ async fn test_rewards_altair_inactivity_leak_justification_epoch() {
560592
assert_eq!(expected_balances, balances);
561593
}
562594

595+
#[tokio::test]
596+
async fn test_rewards_electra() {
597+
let spec = ForkName::Electra.make_genesis_spec(E::default_spec());
598+
let harness = get_electra_harness(spec.clone());
599+
let target_epoch = 0;
600+
601+
// advance until epoch N + 1 and get initial balances
602+
harness
603+
.extend_slots((E::slots_per_epoch() * (target_epoch + 1)) as usize)
604+
.await;
605+
let mut expected_balances = harness.get_current_state().balances().to_vec();
606+
607+
// advance until epoch N + 2 and build proposal rewards map
608+
let mut proposal_rewards_map = HashMap::new();
609+
let mut sync_committee_rewards_map = HashMap::new();
610+
for _ in 0..E::slots_per_epoch() {
611+
let state = harness.get_current_state();
612+
let slot = state.slot() + Slot::new(1);
613+
614+
// calculate beacon block rewards / penalties
615+
let ((signed_block, _maybe_blob_sidecars), mut state) =
616+
harness.make_block_return_pre_state(state, slot).await;
617+
let beacon_block_reward = harness
618+
.chain
619+
.compute_beacon_block_reward(signed_block.message(), &mut state)
620+
.unwrap();
621+
622+
let total_proposer_reward = proposal_rewards_map
623+
.entry(beacon_block_reward.proposer_index)
624+
.or_insert(0);
625+
*total_proposer_reward += beacon_block_reward.total as i64;
626+
627+
// calculate sync committee rewards / penalties
628+
let reward_payload = harness
629+
.chain
630+
.compute_sync_committee_rewards(signed_block.message(), &mut state)
631+
.unwrap();
632+
633+
for reward in reward_payload {
634+
let total_sync_reward = sync_committee_rewards_map
635+
.entry(reward.validator_index)
636+
.or_insert(0);
637+
*total_sync_reward += reward.reward;
638+
}
639+
640+
harness.extend_slots(1).await;
641+
}
642+
643+
// compute reward deltas for all validators in epoch N
644+
let StandardAttestationRewards {
645+
ideal_rewards,
646+
total_rewards,
647+
} = harness
648+
.chain
649+
.compute_attestation_rewards(Epoch::new(target_epoch), vec![])
650+
.unwrap();
651+
652+
// assert ideal rewards are greater than 0
653+
assert_eq!(
654+
ideal_rewards.len() as u64,
655+
spec.max_effective_balance_electra / spec.effective_balance_increment
656+
);
657+
assert!(ideal_rewards
658+
.iter()
659+
.all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0));
660+
661+
// apply attestation, proposal, and sync committee rewards and penalties to initial balances
662+
apply_attestation_rewards(&mut expected_balances, total_rewards);
663+
apply_other_rewards(&mut expected_balances, &proposal_rewards_map);
664+
apply_other_rewards(&mut expected_balances, &sync_committee_rewards_map);
665+
666+
// verify expected balances against actual balances
667+
let balances: Vec<u64> = harness.get_current_state().balances().to_vec();
668+
669+
assert_eq!(expected_balances, balances);
670+
}
671+
563672
#[tokio::test]
564673
async fn test_rewards_base_subset_only() {
565674
let spec = ForkName::Base.make_genesis_spec(E::default_spec());

0 commit comments

Comments
 (0)