Skip to content

Commit d766764

Browse files
authored
Merge of #8130
2 parents 79716f6 + 6d1b27d commit d766764

File tree

8 files changed

+359
-59
lines changed

8 files changed

+359
-59
lines changed

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4726,6 +4726,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
47264726
// efficient packing of execution blocks.
47274727
Err(Error::SkipProposerPreparation)
47284728
} else {
4729+
debug!(
4730+
?shuffling_decision_root,
4731+
epoch = %proposal_epoch,
4732+
"Proposer shuffling cache miss for proposer prep"
4733+
);
47294734
let head = self.canonical_head.cached_head();
47304735
Ok((
47314736
head.head_state_root(),
@@ -6557,6 +6562,26 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
65576562
}
65586563
}
65596564

6565+
/// This function provides safe and efficient multi-threaded access to the beacon proposer cache.
6566+
///
6567+
/// The arguments are:
6568+
///
6569+
/// - `shuffling_decision_block`: The block root of the decision block for the desired proposer
6570+
/// shuffling. This should be computed using one of the methods for computing proposer
6571+
/// shuffling decision roots, e.g. `BeaconState::proposer_shuffling_decision_root_at_epoch`.
6572+
/// - `proposal_epoch`: The epoch at which the proposer shuffling is required.
6573+
/// - `accessor`: A closure to run against the proposers for the selected epoch. Usually this
6574+
/// closure just grabs a single proposer, or takes the vec of proposers for the epoch.
6575+
/// - `state_provider`: A closure to compute a state suitable for determining the shuffling.
6576+
/// This closure is evaluated lazily ONLY in the case that a cache miss occurs. It is
6577+
/// recommended for code that wants to keep track of cache misses to produce a log and/or
6578+
/// increment a metric inside this closure .
6579+
///
6580+
/// This function makes use of closures in order to efficiently handle concurrent accesses to
6581+
/// the cache.
6582+
///
6583+
/// The error type is polymorphic, if in doubt you can use `BeaconChainError`. You might need
6584+
/// to use a turbofish if type inference can't work it out.
65606585
pub fn with_proposer_cache<V, E: From<BeaconChainError> + From<BeaconStateError>>(
65616586
&self,
65626587
shuffling_decision_block: Hash256,
@@ -6575,12 +6600,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
65756600
// If it is already initialised, then `get_or_try_init` will return immediately without
65766601
// executing the initialisation code at all.
65776602
let epoch_block_proposers = cache_entry.get_or_try_init(|| {
6578-
debug!(
6579-
?shuffling_decision_block,
6580-
%proposal_epoch,
6581-
"Proposer shuffling cache miss"
6582-
);
6583-
65846603
// Fetch the state on-demand if the required epoch was missing from the cache.
65856604
// If the caller wants to not compute the state they must return an error here and then
65866605
// catch it at the call site.
@@ -6610,11 +6629,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
66106629
}
66116630

66126631
let proposers = state.get_beacon_proposer_indices(proposal_epoch, &self.spec)?;
6613-
Ok::<_, E>(EpochBlockProposers::new(
6614-
proposal_epoch,
6615-
state.fork(),
6616-
proposers,
6617-
))
6632+
6633+
// Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have
6634+
// advanced the state completely into the new epoch.
6635+
let fork = self.spec.fork_at_epoch(proposal_epoch);
6636+
6637+
debug!(
6638+
?shuffling_decision_block,
6639+
epoch = %proposal_epoch,
6640+
"Priming proposer shuffling cache"
6641+
);
6642+
6643+
Ok::<_, E>(EpochBlockProposers::new(proposal_epoch, fork, proposers))
66186644
})?;
66196645

66206646
// Run the accessor function on the computed epoch proposers.

beacon_node/beacon_chain/src/beacon_proposer_cache.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use smallvec::SmallVec;
1717
use state_processing::state_advance::partial_state_advance;
1818
use std::num::NonZeroUsize;
1919
use std::sync::Arc;
20+
use tracing::instrument;
2021
use types::non_zero_usize::new_non_zero_usize;
2122
use types::{
2223
BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned,
@@ -199,11 +200,14 @@ pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
199200
.map_err(BeaconChainError::from)?;
200201

201202
let dependent_root = state
202-
// The only block which decides its own shuffling is the genesis block.
203-
.proposer_shuffling_decision_root(chain.genesis_block_root, &chain.spec)
203+
.proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root, &chain.spec)
204204
.map_err(BeaconChainError::from)?;
205205

206-
Ok((indices, dependent_root, execution_status, state.fork()))
206+
// Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have advanced
207+
// the state completely into the new epoch.
208+
let fork = chain.spec.fork_at_epoch(request_epoch);
209+
210+
Ok((indices, dependent_root, execution_status, fork))
207211
}
208212

209213
/// If required, advance `state` to the epoch required to determine proposer indices in `target_epoch`.
@@ -214,6 +218,7 @@ pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
214218
/// - No-op if `state.current_epoch() == target_epoch`.
215219
/// - It must be the case that `state.canonical_root() == state_root`, but this function will not
216220
/// check that.
221+
#[instrument(skip_all, fields(?state_root, %target_epoch, state_slot = %state.slot()), level = "debug")]
217222
pub fn ensure_state_can_determine_proposers_for_epoch<E: EthSpec>(
218223
state: &mut BeaconState<E>,
219224
state_root: Hash256,
@@ -234,14 +239,6 @@ pub fn ensure_state_can_determine_proposers_for_epoch<E: EthSpec>(
234239
if state.current_epoch() > maximum_epoch {
235240
Err(BeaconStateError::SlotOutOfBounds.into())
236241
} else if state.current_epoch() >= minimum_epoch {
237-
if target_epoch > state.current_epoch() {
238-
let target_slot = target_epoch.start_slot(E::slots_per_epoch());
239-
240-
// Advance the state into the same epoch as the block. Use the "partial" method since state
241-
// roots are not important for proposer/attester shuffling.
242-
partial_state_advance(state, Some(state_root), target_slot, spec)
243-
.map_err(BeaconChainError::from)?;
244-
}
245242
Ok(())
246243
} else {
247244
// State's current epoch is less than the minimum epoch.

beacon_node/beacon_chain/src/block_verification.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -950,8 +950,6 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> {
950950
let proposer_shuffling_decision_block =
951951
parent_block.proposer_shuffling_root_for_child_block(block_epoch, &chain.spec);
952952

953-
// We assign to a variable instead of using `if let Some` directly to ensure we drop the
954-
// write lock before trying to acquire it again in the `else` clause.
955953
let block_slot = block.slot();
956954
let mut opt_parent = None;
957955
let proposer = chain.with_proposer_cache::<_, BlockError>(

beacon_node/beacon_chain/src/state_advance_timer.rs

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -333,25 +333,54 @@ fn advance_head<T: BeaconChainTypes>(beacon_chain: &Arc<BeaconChain<T>>) -> Resu
333333
.build_committee_cache(RelativeEpoch::Next, &beacon_chain.spec)
334334
.map_err(BeaconChainError::from)?;
335335

336-
// If the `pre_state` is in a later epoch than `state`, pre-emptively add the proposer shuffling
337-
// for the state's current epoch and the committee cache for the state's next epoch.
336+
// The state root is required to prime the proposer cache AND for writing it to disk.
337+
let advanced_state_root = state.update_tree_hash_cache()?;
338+
339+
// If the `pre_state` is in a later epoch than `state`, pre-emptively update the proposer
340+
// shuffling and attester shuffling caches.
338341
if initial_epoch < state.current_epoch() {
339-
// Update the proposer cache.
340-
//
341-
// We supply the `head_block_root` as the decision block since the prior `if` statement guarantees
342-
// the head root is the latest block from the prior epoch.
343-
beacon_chain
344-
.beacon_proposer_cache
345-
.lock()
346-
.insert(
347-
state.current_epoch(),
348-
head_block_root,
349-
state
350-
.get_beacon_proposer_indices(state.current_epoch(), &beacon_chain.spec)
351-
.map_err(BeaconChainError::from)?,
352-
state.fork(),
353-
)
354-
.map_err(BeaconChainError::from)?;
342+
// Include the proposer shuffling from the current epoch, which is likely to be useful
343+
// pre-Fulu, and probably redundant post-Fulu (it should already have been in the cache).
344+
let current_epoch_decision_root = state.proposer_shuffling_decision_root_at_epoch(
345+
state.current_epoch(),
346+
head_block_root,
347+
&beacon_chain.spec,
348+
)?;
349+
beacon_chain.with_proposer_cache(
350+
current_epoch_decision_root,
351+
state.current_epoch(),
352+
|_| Ok(()),
353+
|| {
354+
debug!(
355+
shuffling_decision_root = ?current_epoch_decision_root,
356+
epoch = %state.current_epoch(),
357+
"Computing current epoch proposer shuffling in state advance"
358+
);
359+
Ok::<_, Error>((advanced_state_root, state.clone()))
360+
},
361+
)?;
362+
363+
// For epochs *greater than* the Fulu fork epoch, we have also determined the proposer
364+
// shuffling for the next epoch.
365+
let next_epoch = state.next_epoch()?;
366+
let next_epoch_decision_root = state.proposer_shuffling_decision_root_at_epoch(
367+
next_epoch,
368+
head_block_root,
369+
&beacon_chain.spec,
370+
)?;
371+
beacon_chain.with_proposer_cache(
372+
next_epoch_decision_root,
373+
next_epoch,
374+
|_| Ok(()),
375+
|| {
376+
debug!(
377+
shuffling_decision_root = ?next_epoch_decision_root,
378+
epoch = %next_epoch,
379+
"Computing next epoch proposer shuffling in state advance"
380+
);
381+
Ok::<_, Error>((advanced_state_root, state.clone()))
382+
},
383+
)?;
355384

356385
// Update the attester cache.
357386
let shuffling_id =
@@ -406,7 +435,6 @@ fn advance_head<T: BeaconChainTypes>(beacon_chain: &Arc<BeaconChain<T>>) -> Resu
406435
// even if we race with the deletion of this state by the finalization pruning code, the worst
407436
// case is we end up with a finalized state stored, that will get pruned the next time pruning
408437
// runs.
409-
let advanced_state_root = state.update_tree_hash_cache()?;
410438
beacon_chain.store.put_state(&advanced_state_root, &state)?;
411439

412440
debug!(

beacon_node/beacon_chain/tests/store_tests.rs

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ use beacon_chain::test_utils::{
1313
use beacon_chain::{
1414
BeaconChain, BeaconChainError, BeaconChainTypes, BeaconSnapshot, BlockError, ChainConfig,
1515
NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped,
16-
data_availability_checker::MaybeAvailableBlock, historical_blocks::HistoricalBlockError,
16+
beacon_proposer_cache::{
17+
compute_proposer_duties_from_head, ensure_state_can_determine_proposers_for_epoch,
18+
},
19+
data_availability_checker::MaybeAvailableBlock,
20+
historical_blocks::HistoricalBlockError,
1721
migrate::MigratorConfig,
1822
};
1923
use logging::create_test_tracing_subscriber;
@@ -1273,19 +1277,34 @@ async fn proposer_shuffling_root_consistency_test(
12731277
#[tokio::test]
12741278
async fn proposer_shuffling_root_consistency_same_epoch() {
12751279
let spec = test_spec::<E>();
1276-
proposer_shuffling_root_consistency_test(spec, 32, 39).await;
1280+
proposer_shuffling_root_consistency_test(
1281+
spec,
1282+
4 * E::slots_per_epoch(),
1283+
5 * E::slots_per_epoch() - 1,
1284+
)
1285+
.await;
12771286
}
12781287

12791288
#[tokio::test]
12801289
async fn proposer_shuffling_root_consistency_next_epoch() {
12811290
let spec = test_spec::<E>();
1282-
proposer_shuffling_root_consistency_test(spec, 32, 47).await;
1291+
proposer_shuffling_root_consistency_test(
1292+
spec,
1293+
4 * E::slots_per_epoch(),
1294+
6 * E::slots_per_epoch() - 1,
1295+
)
1296+
.await;
12831297
}
12841298

12851299
#[tokio::test]
12861300
async fn proposer_shuffling_root_consistency_two_epochs() {
12871301
let spec = test_spec::<E>();
1288-
proposer_shuffling_root_consistency_test(spec, 32, 55).await;
1302+
proposer_shuffling_root_consistency_test(
1303+
spec,
1304+
4 * E::slots_per_epoch(),
1305+
7 * E::slots_per_epoch() - 1,
1306+
)
1307+
.await;
12891308
}
12901309

12911310
#[tokio::test]
@@ -1501,6 +1520,120 @@ async fn proposer_shuffling_changing_with_lookahead() {
15011520
);
15021521
}
15031522

1523+
#[tokio::test]
1524+
async fn proposer_duties_from_head_fulu() {
1525+
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
1526+
1527+
let db_path = tempdir().unwrap();
1528+
let store = get_store_generic(&db_path, Default::default(), spec.clone());
1529+
let validators_keypairs =
1530+
types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT);
1531+
let harness = TestHarness::builder(MinimalEthSpec)
1532+
.spec(spec.into())
1533+
.keypairs(validators_keypairs)
1534+
.fresh_disk_store(store)
1535+
.mock_execution_layer()
1536+
.build();
1537+
let spec = &harness.chain.spec;
1538+
1539+
let initial_blocks = E::slots_per_epoch() * 3;
1540+
1541+
// Build chain out to parent block.
1542+
let initial_slots: Vec<Slot> = (1..=initial_blocks).map(Into::into).collect();
1543+
let (state, state_root) = harness.get_current_state_and_root();
1544+
let all_validators = harness.get_all_validators();
1545+
let (_, _, head_block_root, head_state) = harness
1546+
.add_attested_blocks_at_slots(state, state_root, &initial_slots, &all_validators)
1547+
.await;
1548+
1549+
// Compute the proposer duties at the next epoch from the head
1550+
let next_epoch = head_state.next_epoch().unwrap();
1551+
let (_indices, dependent_root, _, fork) =
1552+
compute_proposer_duties_from_head(next_epoch, &harness.chain).unwrap();
1553+
1554+
assert_eq!(
1555+
dependent_root,
1556+
head_state
1557+
.proposer_shuffling_decision_root_at_epoch(next_epoch, head_block_root.into(), spec)
1558+
.unwrap()
1559+
);
1560+
assert_eq!(fork, head_state.fork());
1561+
}
1562+
1563+
/// Test that we can compute the proposer shuffling for the Gloas fork epoch itself using lookahead!
1564+
#[tokio::test]
1565+
async fn proposer_lookahead_gloas_fork_epoch() {
1566+
let gloas_fork_epoch = Epoch::new(4);
1567+
let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
1568+
spec.gloas_fork_epoch = Some(gloas_fork_epoch);
1569+
1570+
let db_path = tempdir().unwrap();
1571+
let store = get_store_generic(&db_path, Default::default(), spec.clone());
1572+
let validators_keypairs =
1573+
types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT);
1574+
let harness = TestHarness::builder(E::default())
1575+
.spec(spec.into())
1576+
.keypairs(validators_keypairs)
1577+
.fresh_disk_store(store)
1578+
.mock_execution_layer()
1579+
.build();
1580+
let spec = &harness.chain.spec;
1581+
1582+
let initial_blocks = (gloas_fork_epoch - 1)
1583+
.start_slot(E::slots_per_epoch())
1584+
.as_u64();
1585+
1586+
// Build chain out to parent block.
1587+
let initial_slots: Vec<Slot> = (1..=initial_blocks).map(Into::into).collect();
1588+
let (state, state_root) = harness.get_current_state_and_root();
1589+
let all_validators = harness.get_all_validators();
1590+
let (_, _, head_block_root, mut head_state) = harness
1591+
.add_attested_blocks_at_slots(state, state_root, &initial_slots, &all_validators)
1592+
.await;
1593+
let head_state_root = head_state.canonical_root().unwrap();
1594+
1595+
// Check that we have access to the next epoch shuffling according to
1596+
// `ensure_state_can_determine_proposers_for_epoch`.
1597+
ensure_state_can_determine_proposers_for_epoch(
1598+
&mut head_state,
1599+
head_state_root,
1600+
gloas_fork_epoch,
1601+
spec,
1602+
)
1603+
.unwrap();
1604+
assert_eq!(head_state.current_epoch(), gloas_fork_epoch - 1);
1605+
1606+
// Compute the proposer duties at the fork epoch from the head.
1607+
let (indices, dependent_root, _, fork) =
1608+
compute_proposer_duties_from_head(gloas_fork_epoch, &harness.chain).unwrap();
1609+
1610+
assert_eq!(
1611+
dependent_root,
1612+
head_state
1613+
.proposer_shuffling_decision_root_at_epoch(
1614+
gloas_fork_epoch,
1615+
head_block_root.into(),
1616+
spec
1617+
)
1618+
.unwrap()
1619+
);
1620+
assert_ne!(fork, head_state.fork());
1621+
assert_eq!(fork, spec.fork_at_epoch(gloas_fork_epoch));
1622+
1623+
// Build a block in the Gloas fork epoch and assert that the shuffling does not change.
1624+
let gloas_slots = vec![gloas_fork_epoch.start_slot(E::slots_per_epoch())];
1625+
let (_, _, _, _) = harness
1626+
.add_attested_blocks_at_slots(head_state, head_state_root, &gloas_slots, &all_validators)
1627+
.await;
1628+
1629+
let (no_lookahead_indices, no_lookahead_dependent_root, _, no_lookahead_fork) =
1630+
compute_proposer_duties_from_head(gloas_fork_epoch, &harness.chain).unwrap();
1631+
1632+
assert_eq!(no_lookahead_indices, indices);
1633+
assert_eq!(no_lookahead_dependent_root, dependent_root);
1634+
assert_eq!(no_lookahead_fork, fork);
1635+
}
1636+
15041637
// Ensure blocks from abandoned forks are pruned from the Hot DB
15051638
#[tokio::test]
15061639
async fn prunes_abandoned_fork_between_two_finalized_checkpoints() {

0 commit comments

Comments
 (0)