Skip to content

Commit 16fb3a5

Browse files
committed
Use pending flashblocks state for bundle metering
Integrate flashblocks state into metering to execute bundles on top of pending flashblock state rather than canonical block state. This ensures metered gas usage and execution time accurately reflect the effects of pending transactions (nonces, balances, storage, code changes). Implementation: - Add flashblocks-rpc dependency to metering crate - Update meter_bundle() to accept optional db_cache parameter - Implement three-layer state architecture: 1. StateProviderDatabase (canonical block base state) 2. CacheDB (applies flashblock pending changes via cache) 3. State wrapper (for EVM builder compatibility) - Update MeteringApiImpl to accept FlashblocksState - Get pending blocks and db_cache from flashblocks when available - Fall back to canonical block state when no flashblocks available - Update response to include flashblock_index in logs - Require flashblocks to be enabled for metering RPC - Update all tests to pass FlashblocksState parameter The metering RPC now uses the same state as flashblocks eth_call, ensuring consistent simulation results.
1 parent 0982ea5 commit 16fb3a5

File tree

8 files changed

+119
-40
lines changed

8 files changed

+119
-40
lines changed

Cargo.lock

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

crates/metering/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ alloy-eips.workspace = true
3535
# op-alloy
3636
op-alloy-consensus.workspace = true
3737

38+
# base
39+
base-reth-flashblocks-rpc = { path = "../flashblocks-rpc" }
40+
3841
# revm
3942
revm.workspace = true
4043

crates/metering/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Simulates a bundle of transactions, providing gas usage and execution time metri
1111
The method accepts a Bundle object with the following fields:
1212

1313
- `txs`: Array of signed, RLP-encoded transactions (hex strings with 0x prefix)
14-
- `block_number`: Target block number for bundle validity (note: simulation always uses the latest available block state)
14+
- `block_number`: Target block number for bundle validity (note: simulation uses pending flashblocks state when available, otherwise latest canonical block)
1515
- `min_timestamp` (optional): Minimum timestamp for bundle validity (also used as simulation timestamp if provided)
1616
- `max_timestamp` (optional): Maximum timestamp for bundle validity
1717
- `reverting_tx_hashes` (optional): Array of transaction hashes allowed to revert
@@ -26,7 +26,7 @@ The method accepts a Bundle object with the following fields:
2626
- `coinbaseDiff`: Total gas fees paid
2727
- `ethSentToCoinbase`: ETH sent directly to coinbase
2828
- `gasFees`: Total gas fees
29-
- `stateBlockNumber`: Block number used for state (always the latest available block)
29+
- `stateBlockNumber`: Block number used for state (latest flashblock if pending flashblocks exist, otherwise latest canonical block)
3030
- `totalGasUsed`: Total gas consumed
3131
- `totalExecutionTimeUs`: Total execution time (μs)
3232
- `results`: Array of per-transaction results:

crates/metering/src/meter.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use alloy_consensus::{transaction::SignerRecoverable, BlockHeader, Transaction as _};
22
use alloy_primitives::{B256, U256};
33
use eyre::{eyre, Result as EyreResult};
4-
use reth::revm::db::State;
4+
use reth::revm::db::{Cache, CacheDB, State};
55
use reth_evm::execute::BlockBuilder;
66
use reth_evm::ConfigureEvm;
77
use reth_optimism_chainspec::OpChainSpec;
@@ -43,20 +43,37 @@ pub fn meter_bundle<SP>(
4343
decoded_txs: Vec<op_alloy_consensus::OpTxEnvelope>,
4444
header: &SealedHeader,
4545
bundle_with_metadata: &tips_core::types::BundleWithMetadata,
46+
db_cache: Option<Cache>,
4647
) -> EyreResult<MeterBundleOutput>
4748
where
4849
SP: reth_provider::StateProvider,
4950
{
5051
// Get bundle hash from BundleWithMetadata
5152
let bundle_hash = bundle_with_metadata.bundle_hash();
5253

53-
// Create state database
54+
// Create state database with optional flashblocks cache
5455
let state_db = reth::revm::database::StateProviderDatabase::new(state_provider);
55-
let mut db = State::builder()
56+
let base_state = State::builder()
5657
.with_database(state_db)
5758
.with_bundle_update()
5859
.build();
5960

61+
// If we have flashblocks cache, wrap with CacheDB to apply pending changes
62+
let cache_db = if let Some(cache) = db_cache {
63+
CacheDB {
64+
cache,
65+
db: base_state,
66+
}
67+
} else {
68+
CacheDB::new(base_state)
69+
};
70+
71+
// Wrap the CacheDB in a State for the EVM builder
72+
let mut db = State::builder()
73+
.with_database(cache_db)
74+
.with_bundle_update()
75+
.build();
76+
6077
// Set up next block attributes
6178
// Use bundle.min_timestamp if provided, otherwise use header timestamp + BLOCK_TIME
6279
let timestamp = bundle_with_metadata

crates/metering/src/rpc.rs

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
use alloy_consensus::Header;
1+
use alloy_consensus::{Header, Sealed};
22
use alloy_eips::eip2718::Decodable2718;
3-
use alloy_eips::BlockNumberOrTag;
43
use alloy_primitives::U256;
4+
use base_reth_flashblocks_rpc::rpc::{FlashblocksAPI, PendingBlocksAPI};
55
use jsonrpsee::{
66
core::{async_trait, RpcResult},
77
proc_macros::rpc,
88
};
99
use reth::providers::BlockReaderIdExt;
1010
use reth_optimism_chainspec::OpChainSpec;
11+
use reth_primitives_traits::SealedHeader;
1112
use reth_provider::{ChainSpecProvider, StateProviderFactory};
13+
use std::sync::Arc;
1214
use tips_core::types::{Bundle, BundleWithMetadata, MeterBundleResponse};
1315
use tracing::{error, info};
1416

@@ -23,25 +25,30 @@ pub trait MeteringApi {
2325
}
2426

2527
/// Implementation of the metering RPC API
26-
pub struct MeteringApiImpl<Provider> {
28+
pub struct MeteringApiImpl<Provider, FB> {
2729
provider: Provider,
30+
flashblocks_state: Arc<FB>,
2831
}
2932

30-
impl<Provider> MeteringApiImpl<Provider>
33+
impl<Provider, FB> MeteringApiImpl<Provider, FB>
3134
where
3235
Provider: StateProviderFactory
3336
+ ChainSpecProvider<ChainSpec = OpChainSpec>
3437
+ BlockReaderIdExt<Header = Header>
3538
+ Clone,
39+
FB: FlashblocksAPI,
3640
{
3741
/// Creates a new instance of MeteringApi
38-
pub fn new(provider: Provider) -> Self {
39-
Self { provider }
42+
pub fn new(provider: Provider, flashblocks_state: Arc<FB>) -> Self {
43+
Self {
44+
provider,
45+
flashblocks_state,
46+
}
4047
}
4148
}
4249

4350
#[async_trait]
44-
impl<Provider> MeteringApiServer for MeteringApiImpl<Provider>
51+
impl<Provider, FB> MeteringApiServer for MeteringApiImpl<Provider, FB>
4552
where
4653
Provider: StateProviderFactory
4754
+ ChainSpecProvider<ChainSpec = OpChainSpec>
@@ -50,6 +57,7 @@ where
5057
+ Send
5158
+ Sync
5259
+ 'static,
60+
FB: FlashblocksAPI + Send + Sync + 'static,
5361
{
5462
async fn meter_bundle(&self, bundle: Bundle) -> RpcResult<MeterBundleResponse> {
5563
info!(
@@ -58,24 +66,54 @@ where
5866
"Starting bundle metering"
5967
);
6068

61-
// Get the latest header
62-
let header = self
63-
.provider
64-
.sealed_header_by_number_or_tag(BlockNumberOrTag::Latest)
65-
.map_err(|e| {
66-
jsonrpsee::types::ErrorObjectOwned::owned(
67-
jsonrpsee::types::ErrorCode::InternalError.code(),
68-
format!("Failed to get latest header: {}", e),
69-
None::<()>,
70-
)
71-
})?
72-
.ok_or_else(|| {
73-
jsonrpsee::types::ErrorObjectOwned::owned(
74-
jsonrpsee::types::ErrorCode::InternalError.code(),
75-
"Latest block not found".to_string(),
76-
None::<()>,
77-
)
78-
})?;
69+
// Get pending flashblocks state
70+
let pending_blocks = self.flashblocks_state.get_pending_blocks();
71+
72+
// Get header and flashblock index from pending blocks
73+
// If no pending blocks exist, fall back to latest canonical block
74+
let (header, flashblock_index, canonical_block_number) = if let Some(pb) = pending_blocks.as_ref() {
75+
let latest_header: Sealed<Header> = pb.latest_header();
76+
let flashblock_index = pb.latest_flashblock_index();
77+
let canonical_block_number = pb.canonical_block_number();
78+
79+
info!(
80+
latest_block = latest_header.number,
81+
canonical_block = %canonical_block_number,
82+
flashblock_index = flashblock_index,
83+
"Using latest flashblock state for metering"
84+
);
85+
86+
// Convert Sealed<Header> to SealedHeader
87+
let sealed_header = SealedHeader::new(latest_header.inner().clone(), latest_header.hash());
88+
(sealed_header, flashblock_index, canonical_block_number)
89+
} else {
90+
// No pending blocks, use latest canonical block
91+
let canonical_block_number = pending_blocks.get_canonical_block_number();
92+
let header = self
93+
.provider
94+
.sealed_header_by_number_or_tag(canonical_block_number)
95+
.map_err(|e| {
96+
jsonrpsee::types::ErrorObjectOwned::owned(
97+
jsonrpsee::types::ErrorCode::InternalError.code(),
98+
format!("Failed to get canonical block header: {}", e),
99+
None::<()>,
100+
)
101+
})?
102+
.ok_or_else(|| {
103+
jsonrpsee::types::ErrorObjectOwned::owned(
104+
jsonrpsee::types::ErrorCode::InternalError.code(),
105+
"Canonical block not found".to_string(),
106+
None::<()>,
107+
)
108+
})?;
109+
110+
info!(
111+
canonical_block = header.number,
112+
"No flashblocks available, using canonical block state for metering"
113+
);
114+
115+
(header, 0, canonical_block_number)
116+
};
79117

80118
// Manually decode transactions to OpTxEnvelope (op-alloy 0.20) instead of using
81119
// BundleWithMetadata.transactions() which returns op-alloy 0.21 types incompatible with reth.
@@ -101,10 +139,10 @@ where
101139
)
102140
})?;
103141

104-
// Get state provider for the block
142+
// Get state provider for the canonical block
105143
let state_provider = self
106144
.provider
107-
.state_by_block_hash(header.hash())
145+
.state_by_block_number_or_tag(canonical_block_number)
108146
.map_err(|e| {
109147
error!(error = %e, "Failed to get state provider");
110148
jsonrpsee::types::ErrorObjectOwned::owned(
@@ -114,13 +152,17 @@ where
114152
)
115153
})?;
116154

155+
// If we have pending flashblocks, get the db_cache to apply state changes
156+
let db_cache = pending_blocks.as_ref().map(|pb| pb.get_db_cache());
157+
117158
// Meter bundle using utility function
118159
let result = meter_bundle(
119160
state_provider,
120161
self.provider.chain_spec().clone(),
121162
decoded_txs,
122163
&header,
123164
&bundle_with_metadata,
165+
db_cache,
124166
)
125167
.map_err(|e| {
126168
error!(error = %e, "Bundle metering failed");
@@ -144,9 +186,13 @@ where
144186
total_gas_used = result.total_gas_used,
145187
total_execution_time_us = result.total_execution_time_us,
146188
state_root_time_us = result.state_root_time_us,
189+
state_block_number = header.number,
190+
flashblock_index = flashblock_index,
147191
"Bundle metering completed successfully"
148192
);
149193

194+
// TODO: Add flashblock_index to MeterBundleResponse in tips-core
195+
// The response should indicate both the canonical block number and the flashblock index
150196
Ok(MeterBundleResponse {
151197
bundle_gas_price,
152198
bundle_hash: result.bundle_hash,

crates/metering/src/tests/meter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ fn meter_bundle_empty_transactions() -> eyre::Result<()> {
161161
Vec::new(),
162162
&harness.header,
163163
&bundle_with_metadata,
164+
None, // No flashblocks cache in tests
164165
)?;
165166

166167
assert!(output.results.is_empty());
@@ -209,6 +210,7 @@ fn meter_bundle_single_transaction() -> eyre::Result<()> {
209210
vec![envelope],
210211
&harness.header,
211212
&bundle_with_metadata,
213+
None, // No flashblocks cache in tests
212214
)?;
213215

214216
assert_eq!(output.results.len(), 1);
@@ -305,6 +307,7 @@ fn meter_bundle_multiple_transactions() -> eyre::Result<()> {
305307
vec![envelope_1, envelope_2],
306308
&harness.header,
307309
&bundle_with_metadata,
310+
None, // No flashblocks cache in tests
308311
)?;
309312

310313
assert_eq!(output.results.len(), 2);
@@ -393,6 +396,7 @@ fn meter_bundle_state_root_time_invariant() -> eyre::Result<()> {
393396
vec![envelope],
394397
&harness.header,
395398
&bundle_with_metadata,
399+
None, // No flashblocks cache in tests
396400
)?;
397401

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

crates/metering/src/tests/rpc.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod tests {
66
use alloy_primitives::bytes;
77
use alloy_primitives::{address, b256, Bytes, U256};
88
use alloy_rpc_client::RpcClient;
9+
use base_reth_flashblocks_rpc::state::FlashblocksState;
910
use op_alloy_consensus::OpTxEnvelope;
1011
use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs};
1112
use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle};
@@ -91,7 +92,9 @@ mod tests {
9192
.with_components(node.components_builder())
9293
.with_add_ons(node.add_ons())
9394
.extend_rpc_modules(move |ctx| {
94-
let metering_api = MeteringApiImpl::new(ctx.provider().clone());
95+
// Create a FlashblocksState without starting it (no pending blocks for testing)
96+
let flashblocks_state = Arc::new(FlashblocksState::new(ctx.provider().clone()));
97+
let metering_api = MeteringApiImpl::new(ctx.provider().clone(), flashblocks_state);
9598
ctx.modules.merge_configured(metering_api.into_rpc())?;
9699
Ok(())
97100
})

crates/node/src/main.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,6 @@ fn main() {
138138
}
139139
})
140140
.extend_rpc_modules(move |ctx| {
141-
if metering_enabled {
142-
info!(message = "Starting Metering RPC");
143-
let metering_api = MeteringApiImpl::new(ctx.provider().clone());
144-
ctx.modules.merge_configured(metering_api.into_rpc())?;
145-
}
146-
147141
if flashblocks_enabled {
148142
info!(message = "Starting Flashblocks");
149143

@@ -164,12 +158,23 @@ fn main() {
164158
let api_ext = EthApiExt::new(
165159
ctx.registry.eth_api().clone(),
166160
ctx.registry.eth_handlers().filter.clone(),
167-
fb,
161+
fb.clone(),
168162
);
169163

170164
ctx.modules.replace_configured(api_ext.into_rpc())?;
165+
166+
if metering_enabled {
167+
info!(message = "Starting Metering RPC with Flashblocks state");
168+
let metering_api = MeteringApiImpl::new(ctx.provider().clone(), fb);
169+
ctx.modules.merge_configured(metering_api.into_rpc())?;
170+
}
171171
} else {
172172
info!(message = "flashblocks integration is disabled");
173+
if metering_enabled {
174+
return Err(eyre::eyre!(
175+
"Metering RPC requires flashblocks to be enabled (--websocket-url)"
176+
));
177+
}
173178
}
174179
Ok(())
175180
})

0 commit comments

Comments
 (0)