Skip to content

Commit 836c39e

Browse files
Shrink persisted fork choice data (#7805)
Closes: - #7760 - [x] Remove `balances_cache` from `PersistedForkChoiceStore` (~65 MB saving on mainnet) - [x] Remove `justified_balances` from `PersistedForkChoiceStore` (~16 MB saving on mainnet) - [x] Remove `balances` from `ProtoArray`/`SszContainer`. - [x] Implement zstd compression for votes - [x] Fix bug in justified state usage - [x] Bump schema version to V28 and implement migration.
1 parent 08234b2 commit 836c39e

26 files changed

+611
-128
lines changed

Cargo.lock

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

beacon_node/beacon_chain/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
[package]
33
name = "beacon_chain"
44
version = "0.2.0"
@@ -65,6 +65,7 @@ tracing = { workspace = true }
6565
tree_hash = { workspace = true }
6666
tree_hash_derive = { workspace = true }
6767
types = { workspace = true }
68+
zstd = { workspace = true }
6869

6970
[dev-dependencies]
7071
criterion = { workspace = true }

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ use std::sync::Arc;
121121
use std::time::Duration;
122122
use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterator};
123123
use store::{
124-
BlobSidecarListFromRoot, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary,
125-
KeyValueStoreOp, StoreItem, StoreOp,
124+
BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary,
125+
KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp,
126126
};
127127
use task_executor::{ShutdownReason, TaskExecutor};
128128
use tokio_stream::Stream;
@@ -618,12 +618,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
618618
reset_payload_statuses: ResetPayloadStatuses,
619619
spec: &ChainSpec,
620620
) -> Result<Option<BeaconForkChoice<T>>, Error> {
621-
let Some(persisted_fork_choice) =
622-
store.get_item::<PersistedForkChoice>(&FORK_CHOICE_DB_KEY)?
621+
let Some(persisted_fork_choice_bytes) = store
622+
.hot_db
623+
.get_bytes(DBColumn::ForkChoice, FORK_CHOICE_DB_KEY.as_slice())?
623624
else {
624625
return Ok(None);
625626
};
626627

628+
let persisted_fork_choice =
629+
PersistedForkChoice::from_bytes(&persisted_fork_choice_bytes, store.get_config())?;
627630
let fc_store =
628631
BeaconForkChoiceStore::from_persisted(persisted_fork_choice.fork_choice_store, store)?;
629632

beacon_node/beacon_chain/src/beacon_fork_choice_store.rs

Lines changed: 126 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub enum Error {
2727
FailedToReadState(StoreError),
2828
MissingState(Hash256),
2929
BeaconStateError(BeaconStateError),
30+
UnalignedCheckpoint { block_slot: Slot, state_slot: Slot },
3031
Arith(ArithError),
3132
}
3233

@@ -136,7 +137,9 @@ pub struct BeaconForkChoiceStore<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<
136137
finalized_checkpoint: Checkpoint,
137138
justified_checkpoint: Checkpoint,
138139
justified_balances: JustifiedBalances,
140+
justified_state_root: Hash256,
139141
unrealized_justified_checkpoint: Checkpoint,
142+
unrealized_justified_state_root: Hash256,
140143
unrealized_finalized_checkpoint: Checkpoint,
141144
proposer_boost_root: Hash256,
142145
equivocating_indices: BTreeSet<u64>,
@@ -162,30 +165,48 @@ where
162165
/// It is assumed that `anchor` is already persisted in `store`.
163166
pub fn get_forkchoice_store(
164167
store: Arc<HotColdDB<E, Hot, Cold>>,
165-
anchor: &BeaconSnapshot<E>,
168+
anchor: BeaconSnapshot<E>,
166169
) -> Result<Self, Error> {
167-
let anchor_state = &anchor.beacon_state;
170+
let unadvanced_state_root = anchor.beacon_state_root();
171+
let mut anchor_state = anchor.beacon_state;
168172
let mut anchor_block_header = anchor_state.latest_block_header().clone();
169-
if anchor_block_header.state_root == Hash256::zero() {
170-
anchor_block_header.state_root = anchor.beacon_state_root();
173+
174+
// The anchor state MUST be on an epoch boundary (it should be advanced by the caller).
175+
if !anchor_state
176+
.slot()
177+
.as_u64()
178+
.is_multiple_of(E::slots_per_epoch())
179+
{
180+
return Err(Error::UnalignedCheckpoint {
181+
block_slot: anchor_block_header.slot,
182+
state_slot: anchor_state.slot(),
183+
});
184+
}
185+
186+
// Compute the accurate block root for the checkpoint block.
187+
if anchor_block_header.state_root.is_zero() {
188+
anchor_block_header.state_root = unadvanced_state_root;
171189
}
172-
let anchor_root = anchor_block_header.canonical_root();
190+
let anchor_block_root = anchor_block_header.canonical_root();
173191
let anchor_epoch = anchor_state.current_epoch();
174192
let justified_checkpoint = Checkpoint {
175193
epoch: anchor_epoch,
176-
root: anchor_root,
194+
root: anchor_block_root,
177195
};
178196
let finalized_checkpoint = justified_checkpoint;
179-
let justified_balances = JustifiedBalances::from_justified_state(anchor_state)?;
197+
let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?;
198+
let justified_state_root = anchor_state.canonical_root()?;
180199

181200
Ok(Self {
182201
store,
183202
balances_cache: <_>::default(),
184203
time: anchor_state.slot(),
185204
justified_checkpoint,
186205
justified_balances,
206+
justified_state_root,
187207
finalized_checkpoint,
188208
unrealized_justified_checkpoint: justified_checkpoint,
209+
unrealized_justified_state_root: justified_state_root,
189210
unrealized_finalized_checkpoint: finalized_checkpoint,
190211
proposer_boost_root: Hash256::zero(),
191212
equivocating_indices: BTreeSet::new(),
@@ -197,33 +218,72 @@ where
197218
/// on-disk database.
198219
pub fn to_persisted(&self) -> PersistedForkChoiceStore {
199220
PersistedForkChoiceStore {
200-
balances_cache: self.balances_cache.clone(),
201221
time: self.time,
202222
finalized_checkpoint: self.finalized_checkpoint,
203223
justified_checkpoint: self.justified_checkpoint,
204-
justified_balances: self.justified_balances.effective_balances.clone(),
224+
justified_state_root: self.justified_state_root,
205225
unrealized_justified_checkpoint: self.unrealized_justified_checkpoint,
226+
unrealized_justified_state_root: self.unrealized_justified_state_root,
206227
unrealized_finalized_checkpoint: self.unrealized_finalized_checkpoint,
207228
proposer_boost_root: self.proposer_boost_root,
208229
equivocating_indices: self.equivocating_indices.clone(),
209230
}
210231
}
211232

212233
/// Restore `Self` from a previously-generated `PersistedForkChoiceStore`.
213-
pub fn from_persisted(
214-
persisted: PersistedForkChoiceStore,
234+
///
235+
/// DEPRECATED. Can be deleted once migrations no longer require it.
236+
pub fn from_persisted_v17(
237+
persisted: PersistedForkChoiceStoreV17,
238+
justified_state_root: Hash256,
239+
unrealized_justified_state_root: Hash256,
215240
store: Arc<HotColdDB<E, Hot, Cold>>,
216241
) -> Result<Self, Error> {
217242
let justified_balances =
218243
JustifiedBalances::from_effective_balances(persisted.justified_balances)?;
244+
219245
Ok(Self {
220246
store,
221-
balances_cache: persisted.balances_cache,
247+
balances_cache: <_>::default(),
222248
time: persisted.time,
223249
finalized_checkpoint: persisted.finalized_checkpoint,
224250
justified_checkpoint: persisted.justified_checkpoint,
225251
justified_balances,
252+
justified_state_root,
226253
unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint,
254+
unrealized_justified_state_root,
255+
unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint,
256+
proposer_boost_root: persisted.proposer_boost_root,
257+
equivocating_indices: persisted.equivocating_indices,
258+
_phantom: PhantomData,
259+
})
260+
}
261+
262+
/// Restore `Self` from a previously-generated `PersistedForkChoiceStore`.
263+
pub fn from_persisted(
264+
persisted: PersistedForkChoiceStore,
265+
store: Arc<HotColdDB<E, Hot, Cold>>,
266+
) -> Result<Self, Error> {
267+
let justified_checkpoint = persisted.justified_checkpoint;
268+
let justified_state_root = persisted.justified_state_root;
269+
270+
let update_cache = true;
271+
let justified_state = store
272+
.get_hot_state(&justified_state_root, update_cache)
273+
.map_err(Error::FailedToReadState)?
274+
.ok_or(Error::MissingState(justified_state_root))?;
275+
276+
let justified_balances = JustifiedBalances::from_justified_state(&justified_state)?;
277+
Ok(Self {
278+
store,
279+
balances_cache: <_>::default(),
280+
time: persisted.time,
281+
finalized_checkpoint: persisted.finalized_checkpoint,
282+
justified_checkpoint,
283+
justified_balances,
284+
justified_state_root,
285+
unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint,
286+
unrealized_justified_state_root: persisted.unrealized_justified_state_root,
227287
unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint,
228288
proposer_boost_root: persisted.proposer_boost_root,
229289
equivocating_indices: persisted.equivocating_indices,
@@ -261,6 +321,10 @@ where
261321
&self.justified_checkpoint
262322
}
263323

324+
fn justified_state_root(&self) -> Hash256 {
325+
self.justified_state_root
326+
}
327+
264328
fn justified_balances(&self) -> &JustifiedBalances {
265329
&self.justified_balances
266330
}
@@ -273,6 +337,10 @@ where
273337
&self.unrealized_justified_checkpoint
274338
}
275339

340+
fn unrealized_justified_state_root(&self) -> Hash256 {
341+
self.unrealized_justified_state_root
342+
}
343+
276344
fn unrealized_finalized_checkpoint(&self) -> &Checkpoint {
277345
&self.unrealized_finalized_checkpoint
278346
}
@@ -285,8 +353,13 @@ where
285353
self.finalized_checkpoint = checkpoint
286354
}
287355

288-
fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Error> {
356+
fn set_justified_checkpoint(
357+
&mut self,
358+
checkpoint: Checkpoint,
359+
justified_state_root: Hash256,
360+
) -> Result<(), Error> {
289361
self.justified_checkpoint = checkpoint;
362+
self.justified_state_root = justified_state_root;
290363

291364
if let Some(balances) = self.balances_cache.get(
292365
self.justified_checkpoint.root,
@@ -297,36 +370,24 @@ where
297370
self.justified_balances = JustifiedBalances::from_effective_balances(balances)?;
298371
} else {
299372
metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES);
300-
let justified_block = self
301-
.store
302-
.get_blinded_block(&self.justified_checkpoint.root)
303-
.map_err(Error::FailedToReadBlock)?
304-
.ok_or(Error::MissingBlock(self.justified_checkpoint.root))?
305-
.deconstruct()
306-
.0;
307-
308-
let max_slot = self
309-
.justified_checkpoint
310-
.epoch
311-
.start_slot(E::slots_per_epoch());
312-
let (_, state) = self
373+
374+
// Justified state is reasonably useful to cache, it might be finalized soon.
375+
let update_cache = true;
376+
let state = self
313377
.store
314-
.get_advanced_hot_state(
315-
self.justified_checkpoint.root,
316-
max_slot,
317-
justified_block.state_root(),
318-
)
378+
.get_hot_state(&self.justified_state_root, update_cache)
319379
.map_err(Error::FailedToReadState)?
320-
.ok_or_else(|| Error::MissingState(justified_block.state_root()))?;
380+
.ok_or_else(|| Error::MissingState(self.justified_state_root))?;
321381

322382
self.justified_balances = JustifiedBalances::from_justified_state(&state)?;
323383
}
324384

325385
Ok(())
326386
}
327387

328-
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint) {
388+
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256) {
329389
self.unrealized_justified_checkpoint = checkpoint;
390+
self.unrealized_justified_state_root = state_root;
330391
}
331392

332393
fn set_unrealized_finalized_checkpoint(&mut self, checkpoint: Checkpoint) {
@@ -346,18 +407,48 @@ where
346407
}
347408
}
348409

349-
pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV17;
410+
pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV28;
350411

351412
/// A container which allows persisting the `BeaconForkChoiceStore` to the on-disk database.
352-
#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)]
413+
#[superstruct(
414+
variants(V17, V28),
415+
variant_attributes(derive(Encode, Decode)),
416+
no_enum
417+
)]
353418
pub struct PersistedForkChoiceStore {
419+
/// The balances cache was removed from disk storage in schema V28.
420+
#[superstruct(only(V17))]
354421
pub balances_cache: BalancesCacheV8,
355422
pub time: Slot,
356423
pub finalized_checkpoint: Checkpoint,
357424
pub justified_checkpoint: Checkpoint,
425+
/// The justified balances were removed from disk storage in schema V28.
426+
#[superstruct(only(V17))]
358427
pub justified_balances: Vec<u64>,
428+
/// The justified state root is stored so that it can be used to load the justified balances.
429+
#[superstruct(only(V28))]
430+
pub justified_state_root: Hash256,
359431
pub unrealized_justified_checkpoint: Checkpoint,
432+
#[superstruct(only(V28))]
433+
pub unrealized_justified_state_root: Hash256,
360434
pub unrealized_finalized_checkpoint: Checkpoint,
361435
pub proposer_boost_root: Hash256,
362436
pub equivocating_indices: BTreeSet<u64>,
363437
}
438+
439+
// Convert V28 to V17 by adding balances and removing justified state roots.
440+
impl From<(PersistedForkChoiceStoreV28, JustifiedBalances)> for PersistedForkChoiceStoreV17 {
441+
fn from((v28, balances): (PersistedForkChoiceStoreV28, JustifiedBalances)) -> Self {
442+
Self {
443+
balances_cache: Default::default(),
444+
time: v28.time,
445+
finalized_checkpoint: v28.finalized_checkpoint,
446+
justified_checkpoint: v28.justified_checkpoint,
447+
justified_balances: balances.effective_balances,
448+
unrealized_justified_checkpoint: v28.unrealized_justified_checkpoint,
449+
unrealized_finalized_checkpoint: v28.unrealized_finalized_checkpoint,
450+
proposer_boost_root: v28.proposer_boost_root,
451+
equivocating_indices: v28.equivocating_indices,
452+
}
453+
}
454+
}

beacon_node/beacon_chain/src/builder.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ where
394394
.map_err(|e| format!("Failed to initialize genesis data column info: {:?}", e))?,
395395
);
396396

397-
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis)
397+
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, genesis.clone())
398398
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
399399
let current_slot = None;
400400

@@ -616,7 +616,7 @@ where
616616
beacon_state: weak_subj_state,
617617
};
618618

619-
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &snapshot)
619+
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, snapshot.clone())
620620
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
621621

622622
let fork_choice = ForkChoice::from_anchor(
@@ -887,8 +887,9 @@ where
887887
self.pending_io_batch.push(BeaconChain::<
888888
Witness<TSlotClock, E, THotStore, TColdStore>,
889889
>::persist_fork_choice_in_batch_standalone(
890-
&fork_choice
891-
));
890+
&fork_choice,
891+
store.get_config(),
892+
).map_err(|e| format!("Fork choice compression error: {e:?}"))?);
892893
store
893894
.hot_db
894895
.do_atomically(self.pending_io_batch)

0 commit comments

Comments
 (0)