Skip to content

Commit c574f81

Browse files
committed
Fix block backfill with genesis skip slots (#4820)
## Issue Addressed Closes #4817. ## Proposed Changes - Fill in the linear block roots array between 0 and the slot of the first block (e.g. slots 0 and 1 on Holesky). - Backport the `--freezer`, `--skip` and `--limit` options for `lighthouse db inspect` from tree-states. This allows us to easily view the database corruption of 4817 using `lighthouse db inspect --network holesky --freezer --column bbr --output values --limit 2`. - Backport the `iter_column_from` change and `MemoryStore` overhaul from tree-states. These are required to enable `lighthouse db inspect`. - Rework `freezer_upper_limit` to allow state lookups for slots below the `state_lower_limit`. Currently state lookups will fail until state reconstruction completes entirely. There is a new regression test for the main bug, but no test for the `freezer_upper_limit` fix because we don't currently support running state reconstruction partially (see #3026). This will be fixed once we merge `tree-states`! In lieu of an automated test, I've tested manually on a Holesky node while it was reconstructing. ## Additional Info Users who backfilled Holesky to slot 0 (e.g. using `--reconstruct-historic-states`) need to either: - Re-sync from genesis. - Re-sync using checkpoint sync and the changes from this PR. Due to the recency of the Holesky genesis, writing a custom pass to fix up broken databases (which would require its own thorough testing) was deemed unnecessary. This is the primary reason for this PR being marked `backwards-incompat`. This will create few conflicts with Deneb, which I've already resolved on `tree-states-deneb` and will be happy to backport to Deneb once this PR is merged to unstable.
1 parent b82d1a9 commit c574f81

File tree

11 files changed

+207
-101
lines changed

11 files changed

+207
-101
lines changed

Cargo.lock

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

beacon_node/beacon_chain/src/historical_blocks.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
135135
prev_block_slot = block.slot();
136136
expected_block_root = block.message().parent_root();
137137

138-
// If we've reached genesis, add the genesis block root to the batch and set the
139-
// anchor slot to 0 to indicate completion.
138+
// If we've reached genesis, add the genesis block root to the batch for all slots
139+
// between 0 and the first block slot, and set the anchor slot to 0 to indicate
140+
// completion.
140141
if expected_block_root == self.genesis_block_root {
141142
let genesis_slot = self.spec.genesis_slot;
142-
chunk_writer.set(
143-
genesis_slot.as_usize(),
144-
self.genesis_block_root,
145-
&mut cold_batch,
146-
)?;
143+
for slot in genesis_slot.as_usize()..block.slot().as_usize() {
144+
chunk_writer.set(slot, self.genesis_block_root, &mut cold_batch)?;
145+
}
147146
prev_block_slot = genesis_slot;
148147
expected_block_root = Hash256::zero();
149148
break;

beacon_node/beacon_chain/src/otb_verification_service.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,13 @@ pub fn start_otb_verification_service<T: BeaconChainTypes>(
119119
pub fn load_optimistic_transition_blocks<T: BeaconChainTypes>(
120120
chain: &BeaconChain<T>,
121121
) -> Result<Vec<OptimisticTransitionBlock>, StoreError> {
122-
process_results(chain.store.hot_db.iter_column(OTBColumn), |iter| {
123-
iter.map(|(_, bytes)| OptimisticTransitionBlock::from_store_bytes(&bytes))
124-
.collect()
125-
})?
122+
process_results(
123+
chain.store.hot_db.iter_column::<Hash256>(OTBColumn),
124+
|iter| {
125+
iter.map(|(_, bytes)| OptimisticTransitionBlock::from_store_bytes(&bytes))
126+
.collect()
127+
},
128+
)?
126129
}
127130

128131
#[derive(Debug)]

beacon_node/beacon_chain/tests/store_tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,6 +2273,18 @@ async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() {
22732273
weak_subjectivity_sync_test(slots, checkpoint_slot).await
22742274
}
22752275

2276+
// Regression test for https://github.com/sigp/lighthouse/issues/4817
2277+
// Skip 3 slots immediately after genesis, creating a gap between the genesis block and the first
2278+
// real block.
2279+
#[tokio::test]
2280+
async fn weak_subjectivity_sync_skips_at_genesis() {
2281+
let start_slot = 4;
2282+
let end_slot = E::slots_per_epoch() * 4;
2283+
let slots = (start_slot..end_slot).map(Slot::new).collect();
2284+
let checkpoint_slot = Slot::new(E::slots_per_epoch() * 2);
2285+
weak_subjectivity_sync_test(slots, checkpoint_slot).await
2286+
}
2287+
22762288
async fn weak_subjectivity_sync_test(slots: Vec<Slot>, checkpoint_slot: Slot) {
22772289
// Build an initial chain on one harness, representing a synced node with full history.
22782290
let num_final_blocks = E::slots_per_epoch() * 2;

beacon_node/store/src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ pub enum Error {
4545
BlockReplayError(BlockReplayError),
4646
AddPayloadLogicError,
4747
SlotClockUnavailableForMigration,
48+
InvalidKey,
49+
InvalidBytes,
4850
UnableToDowngrade,
4951
InconsistentFork(InconsistentFork),
5052
}

beacon_node/store/src/hot_cold_store.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,10 +1490,17 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
14901490
let split_slot = self.get_split_slot();
14911491
let anchor = self.get_anchor_info();
14921492

1493-
// There are no restore points stored if the state upper limit lies in the hot database.
1494-
// It hasn't been reached yet, and may never be.
1495-
if anchor.map_or(false, |a| a.state_upper_limit >= split_slot) {
1493+
// There are no restore points stored if the state upper limit lies in the hot database,
1494+
// and the lower limit is zero. It hasn't been reached yet, and may never be.
1495+
if anchor.as_ref().map_or(false, |a| {
1496+
a.state_upper_limit >= split_slot && a.state_lower_limit == 0
1497+
}) {
14961498
None
1499+
} else if let Some(lower_limit) = anchor
1500+
.map(|a| a.state_lower_limit)
1501+
.filter(|limit| *limit > 0)
1502+
{
1503+
Some(lower_limit)
14971504
} else {
14981505
Some(
14991506
(split_slot - 1) / self.config.slots_per_restore_point

beacon_node/store/src/leveldb_store.rs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use super::*;
22
use crate::hot_cold_store::HotColdDBError;
33
use crate::metrics;
4-
use db_key::Key;
54
use leveldb::compaction::Compaction;
65
use leveldb::database::batch::{Batch, Writebatch};
76
use leveldb::database::kv::KV;
@@ -176,24 +175,21 @@ impl<E: EthSpec> KeyValueStore<E> for LevelDB<E> {
176175
Ok(())
177176
}
178177

179-
/// Iterate through all keys and values in a particular column.
180-
fn iter_column(&self, column: DBColumn) -> ColumnIter {
181-
let start_key =
182-
BytesKey::from_vec(get_key_for_col(column.into(), Hash256::zero().as_bytes()));
178+
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<K> {
179+
let start_key = BytesKey::from_vec(get_key_for_col(column.into(), from));
183180

184181
let iter = self.db.iter(self.read_options());
185182
iter.seek(&start_key);
186183

187184
Box::new(
188185
iter.take_while(move |(key, _)| key.matches_column(column))
189186
.map(move |(bytes_key, value)| {
190-
let key =
191-
bytes_key
192-
.remove_column(column)
193-
.ok_or(HotColdDBError::IterationError {
194-
unexpected_key: bytes_key,
195-
})?;
196-
Ok((key, value))
187+
let key = bytes_key.remove_column_variable(column).ok_or_else(|| {
188+
HotColdDBError::IterationError {
189+
unexpected_key: bytes_key.clone(),
190+
}
191+
})?;
192+
Ok((K::from_bytes(key)?, value))
197193
}),
198194
)
199195
}
@@ -254,12 +250,12 @@ impl<E: EthSpec> KeyValueStore<E> for LevelDB<E> {
254250
impl<E: EthSpec> ItemStore<E> for LevelDB<E> {}
255251

256252
/// Used for keying leveldb.
257-
#[derive(Debug, PartialEq)]
253+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
258254
pub struct BytesKey {
259255
key: Vec<u8>,
260256
}
261257

262-
impl Key for BytesKey {
258+
impl db_key::Key for BytesKey {
263259
fn from_u8(key: &[u8]) -> Self {
264260
Self { key: key.to_vec() }
265261
}
@@ -275,12 +271,20 @@ impl BytesKey {
275271
self.key.starts_with(column.as_bytes())
276272
}
277273

278-
/// Remove the column from a key, returning its `Hash256` portion.
274+
/// Remove the column from a 32 byte key, yielding the `Hash256` key.
279275
pub fn remove_column(&self, column: DBColumn) -> Option<Hash256> {
276+
let key = self.remove_column_variable(column)?;
277+
(column.key_size() == 32).then(|| Hash256::from_slice(key))
278+
}
279+
280+
/// Remove the column from a key.
281+
///
282+
/// Will return `None` if the value doesn't match the column or has the wrong length.
283+
pub fn remove_column_variable(&self, column: DBColumn) -> Option<&[u8]> {
280284
if self.matches_column(column) {
281285
let subkey = &self.key[column.as_bytes().len()..];
282-
if subkey.len() == 32 {
283-
return Some(Hash256::from_slice(subkey));
286+
if subkey.len() == column.key_size() {
287+
return Some(subkey);
284288
}
285289
}
286290
None

beacon_node/store/src/lib.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use std::sync::Arc;
4444
use strum::{EnumString, IntoStaticStr};
4545
pub use types::*;
4646

47-
pub type ColumnIter<'a> = Box<dyn Iterator<Item = Result<(Hash256, Vec<u8>), Error>> + 'a>;
47+
pub type ColumnIter<'a, K> = Box<dyn Iterator<Item = Result<(K, Vec<u8>), Error>> + 'a>;
4848
pub type ColumnKeyIter<'a> = Box<dyn Iterator<Item = Result<Hash256, Error>> + 'a>;
4949

5050
pub type RawEntryIter<'a> = Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), Error>> + 'a>;
@@ -84,11 +84,12 @@ pub trait KeyValueStore<E: EthSpec>: Sync + Send + Sized + 'static {
8484
fn compact(&self) -> Result<(), Error>;
8585

8686
/// Iterate through all keys and values in a particular column.
87-
fn iter_column(&self, _column: DBColumn) -> ColumnIter {
88-
// Default impl for non LevelDB databases
89-
Box::new(std::iter::empty())
87+
fn iter_column<K: Key>(&self, column: DBColumn) -> ColumnIter<K> {
88+
self.iter_column_from(column, &vec![0; column.key_size()])
9089
}
9190

91+
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<K>;
92+
9293
fn iter_raw_entries(&self, _column: DBColumn, _prefix: &[u8]) -> RawEntryIter {
9394
Box::new(std::iter::empty())
9495
}
@@ -98,9 +99,26 @@ pub trait KeyValueStore<E: EthSpec>: Sync + Send + Sized + 'static {
9899
}
99100

100101
/// Iterate through all keys in a particular column.
101-
fn iter_column_keys(&self, _column: DBColumn) -> ColumnKeyIter {
102-
// Default impl for non LevelDB databases
103-
Box::new(std::iter::empty())
102+
fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter;
103+
}
104+
105+
pub trait Key: Sized + 'static {
106+
fn from_bytes(key: &[u8]) -> Result<Self, Error>;
107+
}
108+
109+
impl Key for Hash256 {
110+
fn from_bytes(key: &[u8]) -> Result<Self, Error> {
111+
if key.len() == 32 {
112+
Ok(Hash256::from_slice(key))
113+
} else {
114+
Err(Error::InvalidKey)
115+
}
116+
}
117+
}
118+
119+
impl Key for Vec<u8> {
120+
fn from_bytes(key: &[u8]) -> Result<Self, Error> {
121+
Ok(key.to_vec())
104122
}
105123
}
106124

@@ -250,6 +268,35 @@ impl DBColumn {
250268
pub fn as_bytes(self) -> &'static [u8] {
251269
self.as_str().as_bytes()
252270
}
271+
272+
/// Most database keys are 32 bytes, but some freezer DB keys are 8 bytes.
273+
///
274+
/// This function returns the number of bytes used by keys in a given column.
275+
pub fn key_size(self) -> usize {
276+
match self {
277+
Self::OverflowLRUCache => 40,
278+
Self::BeaconMeta
279+
| Self::BeaconBlock
280+
| Self::BeaconState
281+
| Self::BeaconBlob
282+
| Self::BeaconStateSummary
283+
| Self::BeaconStateTemporary
284+
| Self::ExecPayload
285+
| Self::BeaconChain
286+
| Self::OpPool
287+
| Self::Eth1Cache
288+
| Self::ForkChoice
289+
| Self::PubkeyCache
290+
| Self::BeaconRestorePoint
291+
| Self::DhtEnrs
292+
| Self::OptimisticTransitionBlock => 32,
293+
Self::BeaconBlockRoots
294+
| Self::BeaconStateRoots
295+
| Self::BeaconHistoricalRoots
296+
| Self::BeaconHistoricalSummaries
297+
| Self::BeaconRandaoMixes => 8,
298+
}
299+
}
253300
}
254301

255302
/// An item that may stored in a `Store` by serializing and deserializing from bytes.

0 commit comments

Comments
 (0)