Skip to content

Commit 9c785a9

Browse files
committed
Optimize process_attestation with active balance cache (#2560)
## Proposed Changes Cache the total active balance for the current epoch in the `BeaconState`. Computing this value takes around 1ms, and this was negatively impacting block processing times on Prater, particularly when reconstructing states. With a large number of attestations in each block, I saw the `process_attestations` function taking 150ms, which means that reconstructing hot states can take up to 4.65s (31 * 150ms), and reconstructing freezer states can take up to 307s (2047 * 150ms). I opted to add the cache to the beacon state rather than computing the total active balance at the start of state processing and threading it through. Although this would be simpler in a way, it would waste time, particularly during block replay, as the total active balance doesn't change for the duration of an epoch. So we save ~32ms for hot states, and up to 8.1s for freezer states (using `--slots-per-restore-point 8192`).
1 parent f4aa1d8 commit 9c785a9

File tree

10 files changed

+100
-24
lines changed

10 files changed

+100
-24
lines changed

beacon_node/operation_pool/src/lib.rs

+4-10
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ use std::ptr;
2828
use types::{
2929
sync_aggregate::Error as SyncAggregateError, typenum::Unsigned, Attestation, AttesterSlashing,
3030
BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, Hash256,
31-
ProposerSlashing, RelativeEpoch, SignedVoluntaryExit, Slot, SyncAggregate,
32-
SyncCommitteeContribution, Validator,
31+
ProposerSlashing, SignedVoluntaryExit, Slot, SyncAggregate, SyncCommitteeContribution,
32+
Validator,
3333
};
3434

3535
type SyncContributions<T> = RwLock<HashMap<SyncAggregateId, Vec<SyncCommitteeContribution<T>>>>;
@@ -259,11 +259,8 @@ impl<T: EthSpec> OperationPool<T> {
259259
let prev_epoch = state.previous_epoch();
260260
let current_epoch = state.current_epoch();
261261
let all_attestations = self.attestations.read();
262-
let active_indices = state
263-
.get_cached_active_validator_indices(RelativeEpoch::Current)
264-
.map_err(OpPoolError::GetAttestationsTotalBalanceError)?;
265262
let total_active_balance = state
266-
.get_total_balance(active_indices, spec)
263+
.get_total_active_balance()
267264
.map_err(OpPoolError::GetAttestationsTotalBalanceError)?;
268265

269266
// Split attestations for the previous & current epochs, so that we
@@ -1143,10 +1140,7 @@ mod release_tests {
11431140
.expect("should have valid best attestations");
11441141
assert_eq!(best_attestations.len(), max_attestations);
11451142

1146-
let active_indices = state
1147-
.get_cached_active_validator_indices(RelativeEpoch::Current)
1148-
.unwrap();
1149-
let total_active_balance = state.get_total_balance(active_indices, spec).unwrap();
1143+
let total_active_balance = state.get_total_active_balance().unwrap();
11501144

11511145
// Set of indices covered by previous attestations in `best_attestations`.
11521146
let mut seen_indices = BTreeSet::new();

beacon_node/store/src/partial_beacon_state.rs

+1
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ macro_rules! impl_try_into_beacon_state {
300300
finalized_checkpoint: $inner.finalized_checkpoint,
301301

302302
// Caching
303+
total_active_balance: <_>::default(),
303304
committee_caches: <_>::default(),
304305
pubkey_cache: <_>::default(),
305306
exit_cache: <_>::default(),

consensus/state_processing/src/per_block_processing/altair/sync_committee.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub fn process_sync_aggregate<T: EthSpec>(
4242
}
4343

4444
// Compute participant and proposer rewards
45-
let total_active_balance = state.get_total_active_balance(spec)?;
45+
let total_active_balance = state.get_total_active_balance()?;
4646
let total_active_increments =
4747
total_active_balance.safe_div(spec.effective_balance_increment)?;
4848
let total_base_rewards = get_base_reward_per_increment(total_active_balance, spec)?

consensus/state_processing/src/per_block_processing/process_operations.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ pub mod altair {
127127
get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?;
128128

129129
// Update epoch participation flags.
130-
let total_active_balance = state.get_total_active_balance(spec)?;
130+
let total_active_balance = state.get_total_active_balance()?;
131131
let mut proposer_reward_numerator = 0;
132132
for index in &indexed_attestation.attesting_indices {
133133
let index = *index as usize;

consensus/state_processing/src/per_epoch_processing/altair.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pub fn process_epoch<T: EthSpec>(
7272
process_sync_committee_updates(state, spec)?;
7373

7474
// Rotate the epoch caches to suit the epoch transition.
75-
state.advance_caches()?;
75+
state.advance_caches(spec)?;
7676

7777
Ok(EpochProcessingSummary::Altair {
7878
participation_cache,

consensus/state_processing/src/per_epoch_processing/base.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub fn process_epoch<T: EthSpec>(
6666
process_participation_record_updates(state)?;
6767

6868
// Rotate the epoch caches to suit the epoch transition.
69-
state.advance_caches()?;
69+
state.advance_caches(spec)?;
7070

7171
Ok(EpochProcessingSummary::Base {
7272
total_balances: validator_statuses.total_balances,

consensus/state_processing/src/upgrade/altair.rs

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ pub fn upgrade_to_altair<E: EthSpec>(
100100
current_sync_committee: temp_sync_committee.clone(), // not read
101101
next_sync_committee: temp_sync_committee, // not read
102102
// Caches
103+
total_active_balance: pre.total_active_balance,
103104
committee_caches: mem::take(&mut pre.committee_caches),
104105
pubkey_cache: mem::take(&mut pre.pubkey_cache),
105106
exit_cache: mem::take(&mut pre.exit_cache),

consensus/types/src/beacon_state.rs

+86-9
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ pub enum Error {
8686
},
8787
PreviousCommitteeCacheUninitialized,
8888
CurrentCommitteeCacheUninitialized,
89+
TotalActiveBalanceCacheUninitialized,
90+
TotalActiveBalanceCacheInconsistent {
91+
initialized_epoch: Epoch,
92+
current_epoch: Epoch,
93+
},
8994
RelativeEpochError(RelativeEpochError),
9095
ExitCacheUninitialized,
9196
CommitteeCacheUninitialized(Option<RelativeEpoch>),
@@ -275,6 +280,13 @@ where
275280
#[tree_hash(skip_hashing)]
276281
#[test_random(default)]
277282
#[derivative(Clone(clone_with = "clone_default"))]
283+
pub total_active_balance: Option<(Epoch, u64)>,
284+
#[serde(skip_serializing, skip_deserializing)]
285+
#[ssz(skip_serializing)]
286+
#[ssz(skip_deserializing)]
287+
#[tree_hash(skip_hashing)]
288+
#[test_random(default)]
289+
#[derivative(Clone(clone_with = "clone_default"))]
278290
pub committee_caches: [CommitteeCache; CACHED_EPOCHS],
279291
#[serde(skip_serializing, skip_deserializing)]
280292
#[ssz(skip_serializing)]
@@ -353,6 +365,7 @@ impl<T: EthSpec> BeaconState<T> {
353365
finalized_checkpoint: Checkpoint::default(),
354366

355367
// Caching (not in spec)
368+
total_active_balance: None,
356369
committee_caches: [
357370
CommitteeCache::default(),
358371
CommitteeCache::default(),
@@ -1226,12 +1239,45 @@ impl<T: EthSpec> BeaconState<T> {
12261239
}
12271240

12281241
/// Implementation of `get_total_active_balance`, matching the spec.
1229-
pub fn get_total_active_balance(&self, spec: &ChainSpec) -> Result<u64, Error> {
1242+
///
1243+
/// Requires the total active balance cache to be initialised, which is initialised whenever
1244+
/// the current committee cache is.
1245+
///
1246+
/// Returns minimum `EFFECTIVE_BALANCE_INCREMENT`, to avoid div by 0.
1247+
pub fn get_total_active_balance(&self) -> Result<u64, Error> {
1248+
let (initialized_epoch, balance) = self
1249+
.total_active_balance()
1250+
.ok_or(Error::TotalActiveBalanceCacheUninitialized)?;
1251+
1252+
let current_epoch = self.current_epoch();
1253+
if initialized_epoch == current_epoch {
1254+
Ok(balance)
1255+
} else {
1256+
Err(Error::TotalActiveBalanceCacheInconsistent {
1257+
initialized_epoch,
1258+
current_epoch,
1259+
})
1260+
}
1261+
}
1262+
1263+
/// Build the total active balance cache.
1264+
///
1265+
/// This function requires the current committee cache to be already built. It is called
1266+
/// automatically when `build_committee_cache` is called for the current epoch.
1267+
fn build_total_active_balance_cache(&mut self, spec: &ChainSpec) -> Result<(), Error> {
12301268
// Order is irrelevant, so use the cached indices.
1231-
self.get_total_balance(
1269+
let current_epoch = self.current_epoch();
1270+
let total_active_balance = self.get_total_balance(
12321271
self.get_cached_active_validator_indices(RelativeEpoch::Current)?,
12331272
spec,
1234-
)
1273+
)?;
1274+
*self.total_active_balance_mut() = Some((current_epoch, total_active_balance));
1275+
Ok(())
1276+
}
1277+
1278+
/// Set the cached total active balance to `None`, representing no known value.
1279+
pub fn drop_total_active_balance_cache(&mut self) {
1280+
*self.total_active_balance_mut() = None;
12351281
}
12361282

12371283
/// Get a mutable reference to the epoch participation flags for `epoch`.
@@ -1294,6 +1340,7 @@ impl<T: EthSpec> BeaconState<T> {
12941340

12951341
/// Drop all caches on the state.
12961342
pub fn drop_all_caches(&mut self) -> Result<(), Error> {
1343+
self.drop_total_active_balance_cache();
12971344
self.drop_committee_cache(RelativeEpoch::Previous)?;
12981345
self.drop_committee_cache(RelativeEpoch::Current)?;
12991346
self.drop_committee_cache(RelativeEpoch::Next)?;
@@ -1323,11 +1370,14 @@ impl<T: EthSpec> BeaconState<T> {
13231370
.committee_cache_at_index(i)?
13241371
.is_initialized_at(relative_epoch.into_epoch(self.current_epoch()));
13251372

1326-
if is_initialized {
1327-
Ok(())
1328-
} else {
1329-
self.force_build_committee_cache(relative_epoch, spec)
1373+
if !is_initialized {
1374+
self.force_build_committee_cache(relative_epoch, spec)?;
1375+
}
1376+
1377+
if self.total_active_balance().is_none() && relative_epoch == RelativeEpoch::Current {
1378+
self.build_total_active_balance_cache(spec)?;
13301379
}
1380+
Ok(())
13311381
}
13321382

13331383
/// Always builds the previous epoch cache, even if it is already initialized.
@@ -1359,10 +1409,36 @@ impl<T: EthSpec> BeaconState<T> {
13591409
///
13601410
/// This should be used if the `slot` of this state is advanced beyond an epoch boundary.
13611411
///
1362-
/// Note: whilst this function will preserve already-built caches, it will not build any.
1363-
pub fn advance_caches(&mut self) -> Result<(), Error> {
1412+
/// Note: this function will not build any new committee caches, but will build the total
1413+
/// balance cache if the (new) current epoch cache is initialized.
1414+
pub fn advance_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> {
13641415
self.committee_caches_mut().rotate_left(1);
13651416

1417+
// Re-compute total active balance for current epoch.
1418+
//
1419+
// This can only be computed once the state's effective balances have been updated
1420+
// for the current epoch. I.e. it is not possible to know this value with the same
1421+
// lookahead as the committee shuffling.
1422+
let curr = Self::committee_cache_index(RelativeEpoch::Current);
1423+
let curr_cache = mem::take(self.committee_cache_at_index_mut(curr)?);
1424+
1425+
// If current epoch cache is initialized, compute the total active balance from its
1426+
// indices. We check that the cache is initialized at the _next_ epoch because the slot has
1427+
// not yet been advanced.
1428+
let new_current_epoch = self.next_epoch()?;
1429+
if curr_cache.is_initialized_at(new_current_epoch) {
1430+
*self.total_active_balance_mut() = Some((
1431+
new_current_epoch,
1432+
self.get_total_balance(curr_cache.active_validator_indices(), spec)?,
1433+
));
1434+
}
1435+
// If the cache is not initialized, then the previous cached value for the total balance is
1436+
// wrong, so delete it.
1437+
else {
1438+
self.drop_total_active_balance_cache();
1439+
}
1440+
*self.committee_cache_at_index_mut(curr)? = curr_cache;
1441+
13661442
let next = Self::committee_cache_index(RelativeEpoch::Next);
13671443
*self.committee_cache_at_index_mut(next)? = CommitteeCache::default();
13681444
Ok(())
@@ -1504,6 +1580,7 @@ impl<T: EthSpec> BeaconState<T> {
15041580
};
15051581
if config.committee_caches {
15061582
*res.committee_caches_mut() = self.committee_caches().clone();
1583+
*res.total_active_balance_mut() = *self.total_active_balance();
15071584
}
15081585
if config.pubkey_cache {
15091586
*res.pubkey_cache_mut() = self.pubkey_cache().clone();

consensus/types/src/beacon_state/tests.rs

+3
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ fn test_clone_config<E: EthSpec>(base_state: &BeaconState<E>, clone_config: Clon
165165
state
166166
.committee_cache(RelativeEpoch::Next)
167167
.expect("committee cache exists");
168+
state
169+
.total_active_balance()
170+
.expect("total active balance exists");
168171
} else {
169172
state
170173
.committee_cache(RelativeEpoch::Previous)

testing/ef_tests/src/cases/rewards.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl<E: EthSpec> Case for RewardsTest<E> {
123123

124124
Ok(convert_all_base_deltas(&deltas))
125125
} else {
126-
let total_active_balance = state.get_total_active_balance(spec)?;
126+
let total_active_balance = state.get_total_active_balance()?;
127127

128128
let source_deltas = compute_altair_flag_deltas(
129129
&state,

0 commit comments

Comments
 (0)