Skip to main content

reth_bench/bench/
gas_limit_ramp.rs

1//! Benchmarks empty block processing by ramping the block gas limit.
2
3use crate::{
4    authenticated_transport::AuthenticatedTransportConnect,
5    bench::{
6        helpers::{build_payload, parse_gas_limit, prepare_payload_request, rpc_block_to_header},
7        output::GasRampPayloadFile,
8    },
9    valid_payload::{call_forkchoice_updated, call_new_payload, payload_to_new_payload},
10};
11use alloy_eips::BlockNumberOrTag;
12use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
13use alloy_rpc_client::ClientBuilder;
14use alloy_rpc_types_engine::{ExecutionPayload, ForkchoiceState, JwtSecret};
15
16use clap::Parser;
17use reqwest::Url;
18use reth_chainspec::ChainSpec;
19use reth_cli_runner::CliContext;
20use reth_ethereum_primitives::TransactionSigned;
21use reth_primitives_traits::constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK};
22use std::{path::PathBuf, time::Instant};
23use tracing::info;
24
25/// `reth benchmark gas-limit-ramp` command.
26#[derive(Debug, Parser)]
27pub struct Command {
28    /// Number of blocks to generate. Mutually exclusive with --target-gas-limit.
29    #[arg(long, value_name = "BLOCKS", conflicts_with = "target_gas_limit")]
30    blocks: Option<u64>,
31
32    /// Target gas limit to ramp up to. The benchmark will generate blocks until the gas limit
33    /// reaches or exceeds this value. Mutually exclusive with --blocks.
34    /// Accepts short notation: K for thousand, M for million, G for billion (e.g., 2G = 2
35    /// billion).
36    #[arg(long, value_name = "TARGET_GAS_LIMIT", conflicts_with = "blocks", value_parser = parse_gas_limit)]
37    target_gas_limit: Option<u64>,
38
39    /// The Engine API RPC URL.
40    #[arg(long = "engine-rpc-url", value_name = "ENGINE_RPC_URL")]
41    engine_rpc_url: String,
42
43    /// Path to the JWT secret for Engine API authentication.
44    #[arg(long = "jwt-secret", value_name = "JWT_SECRET")]
45    jwt_secret: PathBuf,
46
47    /// Output directory for benchmark results and generated payloads.
48    #[arg(long, value_name = "OUTPUT")]
49    output: PathBuf,
50}
51
52/// Mode for determining when to stop ramping.
53#[derive(Debug, Clone, Copy)]
54enum RampMode {
55    /// Ramp for a fixed number of blocks.
56    Blocks(u64),
57    /// Ramp until reaching or exceeding target gas limit.
58    TargetGasLimit(u64),
59}
60
61impl Command {
62    /// Execute `benchmark gas-limit-ramp` command.
63    pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
64        let mode = match (self.blocks, self.target_gas_limit) {
65            (Some(blocks), None) => {
66                if blocks == 0 {
67                    return Err(eyre::eyre!("--blocks must be greater than 0"));
68                }
69                RampMode::Blocks(blocks)
70            }
71            (None, Some(target)) => {
72                if target == 0 {
73                    return Err(eyre::eyre!("--target-gas-limit must be greater than 0"));
74                }
75                RampMode::TargetGasLimit(target)
76            }
77            _ => {
78                return Err(eyre::eyre!(
79                    "Exactly one of --blocks or --target-gas-limit must be specified"
80                ));
81            }
82        };
83
84        // Ensure output directory exists
85        if self.output.is_file() {
86            return Err(eyre::eyre!("Output path must be a directory"));
87        }
88        if !self.output.exists() {
89            std::fs::create_dir_all(&self.output)?;
90            info!(target: "reth-bench", "Created output directory: {:?}", self.output);
91        }
92
93        // Set up authenticated provider (used for both Engine API and eth_ methods)
94        let jwt = std::fs::read_to_string(&self.jwt_secret)?;
95        let jwt = JwtSecret::from_hex(jwt)?;
96        let auth_url = Url::parse(&self.engine_rpc_url)?;
97
98        info!(target: "reth-bench", "Connecting to Engine RPC at {}", auth_url);
99        let auth_transport = AuthenticatedTransportConnect::new(auth_url, jwt);
100        let client = ClientBuilder::default().connect_with(auth_transport).await?;
101        let provider = RootProvider::<AnyNetwork>::new(client);
102
103        // Get chain spec - required for fork detection
104        let chain_id = provider.get_chain_id().await?;
105        let chain_spec = ChainSpec::from_chain_id(chain_id)
106            .ok_or_else(|| eyre::eyre!("Unsupported chain id: {chain_id}"))?;
107
108        // Fetch the current head block as parent
109        let parent_block = provider
110            .get_block_by_number(BlockNumberOrTag::Latest)
111            .full()
112            .await?
113            .ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?;
114
115        let (mut parent_header, mut parent_hash) = rpc_block_to_header(parent_block);
116
117        let canonical_parent = parent_header.number;
118        let start_block = canonical_parent + 1;
119
120        match mode {
121            RampMode::Blocks(blocks) => {
122                info!(
123                    target: "reth-bench",
124                    canonical_parent,
125                    start_block,
126                    end_block = start_block + blocks - 1,
127                    "Starting gas limit ramp benchmark (block count mode)"
128                );
129            }
130            RampMode::TargetGasLimit(target) => {
131                info!(
132                    target: "reth-bench",
133                    canonical_parent,
134                    start_block,
135                    current_gas_limit = parent_header.gas_limit,
136                    target_gas_limit = target,
137                    "Starting gas limit ramp benchmark (target gas limit mode)"
138                );
139            }
140        }
141
142        let mut blocks_processed = 0u64;
143        let total_benchmark_duration = Instant::now();
144
145        while !should_stop(mode, blocks_processed, parent_header.gas_limit) {
146            let timestamp = parent_header.timestamp.saturating_add(1);
147
148            let request = prepare_payload_request(&chain_spec, timestamp, parent_hash);
149            let new_payload_version = request.new_payload_version;
150
151            let (payload, sidecar) = build_payload(&provider, request).await?;
152
153            let mut block =
154                payload.clone().try_into_block_with_sidecar::<TransactionSigned>(&sidecar)?;
155
156            let max_increase = max_gas_limit_increase(parent_header.gas_limit);
157            let gas_limit =
158                parent_header.gas_limit.saturating_add(max_increase).min(MAXIMUM_GAS_LIMIT_BLOCK);
159
160            block.header.gas_limit = gas_limit;
161
162            let block_hash = block.header.hash_slow();
163            // Regenerate the payload from the modified block, but keep the original sidecar
164            // which contains the actual execution requests data (not just the hash)
165            let (payload, _) = ExecutionPayload::from_block_unchecked(block_hash, &block);
166            let (version, params) = payload_to_new_payload(
167                payload,
168                sidecar,
169                false,
170                block.header.withdrawals_root,
171                Some(new_payload_version),
172            )?;
173
174            // Save payload to file with version info for replay
175            let payload_path =
176                self.output.join(format!("payload_block_{}.json", block.header.number));
177            let file =
178                GasRampPayloadFile { version: version as u8, block_hash, params: params.clone() };
179            let payload_json = serde_json::to_string_pretty(&file)?;
180            std::fs::write(&payload_path, &payload_json)?;
181            info!(target: "reth-bench", block_number = block.header.number, path = %payload_path.display(), "Saved payload");
182
183            call_new_payload(&provider, version, params).await?;
184
185            let forkchoice_state = ForkchoiceState {
186                head_block_hash: block_hash,
187                safe_block_hash: block_hash,
188                finalized_block_hash: block_hash,
189            };
190            call_forkchoice_updated(&provider, version, forkchoice_state, None).await?;
191
192            parent_header = block.header;
193            parent_hash = block_hash;
194            blocks_processed += 1;
195
196            let progress = match mode {
197                RampMode::Blocks(total) => format!("{blocks_processed}/{total}"),
198                RampMode::TargetGasLimit(target) => {
199                    let pct = (parent_header.gas_limit as f64 / target as f64 * 100.0).min(100.0);
200                    format!("{pct:.1}%")
201                }
202            };
203            info!(target: "reth-bench", progress, block_number = parent_header.number, gas_limit = parent_header.gas_limit, "Block processed");
204        }
205
206        let final_gas_limit = parent_header.gas_limit;
207        info!(
208            target: "reth-bench",
209            total_duration=?total_benchmark_duration.elapsed(),
210            blocks_processed,
211            final_gas_limit,
212            "Benchmark complete"
213        );
214
215        Ok(())
216    }
217}
218
219const fn max_gas_limit_increase(parent_gas_limit: u64) -> u64 {
220    (parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR).saturating_sub(1)
221}
222
223const fn should_stop(mode: RampMode, blocks_processed: u64, current_gas_limit: u64) -> bool {
224    match mode {
225        RampMode::Blocks(target_blocks) => blocks_processed >= target_blocks,
226        RampMode::TargetGasLimit(target) => current_gas_limit >= target,
227    }
228}