Skip to content

Commit f76c7cb

Browse files
authored
Add base_meterBundle RPC for transaction bundle metering (#142)
Implements JSON-RPC endpoint to simulate and meter Optimism transaction bundles, returning detailed gas usage, execution time, and bundle hash using Flashbots methodology.
1 parent 44f3661 commit f76c7cb

File tree

14 files changed

+1995
-612
lines changed

14 files changed

+1995
-612
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repository = "https://github.com/base/node-reth"
1010
resolver = "2"
1111
members = [
1212
"crates/flashblocks-rpc",
13+
"crates/metering",
1314
"crates/node",
1415
"crates/transaction-tracing",
1516
]
@@ -38,9 +39,14 @@ codegen-units = 1
3839
[workspace.dependencies]
3940
# internal
4041
base-reth-flashblocks-rpc = { path = "crates/flashblocks-rpc" }
42+
base-reth-metering = { path = "crates/metering" }
4143
base-reth-node = { path = "crates/node" }
4244
base-reth-transaction-tracing = { path = "crates/transaction-tracing" }
4345

46+
# base/tips
47+
# Note: default-features = false avoids version conflicts with reth's alloy/op-alloy dependencies
48+
tips-core = { git = "https://github.com/base/tips", rev = "27674ae051a86033ece61ae24434aeacdb28ce73", default-features = false }
49+
4450
# reth
4551
reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
4652
reth-optimism-node = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
@@ -55,6 +61,7 @@ reth-optimism-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "
5561
reth-provider = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
5662
reth-tracing = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
5763
reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
64+
reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
5865
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
5966
reth-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
6067
reth-cli-util = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
@@ -71,16 +78,17 @@ revm-bytecode = { version = "6.2.2", default-features = false }
7178
alloy-primitives = { version = "1.3.1", default-features = false, features = [
7279
"map-foldhash",
7380
] }
74-
alloy-genesis = { version = "1.0.35", default-features = false }
75-
alloy-eips = { version = "1.0.35", default-features = false }
76-
alloy-rpc-types = { version = "1.0.35", default-features = false }
77-
alloy-rpc-types-engine = { version = "1.0.35", default-features = false }
78-
alloy-rpc-types-eth = { version = "1.0.35" }
79-
alloy-consensus = { version = "1.0.35" }
81+
alloy-genesis = { version = "1.0.41", default-features = false }
82+
alloy-eips = { version = "1.0.41", default-features = false }
83+
alloy-rpc-types = { version = "1.0.41", default-features = false }
84+
alloy-rpc-types-engine = { version = "1.0.41", default-features = false }
85+
alloy-rpc-types-eth = { version = "1.0.41" }
86+
alloy-consensus = { version = "1.0.41" }
8087
alloy-trie = { version = "0.9.1", default-features = false }
81-
alloy-provider = { version = "1.0.35" }
88+
alloy-provider = { version = "1.0.41" }
8289
alloy-hardforks = "0.3.5"
83-
alloy-rpc-client = { version = "1.0.35" }
90+
alloy-rpc-client = { version = "1.0.41" }
91+
alloy-serde = { version = "1.0.41" }
8492

8593
# op-alloy
8694
op-alloy-rpc-types = { version = "0.20.0", default-features = false }

crates/metering/Cargo.toml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
[package]
2+
name = "base-reth-metering"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
license.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
description = "Transaction Metering RPC Support"
10+
11+
[lints]
12+
workspace = true
13+
14+
[dependencies]
15+
# base/tips
16+
tips-core.workspace = true
17+
18+
# reth
19+
reth.workspace = true
20+
reth-provider.workspace = true
21+
reth-primitives.workspace = true
22+
reth-primitives-traits.workspace = true
23+
reth-evm.workspace = true
24+
reth-optimism-evm.workspace = true
25+
reth-optimism-chainspec.workspace = true
26+
reth-optimism-primitives.workspace = true
27+
reth-transaction-pool.workspace = true
28+
reth-optimism-cli.workspace = true # Enables serde & codec traits for OpReceipt/OpTxEnvelope
29+
30+
# alloy
31+
alloy-primitives.workspace = true
32+
alloy-consensus.workspace = true
33+
alloy-eips.workspace = true
34+
35+
# op-alloy
36+
op-alloy-consensus.workspace = true
37+
38+
# revm
39+
revm.workspace = true
40+
41+
# rpc
42+
jsonrpsee.workspace = true
43+
44+
# misc
45+
tracing.workspace = true
46+
serde.workspace = true
47+
eyre.workspace = true
48+
49+
[dev-dependencies]
50+
alloy-genesis.workspace = true
51+
alloy-rpc-client.workspace = true
52+
rand.workspace = true
53+
reth-db = { workspace = true, features = ["test-utils"] }
54+
reth-db-common.workspace = true
55+
reth-e2e-test-utils.workspace = true
56+
reth-optimism-node.workspace = true
57+
reth-testing-utils.workspace = true
58+
reth-tracing.workspace = true
59+
reth-transaction-pool = { workspace = true, features = ["test-utils"] }
60+
serde_json.workspace = true
61+
tokio.workspace = true
62+
63+

crates/metering/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Transaction Metering RPC
2+
3+
RPC endpoints for simulating and metering transaction bundles on Optimism.
4+
5+
## `base_meterBundle`
6+
7+
Simulates a bundle of transactions, providing gas usage and execution time metrics. The response format is derived from `eth_callBundle`, but the request uses the [TIPS Bundle format](https://github.com/base/tips) to support TIPS's additional bundle features.
8+
9+
**Parameters:**
10+
11+
The method accepts a Bundle object with the following fields:
12+
13+
- `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)
15+
- `min_timestamp` (optional): Minimum timestamp for bundle validity (also used as simulation timestamp if provided)
16+
- `max_timestamp` (optional): Maximum timestamp for bundle validity
17+
- `reverting_tx_hashes` (optional): Array of transaction hashes allowed to revert
18+
- `replacement_uuid` (optional): UUID for bundle replacement
19+
- `flashblock_number_min` (optional): Minimum flashblock number constraint
20+
- `flashblock_number_max` (optional): Maximum flashblock number constraint
21+
- `dropping_tx_hashes` (optional): Transaction hashes to exclude from bundle
22+
23+
**Returns:**
24+
- `bundleGasPrice`: Average gas price
25+
- `bundleHash`: Bundle identifier
26+
- `coinbaseDiff`: Total gas fees paid
27+
- `ethSentToCoinbase`: ETH sent directly to coinbase
28+
- `gasFees`: Total gas fees
29+
- `stateBlockNumber`: Block number used for state (always the latest available block)
30+
- `totalGasUsed`: Total gas consumed
31+
- `totalExecutionTimeUs`: Total execution time (μs)
32+
- `results`: Array of per-transaction results:
33+
- `txHash`, `fromAddress`, `toAddress`, `value`
34+
- `gasUsed`, `gasPrice`, `gasFees`, `coinbaseDiff`
35+
- `ethSentToCoinbase`: Always "0" currently
36+
- `executionTimeUs`: Transaction execution time (μs)
37+
38+
**Example:**
39+
40+
```json
41+
{
42+
"jsonrpc": "2.0",
43+
"id": 1,
44+
"method": "base_meterBundle",
45+
"params": [{
46+
"txs": ["0x02f8...", "0x02f9..."],
47+
"blockNumber": 1748028,
48+
"minTimestamp": 1234567890,
49+
"revertingTxHashes": []
50+
}]
51+
}
52+
```
53+
54+
Note: While some fields like `revertingTxHashes` are part of the TIPS Bundle format, they are currently ignored during simulation. The metering focuses on gas usage and execution time measurement.
55+
56+
## Implementation
57+
58+
- Executes transactions sequentially using Optimism EVM configuration
59+
- Tracks microsecond-precision execution time per transaction
60+
- Stops on first failure
61+
- Automatically registered in `base` namespace
62+

crates/metering/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
mod meter;
2+
mod rpc;
3+
#[cfg(test)]
4+
mod tests;
5+
6+
pub use meter::meter_bundle;
7+
pub use rpc::{MeteringApiImpl, MeteringApiServer};
8+
pub use tips_core::types::{Bundle, MeterBundleResponse, TransactionResult};

crates/metering/src/meter.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use alloy_consensus::{transaction::SignerRecoverable, BlockHeader, Transaction as _};
2+
use alloy_primitives::{B256, U256};
3+
use eyre::{eyre, Result as EyreResult};
4+
use reth::revm::db::State;
5+
use reth_evm::execute::BlockBuilder;
6+
use reth_evm::ConfigureEvm;
7+
use reth_optimism_chainspec::OpChainSpec;
8+
use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes};
9+
use reth_primitives_traits::SealedHeader;
10+
use std::sync::Arc;
11+
use std::time::Instant;
12+
13+
use crate::TransactionResult;
14+
15+
const BLOCK_TIME: u64 = 2; // 2 seconds per block
16+
17+
/// Simulates and meters a bundle of transactions
18+
///
19+
/// Takes a state provider, chain spec, decoded transactions, block header, and bundle metadata,
20+
/// and executes transactions in sequence to measure gas usage and execution time.
21+
///
22+
/// Returns a tuple of:
23+
/// - Vector of transaction results
24+
/// - Total gas used
25+
/// - Total gas fees paid
26+
/// - Bundle hash
27+
/// - Total execution time in microseconds
28+
pub fn meter_bundle<SP>(
29+
state_provider: SP,
30+
chain_spec: Arc<OpChainSpec>,
31+
decoded_txs: Vec<op_alloy_consensus::OpTxEnvelope>,
32+
header: &SealedHeader,
33+
bundle_with_metadata: &tips_core::types::BundleWithMetadata,
34+
) -> EyreResult<(Vec<TransactionResult>, u64, U256, B256, u128)>
35+
where
36+
SP: reth_provider::StateProvider,
37+
{
38+
// Get bundle hash from BundleWithMetadata
39+
let bundle_hash = bundle_with_metadata.bundle_hash();
40+
41+
// Create state database
42+
let state_db = reth::revm::database::StateProviderDatabase::new(state_provider);
43+
let mut db = State::builder()
44+
.with_database(state_db)
45+
.with_bundle_update()
46+
.build();
47+
48+
// Set up next block attributes
49+
// Use bundle.min_timestamp if provided, otherwise use header timestamp + BLOCK_TIME
50+
let timestamp = bundle_with_metadata
51+
.bundle()
52+
.min_timestamp
53+
.unwrap_or_else(|| header.timestamp() + BLOCK_TIME);
54+
let attributes = OpNextBlockEnvAttributes {
55+
timestamp,
56+
suggested_fee_recipient: header.beneficiary(),
57+
prev_randao: header.mix_hash().unwrap_or(B256::random()),
58+
gas_limit: header.gas_limit(),
59+
parent_beacon_block_root: header.parent_beacon_block_root(),
60+
extra_data: header.extra_data().clone(),
61+
};
62+
63+
// Execute transactions
64+
let mut results = Vec::new();
65+
let mut total_gas_used = 0u64;
66+
let mut total_gas_fees = U256::ZERO;
67+
68+
let execution_start = Instant::now();
69+
{
70+
let evm_config = OpEvmConfig::optimism(chain_spec);
71+
let mut builder = evm_config.builder_for_next_block(&mut db, header, attributes)?;
72+
73+
builder.apply_pre_execution_changes()?;
74+
75+
for tx in decoded_txs {
76+
let tx_start = Instant::now();
77+
let tx_hash = tx.tx_hash();
78+
let from = tx.recover_signer()?;
79+
let to = tx.to();
80+
let value = tx.value();
81+
let gas_price = tx.max_fee_per_gas();
82+
83+
let recovered_tx =
84+
alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), from);
85+
86+
let gas_used = builder
87+
.execute_transaction(recovered_tx)
88+
.map_err(|e| eyre!("Transaction {} execution failed: {}", tx_hash, e))?;
89+
90+
let gas_fees = U256::from(gas_used) * U256::from(gas_price);
91+
total_gas_used = total_gas_used.saturating_add(gas_used);
92+
total_gas_fees = total_gas_fees.saturating_add(gas_fees);
93+
94+
let execution_time = tx_start.elapsed().as_micros();
95+
96+
results.push(TransactionResult {
97+
coinbase_diff: gas_fees.to_string(),
98+
eth_sent_to_coinbase: "0".to_string(),
99+
from_address: from,
100+
gas_fees: gas_fees.to_string(),
101+
gas_price: gas_price.to_string(),
102+
gas_used,
103+
to_address: to,
104+
tx_hash,
105+
value: value.to_string(),
106+
execution_time_us: execution_time,
107+
});
108+
}
109+
}
110+
let total_execution_time = execution_start.elapsed().as_micros();
111+
112+
Ok((
113+
results,
114+
total_gas_used,
115+
total_gas_fees,
116+
bundle_hash,
117+
total_execution_time,
118+
))
119+
}

0 commit comments

Comments
 (0)