diff --git a/Cargo.lock b/Cargo.lock index 4f9fb143..aa1b04e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2201,6 +2201,7 @@ dependencies = [ "rand 0.10.0", "rocksdb", "tempfile", + "thiserror 2.0.18", "tracing", ] diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 04bda440..f6d3ca3e 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -640,7 +640,9 @@ async fn fetch_initial_state( // overlaps with what `get_forkchoice_store` already wrote, but it's // idempotent and the only path that also stores `BlockSignatures`. let anchor_root = signed_block.message.header().hash_tree_root(); - let mut store = Store::get_forkchoice_store(backend, state, signed_block.message.clone()); + let mut store = Store::get_forkchoice_store(backend, state, signed_block.message.clone()) + .inspect_err(|err| error!(%err, "Failed to initialize store from anchor state and block")) + .map_err(|_| checkpoint_sync::CheckpointSyncError::AnchorPairingMismatch)?; store.insert_signed_block(anchor_root, signed_block); Ok(store) } diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index de2eb359..e095991d 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -70,7 +70,8 @@ fn run(path: &Path) -> datatest_stable::Result<()> { } let backend = Arc::new(InMemoryBackend::new()); - let mut store = Store::get_forkchoice_store(backend, anchor_state, anchor_block); + let mut store = Store::get_forkchoice_store(backend, anchor_state, anchor_block) + .expect("anchor state and block must match"); // Block registry: maps block labels to their roots let mut block_registry: HashMap = HashMap::new(); diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index fdba2e56..5f6b0bd8 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -42,7 +42,8 @@ fn run(path: &Path) -> datatest_stable::Result<()> { // Initialize the store with the anchor state and block let genesis_time = anchor_state.config.genesis_time; let backend = Arc::new(InMemoryBackend::new()); - let mut st = Store::get_forkchoice_store(backend, anchor_state, anchor_block); + let mut st = Store::get_forkchoice_store(backend, anchor_state, anchor_block) + .expect("anchor state and block must match"); // Step 2: Run the state transition function with the block fixture let signed_block: SignedBlock = test.signed_block.into(); diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index d389a3a9..f5b2ca58 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -14,6 +14,7 @@ ethlambda-types.workspace = true tracing.workspace = true rocksdb.workspace = true +thiserror.workspace = true libssz.workspace = true libssz-derive.workspace = true diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 9c30f9c8..9662a36c 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -3,4 +3,4 @@ pub mod backend; mod store; pub use api::{ALL_TABLES, StorageBackend, StorageReadView, StorageWriteBatch, Table}; -pub use store::{ForkCheckpoints, Store}; +pub use store::{ForkCheckpoints, GetForkchoiceStoreError, Store}; diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index c0ed50e9..86a258ff 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -20,8 +20,22 @@ use ethlambda_types::{ state::{ChainConfig, State, anchor_pair_is_consistent}, }; use libssz::{SszDecode, SszEncode}; +use thiserror::Error; use tracing::info; +/// Errors returned by [`Store::get_forkchoice_store`]. +#[derive(Debug, Error)] +pub enum GetForkchoiceStoreError { + #[error( + "anchor block doesn't match anchor state: \ + state header = {anchor_state:?}, block = {anchor_block:?}" + )] + AnchorPairInconsistent { + anchor_state: Box, + anchor_block: Box, + }, +} + /// Checkpoints to update in the forkchoice store. /// /// Used with `Store::update_checkpoints` to update head and optionally @@ -470,20 +484,28 @@ impl Store { /// The block must match the state's `latest_block_header`. /// Named to mirror the spec's `get_forkchoice_store` function. /// - /// # Panics + /// # Errors /// - /// Panics if [`anchor_pair_is_consistent`] would reject the pair. + /// Returns [`GetForkchoiceStoreError::AnchorPairInconsistent`] if the block's header + /// doesn't match the state's `latest_block_header` (comparing all fields + /// except `state_root`, which is computed internally). pub fn get_forkchoice_store( backend: Arc, mut anchor_state: State, anchor_block: Block, - ) -> Self { - assert!( - anchor_pair_is_consistent(&mut anchor_state, &anchor_block), - "anchor block does not match anchor state" - ); + ) -> Result { + if !anchor_pair_is_consistent(&mut anchor_state, &anchor_block) { + return Err(GetForkchoiceStoreError::AnchorPairInconsistent { + anchor_state: Box::new(anchor_state), + anchor_block: Box::new(anchor_block), + }); + } - Self::init_store(backend, anchor_state, Some(anchor_block.body)) + Ok(Self::init_store( + backend, + anchor_state, + Some(anchor_block.body), + )) } /// Internal helper to initialize the store with anchor data.