Skip to content

Commit 194a01a

Browse files
0x00101010mattsse
andauthored
feat(engine): Update execution cache on inserted executed blocks (#19822)
Co-authored-by: Matthias Seitz <[email protected]>
1 parent c5764f5 commit 194a01a

File tree

3 files changed

+129
-0
lines changed

3 files changed

+129
-0
lines changed

crates/engine/tree/src/tree/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,7 @@ where
13741374
}
13751375

13761376
self.state.tree_state.insert_executed(block.clone());
1377+
self.payload_validator.on_inserted_executed_block(block.clone());
13771378
self.metrics.engine.inserted_already_executed_blocks.increment(1);
13781379
self.emit_event(EngineApiEvent::BeaconConsensus(
13791380
ConsensusEngineEvent::CanonicalBlockAdded(block, now.elapsed()),

crates/engine/tree/src/tree/payload_processor/mod.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::tree::{
1313
sparse_trie::SparseTrieTask,
1414
StateProviderBuilder, TreeConfig,
1515
};
16+
use alloy_eips::eip1898::BlockWithParent;
1617
use alloy_evm::{block::StateChangeSource, ToTxEnv};
1718
use alloy_primitives::B256;
1819
use crossbeam_channel::Sender as CrossbeamSender;
@@ -464,6 +465,54 @@ where
464465
cleared_sparse_trie.lock().replace(cleared_trie);
465466
});
466467
}
468+
469+
/// Updates the execution cache with the post-execution state from an inserted block.
470+
///
471+
/// This is used when blocks are inserted directly (e.g., locally built blocks by sequencers)
472+
/// to ensure the cache remains warm for subsequent block execution.
473+
///
474+
/// The cache enables subsequent blocks to reuse account, storage, and bytecode data without
475+
/// hitting the database, maintaining performance consistency.
476+
pub(crate) fn on_inserted_executed_block(
477+
&self,
478+
block_with_parent: BlockWithParent,
479+
bundle_state: &BundleState,
480+
) {
481+
self.execution_cache.update_with_guard(|cached| {
482+
if cached.as_ref().is_some_and(|c| c.executed_block_hash() != block_with_parent.parent) {
483+
debug!(
484+
target: "engine::caching",
485+
parent_hash = %block_with_parent.parent,
486+
"Cannot find cache for parent hash, skip updating cache with new state for inserted executed block",
487+
);
488+
return;
489+
}
490+
491+
// Take existing cache (if any) or create fresh caches
492+
let (caches, cache_metrics) = match cached.take() {
493+
Some(existing) => {
494+
existing.split()
495+
}
496+
None => (
497+
ExecutionCacheBuilder::default().build_caches(self.cross_block_cache_size),
498+
CachedStateMetrics::zeroed(),
499+
),
500+
};
501+
502+
// Insert the block's bundle state into cache
503+
let new_cache = SavedCache::new(block_with_parent.block.hash, caches, cache_metrics);
504+
if new_cache.cache().insert_state(bundle_state).is_err() {
505+
*cached = None;
506+
debug!(target: "engine::caching", "cleared execution cache on update error");
507+
return;
508+
}
509+
new_cache.update_metrics();
510+
511+
// Replace with the updated cache
512+
*cached = Some(new_cache);
513+
debug!(target: "engine::caching", ?block_with_parent, "Updated execution cache for inserted block");
514+
});
515+
}
467516
}
468517

469518
/// Handle to all the spawned tasks.
@@ -703,6 +752,7 @@ mod tests {
703752
precompile_cache::PrecompileCacheMap,
704753
StateProviderBuilder, TreeConfig,
705754
};
755+
use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};
706756
use alloy_evm::block::StateChangeSource;
707757
use rand::Rng;
708758
use reth_chainspec::ChainSpec;
@@ -716,6 +766,7 @@ mod tests {
716766
test_utils::create_test_provider_factory_with_chain_spec,
717767
ChainSpecProvider, HashingWriter,
718768
};
769+
use reth_revm::db::BundleState;
719770
use reth_testing_utils::generators;
720771
use reth_trie::{test_utils::state_root, HashedPostState};
721772
use revm_primitives::{Address, HashMap, B256, KECCAK_EMPTY, U256};
@@ -793,6 +844,70 @@ mod tests {
793844
assert!(new_checkout.is_some(), "new checkout should succeed after release and update");
794845
}
795846

847+
#[test]
848+
fn on_inserted_executed_block_populates_cache() {
849+
let payload_processor = PayloadProcessor::new(
850+
WorkloadExecutor::default(),
851+
EthEvmConfig::new(Arc::new(ChainSpec::default())),
852+
&TreeConfig::default(),
853+
PrecompileCacheMap::default(),
854+
);
855+
856+
let parent_hash = B256::from([1u8; 32]);
857+
let block_hash = B256::from([10u8; 32]);
858+
let block_with_parent = BlockWithParent {
859+
block: BlockNumHash { hash: block_hash, number: 1 },
860+
parent: parent_hash,
861+
};
862+
let bundle_state = BundleState::default();
863+
864+
// Cache should be empty initially
865+
assert!(payload_processor.execution_cache.get_cache_for(block_hash).is_none());
866+
867+
// Update cache with inserted block
868+
payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
869+
870+
// Cache should now exist for the block hash
871+
let cached = payload_processor.execution_cache.get_cache_for(block_hash);
872+
assert!(cached.is_some());
873+
assert_eq!(cached.unwrap().executed_block_hash(), block_hash);
874+
}
875+
876+
#[test]
877+
fn on_inserted_executed_block_skips_on_parent_mismatch() {
878+
let payload_processor = PayloadProcessor::new(
879+
WorkloadExecutor::default(),
880+
EthEvmConfig::new(Arc::new(ChainSpec::default())),
881+
&TreeConfig::default(),
882+
PrecompileCacheMap::default(),
883+
);
884+
885+
// Setup: populate cache with block 1
886+
let block1_hash = B256::from([1u8; 32]);
887+
payload_processor
888+
.execution_cache
889+
.update_with_guard(|slot| *slot = Some(make_saved_cache(block1_hash)));
890+
891+
// Try to insert block 3 with wrong parent (should skip and keep block 1's cache)
892+
let wrong_parent = B256::from([99u8; 32]);
893+
let block3_hash = B256::from([3u8; 32]);
894+
let block_with_parent = BlockWithParent {
895+
block: BlockNumHash { hash: block3_hash, number: 3 },
896+
parent: wrong_parent,
897+
};
898+
let bundle_state = BundleState::default();
899+
900+
payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
901+
902+
// Cache should still be for block 1 (unchanged)
903+
let cached = payload_processor.execution_cache.get_cache_for(block1_hash);
904+
assert!(cached.is_some(), "Original cache should be preserved");
905+
906+
// Cache for block 3 should not exist
907+
let cached3 = payload_processor.execution_cache.get_cache_for(block3_hash);
908+
assert!(cached3.is_none(), "New block cache should not be created on mismatch");
909+
}
910+
796911
fn create_mock_state_updates(num_accounts: usize, updates_per_account: usize) -> Vec<EvmState> {
797912
let mut rng = generators::rng();
798913
let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();

crates/engine/tree/src/tree/payload_validator.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,12 @@ pub trait EngineValidator<
11151115
block: RecoveredBlock<N::Block>,
11161116
ctx: TreeCtx<'_, N>,
11171117
) -> ValidationOutcome<N>;
1118+
1119+
/// Hook called after an executed block is inserted directly into the tree.
1120+
///
1121+
/// This is invoked when blocks are inserted via `InsertExecutedBlock` (e.g., locally built
1122+
/// blocks by sequencers) to allow implementations to update internal state such as caches.
1123+
fn on_inserted_executed_block(&self, block: ExecutedBlock<N>);
11181124
}
11191125

11201126
impl<N, Types, P, Evm, V> EngineValidator<Types> for BasicEngineValidator<P, Evm, V>
@@ -1163,6 +1169,13 @@ where
11631169
) -> ValidationOutcome<N> {
11641170
self.validate_block_with_state(BlockOrPayload::Block(block), ctx)
11651171
}
1172+
1173+
fn on_inserted_executed_block(&self, block: ExecutedBlock<N>) {
1174+
self.payload_processor.on_inserted_executed_block(
1175+
block.recovered_block.block_with_parent(),
1176+
block.execution_output.state(),
1177+
);
1178+
}
11661179
}
11671180

11681181
/// Enum representing either block or payload being validated.

0 commit comments

Comments
 (0)