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::{
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#[derive(Debug, Parser)]
30pub struct Command {
31 #[arg(long, value_name = "BLOCKS", conflicts_with = "target_gas_limit")]
33 blocks: Option<u64>,
34
35 #[arg(long, value_name = "TARGET_GAS_LIMIT", conflicts_with = "blocks", value_parser = parse_gas_limit)]
40 target_gas_limit: Option<u64>,
41
42 #[arg(long = "engine-rpc-url", value_name = "ENGINE_RPC_URL")]
44 engine_rpc_url: String,
45
46 #[arg(long = "jwt-secret", value_name = "JWT_SECRET")]
48 jwt_secret: PathBuf,
49
50 #[arg(long, value_name = "OUTPUT")]
52 output: PathBuf,
53
54 #[arg(long, default_value = "false", verbatim_doc_comment)]
60 reth_new_payload: bool,
61}
62
63#[derive(Debug, Clone, Copy)]
65enum RampMode {
66 Blocks(u64),
68 TargetGasLimit(u64),
70}
71
72impl Command {
73 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 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 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 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 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 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 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}