1mod invalidation;
4use alloy_rpc_client::ClientBuilder;
5use invalidation::InvalidationConfig;
6
7use crate::bench::helpers::fetch_block_access_list;
8
9use super::helpers::{load_jwt_secret, read_input};
10use alloy_consensus::TxEnvelope;
11use alloy_primitives::{Address, Bytes, B256};
12use alloy_provider::{
13 network::{AnyNetwork, AnyRpcBlock},
14 RootProvider,
15};
16use alloy_rpc_types_engine::ExecutionPayload;
17use clap::Parser;
18use eyre::{OptionExt, Result};
19use reth_cli_runner::CliContext;
20use std::io::Write;
21
22#[derive(Debug, Parser)]
28pub struct Command {
29 #[arg(short, long, help_heading = "Input Options")]
32 path: Option<String>,
33
34 #[arg(
36 short,
37 long,
38 help_heading = "Input Options",
39 required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
40 required_unless_present("mode")
41 )]
42 rpc_url: Option<String>,
43
44 #[arg(short, long, help_heading = "Input Options")]
47 jwt_secret: Option<String>,
48
49 #[arg(long, default_value_t = 3, help_heading = "Input Options")]
51 new_payload_version: u8,
52
53 #[arg(long, value_enum, default_value = "execute", help_heading = "Input Options")]
55 mode: Mode,
56
57 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
60 parent_hash: Option<B256>,
61
62 #[arg(long, value_name = "ADDR", help_heading = "Explicit Value Overrides")]
64 fee_recipient: Option<Address>,
65
66 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
68 state_root: Option<B256>,
69
70 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
72 receipts_root: Option<B256>,
73
74 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
76 block_number: Option<u64>,
77
78 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
80 gas_limit: Option<u64>,
81
82 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
84 gas_used: Option<u64>,
85
86 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
88 timestamp: Option<u64>,
89
90 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
92 base_fee_per_gas: Option<u64>,
93
94 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
96 block_hash: Option<B256>,
97
98 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
100 blob_gas_used: Option<u64>,
101
102 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
104 excess_blob_gas: Option<u64>,
105
106 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
108 parent_beacon_block_root: Option<B256>,
109
110 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
112 requests_hash: Option<B256>,
113
114 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
116 slot_number: Option<u64>,
117 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
120 invalidate_parent_hash: bool,
121
122 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
124 invalidate_state_root: bool,
125
126 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
128 invalidate_receipts_root: bool,
129
130 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
132 invalidate_gas_used: bool,
133
134 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
136 invalidate_block_number: bool,
137
138 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
140 invalidate_timestamp: bool,
141
142 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
144 invalidate_base_fee: bool,
145
146 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
148 invalidate_transactions: bool,
149
150 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
152 invalidate_block_hash: bool,
153
154 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
156 invalidate_withdrawals: bool,
157
158 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
160 invalidate_blob_gas_used: bool,
161
162 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
164 invalidate_excess_blob_gas: bool,
165
166 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
168 invalidate_requests_hash: bool,
169
170 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
172 invalidate_block_access_list: bool,
173
174 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
176 invalidate_slot_number: bool,
177
178 #[arg(long, default_value_t = false, help_heading = "Meta Flags")]
181 skip_hash_recalc: bool,
182
183 #[arg(long, default_value_t = false, help_heading = "Meta Flags")]
185 dry_run: bool,
186}
187
188#[derive(Debug, Clone, clap::ValueEnum)]
189enum Mode {
190 Execute,
193 Cast,
196 Json,
198}
199
200impl Command {
201 const fn build_invalidation_config(&self) -> InvalidationConfig {
203 InvalidationConfig {
204 parent_hash: self.parent_hash,
205 fee_recipient: self.fee_recipient,
206 state_root: self.state_root,
207 receipts_root: self.receipts_root,
208 logs_bloom: None,
209 prev_randao: None,
210 block_number: self.block_number,
211 gas_limit: self.gas_limit,
212 gas_used: self.gas_used,
213 timestamp: self.timestamp,
214 extra_data: None,
215 base_fee_per_gas: self.base_fee_per_gas,
216 block_hash: self.block_hash,
217 blob_gas_used: self.blob_gas_used,
218 excess_blob_gas: self.excess_blob_gas,
219 slot_number: self.slot_number,
220 invalidate_parent_hash: self.invalidate_parent_hash,
221 invalidate_state_root: self.invalidate_state_root,
222 invalidate_receipts_root: self.invalidate_receipts_root,
223 invalidate_gas_used: self.invalidate_gas_used,
224 invalidate_block_number: self.invalidate_block_number,
225 invalidate_timestamp: self.invalidate_timestamp,
226 invalidate_base_fee: self.invalidate_base_fee,
227 invalidate_transactions: self.invalidate_transactions,
228 invalidate_block_hash: self.invalidate_block_hash,
229 invalidate_withdrawals: self.invalidate_withdrawals,
230 invalidate_blob_gas_used: self.invalidate_blob_gas_used,
231 invalidate_excess_blob_gas: self.invalidate_excess_blob_gas,
232 invalidate_block_access_list: self.invalidate_block_access_list,
233 invalidate_slot_number: self.invalidate_slot_number,
234 }
235 }
236
237 pub async fn execute(self, _ctx: CliContext) -> Result<()> {
239 let block_json = read_input(self.path.as_deref())?;
240 let jwt_secret = load_jwt_secret(self.jwt_secret.as_deref())?;
241
242 let block = serde_json::from_str::<AnyRpcBlock>(&block_json)?
243 .into_inner()
244 .map_header(|header| header.map(|h| h.into_header_with_defaults()))
245 .try_map_transactions(|tx| -> eyre::Result<TxEnvelope> {
246 tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type"))
247 })?
248 .into_consensus();
249
250 let config = self.build_invalidation_config();
251
252 let parent_beacon_block_root =
253 self.parent_beacon_block_root.or(block.header.parent_beacon_block_root);
254 let blob_versioned_hashes =
255 block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
256 let use_v4 = block.header.requests_hash.is_some();
257 let use_v5 = block.header.block_access_list_hash.is_some();
258 let requests_hash = self.requests_hash.or(block.header.requests_hash);
259
260 let mut execution_payload = if use_v5 {
261 let encoded_bal = self.fetch_encoded_block_access_list(block.header.number).await?;
262 ExecutionPayload::from_block_slow_with_bal(&block, encoded_bal).0
263 } else {
264 ExecutionPayload::from_block_slow(&block).0
265 };
266
267 let changes = match &mut execution_payload {
268 ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
269 ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
270 ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
271 ExecutionPayload::V4(p) => config.apply_to_payload_v4(p),
272 };
273
274 let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
275 if !skip_recalc {
276 let new_hash = match execution_payload.clone().into_block_raw() {
277 Ok(block) => block.header.hash_slow(),
278 Err(e) => {
279 eprintln!(
280 "Warning: Could not recalculate block hash: {e}. Using original hash."
281 );
282 match &execution_payload {
283 ExecutionPayload::V1(p) => p.block_hash,
284 ExecutionPayload::V2(p) => p.payload_inner.block_hash,
285 ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
286 ExecutionPayload::V4(p) => {
287 p.payload_inner.payload_inner.payload_inner.block_hash
288 }
289 }
290 }
291 };
292
293 match &mut execution_payload {
294 ExecutionPayload::V1(p) => p.block_hash = new_hash,
295 ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
296 ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
297 ExecutionPayload::V4(p) => {
298 p.payload_inner.payload_inner.payload_inner.block_hash = new_hash
299 }
300 }
301 }
302
303 if self.dry_run {
304 println!("=== Dry Run ===");
305 println!("Changes that would be applied:");
306 for change in &changes {
307 println!(" - {}", change);
308 }
309 if changes.is_empty() {
310 println!(" (no changes)");
311 }
312 if skip_recalc {
313 println!(" - Block hash recalculation: SKIPPED");
314 } else {
315 println!(" - Block hash recalculation: PERFORMED");
316 }
317 println!("\nResulting payload JSON:");
318 let json = serde_json::to_string_pretty(&execution_payload)?;
319 println!("{}", json);
320 return Ok(());
321 }
322
323 let json_request = if use_v4 {
324 serde_json::to_string(&(
325 execution_payload,
326 blob_versioned_hashes,
327 parent_beacon_block_root,
328 requests_hash.unwrap_or_default(),
329 ))?
330 } else {
331 serde_json::to_string(&(
332 execution_payload,
333 blob_versioned_hashes,
334 parent_beacon_block_root,
335 ))?
336 };
337
338 match self.mode {
339 Mode::Execute => {
340 let mut command = std::process::Command::new("cast");
341 let method = if use_v5 {
342 "engine_newPayloadV5"
343 } else if use_v4 {
344 "engine_newPayloadV4"
345 } else {
346 "engine_newPayloadV3"
347 };
348 command.arg("rpc").arg(method).arg("--raw");
349 if let Some(rpc_url) = self.rpc_url {
350 command.arg("--rpc-url").arg(rpc_url);
351 }
352 if let Some(secret) = &jwt_secret {
353 command.arg("--jwt-secret").arg(secret);
354 }
355
356 let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
357
358 process
359 .stdin
360 .take()
361 .ok_or_eyre("stdin not available")?
362 .write_all(json_request.as_bytes())?;
363
364 process.wait()?;
365 }
366 Mode::Cast => {
367 let mut cmd = format!(
368 "cast rpc engine_newPayloadV{} --raw '{}'",
369 self.new_payload_version, json_request
370 );
371
372 if let Some(rpc_url) = self.rpc_url {
373 cmd += &format!(" --rpc-url {rpc_url}");
374 }
375 if let Some(secret) = &jwt_secret {
376 cmd += &format!(" --jwt-secret {secret}");
377 }
378
379 println!("{cmd}");
380 }
381 Mode::Json => {
382 println!("{json_request}");
383 }
384 }
385
386 Ok(())
387 }
388
389 async fn fetch_encoded_block_access_list(&self, block_number: u64) -> Result<Bytes> {
390 let rpc_url = self
391 .rpc_url
392 .as_deref()
393 .ok_or_eyre("--rpc-url is required to fetch the block access list for V5 payloads")?;
394 let client = ClientBuilder::default()
395 .layer(alloy_transport::layers::RetryBackoffLayer::new(10, 800, u64::MAX))
396 .http(rpc_url.parse()?);
397 let provider = RootProvider::<AnyNetwork>::new(client);
398 let bal = fetch_block_access_list(&provider, block_number).await?;
399 Ok(alloy_rlp::encode(bal).into())
400 }
401}