Skip to content

Commit 3fd6df5

Browse files
Remove checkpoint alignment requirements and enable historic state pruning (#4610)
## Issue Addressed Closes #3210 Closes #3211 ## Proposed Changes - Checkpoint sync from the latest finalized state regardless of its alignment. - Add the `block_root` to the database's split point. This is _only_ added to the in-memory split in order to avoid a schema migration. See `load_split`. - Add a new method to the DB called `get_advanced_state`, which looks up a state _by block root_, with a `state_root` as fallback. Using this method prevents accidental accesses of the split's unadvanced state, which does not exist in the hot DB and is not guaranteed to exist in the freezer DB at all. Previously Lighthouse would look up this state _from the freezer DB_, even if it was required for block/attestation processing, which was suboptimal. - Replace several state look-ups in block and attestation processing with `get_advanced_state` so that they can't hit the split block's unadvanced state. - Do not store any states in the freezer database by default. All states will be deleted upon being evicted from the hot database unless `--reconstruct-historic-states` is set. The anchor info which was previously used for checkpoint sync is used to implement this, including when syncing from genesis. ## Additional Info Needs further testing. I want to stress-test the pruned database under Hydra. The `get_advanced_state` method is intended to become more relevant over time: `tree-states` includes an identically named method that returns advanced states from its in-memory cache. Co-authored-by: realbigsean <[email protected]>
1 parent 687c58f commit 3fd6df5

File tree

25 files changed

+633
-301
lines changed

25 files changed

+633
-301
lines changed

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4656,6 +4656,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
46564656
self.log,
46574657
"Produced block on state";
46584658
"block_size" => block_size,
4659+
"slot" => block.slot(),
46594660
);
46604661

46614662
metrics::observe(&metrics::BLOCK_SIZE, block_size as f64);
@@ -5571,14 +5572,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
55715572
let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt {
55725573
(state, state_root)
55735574
} else {
5574-
let state_root = head_block.state_root;
5575-
let state = self
5575+
let block_state_root = head_block.state_root;
5576+
let max_slot = shuffling_epoch.start_slot(T::EthSpec::slots_per_epoch());
5577+
let (state_root, state) = self
55765578
.store
55775579
.get_inconsistent_state_for_attestation_verification_only(
5578-
&state_root,
5579-
Some(head_block.slot),
5580+
&head_block_root,
5581+
max_slot,
5582+
block_state_root,
55805583
)?
5581-
.ok_or(Error::MissingBeaconState(head_block.state_root))?;
5584+
.ok_or(Error::MissingBeaconState(block_state_root))?;
55825585
(state, state_root)
55835586
};
55845587

beacon_node/beacon_chain/src/beacon_fork_choice_store.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,17 @@ where
321321
.deconstruct()
322322
.0;
323323

324-
let state = self
324+
let max_slot = self
325+
.justified_checkpoint
326+
.epoch
327+
.start_slot(E::slots_per_epoch());
328+
let (_, state) = self
325329
.store
326-
.get_state(&justified_block.state_root(), Some(justified_block.slot()))
330+
.get_advanced_hot_state(
331+
self.justified_checkpoint.root,
332+
max_slot,
333+
justified_block.state_root(),
334+
)
327335
.map_err(Error::FailedToReadState)?
328336
.ok_or_else(|| Error::MissingState(justified_block.state_root()))?;
329337

beacon_node/beacon_chain/src/block_verification.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,7 +1261,7 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
12611261

12621262
// Perform a sanity check on the pre-state.
12631263
let parent_slot = parent.beacon_block.slot();
1264-
if state.slot() < parent_slot || state.slot() > parent_slot + 1 {
1264+
if state.slot() < parent_slot || state.slot() > block.slot() {
12651265
return Err(BeaconChainError::BadPreState {
12661266
parent_root: parent.beacon_block_root,
12671267
parent_slot,
@@ -1760,13 +1760,18 @@ fn load_parent<T: BeaconChainTypes>(
17601760
BlockError::from(BeaconChainError::MissingBeaconBlock(block.parent_root()))
17611761
})?;
17621762

1763-
// Load the parent blocks state from the database, returning an error if it is not found.
1763+
// Load the parent block's state from the database, returning an error if it is not found.
17641764
// It is an error because if we know the parent block we should also know the parent state.
1765-
let parent_state_root = parent_block.state_root();
1766-
let parent_state = chain
1767-
.get_state(&parent_state_root, Some(parent_block.slot()))?
1765+
// Retrieve any state that is advanced through to at most `block.slot()`: this is
1766+
// particularly important if `block` descends from the finalized/split block, but at a slot
1767+
// prior to the finalized slot (which is invalid and inaccessible in our DB schema).
1768+
let (parent_state_root, parent_state) = chain
1769+
.store
1770+
.get_advanced_hot_state(root, block.slot(), parent_block.state_root())?
17681771
.ok_or_else(|| {
1769-
BeaconChainError::DBInconsistent(format!("Missing state {:?}", parent_state_root))
1772+
BeaconChainError::DBInconsistent(
1773+
format!("Missing state for parent block {root:?}",),
1774+
)
17701775
})?;
17711776

17721777
metrics::inc_counter(&metrics::BLOCK_PROCESSING_SNAPSHOT_CACHE_MISSES);

beacon_node/beacon_chain/src/builder.rs

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ use operation_pool::{OperationPool, PersistedOperationPool};
2424
use parking_lot::RwLock;
2525
use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold};
2626
use slasher::Slasher;
27-
use slog::{crit, error, info, Logger};
27+
use slog::{crit, debug, error, info, Logger};
2828
use slot_clock::{SlotClock, TestingSlotClock};
29+
use state_processing::per_slot_processing;
2930
use std::marker::PhantomData;
3031
use std::sync::Arc;
3132
use std::time::Duration;
@@ -287,7 +288,7 @@ where
287288
let genesis_state = store
288289
.get_state(&genesis_block.state_root(), Some(genesis_block.slot()))
289290
.map_err(|e| descriptive_db_error("genesis state", &e))?
290-
.ok_or("Genesis block not found in store")?;
291+
.ok_or("Genesis state not found in store")?;
291292

292293
self.genesis_time = Some(genesis_state.genesis_time());
293294

@@ -382,6 +383,16 @@ where
382383
let (genesis, updated_builder) = self.set_genesis_state(beacon_state)?;
383384
self = updated_builder;
384385

386+
// Stage the database's metadata fields for atomic storage when `build` is called.
387+
// Since v4.4.0 we will set the anchor with a dummy state upper limit in order to prevent
388+
// historic states from being retained (unless `--reconstruct-historic-states` is set).
389+
let retain_historic_states = self.chain_config.reconstruct_historic_states;
390+
self.pending_io_batch.push(
391+
store
392+
.init_anchor_info(genesis.beacon_block.message(), retain_historic_states)
393+
.map_err(|e| format!("Failed to initialize genesis anchor: {:?}", e))?,
394+
);
395+
385396
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis)
386397
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
387398
let current_slot = None;
@@ -408,46 +419,48 @@ where
408419
weak_subj_block: SignedBeaconBlock<TEthSpec>,
409420
genesis_state: BeaconState<TEthSpec>,
410421
) -> Result<Self, String> {
411-
let store = self.store.clone().ok_or("genesis_state requires a store")?;
412-
413-
let weak_subj_slot = weak_subj_state.slot();
414-
let weak_subj_block_root = weak_subj_block.canonical_root();
415-
let weak_subj_state_root = weak_subj_block.state_root();
416-
417-
// Check that the given block lies on an epoch boundary. Due to the database only storing
418-
// full states on epoch boundaries and at restore points it would be difficult to support
419-
// starting from a mid-epoch state.
420-
if weak_subj_slot % TEthSpec::slots_per_epoch() != 0 {
421-
return Err(format!(
422-
"Checkpoint block at slot {} is not aligned to epoch start. \
423-
Please supply an aligned checkpoint with block.slot % 32 == 0",
424-
weak_subj_block.slot(),
425-
));
426-
}
422+
let store = self
423+
.store
424+
.clone()
425+
.ok_or("weak_subjectivity_state requires a store")?;
426+
let log = self
427+
.log
428+
.as_ref()
429+
.ok_or("weak_subjectivity_state requires a log")?;
427430

428-
// Check that the block and state have consistent slots and state roots.
429-
if weak_subj_state.slot() != weak_subj_block.slot() {
430-
return Err(format!(
431-
"Slot of snapshot block ({}) does not match snapshot state ({})",
432-
weak_subj_block.slot(),
433-
weak_subj_state.slot(),
434-
));
431+
// Ensure the state is advanced to an epoch boundary.
432+
let slots_per_epoch = TEthSpec::slots_per_epoch();
433+
if weak_subj_state.slot() % slots_per_epoch != 0 {
434+
debug!(
435+
log,
436+
"Advancing checkpoint state to boundary";
437+
"state_slot" => weak_subj_state.slot(),
438+
"block_slot" => weak_subj_block.slot(),
439+
);
440+
while weak_subj_state.slot() % slots_per_epoch != 0 {
441+
per_slot_processing(&mut weak_subj_state, None, &self.spec)
442+
.map_err(|e| format!("Error advancing state: {e:?}"))?;
443+
}
435444
}
436445

437446
// Prime all caches before storing the state in the database and computing the tree hash
438447
// root.
439448
weak_subj_state
440449
.build_caches(&self.spec)
441450
.map_err(|e| format!("Error building caches on checkpoint state: {e:?}"))?;
442-
443-
let computed_state_root = weak_subj_state
451+
let weak_subj_state_root = weak_subj_state
444452
.update_tree_hash_cache()
445453
.map_err(|e| format!("Error computing checkpoint state root: {:?}", e))?;
446454

447-
if weak_subj_state_root != computed_state_root {
455+
let weak_subj_slot = weak_subj_state.slot();
456+
let weak_subj_block_root = weak_subj_block.canonical_root();
457+
458+
// Validate the state's `latest_block_header` against the checkpoint block.
459+
let state_latest_block_root = weak_subj_state.get_latest_block_root(weak_subj_state_root);
460+
if weak_subj_block_root != state_latest_block_root {
448461
return Err(format!(
449-
"Snapshot state root does not match block, expected: {:?}, got: {:?}",
450-
weak_subj_state_root, computed_state_root
462+
"Snapshot state's most recent block root does not match block, expected: {:?}, got: {:?}",
463+
weak_subj_block_root, state_latest_block_root
451464
));
452465
}
453466

@@ -464,7 +477,7 @@ where
464477

465478
// Set the store's split point *before* storing genesis so that genesis is stored
466479
// immediately in the freezer DB.
467-
store.set_split(weak_subj_slot, weak_subj_state_root);
480+
store.set_split(weak_subj_slot, weak_subj_state_root, weak_subj_block_root);
468481
let (_, updated_builder) = self.set_genesis_state(genesis_state)?;
469482
self = updated_builder;
470483

@@ -480,10 +493,11 @@ where
480493
// Stage the database's metadata fields for atomic storage when `build` is called.
481494
// This prevents the database from restarting in an inconsistent state if the anchor
482495
// info or split point is written before the `PersistedBeaconChain`.
496+
let retain_historic_states = self.chain_config.reconstruct_historic_states;
483497
self.pending_io_batch.push(store.store_split_in_batch());
484498
self.pending_io_batch.push(
485499
store
486-
.init_anchor_info(weak_subj_block.message())
500+
.init_anchor_info(weak_subj_block.message(), retain_historic_states)
487501
.map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?,
488502
);
489503

@@ -503,13 +517,12 @@ where
503517
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &snapshot)
504518
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
505519

506-
let current_slot = Some(snapshot.beacon_block.slot());
507520
let fork_choice = ForkChoice::from_anchor(
508521
fc_store,
509522
snapshot.beacon_block_root,
510523
&snapshot.beacon_block,
511524
&snapshot.beacon_state,
512-
current_slot,
525+
Some(weak_subj_slot),
513526
&self.spec,
514527
)
515528
.map_err(|e| format!("Unable to initialize ForkChoice: {:?}", e))?;
@@ -672,9 +685,8 @@ where
672685
Err(e) => return Err(descriptive_db_error("head block", &e)),
673686
};
674687

675-
let head_state_root = head_block.state_root();
676-
let head_state = store
677-
.get_state(&head_state_root, Some(head_block.slot()))
688+
let (_head_state_root, head_state) = store
689+
.get_advanced_hot_state(head_block_root, current_slot, head_block.state_root())
678690
.map_err(|e| descriptive_db_error("head state", &e))?
679691
.ok_or("Head state not found in store")?;
680692

beacon_node/beacon_chain/src/canonical_head.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ use crate::{
4747
};
4848
use eth2::types::{EventKind, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead};
4949
use fork_choice::{
50-
ExecutionStatus, ForkChoiceView, ForkchoiceUpdateParameters, ProtoBlock, ResetPayloadStatuses,
50+
ExecutionStatus, ForkChoiceStore, ForkChoiceView, ForkchoiceUpdateParameters, ProtoBlock,
51+
ResetPayloadStatuses,
5152
};
5253
use itertools::process_results;
5354
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
@@ -298,10 +299,10 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
298299
let beacon_block = store
299300
.get_full_block(&beacon_block_root)?
300301
.ok_or(Error::MissingBeaconBlock(beacon_block_root))?;
301-
let beacon_state_root = beacon_block.state_root();
302-
let beacon_state = store
303-
.get_state(&beacon_state_root, Some(beacon_block.slot()))?
304-
.ok_or(Error::MissingBeaconState(beacon_state_root))?;
302+
let current_slot = fork_choice.fc_store().get_current_slot();
303+
let (_, beacon_state) = store
304+
.get_advanced_hot_state(beacon_block_root, current_slot, beacon_block.state_root())?
305+
.ok_or(Error::MissingBeaconState(beacon_block.state_root()))?;
305306

306307
let snapshot = BeaconSnapshot {
307308
beacon_block_root,
@@ -669,10 +670,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
669670
.get_full_block(&new_view.head_block_root)?
670671
.ok_or(Error::MissingBeaconBlock(new_view.head_block_root))?;
671672

672-
let beacon_state_root = beacon_block.state_root();
673-
let beacon_state: BeaconState<T::EthSpec> = self
674-
.get_state(&beacon_state_root, Some(beacon_block.slot()))?
675-
.ok_or(Error::MissingBeaconState(beacon_state_root))?;
673+
let (_, beacon_state) = self
674+
.store
675+
.get_advanced_hot_state(
676+
new_view.head_block_root,
677+
current_slot,
678+
beacon_block.state_root(),
679+
)?
680+
.ok_or(Error::MissingBeaconState(beacon_block.state_root()))?;
676681

677682
Ok(BeaconSnapshot {
678683
beacon_block: Arc::new(beacon_block),

beacon_node/beacon_chain/src/migrate.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> BackgroundMigrator<E, Ho
266266
debug!(log, "Database consolidation started");
267267

268268
let finalized_state_root = notif.finalized_state_root;
269+
let finalized_block_root = notif.finalized_checkpoint.root;
269270

270271
let finalized_state = match db.get_state(&finalized_state_root.into(), None) {
271272
Ok(Some(state)) => state,
@@ -319,7 +320,12 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> BackgroundMigrator<E, Ho
319320
}
320321
};
321322

322-
match migrate_database(db.clone(), finalized_state_root.into(), &finalized_state) {
323+
match migrate_database(
324+
db.clone(),
325+
finalized_state_root.into(),
326+
finalized_block_root,
327+
&finalized_state,
328+
) {
323329
Ok(()) => {}
324330
Err(Error::HotColdDBError(HotColdDBError::FreezeSlotUnaligned(slot))) => {
325331
debug!(

beacon_node/beacon_chain/tests/attestation_verification.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use beacon_chain::{
99
test_utils::{
1010
test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType,
1111
},
12-
BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped,
12+
BeaconChain, BeaconChainError, BeaconChainTypes, ChainConfig, WhenSlotSkipped,
1313
};
1414
use genesis::{interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH};
1515
use int_to_bytes::int_to_bytes32;
@@ -47,6 +47,10 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessTyp
4747

4848
let harness = BeaconChainHarness::builder(MainnetEthSpec)
4949
.spec(spec)
50+
.chain_config(ChainConfig {
51+
reconstruct_historic_states: true,
52+
..ChainConfig::default()
53+
})
5054
.keypairs(KEYPAIRS[0..validator_count].to_vec())
5155
.fresh_ephemeral_store()
5256
.mock_execution_layer()
@@ -79,6 +83,10 @@ fn get_harness_capella_spec(
7983

8084
let harness = BeaconChainHarness::builder(MainnetEthSpec)
8185
.spec(spec.clone())
86+
.chain_config(ChainConfig {
87+
reconstruct_historic_states: true,
88+
..ChainConfig::default()
89+
})
8290
.keypairs(validator_keypairs)
8391
.withdrawal_keypairs(
8492
KEYPAIRS[0..validator_count]

beacon_node/beacon_chain/tests/block_verification.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use beacon_chain::test_utils::{
44
AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType,
55
};
66
use beacon_chain::{
7-
BeaconSnapshot, BlockError, ChainSegmentResult, IntoExecutionPendingBlock, NotifyExecutionLayer,
7+
BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock,
8+
NotifyExecutionLayer,
89
};
910
use lazy_static::lazy_static;
1011
use logging::test_logger;
@@ -69,6 +70,10 @@ async fn get_chain_segment() -> Vec<BeaconSnapshot<E>> {
6970
fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessType<E>> {
7071
let harness = BeaconChainHarness::builder(MainnetEthSpec)
7172
.default_spec()
73+
.chain_config(ChainConfig {
74+
reconstruct_historic_states: true,
75+
..ChainConfig::default()
76+
})
7277
.keypairs(KEYPAIRS[0..validator_count].to_vec())
7378
.fresh_ephemeral_store()
7479
.mock_execution_layer()

beacon_node/beacon_chain/tests/payload_invalidation.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use beacon_chain::otb_verification_service::{
77
use beacon_chain::{
88
canonical_head::{CachedHead, CanonicalHead},
99
test_utils::{BeaconChainHarness, EphemeralHarnessType},
10-
BeaconChainError, BlockError, ExecutionPayloadError, NotifyExecutionLayer,
10+
BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, NotifyExecutionLayer,
1111
OverrideForkchoiceUpdate, StateSkipConfig, WhenSlotSkipped,
1212
INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON,
1313
INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON,
@@ -59,6 +59,10 @@ impl InvalidPayloadRig {
5959

6060
let harness = BeaconChainHarness::builder(MainnetEthSpec)
6161
.spec(spec)
62+
.chain_config(ChainConfig {
63+
reconstruct_historic_states: true,
64+
..ChainConfig::default()
65+
})
6266
.logger(test_logger())
6367
.deterministic_keypairs(VALIDATOR_COUNT)
6468
.mock_execution_layer()

0 commit comments

Comments
 (0)