fix(provider): check executed block before returning historical state (#21571)

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
This commit is contained in:
Georgios Konstantopoulos
2026-01-29 05:54:50 -08:00
committed by GitHub
parent e9fe0283a9
commit 70bfdafd26
2 changed files with 64 additions and 9 deletions

View File

@@ -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.

View File

@@ -825,10 +825,19 @@ impl<TX: DbTx + 'static, N: NodeTypes> TryIntoHistoricalStateProvider for Databa
self,
mut block_number: BlockNumber,
) -> ProviderResult<StateProviderBox> {
// 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"),
}
}
}