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!("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!("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                    canonical_parent,
124                    start_block,
125                    end_block = start_block + blocks - 1,
126                    "Starting gas limit ramp benchmark (block count mode)"
127                );
128            }
129            RampMode::TargetGasLimit(target) => {
130                info!(
131                    canonical_parent,
132                    start_block,
133                    current_gas_limit = parent_header.gas_limit,
134                    target_gas_limit = target,
135                    "Starting gas limit ramp benchmark (target gas limit mode)"
136                );
137            }
138        }
139
140        let mut blocks_processed = 0u64;
141        let total_benchmark_duration = Instant::now();
142
143        while !should_stop(mode, blocks_processed, parent_header.gas_limit) {
144            let timestamp = parent_header.timestamp.saturating_add(1);
145
146            let request = prepare_payload_request(&chain_spec, timestamp, parent_hash);
147            let new_payload_version = request.new_payload_version;
148
149            let (payload, sidecar) = build_payload(&provider, request).await?;
150
151            let mut block =
152                payload.clone().try_into_block_with_sidecar::<TransactionSigned>(&sidecar)?;
153
154            let max_increase = max_gas_limit_increase(parent_header.gas_limit);
155            let gas_limit =
156                parent_header.gas_limit.saturating_add(max_increase).min(MAXIMUM_GAS_LIMIT_BLOCK);
157
158            block.header.gas_limit = gas_limit;
159
160            let block_hash = block.header.hash_slow();
161            // Regenerate the payload from the modified block, but keep the original sidecar
162            // which contains the actual execution requests data (not just the hash)
163            let (payload, _) = ExecutionPayload::from_block_unchecked(block_hash, &block);
164            let (version, params) = payload_to_new_payload(
165                payload,
166                sidecar,
167                false,
168                block.header.withdrawals_root,
169                Some(new_payload_version),
170            )?;
171
172            // Save payload to file with version info for replay
173            let payload_path =
174                self.output.join(format!("payload_block_{}.json", block.header.number));
175            let file =
176                GasRampPayloadFile { version: version as u8, block_hash, params: params.clone() };
177            let payload_json = serde_json::to_string_pretty(&file)?;
178            std::fs::write(&payload_path, &payload_json)?;
179            info!(block_number = block.header.number, path = %payload_path.display(), "Saved payload");
180
181            call_new_payload(&provider, version, params).await?;
182
183            let forkchoice_state = ForkchoiceState {
184                head_block_hash: block_hash,
185                safe_block_hash: block_hash,
186                finalized_block_hash: block_hash,
187            };
188            call_forkchoice_updated(&provider, version, forkchoice_state, None).await?;
189
190            parent_header = block.header;
191            parent_hash = block_hash;
192            blocks_processed += 1;
193        }
194
195        let final_gas_limit = parent_header.gas_limit;
196        info!(
197            total_duration=?total_benchmark_duration.elapsed(),
198            blocks_processed,
199            final_gas_limit,
200            "Benchmark complete"
201        );
202
203        Ok(())
204    }
205}
206
207const fn max_gas_limit_increase(parent_gas_limit: u64) -> u64 {
208    (parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR).saturating_sub(1)
209}
210
211const fn should_stop(mode: RampMode, blocks_processed: u64, current_gas_limit: u64) -> bool {
212    match mode {
213        RampMode::Blocks(target_blocks) => blocks_processed >= target_blocks,
214        RampMode::TargetGasLimit(target) => current_gas_limit >= target,
215    }
216}