Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions beacon_node/beacon_chain/src/beacon_block_reward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
initialize_epoch_cache(state, &self.spec)?;

// [New in Gloas:EIP7732] Since payload processing is deferred to the next block, the
// state doesn't have the parent's payload availability bit set yet. Set it here (if the
// payload is available) so that the head vote check on attestations matches block processing.
if state.fork_name_unchecked().gloas_enabled() {
let parent_bid = state.latest_execution_payload_bid()?;
let bid_parent_block_hash = block
.body()
.signed_execution_payload_bid()?
.message
.parent_block_hash;
if bid_parent_block_hash == parent_bid.block_hash {
let availability_index = parent_bid
.slot
.as_usize()
.safe_rem(T::EthSpec::slots_per_historical_root())?;
state
.execution_payload_availability_mut()?
.set(availability_index, true)?;
}
}

self.compute_beacon_block_reward_with_cache(block, state)
}

Expand Down
15 changes: 15 additions & 0 deletions beacon_node/beacon_chain/src/block_production/gloas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
randao_reveal,
graffiti,
&parent_execution_requests_ref,
should_build_on_full,
)
},
"produce_partial_beacon_block_gloas",
Expand Down Expand Up @@ -289,6 +290,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
randao_reveal: Signature,
graffiti: Graffiti,
parent_execution_requests: &ExecutionRequestsGloas<T::EthSpec>,
should_build_on_full: bool,
) -> Result<(PartialBeaconBlock<T::EthSpec>, BeaconState<T::EthSpec>), BlockProductionError>
{
// It is invalid to try to produce a block using a state from a future slot.
Expand Down Expand Up @@ -374,6 +376,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
state.build_total_active_balance_cache(&self.spec)?;
initialize_epoch_cache(&mut state, &self.spec)?;

// [New in Gloas:EIP7732] Since payload processing is deferred to the next block, the
// state doesn't have the parent's payload availability bit set yet. Set it here (if
// building on the parent's payload) so that attestation packing scores head votes like
// block processing does.
if should_build_on_full {
let parent_slot = state.latest_execution_payload_bid()?.slot;
let availability_index =
parent_slot.as_usize() % T::EthSpec::slots_per_historical_root();
state
.execution_payload_availability_mut()?
.set(availability_index, true)?;
}

let mut prev_filter_cache = HashMap::new();
let prev_attestation_filter = |att: &CompactAttestationRef<T::EthSpec>| {
self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state)
Expand Down
5 changes: 5 additions & 0 deletions beacon_node/beacon_chain/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use futures::channel::mpsc::TrySendError;
use milhouse::Error as MilhouseError;
use operation_pool::OpPoolError;
use safe_arith::ArithError;
use ssz::BitfieldError;
use ssz_types::Error as SszTypesError;
use state_processing::envelope_processing::EnvelopeProcessingError;
use state_processing::{
Expand Down Expand Up @@ -81,6 +82,7 @@ pub enum BeaconChainError {
BlsExecutionChangeValidationError(BlsExecutionChangeValidationError),
MissingFinalizedStateRoot(Slot),
SszTypesError(SszTypesError),
BitfieldError(BitfieldError),
NoProposerForSlot(Slot),
CanonicalHeadLockTimeout,
AttestationCacheLockTimeout,
Expand Down Expand Up @@ -269,6 +271,7 @@ easy_from_to!(ProposerSlashingValidationError, BeaconChainError);
easy_from_to!(AttesterSlashingValidationError, BeaconChainError);
easy_from_to!(BlsExecutionChangeValidationError, BeaconChainError);
easy_from_to!(SszTypesError, BeaconChainError);
easy_from_to!(BitfieldError, BeaconChainError);
easy_from_to!(OpPoolError, BeaconChainError);
easy_from_to!(NaiveAggregationError, BeaconChainError);
easy_from_to!(ObservedAttestationsError, BeaconChainError);
Expand Down Expand Up @@ -298,6 +301,7 @@ pub enum BlockProductionError {
EpochCacheError(EpochCacheError),
ForkChoiceError(ForkChoiceError),
BeaconStateError(BeaconStateError),
BitfieldError(BitfieldError),
StateAdvanceError(StateAdvanceError),
OpPoolError(OpPoolError),
StateSlotTooHigh {
Expand Down Expand Up @@ -337,6 +341,7 @@ pub enum BlockProductionError {

easy_from_to!(BlockProcessingError, BlockProductionError);
easy_from_to!(BeaconStateError, BlockProductionError);
easy_from_to!(BitfieldError, BlockProductionError);
easy_from_to!(SlotProcessingError, BlockProductionError);
easy_from_to!(StateAdvanceError, BlockProductionError);
easy_from_to!(ForkChoiceError, BlockProductionError);
Expand Down
25 changes: 22 additions & 3 deletions beacon_node/beacon_chain/src/validator_monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -728,10 +728,13 @@ impl<E: EthSpec> ValidatorMonitor<E> {

let data = unaggregated_attestation.data();

// Score the attestation as if it were included in the next block. That block
// would build on the canonical block at `data.slot`, which may have been proposed
// in an earlier slot if `data.slot` was skipped.
let parent_slot = state
.latest_execution_payload_bid()
.ok()
.map(|bid| bid.slot);
.fork_name_unchecked()
.gloas_enabled()
.then(|| canonical_block_slot(state, data.slot));

// Get the reward indices for the unaggregated attestation or log an error
match get_attestation_participation_flag_indices(
Expand Down Expand Up @@ -2053,6 +2056,22 @@ impl<E: EthSpec> ValidatorMonitor<E> {
}
}

/// Returns the slot of the canonical block at `slot`, accounting for skipped slots.
fn canonical_block_slot<E: EthSpec>(state: &BeaconState<E>, mut slot: Slot) -> Slot {
let Ok(block_root) = state.get_block_root(slot) else {
return slot;
};

while slot > 0
&& state
.get_block_root(slot - 1)
.is_ok_and(|root| root == block_root)
{
slot -= 1;
}
slot
}

fn register_simulated_attestation(
data: &AttestationData,
head_hit: bool,
Expand Down
148 changes: 147 additions & 1 deletion beacon_node/beacon_chain/tests/attestation_production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use bls::{AggregateSignature, Keypair};
use slot_clock::SlotClock;
use std::sync::{Arc, LazyLock};
use tree_hash::TreeHash;
use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot};
use types::{Attestation, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot};

pub const VALIDATOR_COUNT: usize = 32;

Expand Down Expand Up @@ -105,6 +105,152 @@ async fn produces_attestations_from_attestation_simulator_service() {
});
}

/// Checks that the attestation simulator reports a head hit for a gloas attestation made on a
/// skipped slot, which votes for the previous block's payload (`data.index == 1`).
#[tokio::test]
async fn gloas_attestation_simulator_head_hit_on_skipped_slot() {
let spec = ForkName::Gloas.make_genesis_spec(MainnetEthSpec::default_spec());
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.spec(Arc::new(spec))
.keypairs(KEYPAIRS[..].to_vec())
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
let chain = &harness.chain;

// Simulated attestations are scored once the chain is `UNAGGREGATED_ATTESTATION_LAG_SLOTS`
// past them, so produce enough blocks after the skipped slot for it to be scored.
let skipped_slot = Slot::new(3);
let last_slot = skipped_slot + UNAGGREGATED_ATTESTATION_LAG_SLOTS as u64 + 2;
for slot in 1..=last_slot.as_u64() {
harness.advance_slot();
if slot != skipped_slot.as_u64() {
harness
.extend_chain(
1,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
}
produce_unaggregated_attestation(chain.clone(), chain.slot().unwrap());

if slot == skipped_slot.as_u64() {
let validator_monitor = chain.validator_monitor.read();
let attestation = validator_monitor
.get_unaggregated_attestation(skipped_slot)
.expect("should get unaggregated attestation");
assert_eq!(
attestation.data().index,
1,
"the attestation on the skipped slot should vote for the previous block's payload"
);
}
}

// Every scored attestation is a head hit, including the one on the skipped slot.
let expected_hits = last_slot.as_u64() - UNAGGREGATED_ATTESTATION_LAG_SLOTS as u64 - 1;
assert!(expected_hits > skipped_slot.as_u64());
metrics::gather().iter().for_each(|mf| {
if mf.get_name() == metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_HIT_TOTAL
{
assert_eq!(
mf.get_metric()[0].get_counter().get_value() as u64,
expected_hits
);
}
if mf.get_name()
== metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_MISS_TOTAL
{
assert_eq!(mf.get_metric()[0].get_counter().get_value() as u64, 0);
}
});
}

/// Checks that the attestation simulator reports a head hit for a gloas attestation made on a
/// skipped slot when the previous block's payload is unavailable (`data.index == 0`).
#[tokio::test]
async fn gloas_attestation_simulator_head_hit_on_skipped_slot_without_payload() {
let spec = ForkName::Gloas.make_genesis_spec(MainnetEthSpec::default_spec());
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.spec(Arc::new(spec))
.keypairs(KEYPAIRS[..].to_vec())
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
let chain = &harness.chain;

// Build slots 1 and 2 normally, importing their payload envelopes.
harness.advance_slot();
harness
.extend_chain(
2,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;

// Import the block at slot 3 without its payload envelope.
harness.advance_slot();
let payload_unavailable_slot = Slot::new(3);
let (block_contents, _envelope, _new_state) = harness
.make_block_with_envelope(harness.get_current_state(), payload_unavailable_slot)
.await;
let block_root = block_contents.0.canonical_root();
harness
.process_block(payload_unavailable_slot, block_root, block_contents)
.await
.expect("block should import without envelope");
assert!(
!chain
.canonical_head
.fork_choice_read_lock()
.get_block(&block_root)
.expect("block should be in fork choice")
.payload_received,
"the head block's payload should be unavailable"
);

// Skip slot 4 and simulate an attestation to the payload-unavailable head block.
harness.advance_slot();
let skipped_slot = Slot::new(4);
produce_unaggregated_attestation(chain.clone(), skipped_slot);
let validator_monitor = chain.validator_monitor.read();
let attestation = validator_monitor
.get_unaggregated_attestation(skipped_slot)
.expect("should get unaggregated attestation");
assert_eq!(
attestation.data().index,
0,
"the attestation on the skipped slot should vote that the previous block's payload is unavailable"
);
drop(validator_monitor);

// Advance far enough for the simulated attestation to be scored.
for _ in 0..=UNAGGREGATED_ATTESTATION_LAG_SLOTS {
harness.advance_slot();
harness
.extend_chain(
1,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
}

metrics::gather().iter().for_each(|mf| {
if mf.get_name() == metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_HIT_TOTAL
{
assert_eq!(mf.get_metric()[0].get_counter().get_value() as u64, 1);
}
if mf.get_name()
== metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_MISS_TOTAL
{
assert_eq!(mf.get_metric()[0].get_counter().get_value() as u64, 0);
}
});
}

/// This test builds a chain that is just long enough to finalize an epoch then it produces an
/// attestation at each slot from genesis through to three epochs past the head.
///
Expand Down
85 changes: 85 additions & 0 deletions beacon_node/beacon_chain/tests/rewards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,91 @@ async fn test_rewards_electra() {
assert_eq!(expected_balances, balances);
}

/// Checks that the computed block reward matches the proposer's actual balance change when the
/// block includes attestations from a skipped slot that vote for an available payload
/// (`data.index == 1`).
#[tokio::test]
async fn test_rewards_gloas_non_same_slot_attestations() {
let spec = ForkName::Gloas.make_genesis_spec(E::default_spec());
let harness = get_harness(spec.clone());

harness.extend_slots(2).await;

let head = harness.chain.head_snapshot();
let head_root = head.beacon_block_root;
let head_slot = head.beacon_block.slot();

// Skip a slot and attest to the head. These attestations are not same-slot, and the
// head's payload is available, so `data.index == 1`.
harness.advance_slot();
let attestation_slot = head_slot + 1;
let attestations = harness.make_attestations(
&harness.get_all_validators(),
&head.beacon_state,
head.beacon_state_root(),
head_root.into(),
attestation_slot,
);
assert!(
attestations
.iter()
.flat_map(|(unaggregated, _)| unaggregated.iter())
.all(|(attestation, _)| attestation.data().index == 1),
"attestations for the skipped slot should vote for an available payload"
);
harness.process_attestations(attestations, &head.beacon_state);

// Produce the next block, which includes those attestations.
harness.advance_slot();
let block_slot = head_slot + 2;
let ((signed_block, _), mut pre_state) = harness
.make_block_return_pre_state(harness.get_current_state(), block_slot)
.await;
assert!(
signed_block
.message()
.body()
.attestations()
.any(|attestation| attestation.data().index == 1),
"the block should include attestations with data.index == 1"
);

let proposer_index = signed_block.message().proposer_index();
let balance_before = *pre_state.balances().get(proposer_index as usize).unwrap();

let block_reward = harness
.chain
.compute_beacon_block_reward(signed_block.message(), &mut pre_state)
.unwrap();
let proposer_sync_reward: i64 = harness
.chain
.compute_sync_committee_rewards(signed_block.message(), &mut pre_state)
.unwrap()
.iter()
.filter(|reward| reward.validator_index == proposer_index)
.map(|reward| reward.reward)
.sum();

harness
.process_block(
block_slot,
signed_block.canonical_root(),
(signed_block.clone(), None),
)
.await
.unwrap();

let balance_after = *harness
.get_current_state()
.balances()
.get(proposer_index as usize)
.unwrap();
assert_eq!(
balance_after as i64 - balance_before as i64,
block_reward.total as i64 + proposer_sync_reward
);
}

#[tokio::test]
async fn test_rewards_base_subset_only() {
let spec = ForkName::Base.make_genesis_spec(E::default_spec());
Expand Down
Loading
Loading