Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,210 changes: 606 additions & 604 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repository = "https://github.com/base/node-reth"
resolver = "2"
members = [
"crates/flashblocks-rpc",
"crates/metering",
"crates/node",
"crates/transaction-tracing",
]
Expand Down Expand Up @@ -38,9 +39,14 @@ codegen-units = 1
[workspace.dependencies]
# internal
base-reth-flashblocks-rpc = { path = "crates/flashblocks-rpc" }
base-reth-metering = { path = "crates/metering" }
base-reth-node = { path = "crates/node" }
base-reth-transaction-tracing = { path = "crates/transaction-tracing" }

# base/tips
# Note: default-features = false avoids version conflicts with reth's alloy/op-alloy dependencies
tips-core = { git = "https://github.com/base/tips", rev = "27674ae051a86033ece61ae24434aeacdb28ce73", default-features = false }

# reth
reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
reth-optimism-node = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
Expand All @@ -55,6 +61,7 @@ reth-optimism-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "
reth-provider = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
reth-tracing = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
reth-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
reth-cli-util = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
Expand All @@ -71,16 +78,17 @@ revm-bytecode = { version = "6.2.2", default-features = false }
alloy-primitives = { version = "1.3.1", default-features = false, features = [
"map-foldhash",
] }
alloy-genesis = { version = "1.0.35", default-features = false }
alloy-eips = { version = "1.0.35", default-features = false }
alloy-rpc-types = { version = "1.0.35", default-features = false }
alloy-rpc-types-engine = { version = "1.0.35", default-features = false }
alloy-rpc-types-eth = { version = "1.0.35" }
alloy-consensus = { version = "1.0.35" }
alloy-genesis = { version = "1.0.41", default-features = false }
alloy-eips = { version = "1.0.41", default-features = false }
alloy-rpc-types = { version = "1.0.41", default-features = false }
alloy-rpc-types-engine = { version = "1.0.41", default-features = false }
alloy-rpc-types-eth = { version = "1.0.41" }
alloy-consensus = { version = "1.0.41" }
alloy-trie = { version = "0.9.1", default-features = false }
alloy-provider = { version = "1.0.35" }
alloy-provider = { version = "1.0.41" }
alloy-hardforks = "0.3.5"
alloy-rpc-client = { version = "1.0.35" }
alloy-rpc-client = { version = "1.0.41" }
alloy-serde = { version = "1.0.41" }

# op-alloy
op-alloy-rpc-types = { version = "0.20.0", default-features = false }
Expand Down
63 changes: 63 additions & 0 deletions crates/metering/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[package]
name = "base-reth-metering"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
description = "Transaction Metering RPC Support"

[lints]
workspace = true

[dependencies]
# base/tips
tips-core.workspace = true

# reth
reth.workspace = true
reth-provider.workspace = true
reth-primitives.workspace = true
reth-primitives-traits.workspace = true
reth-evm.workspace = true
reth-optimism-evm.workspace = true
reth-optimism-chainspec.workspace = true
reth-optimism-primitives.workspace = true
reth-transaction-pool.workspace = true
reth-optimism-cli.workspace = true # Enables serde & codec traits for OpReceipt/OpTxEnvelope

# alloy
alloy-primitives.workspace = true
alloy-consensus.workspace = true
alloy-eips.workspace = true

# op-alloy
op-alloy-consensus.workspace = true

# revm
revm.workspace = true

# rpc
jsonrpsee.workspace = true

# misc
tracing.workspace = true
serde.workspace = true
eyre.workspace = true

[dev-dependencies]
alloy-genesis.workspace = true
alloy-rpc-client.workspace = true
rand.workspace = true
reth-db = { workspace = true, features = ["test-utils"] }
reth-db-common.workspace = true
reth-e2e-test-utils.workspace = true
reth-optimism-node.workspace = true
reth-testing-utils.workspace = true
reth-tracing.workspace = true
reth-transaction-pool = { workspace = true, features = ["test-utils"] }
serde_json.workspace = true
tokio.workspace = true


62 changes: 62 additions & 0 deletions crates/metering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Transaction Metering RPC

RPC endpoints for simulating and metering transaction bundles on Optimism.

## `base_meterBundle`

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.

**Parameters:**

The method accepts a Bundle object with the following fields:

- `txs`: Array of signed, RLP-encoded transactions (hex strings with 0x prefix)
- `block_number`: Target block number for bundle validity (note: simulation always uses the latest available block state)
- `min_timestamp` (optional): Minimum timestamp for bundle validity (also used as simulation timestamp if provided)
- `max_timestamp` (optional): Maximum timestamp for bundle validity
- `reverting_tx_hashes` (optional): Array of transaction hashes allowed to revert
- `replacement_uuid` (optional): UUID for bundle replacement
- `flashblock_number_min` (optional): Minimum flashblock number constraint
- `flashblock_number_max` (optional): Maximum flashblock number constraint
- `dropping_tx_hashes` (optional): Transaction hashes to exclude from bundle

**Returns:**
- `bundleGasPrice`: Average gas price
- `bundleHash`: Bundle identifier
- `coinbaseDiff`: Total gas fees paid
- `ethSentToCoinbase`: ETH sent directly to coinbase
- `gasFees`: Total gas fees
- `stateBlockNumber`: Block number used for state (always the latest available block)
- `totalGasUsed`: Total gas consumed
- `totalExecutionTimeUs`: Total execution time (μs)
- `results`: Array of per-transaction results:
- `txHash`, `fromAddress`, `toAddress`, `value`
- `gasUsed`, `gasPrice`, `gasFees`, `coinbaseDiff`
- `ethSentToCoinbase`: Always "0" currently
- `executionTimeUs`: Transaction execution time (μs)

**Example:**

```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "base_meterBundle",
"params": [{
"txs": ["0x02f8...", "0x02f9..."],
"blockNumber": 1748028,
"minTimestamp": 1234567890,
"revertingTxHashes": []
}]
}
```

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.

## Implementation

- Executes transactions sequentially using Optimism EVM configuration
- Tracks microsecond-precision execution time per transaction
- Stops on first failure
- Automatically registered in `base` namespace

8 changes: 8 additions & 0 deletions crates/metering/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mod meter;
mod rpc;
#[cfg(test)]
mod tests;

pub use meter::meter_bundle;
pub use rpc::{MeteringApiImpl, MeteringApiServer};
pub use tips_core::types::{Bundle, MeterBundleResponse, TransactionResult};
119 changes: 119 additions & 0 deletions crates/metering/src/meter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use alloy_consensus::{transaction::SignerRecoverable, BlockHeader, Transaction as _};
use alloy_primitives::{B256, U256};
use eyre::{eyre, Result as EyreResult};
use reth::revm::db::State;
use reth_evm::execute::BlockBuilder;
use reth_evm::ConfigureEvm;
use reth_optimism_chainspec::OpChainSpec;
use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes};
use reth_primitives_traits::SealedHeader;
use std::sync::Arc;
use std::time::Instant;

use crate::TransactionResult;

const BLOCK_TIME: u64 = 2; // 2 seconds per block

/// Simulates and meters a bundle of transactions
///
/// Takes a state provider, chain spec, decoded transactions, block header, and bundle metadata,
/// and executes transactions in sequence to measure gas usage and execution time.
///
/// Returns a tuple of:
/// - Vector of transaction results
/// - Total gas used
/// - Total gas fees paid
/// - Bundle hash
/// - Total execution time in microseconds
pub fn meter_bundle<SP>(
state_provider: SP,
chain_spec: Arc<OpChainSpec>,
decoded_txs: Vec<op_alloy_consensus::OpTxEnvelope>,
header: &SealedHeader,
bundle_with_metadata: &tips_core::types::BundleWithMetadata,
) -> EyreResult<(Vec<TransactionResult>, u64, U256, B256, u128)>
where
SP: reth_provider::StateProvider,
{
// Get bundle hash from BundleWithMetadata
let bundle_hash = bundle_with_metadata.bundle_hash();

// Create state database
let state_db = reth::revm::database::StateProviderDatabase::new(state_provider);
let mut db = State::builder()
.with_database(state_db)
.with_bundle_update()
.build();

// Set up next block attributes
// Use bundle.min_timestamp if provided, otherwise use header timestamp + BLOCK_TIME
let timestamp = bundle_with_metadata
.bundle()
.min_timestamp
.unwrap_or_else(|| header.timestamp() + BLOCK_TIME);
let attributes = OpNextBlockEnvAttributes {
timestamp,
suggested_fee_recipient: header.beneficiary(),
prev_randao: header.mix_hash().unwrap_or(B256::random()),
gas_limit: header.gas_limit(),
parent_beacon_block_root: header.parent_beacon_block_root(),
extra_data: header.extra_data().clone(),
};

// Execute transactions
let mut results = Vec::new();
let mut total_gas_used = 0u64;
let mut total_gas_fees = U256::ZERO;

let execution_start = Instant::now();
{
let evm_config = OpEvmConfig::optimism(chain_spec);
let mut builder = evm_config.builder_for_next_block(&mut db, header, attributes)?;

builder.apply_pre_execution_changes()?;

for tx in decoded_txs {
let tx_start = Instant::now();
let tx_hash = tx.tx_hash();
let from = tx.recover_signer()?;
let to = tx.to();
let value = tx.value();
let gas_price = tx.max_fee_per_gas();

let recovered_tx =
alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), from);

let gas_used = builder
.execute_transaction(recovered_tx)
.map_err(|e| eyre!("Transaction {} execution failed: {}", tx_hash, e))?;

let gas_fees = U256::from(gas_used) * U256::from(gas_price);
total_gas_used = total_gas_used.saturating_add(gas_used);
total_gas_fees = total_gas_fees.saturating_add(gas_fees);

let execution_time = tx_start.elapsed().as_micros();

results.push(TransactionResult {
coinbase_diff: gas_fees.to_string(),
eth_sent_to_coinbase: "0".to_string(),
from_address: from,
gas_fees: gas_fees.to_string(),
gas_price: gas_price.to_string(),
gas_used,
to_address: to,
tx_hash,
value: value.to_string(),
execution_time_us: execution_time,
});
}
}
let total_execution_time = execution_start.elapsed().as_micros();

Ok((
results,
total_gas_used,
total_gas_fees,
bundle_hash,
total_execution_time,
))
}
Loading