From 70bfdafd26905a19beb22bc1db7012494919fa22 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Thu, 29 Jan 2026 05:54:50 -0800 Subject: [PATCH] fix(provider): check executed block before returning historical state (#21571) Co-authored-by: Amp Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com> --- crates/storage/errors/src/provider.rs | 8 +++ .../src/providers/database/provider.rs | 65 ++++++++++++++++--- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/crates/storage/errors/src/provider.rs b/crates/storage/errors/src/provider.rs index 6536b255ac..2ef823c5a1 100644 --- a/crates/storage/errors/src/provider.rs +++ b/crates/storage/errors/src/provider.rs @@ -104,6 +104,14 @@ pub enum ProviderError { /// State is not available for the given block number because it is pruned. #[error("state at block #{_0} is pruned")] StateAtBlockPruned(BlockNumber), + /// State is not available because the block has not been executed yet. + #[error("state at block #{requested} is not available, block has not been executed yet (latest executed: #{executed})")] + BlockNotExecuted { + /// The block number that was requested. + requested: BlockNumber, + /// The latest executed block number. + executed: BlockNumber, + }, /// Block data is not available because history has expired. /// /// The requested block number is below the earliest available block. diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index f29893c7fa..005bd6cddc 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -825,10 +825,19 @@ impl TryIntoHistoricalStateProvider for Databa self, mut block_number: BlockNumber, ) -> ProviderResult { - // if the block number is the same as the currently best block number on disk we can use the - // latest state provider here - if block_number == self.best_block_number().unwrap_or_default() { - return Ok(Box::new(LatestStateProvider::new(self))) + let best_block = self.best_block_number().unwrap_or_default(); + + // Reject requests for blocks beyond the best block + if block_number > best_block { + return Err(ProviderError::BlockNotExecuted { + requested: block_number, + executed: best_block, + }); + } + + // If requesting state at the best block, use the latest state provider + if block_number == best_block { + return Ok(Box::new(LatestStateProvider::new(self))); } // +1 as the changeset that we want is the one that was applied after this block. @@ -3605,7 +3614,7 @@ mod tests { let data = BlockchainTestData::default(); let provider_rw = factory.provider_rw().unwrap(); - provider_rw.insert_block(&data.genesis.clone().try_recover().unwrap()).unwrap(); + provider_rw.insert_block(&data.genesis.try_recover().unwrap()).unwrap(); provider_rw .write_state( &ExecutionOutcome { first_block: 0, receipts: vec![vec![]], ..Default::default() }, @@ -3638,7 +3647,7 @@ mod tests { let data = BlockchainTestData::default(); let provider_rw = factory.provider_rw().unwrap(); - provider_rw.insert_block(&data.genesis.clone().try_recover().unwrap()).unwrap(); + provider_rw.insert_block(&data.genesis.try_recover().unwrap()).unwrap(); provider_rw .write_state( &ExecutionOutcome { first_block: 0, receipts: vec![vec![]], ..Default::default() }, @@ -3675,7 +3684,7 @@ mod tests { let data = BlockchainTestData::default(); let provider_rw = factory.provider_rw().unwrap(); - provider_rw.insert_block(&data.genesis.clone().try_recover().unwrap()).unwrap(); + provider_rw.insert_block(&data.genesis.try_recover().unwrap()).unwrap(); provider_rw .write_state( &ExecutionOutcome { first_block: 0, receipts: vec![vec![]], ..Default::default() }, @@ -3713,7 +3722,7 @@ mod tests { let data = BlockchainTestData::default(); let provider_rw = factory.provider_rw().unwrap(); - provider_rw.insert_block(&data.genesis.clone().try_recover().unwrap()).unwrap(); + provider_rw.insert_block(&data.genesis.try_recover().unwrap()).unwrap(); provider_rw .write_state( &ExecutionOutcome { first_block: 0, receipts: vec![vec![]], ..Default::default() }, @@ -3783,7 +3792,7 @@ mod tests { let data = BlockchainTestData::default(); let provider_rw = factory.provider_rw().unwrap(); - provider_rw.insert_block(&data.genesis.clone().try_recover().unwrap()).unwrap(); + provider_rw.insert_block(&data.genesis.try_recover().unwrap()).unwrap(); provider_rw .write_state( &ExecutionOutcome { first_block: 0, receipts: vec![vec![]], ..Default::default() }, @@ -4178,4 +4187,42 @@ mod tests { } } } + + #[test] + fn test_try_into_history_rejects_unexecuted_blocks() { + use reth_storage_api::TryIntoHistoricalStateProvider; + + let factory = create_test_provider_factory(); + + // Insert genesis block to have some data + let data = BlockchainTestData::default(); + let provider_rw = factory.provider_rw().unwrap(); + provider_rw.insert_block(&data.genesis.try_recover().unwrap()).unwrap(); + provider_rw + .write_state( + &ExecutionOutcome { first_block: 0, receipts: vec![vec![]], ..Default::default() }, + crate::OriginalValuesKnown::No, + StateWriteConfig::default(), + ) + .unwrap(); + provider_rw.commit().unwrap(); + + // Get a fresh provider - Execution checkpoint is 0, no receipts written beyond genesis + let provider = factory.provider().unwrap(); + + // Requesting historical state for block 0 (executed) should succeed + let result = provider.try_into_history_at_block(0); + assert!(result.is_ok(), "Block 0 should be available"); + + // Get another provider and request state for block 100 (not executed) + let provider = factory.provider().unwrap(); + let result = provider.try_into_history_at_block(100); + + // Should fail with BlockNotExecuted error + match result { + Err(ProviderError::BlockNotExecuted { requested: 100, .. }) => {} + Err(e) => panic!("Expected BlockNotExecuted error, got: {e:?}"), + Ok(_) => panic!("Expected error, got Ok"), + } + } }