1use 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#[derive(Debug, Parser)]
27pub struct Command {
28 #[arg(long, value_name = "BLOCKS", conflicts_with = "target_gas_limit")]
30 blocks: Option<u64>,
31
32 #[arg(long, value_name = "TARGET_GAS_LIMIT", conflicts_with = "blocks", value_parser = parse_gas_limit)]
37 target_gas_limit: Option<u64>,
38
39 #[arg(long = "engine-rpc-url", value_name = "ENGINE_RPC_URL")]
41 engine_rpc_url: String,
42
43 #[arg(long = "jwt-secret", value_name = "JWT_SECRET")]
45 jwt_secret: PathBuf,
46
47 #[arg(long, value_name = "OUTPUT")]
49 output: PathBuf,
50}
51
52#[derive(Debug, Clone, Copy)]
54enum RampMode {
55 Blocks(u64),
57 TargetGasLimit(u64),
59}
60
61impl Command {
62 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 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 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 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 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 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 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}