Skip to content

Commit 93ec9df

Browse files
authored
Compute proposer shuffling only once in gossip verification (#7304)
When we perform data column gossip verification, we sometimes see multiple proposer shuffling cache miss simultaneously and this results in multiple threads computing the shuffling cache and potentially slows down the gossip verification. Proposal here is to use a `OnceCell` for each shuffling key to make sure it's only computed once. I have only implemented this in data column verification as a PoC, but this can also be applied to blob and block verification Related issues: - #4447 - #7203
1 parent 9779b4b commit 93ec9df

File tree

6 files changed

+62
-35
lines changed

6 files changed

+62
-35
lines changed

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ maplit = "1"
161161
milhouse = "0.5"
162162
mockito = "1.5.0"
163163
num_cpus = "1"
164+
once_cell = "1.17.1"
164165
parking_lot = "0.12"
165166
paste = "1"
166167
prometheus = { version = "0.13", default-features = false }

beacon_node/beacon_chain/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ logging = { workspace = true }
4747
lru = { workspace = true }
4848
merkle_proof = { workspace = true }
4949
metrics = { workspace = true }
50+
once_cell = { workspace = true }
5051
oneshot_broadcast = { path = "../../common/oneshot_broadcast/" }
5152
operation_pool = { workspace = true }
5253
parking_lot = { workspace = true }

beacon_node/beacon_chain/src/beacon_proposer_cache.rs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
1212
use fork_choice::ExecutionStatus;
1313
use lru::LruCache;
14+
use once_cell::sync::OnceCell;
1415
use smallvec::SmallVec;
1516
use state_processing::state_advance::partial_state_advance;
1617
use std::cmp::Ordering;
1718
use std::num::NonZeroUsize;
19+
use std::sync::Arc;
1820
use types::non_zero_usize::new_non_zero_usize;
1921
use types::{
2022
BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned,
@@ -39,21 +41,21 @@ pub struct Proposer {
3941
/// their signatures.
4042
pub struct EpochBlockProposers {
4143
/// The epoch to which the proposers pertain.
42-
epoch: Epoch,
44+
pub(crate) epoch: Epoch,
4345
/// The fork that should be used to verify proposer signatures.
44-
fork: Fork,
46+
pub(crate) fork: Fork,
4547
/// A list of length `T::EthSpec::slots_per_epoch()`, representing the proposers for each slot
4648
/// in that epoch.
4749
///
4850
/// E.g., if `self.epoch == 1`, then `self.proposers[0]` contains the proposer for slot `32`.
49-
proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>,
51+
pub(crate) proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>,
5052
}
5153

5254
/// A cache to store the proposers for some epoch.
5355
///
5456
/// See the module-level documentation for more information.
5557
pub struct BeaconProposerCache {
56-
cache: LruCache<(Epoch, Hash256), EpochBlockProposers>,
58+
cache: LruCache<(Epoch, Hash256), Arc<OnceCell<EpochBlockProposers>>>,
5759
}
5860

5961
impl Default for BeaconProposerCache {
@@ -74,7 +76,8 @@ impl BeaconProposerCache {
7476
) -> Option<Proposer> {
7577
let epoch = slot.epoch(E::slots_per_epoch());
7678
let key = (epoch, shuffling_decision_block);
77-
if let Some(cache) = self.cache.get(&key) {
79+
let cache_opt = self.cache.get(&key).and_then(|cell| cell.get());
80+
if let Some(cache) = cache_opt {
7881
// This `if` statement is likely unnecessary, but it feels like good practice.
7982
if epoch == cache.epoch {
8083
cache
@@ -103,7 +106,26 @@ impl BeaconProposerCache {
103106
epoch: Epoch,
104107
) -> Option<&SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>> {
105108
let key = (epoch, shuffling_decision_block);
106-
self.cache.get(&key).map(|cache| &cache.proposers)
109+
self.cache
110+
.get(&key)
111+
.and_then(|cache_once_cell| cache_once_cell.get().map(|proposers| &proposers.proposers))
112+
}
113+
114+
/// Returns the `OnceCell` for the given `(epoch, shuffling_decision_block)` key,
115+
/// inserting an empty one if it doesn't exist.
116+
///
117+
/// The returned `OnceCell` allows the caller to initialise the value externally
118+
/// using `get_or_try_init`, enabling deferred computation without holding a mutable
119+
/// reference to the cache.
120+
pub fn get_or_insert_key(
121+
&mut self,
122+
epoch: Epoch,
123+
shuffling_decision_block: Hash256,
124+
) -> Arc<OnceCell<EpochBlockProposers>> {
125+
let key = (epoch, shuffling_decision_block);
126+
self.cache
127+
.get_or_insert(key, || Arc::new(OnceCell::new()))
128+
.clone()
107129
}
108130

109131
/// Insert the proposers into the cache.
@@ -120,14 +142,13 @@ impl BeaconProposerCache {
120142
) -> Result<(), BeaconStateError> {
121143
let key = (epoch, shuffling_decision_block);
122144
if !self.cache.contains(&key) {
123-
self.cache.put(
124-
key,
125-
EpochBlockProposers {
126-
epoch,
127-
fork,
128-
proposers: proposers.into(),
129-
},
130-
);
145+
let epoch_proposers = EpochBlockProposers {
146+
epoch,
147+
fork,
148+
proposers: proposers.into(),
149+
};
150+
self.cache
151+
.put(key, Arc::new(OnceCell::with_value(epoch_proposers)));
131152
}
132153

133154
Ok(())

beacon_node/beacon_chain/src/data_column_verification.rs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::beacon_proposer_cache::EpochBlockProposers;
12
use crate::block_verification::{
23
cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache, process_block_slash_info,
34
BlockSlashInfo,
@@ -602,14 +603,19 @@ fn verify_proposer_and_signature<T: BeaconChainTypes>(
602603
parent_block.root
603604
};
604605

605-
let proposer_opt = chain
606+
// We lock the cache briefly to get or insert a OnceCell, then drop the lock
607+
// before doing proposer shuffling calculation via `OnceCell::get_or_try_init`. This avoids
608+
// holding the lock during the computation, while still ensuring the result is cached and
609+
// initialised only once.
610+
//
611+
// This approach exposes the cache internals (`OnceCell` & `EpochBlockProposers`)
612+
// as a trade-off for avoiding lock contention.
613+
let epoch_proposers_cell = chain
606614
.beacon_proposer_cache
607615
.lock()
608-
.get_slot::<T::EthSpec>(proposer_shuffling_root, column_slot);
616+
.get_or_insert_key(column_epoch, proposer_shuffling_root);
609617

610-
let (proposer_index, fork) = if let Some(proposer) = proposer_opt {
611-
(proposer.index, proposer.fork)
612-
} else {
618+
let epoch_proposers = epoch_proposers_cell.get_or_try_init(move || {
613619
debug!(
614620
%block_root,
615621
index = %column_index,
@@ -633,19 +639,20 @@ fn verify_proposer_and_signature<T: BeaconChainTypes>(
633639
)?;
634640

635641
let proposers = state.get_beacon_proposer_indices(&chain.spec)?;
636-
let proposer_index = *proposers
637-
.get(column_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize)
638-
.ok_or_else(|| BeaconChainError::NoProposerForSlot(column_slot))?;
639-
640642
// Prime the proposer shuffling cache with the newly-learned value.
641-
chain.beacon_proposer_cache.lock().insert(
642-
column_epoch,
643-
proposer_shuffling_root,
644-
proposers,
645-
state.fork(),
646-
)?;
647-
(proposer_index, state.fork())
648-
};
643+
Ok::<_, GossipDataColumnError>(EpochBlockProposers {
644+
epoch: column_epoch,
645+
fork: state.fork(),
646+
proposers: proposers.into(),
647+
})
648+
})?;
649+
650+
let proposer_index = *epoch_proposers
651+
.proposers
652+
.get(column_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize)
653+
.ok_or_else(|| BeaconChainError::NoProposerForSlot(column_slot))?;
654+
655+
let fork = epoch_proposers.fork;
649656

650657
// Signature verify the signed block header.
651658
let signature_is_valid = {

common/logging/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ test_logger = [] # Print log output to stderr when running tests instead of drop
1111
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
1212
logroller = { workspace = true }
1313
metrics = { workspace = true }
14-
once_cell = "1.17.1"
15-
parking_lot = { workspace = true }
1614
serde = { workspace = true }
1715
serde_json = { workspace = true }
1816
tokio = { workspace = true, features = [ "time" ] }

0 commit comments

Comments
 (0)