Skip to content

Commit dd98534

Browse files
authored
Hierarchical state diffs in hot DB (#6750)
This PR implements #5978 (tree-states) but on the hot DB. It allows Lighthouse to massively reduce its disk footprint during non-finality and overall I/O in all cases. Closes #6580 Conga into #6744 ### TODOs - [x] Fix OOM in CI #7176 - [x] optimise store_hot_state to avoid storing a duplicate state if the summary already exists (should be safe from races now that pruning is cleaner) - [x] mispelled: get_ancenstor_state_root - [x] get_ancestor_state_root should use state summaries - [x] Prevent split from changing during ancestor calc - [x] Use same hierarchy for hot and cold ### TODO Good optimization for future PRs - [ ] On the migration, if the latest hot snapshot is aligned with the cold snapshot migrate the diffs instead of the full states. ``` align slot time 10485760 Nov-26-2024 12582912 Sep-14-2025 14680064 Jul-02-2026 ``` ### TODO Maybe things good to have - [ ] Rename anchor_slot https://github.com/sigp/lighthouse/compare/tree-states-hot-rebase-oom...dapplion:lighthouse:tree-states-hot-anchor-slot-rename?expand=1 - [ ] Make anchor fields not public such that they must be mutated through a method. To prevent un-wanted changes of the anchor_slot ### NOTTODO - [ ] Use fork-choice and a new method [`descendants_of_checkpoint`](ca2388e#diff-046fbdb517ca16b80e4464c2c824cf001a74a0a94ac0065e635768ac391062a8) to filter only the state summaries that descend of finalized checkpoint]
1 parent 6786b9d commit dd98534

33 files changed

+2690
-807
lines changed

account_manager/src/validator/slashing_protection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub fn cli_run<E: EthSpec>(
9090
let slashing_protection_database =
9191
SlashingDatabase::open_or_create(&slashing_protection_db_path).map_err(|e| {
9292
format!(
93-
"Unable to open database at {}: {:?}",
93+
"Unable to open slashing protection database at {}: {:?}",
9494
slashing_protection_db_path.display(),
9595
e
9696
)
@@ -198,7 +198,7 @@ pub fn cli_run<E: EthSpec>(
198198
let slashing_protection_database = SlashingDatabase::open(&slashing_protection_db_path)
199199
.map_err(|e| {
200200
format!(
201-
"Unable to open database at {}: {:?}",
201+
"Unable to open slashing protection database at {}: {:?}",
202202
slashing_protection_db_path.display(),
203203
e
204204
)

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ use std::time::Duration;
124124
use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterator};
125125
use store::{
126126
BlobSidecarListFromRoot, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary,
127-
KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp,
127+
KeyValueStoreOp, StoreItem, StoreOp,
128128
};
129129
use task_executor::{ShutdownReason, TaskExecutor};
130130
use tokio_stream::Stream;
@@ -4043,8 +4043,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
40434043
ops.push(StoreOp::PutBlock(block_root, signed_block.clone()));
40444044
ops.push(StoreOp::PutState(block.state_root(), &state));
40454045

4046-
let txn_lock = self.store.hot_db.begin_rw_transaction();
4047-
40484046
if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) {
40494047
error!(
40504048
msg = "Restoring fork choice from disk",
@@ -4056,7 +4054,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
40564054
.err()
40574055
.unwrap_or(e.into()));
40584056
}
4059-
drop(txn_lock);
40604057

40614058
// The fork choice write-lock is dropped *after* the on-disk database has been updated.
40624059
// This prevents inconsistency between the two at the expense of concurrency.
@@ -6851,13 +6848,22 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
68516848
#[allow(clippy::type_complexity)]
68526849
pub fn chain_dump(
68536850
&self,
6851+
) -> Result<Vec<BeaconSnapshot<T::EthSpec, BlindedPayload<T::EthSpec>>>, Error> {
6852+
self.chain_dump_from_slot(Slot::new(0))
6853+
}
6854+
6855+
/// As for `chain_dump` but dumping only the portion of the chain newer than `from_slot`.
6856+
#[allow(clippy::type_complexity)]
6857+
pub fn chain_dump_from_slot(
6858+
&self,
6859+
from_slot: Slot,
68546860
) -> Result<Vec<BeaconSnapshot<T::EthSpec, BlindedPayload<T::EthSpec>>>, Error> {
68556861
let mut dump = vec![];
68566862

68576863
let mut prev_block_root = None;
68586864
let mut prev_beacon_state = None;
68596865

6860-
for res in self.forwards_iter_block_roots(Slot::new(0))? {
6866+
for res in self.forwards_iter_block_roots(from_slot)? {
68616867
let (beacon_block_root, _) = res?;
68626868

68636869
// Do not include snapshots at skipped slots.

beacon_node/beacon_chain/src/block_verification.rs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ use std::fmt::Debug;
9090
use std::fs;
9191
use std::io::Write;
9292
use std::sync::Arc;
93-
use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp};
93+
use store::{Error as DBError, KeyValueStore};
9494
use strum::AsRefStr;
9595
use task_executor::JoinHandle;
9696
use tracing::{debug, error};
@@ -1467,28 +1467,19 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
14671467
// processing, but we get early access to it.
14681468
let state_root = state.update_tree_hash_cache()?;
14691469

1470-
// Store the state immediately.
1471-
let txn_lock = chain.store.hot_db.begin_rw_transaction();
1470+
// Store the state immediately. States are ONLY deleted on finalization pruning, so
1471+
// we won't have race conditions where we should have written a state and didn't.
14721472
let state_already_exists =
14731473
chain.store.load_hot_state_summary(&state_root)?.is_some();
14741474

1475-
let state_batch = if state_already_exists {
1475+
if state_already_exists {
14761476
// If the state exists, we do not need to re-write it.
1477-
vec![]
14781477
} else {
1479-
vec![if state.slot() % T::EthSpec::slots_per_epoch() == 0 {
1480-
StoreOp::PutState(state_root, &state)
1481-
} else {
1482-
StoreOp::PutStateSummary(
1483-
state_root,
1484-
HotStateSummary::new(&state_root, &state)?,
1485-
)
1486-
}]
1478+
// Recycle store codepath to create a state summary and store the state / diff
1479+
let mut ops = vec![];
1480+
chain.store.store_hot_state(&state_root, &state, &mut ops)?;
1481+
chain.store.hot_db.do_atomically(ops)?;
14871482
};
1488-
chain
1489-
.store
1490-
.do_atomically_with_block_and_blobs_cache(state_batch)?;
1491-
drop(txn_lock);
14921483

14931484
state_root
14941485
};

beacon_node/beacon_chain/src/builder.rs

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp};
4444
use task_executor::{ShutdownReason, TaskExecutor};
4545
use tracing::{debug, error, info};
4646
use types::{
47-
BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Checkpoint, DataColumnSidecarList, Epoch,
48-
EthSpec, FixedBytesExtended, Hash256, Signature, SignedBeaconBlock, Slot,
47+
BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec,
48+
FixedBytesExtended, Hash256, Signature, SignedBeaconBlock, Slot,
4949
};
5050

5151
/// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing
@@ -382,21 +382,29 @@ where
382382
}
383383

384384
/// Starts a new chain from a genesis state.
385-
pub fn genesis_state(mut self, beacon_state: BeaconState<E>) -> Result<Self, String> {
385+
pub fn genesis_state(mut self, mut beacon_state: BeaconState<E>) -> Result<Self, String> {
386386
let store = self.store.clone().ok_or("genesis_state requires a store")?;
387387

388-
let (genesis, updated_builder) = self.set_genesis_state(beacon_state)?;
389-
self = updated_builder;
390-
391-
// Stage the database's metadata fields for atomic storage when `build` is called.
388+
// Initialize anchor info before attempting to write the genesis state.
392389
// Since v4.4.0 we will set the anchor with a dummy state upper limit in order to prevent
393390
// historic states from being retained (unless `--reconstruct-historic-states` is set).
394391
let retain_historic_states = self.chain_config.reconstruct_historic_states;
392+
let genesis_beacon_block = genesis_block(&mut beacon_state, &self.spec)?;
395393
self.pending_io_batch.push(
396394
store
397-
.init_anchor_info(genesis.beacon_block.message(), retain_historic_states)
395+
.init_anchor_info(
396+
genesis_beacon_block.parent_root(),
397+
genesis_beacon_block.slot(),
398+
Slot::new(0),
399+
retain_historic_states,
400+
)
398401
.map_err(|e| format!("Failed to initialize genesis anchor: {:?}", e))?,
399402
);
403+
404+
let (genesis, updated_builder) = self.set_genesis_state(beacon_state)?;
405+
self = updated_builder;
406+
407+
// Stage the database's metadata fields for atomic storage when `build` is called.
400408
self.pending_io_batch.push(
401409
store
402410
.init_blob_info(genesis.beacon_block.slot())
@@ -521,6 +529,13 @@ where
521529
}
522530
}
523531

532+
debug!(
533+
slot = %weak_subj_slot,
534+
state_root = ?weak_subj_state_root,
535+
block_root = ?weak_subj_block_root,
536+
"Storing split from weak subjectivity state"
537+
);
538+
524539
// Set the store's split point *before* storing genesis so that genesis is stored
525540
// immediately in the freezer DB.
526541
store.set_split(weak_subj_slot, weak_subj_state_root, weak_subj_block_root);
@@ -541,6 +556,26 @@ where
541556
.cold_db
542557
.do_atomically(block_root_batch)
543558
.map_err(|e| format!("Error writing frozen block roots: {e:?}"))?;
559+
debug!(
560+
from = %weak_subj_block.slot(),
561+
to_excl = %weak_subj_state.slot(),
562+
block_root = ?weak_subj_block_root,
563+
"Stored frozen block roots at skipped slots"
564+
);
565+
566+
// Write the anchor to memory before calling `put_state` otherwise hot hdiff can't store
567+
// states that do not align with the `start_slot` grid.
568+
let retain_historic_states = self.chain_config.reconstruct_historic_states;
569+
self.pending_io_batch.push(
570+
store
571+
.init_anchor_info(
572+
weak_subj_block.parent_root(),
573+
weak_subj_block.slot(),
574+
weak_subj_slot,
575+
retain_historic_states,
576+
)
577+
.map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?,
578+
);
544579

545580
// Write the state, block and blobs non-atomically, it doesn't matter if they're forgotten
546581
// about on a crash restart.
@@ -551,6 +586,8 @@ where
551586
weak_subj_state.clone(),
552587
)
553588
.map_err(|e| format!("Failed to set checkpoint state as finalized state: {:?}", e))?;
589+
// Note: post hot hdiff must update the anchor info before attempting to put_state otherwise
590+
// the write will fail if the weak_subj_slot is not aligned with the snapshot moduli.
554591
store
555592
.put_state(&weak_subj_state_root, &weak_subj_state)
556593
.map_err(|e| format!("Failed to store weak subjectivity state: {e:?}"))?;
@@ -580,13 +617,7 @@ where
580617
// Stage the database's metadata fields for atomic storage when `build` is called.
581618
// This prevents the database from restarting in an inconsistent state if the anchor
582619
// info or split point is written before the `PersistedBeaconChain`.
583-
let retain_historic_states = self.chain_config.reconstruct_historic_states;
584620
self.pending_io_batch.push(store.store_split_in_batch());
585-
self.pending_io_batch.push(
586-
store
587-
.init_anchor_info(weak_subj_block.message(), retain_historic_states)
588-
.map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?,
589-
);
590621
self.pending_io_batch.push(
591622
store
592623
.init_blob_info(weak_subj_block.slot())
@@ -598,13 +629,6 @@ where
598629
.map_err(|e| format!("Failed to initialize data column info: {:?}", e))?,
599630
);
600631

601-
// Store pruning checkpoint to prevent attempting to prune before the anchor state.
602-
self.pending_io_batch
603-
.push(store.pruning_checkpoint_store_op(Checkpoint {
604-
root: weak_subj_block_root,
605-
epoch: weak_subj_state.slot().epoch(E::slots_per_epoch()),
606-
}));
607-
608632
let snapshot = BeaconSnapshot {
609633
beacon_block_root: weak_subj_block_root,
610634
beacon_block: Arc::new(weak_subj_block),

beacon_node/beacon_chain/src/historical_blocks.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
151151

152152
// Store block roots, including at all skip slots in the freezer DB.
153153
for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() {
154+
debug!(%slot, ?block_root, "Storing frozen block to root mapping");
154155
cold_batch.push(KeyValueStoreOp::PutKeyValue(
155156
DBColumn::BeaconBlockRoots,
156157
slot.to_be_bytes().to_vec(),

0 commit comments

Comments
 (0)