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