Skip to content

Commit c1f94d9

Browse files
Test database schema stability (#7669)
This PR implements some heuristics to check for breaking database changes. The goal is to prevent accidental changes to the database schema occurring without a version bump.
1 parent 25ea8a8 commit c1f94d9

File tree

4 files changed

+157
-1
lines changed

4 files changed

+157
-1
lines changed

beacon_node/beacon_chain/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub mod observed_block_producers;
4545
pub mod observed_data_sidecars;
4646
pub mod observed_operations;
4747
mod observed_slashable;
48-
mod persisted_beacon_chain;
48+
pub mod persisted_beacon_chain;
4949
pub mod persisted_custody;
5050
mod persisted_fork_choice;
5151
mod pre_finalization_cache;

beacon_node/beacon_chain/tests/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod events;
77
mod op_verification;
88
mod payload_invalidation;
99
mod rewards;
10+
mod schema_stability;
1011
mod store_tests;
1112
mod sync_committee_verification;
1213
mod tests;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use beacon_chain::{
2+
persisted_beacon_chain::PersistedBeaconChain,
3+
persisted_custody::PersistedCustody,
4+
test_utils::{test_spec, BeaconChainHarness, DiskHarnessType},
5+
ChainConfig,
6+
};
7+
use logging::create_test_tracing_subscriber;
8+
use operation_pool::PersistedOperationPool;
9+
use ssz::Encode;
10+
use std::sync::{Arc, LazyLock};
11+
use store::{
12+
database::interface::BeaconNodeBackend, hot_cold_store::Split, metadata::DataColumnInfo,
13+
DBColumn, HotColdDB, StoreConfig, StoreItem,
14+
};
15+
use strum::IntoEnumIterator;
16+
use tempfile::{tempdir, TempDir};
17+
use types::{ChainSpec, Hash256, Keypair, MainnetEthSpec};
18+
19+
type E = MainnetEthSpec;
20+
type Store<E> = Arc<HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>>;
21+
type TestHarness = BeaconChainHarness<DiskHarnessType<E>>;
22+
23+
const VALIDATOR_COUNT: usize = 32;
24+
25+
/// A cached set of keys.
26+
static KEYPAIRS: LazyLock<Vec<Keypair>> =
27+
LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT));
28+
29+
fn get_store(db_path: &TempDir, config: StoreConfig, spec: Arc<ChainSpec>) -> Store<E> {
30+
create_test_tracing_subscriber();
31+
let hot_path = db_path.path().join("chain_db");
32+
let cold_path = db_path.path().join("freezer_db");
33+
let blobs_path = db_path.path().join("blobs_db");
34+
35+
HotColdDB::open(
36+
&hot_path,
37+
&cold_path,
38+
&blobs_path,
39+
|_, _, _| Ok(()),
40+
config,
41+
spec,
42+
)
43+
.expect("disk store should initialize")
44+
}
45+
46+
/// This test checks the database schema stability against previous versions of Lighthouse's code.
47+
///
48+
/// If you are changing something about how Lighthouse stores data on disk, you almost certainly
49+
/// need to implement a database schema change. This is true even if the data being stored only
50+
/// applies to an upcoming fork that isn't live on mainnet. We never want to be in the situation
51+
/// where commit A writes data in some format, and then a later commit B changes that format
52+
/// without a schema change. This is liable to break any nodes that update from A to B, even if
53+
/// these nodes are just testnet nodes.
54+
///
55+
/// This test implements partial, imperfect checks on the DB schema which are designed to quickly
56+
/// catch common changes.
57+
///
58+
/// This test uses hardcoded values, rather than trying to access previous versions of Lighthouse's
59+
/// code. If you've successfully implemented a schema change and you're sure that the new values are
60+
/// correct, you can update the hardcoded values here.
61+
#[tokio::test]
62+
async fn schema_stability() {
63+
let spec = Arc::new(test_spec::<E>());
64+
65+
let datadir = tempdir().unwrap();
66+
let store_config = StoreConfig::default();
67+
let store = get_store(&datadir, store_config, spec.clone());
68+
69+
let chain_config = ChainConfig {
70+
reconstruct_historic_states: true,
71+
..ChainConfig::default()
72+
};
73+
74+
let harness = TestHarness::builder(MainnetEthSpec)
75+
.spec(spec)
76+
.keypairs(KEYPAIRS.to_vec())
77+
.fresh_disk_store(store.clone())
78+
.mock_execution_layer()
79+
.chain_config(chain_config)
80+
.build();
81+
harness.advance_slot();
82+
83+
let chain = &harness.chain;
84+
85+
chain.persist_op_pool().unwrap();
86+
chain.persist_custody_context().unwrap();
87+
88+
check_db_columns();
89+
check_metadata_sizes(&store);
90+
check_op_pool(&store);
91+
check_custody_context(&store);
92+
check_persisted_chain(&store);
93+
94+
// Not covered here:
95+
// - Fork choice (not tested)
96+
// - DBColumn::DhtEnrs (tested in network crate)
97+
}
98+
99+
/// Check that the set of database columns is unchanged.
100+
fn check_db_columns() {
101+
let current_columns: Vec<&'static str> = DBColumn::iter().map(|c| c.as_str()).collect();
102+
let expected_columns = vec![
103+
"bma", "blk", "blb", "bdc", "ste", "hsd", "hsn", "bsn", "bsd", "bss", "bs3", "bcs", "bst",
104+
"exp", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", "bhr", "brm",
105+
"dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy",
106+
];
107+
assert_eq!(expected_columns, current_columns);
108+
}
109+
110+
/// Check the SSZ sizes of known on-disk metadata.
111+
///
112+
/// New types can be added here as the schema evolves.
113+
fn check_metadata_sizes(store: &Store<E>) {
114+
assert_eq!(Split::default().ssz_bytes_len(), 40);
115+
assert_eq!(store.get_anchor_info().ssz_bytes_len(), 64);
116+
assert_eq!(
117+
store.get_blob_info().ssz_bytes_len(),
118+
if store.get_chain_spec().deneb_fork_epoch.is_some() {
119+
14
120+
} else {
121+
6
122+
}
123+
);
124+
assert_eq!(DataColumnInfo::default().ssz_bytes_len(), 5);
125+
}
126+
127+
fn check_op_pool(store: &Store<E>) {
128+
let op_pool = store
129+
.get_item::<PersistedOperationPool<E>>(&Hash256::ZERO)
130+
.unwrap()
131+
.unwrap();
132+
assert!(matches!(op_pool, PersistedOperationPool::V20(_)));
133+
assert_eq!(op_pool.ssz_bytes_len(), 28);
134+
assert_eq!(op_pool.as_store_bytes().len(), 28);
135+
}
136+
137+
fn check_custody_context(store: &Store<E>) {
138+
let custody_context = store
139+
.get_item::<PersistedCustody>(&Hash256::ZERO)
140+
.unwrap()
141+
.unwrap();
142+
assert_eq!(custody_context.as_store_bytes().len(), 9);
143+
}
144+
145+
fn check_persisted_chain(store: &Store<E>) {
146+
let chain = store
147+
.get_item::<PersistedBeaconChain>(&Hash256::ZERO)
148+
.unwrap()
149+
.unwrap();
150+
assert_eq!(chain.as_store_bytes().len(), 32);
151+
}

beacon_node/network/src/persisted_dht.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,9 @@ mod tests {
8686
.unwrap();
8787
let dht: PersistedDht = store.get_item(&DHT_DB_KEY).unwrap().unwrap();
8888
assert_eq!(dht.enrs, enrs);
89+
90+
// This hardcoded length check is for database schema compatibility. If the on-disk format
91+
// of `PersistedDht` changes, we need a DB schema change.
92+
assert_eq!(dht.as_store_bytes().len(), 136);
8993
}
9094
}

0 commit comments

Comments
 (0)