Skip to content

Commit 145561d

Browse files
committed
Cache flashblock trie nodes to optimize bundle metering
Adds a single-entry cache for the latest flashblock's trie nodes, allowing bundle metering operations to reuse the cached trie computation instead of recalculating from scratch each time. Key changes: - Add FlashblockTrieCache: thread-safe single-entry cache for latest flashblock - Add FlashblockTrieData: contains trie updates and hashed state for reuse - Cache keyed by block hash and flashblock index for accuracy - Compute flashblock trie once per flashblock, reuse for all bundle operations - Extract flashblock trie calculation outside timing metrics - Use TrieInput.prepend_cached() to combine flashblock + bundle tries The cache replaces previous entries when a new flashblock is cached, as it's designed only for the current/latest flashblock, not historical ones.
1 parent 1c80299 commit 145561d

File tree

8 files changed

+194
-5
lines changed

8 files changed

+194
-5
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
7575
reth-testing-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
7676
reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
7777
reth-ipc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
78+
reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
7879

7980
# revm
8081
revm = { version = "29.0.0", default-features = false }

crates/metering/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ reth-optimism-chainspec.workspace = true
2626
reth-optimism-primitives.workspace = true
2727
reth-transaction-pool.workspace = true
2828
reth-optimism-cli.workspace = true # Enables serde & codec traits for OpReceipt/OpTxEnvelope
29+
reth-trie-common.workspace = true
2930

3031
# alloy
3132
alloy-primitives.workspace = true
@@ -49,6 +50,7 @@ jsonrpsee.workspace = true
4950
tracing.workspace = true
5051
serde.workspace = true
5152
eyre.workspace = true
53+
arc-swap.workspace = true
5254

5355
[dev-dependencies]
5456
alloy-genesis.workspace = true
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use alloy_primitives::B256;
2+
use arc_swap::ArcSwap;
3+
use eyre::Result as EyreResult;
4+
use reth_provider::StateProvider;
5+
use std::sync::Arc;
6+
7+
use crate::FlashblocksState;
8+
9+
/// Trie nodes and hashed state from computing a flashblock state root.
10+
///
11+
/// These cached nodes can be reused when computing a bundle's state root
12+
/// to avoid recalculating the flashblock portion of the trie.
13+
#[derive(Debug, Clone)]
14+
pub struct FlashblockTrieData {
15+
pub trie_updates: reth_trie_common::updates::TrieUpdates,
16+
pub hashed_state: reth_trie_common::HashedPostState,
17+
}
18+
19+
/// Internal cache entry for a single flashblock.
20+
#[derive(Debug, Clone)]
21+
struct CachedFlashblockTrie {
22+
block_hash: B256,
23+
flashblock_index: u64,
24+
/// The cached trie data
25+
trie_data: FlashblockTrieData,
26+
}
27+
28+
/// Thread-safe single-entry cache for the latest flashblock's trie nodes.
29+
///
30+
/// This cache stores the intermediate trie nodes computed when calculating
31+
/// the latest flashblock's state root. Subsequent bundle metering operations
32+
/// on the same flashblock can reuse these cached nodes instead of recalculating
33+
/// them, significantly improving performance.
34+
///
35+
/// **Important**: This cache holds only ONE flashblock's trie at a time.
36+
/// When a new flashblock is cached, it replaces any previously cached flashblock.
37+
/// This design assumes that bundle metering operations are performed on the
38+
/// current/latest flashblock, not historical ones.
39+
#[derive(Debug, Clone)]
40+
pub struct FlashblockTrieCache {
41+
/// Single-entry cache for the latest flashblock's trie
42+
cache: Arc<ArcSwap<Option<CachedFlashblockTrie>>>,
43+
}
44+
45+
impl FlashblockTrieCache {
46+
/// Creates a new empty flashblock trie cache.
47+
pub fn new() -> Self {
48+
Self {
49+
cache: Arc::new(ArcSwap::from_pointee(None)),
50+
}
51+
}
52+
53+
/// Ensures the trie for the given flashblock is cached and returns it.
54+
///
55+
/// If the cache already contains an entry for the given block_hash and flashblock_index,
56+
/// this returns the cached data immediately without recomputation. Otherwise, it computes
57+
/// the flashblock's state root, caches the resulting trie nodes, **replacing any previously
58+
/// cached flashblock**, and returns the new data.
59+
///
60+
/// # Single-Entry Cache Behavior
61+
///
62+
/// This cache only stores one flashblock's trie at a time. Calling this method with a different
63+
/// flashblock will evict the previous entry. This is by design, as the cache is intended for
64+
/// the latest/current flashblock only.
65+
///
66+
/// # Arguments
67+
///
68+
/// * `block_hash` - Hash of the block containing the flashblock
69+
/// * `flashblock_index` - Index of the flashblock within the block
70+
/// * `flashblocks_state` - The accumulated state from pending flashblocks
71+
/// * `canonical_state_provider` - State provider for the canonical chain
72+
///
73+
/// # Returns
74+
///
75+
/// The cached `FlashblockTrieData` containing the trie updates and hashed state.
76+
pub fn ensure_cached(
77+
&self,
78+
block_hash: B256,
79+
flashblock_index: u64,
80+
flashblocks_state: &FlashblocksState,
81+
canonical_state_provider: &dyn StateProvider,
82+
) -> EyreResult<FlashblockTrieData> {
83+
// Check if we already have a cached trie for this exact flashblock
84+
let cached = self.cache.load();
85+
if let Some(ref cache) = **cached {
86+
if cache.block_hash == block_hash && cache.flashblock_index == flashblock_index {
87+
// Cache is still valid for this flashblock, return it
88+
return Ok(cache.trie_data.clone());
89+
}
90+
}
91+
92+
// Need to compute the flashblock trie (this will replace any existing cache entry)
93+
94+
// Compute hashed post state from the bundle
95+
let hashed_state = canonical_state_provider.hashed_post_state(&flashblocks_state.bundle_state);
96+
97+
// Calculate state root with updates to get the trie nodes
98+
let (_state_root, trie_updates) = canonical_state_provider.state_root_with_updates(hashed_state.clone())?;
99+
100+
// Create the trie data
101+
let trie_data = FlashblockTrieData {
102+
trie_updates,
103+
hashed_state,
104+
};
105+
106+
// Store the new entry, replacing any previous flashblock's cached trie
107+
self.cache.store(Arc::new(Some(CachedFlashblockTrie {
108+
block_hash,
109+
flashblock_index,
110+
trie_data: trie_data.clone(),
111+
})));
112+
113+
Ok(trie_data)
114+
}
115+
}
116+
117+
impl Default for FlashblockTrieCache {
118+
fn default() -> Self {
119+
Self::new()
120+
}
121+
}

crates/metering/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
mod flashblock_trie_cache;
12
mod meter;
23
mod rpc;
34
#[cfg(test)]
45
mod tests;
56

7+
pub use flashblock_trie_cache::{FlashblockTrieCache, FlashblockTrieData};
68
pub use meter::{meter_bundle, FlashblocksState, MeterBundleOutput};
79
pub use rpc::{MeteringApiImpl, MeteringApiServer};
810
pub use tips_core::types::{Bundle, MeterBundleResponse, TransactionResult};

crates/metering/src/meter.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ use alloy_consensus::{transaction::SignerRecoverable, BlockHeader, Transaction a
22
use alloy_primitives::{B256, U256};
33
use eyre::{eyre, Result as EyreResult};
44
use reth::revm::db::{BundleState, Cache, CacheDB, State};
5-
use revm_database::states::bundle_state::BundleRetention;
65
use reth_evm::execute::BlockBuilder;
76
use reth_evm::ConfigureEvm;
87
use reth_optimism_chainspec::OpChainSpec;
98
use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes};
109
use reth_primitives_traits::SealedHeader;
10+
use reth_trie_common::TrieInput;
11+
use revm_database::states::bundle_state::BundleRetention;
1112
use std::sync::Arc;
1213
use std::time::Instant;
1314

@@ -54,13 +55,25 @@ pub fn meter_bundle<SP>(
5455
header: &SealedHeader,
5556
bundle_with_metadata: &tips_core::types::BundleWithMetadata,
5657
flashblocks_state: Option<FlashblocksState>,
58+
cached_flashblock_trie: Option<crate::FlashblockTrieData>,
5759
) -> EyreResult<MeterBundleOutput>
5860
where
5961
SP: reth_provider::StateProvider,
6062
{
6163
// Get bundle hash from BundleWithMetadata
6264
let bundle_hash = bundle_with_metadata.bundle_hash();
6365

66+
// If we have flashblocks but no cached trie, compute the flashblock trie first
67+
// (before starting any timers, since we only want to time the bundle's execution and state root)
68+
let flashblock_trie = if cached_flashblock_trie.is_none() && flashblocks_state.is_some() {
69+
let fb_state = flashblocks_state.as_ref().unwrap();
70+
let fb_hashed_state = state_provider.hashed_post_state(&fb_state.bundle_state);
71+
let (_fb_state_root, fb_trie_updates) = state_provider.state_root_with_updates(fb_hashed_state.clone())?;
72+
Some((fb_trie_updates, fb_hashed_state))
73+
} else {
74+
None
75+
};
76+
6477
// Create state database
6578
let state_db = reth::revm::database::StateProviderDatabase::new(state_provider);
6679

@@ -152,14 +165,28 @@ where
152165
}
153166

154167
// Calculate state root and measure its calculation time
155-
// The bundle already includes flashblocks state if it was provided via with_bundle_prestate
156168
db.merge_transitions(BundleRetention::Reverts);
157169
let bundle = db.take_bundle();
158170
let state_provider = db.database.db.as_ref();
159-
let state_root_start = Instant::now();
160171

172+
let state_root_start = Instant::now();
161173
let hashed_post_state = state_provider.hashed_post_state(&bundle);
162-
let _ = state_provider.state_root_with_updates(hashed_post_state);
174+
175+
if let Some(cached) = cached_flashblock_trie {
176+
// We have cached flashblock trie nodes, use them
177+
let mut trie_input = TrieInput::from_state(hashed_post_state);
178+
trie_input.prepend_cached(cached.trie_updates, cached.hashed_state);
179+
let _ = state_provider.state_root_from_nodes_with_updates(trie_input)?;
180+
} else if let Some((fb_trie_updates, fb_hashed_state)) = flashblock_trie {
181+
// We computed the flashblock trie above, now use it
182+
let mut trie_input = TrieInput::from_state(hashed_post_state);
183+
trie_input.prepend_cached(fb_trie_updates, fb_hashed_state);
184+
let _ = state_provider.state_root_from_nodes_with_updates(trie_input)?;
185+
} else {
186+
// No flashblocks, just calculate bundle state root
187+
let _ = state_provider.state_root_with_updates(hashed_post_state)?;
188+
}
189+
163190
let state_root_time = state_root_start.elapsed().as_micros();
164191

165192
let total_execution_time = execution_start.elapsed().as_micros();

crates/metering/src/rpc.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::sync::Arc;
1414
use tips_core::types::{Bundle, BundleWithMetadata, MeterBundleResponse};
1515
use tracing::{error, info};
1616

17-
use crate::meter_bundle;
17+
use crate::{meter_bundle, FlashblockTrieCache};
1818

1919
/// RPC API for transaction metering
2020
#[rpc(server, namespace = "base")]
@@ -28,6 +28,8 @@ pub trait MeteringApi {
2828
pub struct MeteringApiImpl<Provider, FB> {
2929
provider: Provider,
3030
flashblocks_state: Arc<FB>,
31+
/// Single-entry cache for the latest flashblock's trie nodes
32+
trie_cache: FlashblockTrieCache,
3133
}
3234

3335
impl<Provider, FB> MeteringApiImpl<Provider, FB>
@@ -43,6 +45,7 @@ where
4345
Self {
4446
provider,
4547
flashblocks_state,
48+
trie_cache: FlashblockTrieCache::new(),
4649
}
4750
}
4851
}
@@ -161,6 +164,32 @@ where
161164
// Get the flashblock index if we have pending flashblocks
162165
let state_flashblock_index = pending_blocks.as_ref().map(|pb| pb.latest_flashblock_index());
163166

167+
// If we have flashblocks, ensure the trie is cached and get it
168+
let cached_trie = if let Some(ref fb_state) = flashblocks_state {
169+
let fb_index = state_flashblock_index.unwrap();
170+
171+
// Ensure the flashblock trie is cached and return it
172+
Some(
173+
self.trie_cache
174+
.ensure_cached(
175+
header.hash(),
176+
fb_index,
177+
fb_state,
178+
&*state_provider,
179+
)
180+
.map_err(|e| {
181+
error!(error = %e, "Failed to cache flashblock trie");
182+
jsonrpsee::types::ErrorObjectOwned::owned(
183+
jsonrpsee::types::ErrorCode::InternalError.code(),
184+
format!("Failed to cache flashblock trie: {}", e),
185+
None::<()>,
186+
)
187+
})?,
188+
)
189+
} else {
190+
None
191+
};
192+
164193
// Meter bundle using utility function
165194
let result = meter_bundle(
166195
state_provider,
@@ -169,6 +198,7 @@ where
169198
&header,
170199
&bundle_with_metadata,
171200
flashblocks_state,
201+
cached_trie,
172202
)
173203
.map_err(|e| {
174204
error!(error = %e, "Bundle metering failed");

crates/metering/src/tests/meter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ fn meter_bundle_empty_transactions() -> eyre::Result<()> {
162162
&harness.header,
163163
&bundle_with_metadata,
164164
None,
165+
None,
165166
)?;
166167

167168
assert!(output.results.is_empty());
@@ -211,6 +212,7 @@ fn meter_bundle_single_transaction() -> eyre::Result<()> {
211212
&harness.header,
212213
&bundle_with_metadata,
213214
None,
215+
None,
214216
)?;
215217

216218
assert_eq!(output.results.len(), 1);
@@ -308,6 +310,7 @@ fn meter_bundle_multiple_transactions() -> eyre::Result<()> {
308310
&harness.header,
309311
&bundle_with_metadata,
310312
None,
313+
None,
311314
)?;
312315

313316
assert_eq!(output.results.len(), 2);
@@ -397,6 +400,7 @@ fn meter_bundle_state_root_time_invariant() -> eyre::Result<()> {
397400
&harness.header,
398401
&bundle_with_metadata,
399402
None,
403+
None,
400404
)?;
401405

402406
// Verify invariant: total execution time must include state root time

0 commit comments

Comments
 (0)