1mod invalidation;
4use invalidation::InvalidationConfig;
5
6use alloy_primitives::{Address, B256};
7use alloy_provider::network::AnyRpcBlock;
8use alloy_rpc_types_engine::ExecutionPayload;
9use clap::Parser;
10use eyre::{OptionExt, Result};
11use op_alloy_consensus::OpTxEnvelope;
12use reth_cli_runner::CliContext;
13use std::io::{BufReader, Read, Write};
14
15#[derive(Debug, Parser)]
21pub struct Command {
22 #[arg(short, long, help_heading = "Input Options")]
25 path: Option<String>,
26
27 #[arg(
29 short,
30 long,
31 help_heading = "Input Options",
32 required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
33 required_unless_present("mode")
34 )]
35 rpc_url: Option<String>,
36
37 #[arg(short, long, help_heading = "Input Options")]
40 jwt_secret: Option<String>,
41
42 #[arg(long, default_value_t = 3, help_heading = "Input Options")]
44 new_payload_version: u8,
45
46 #[arg(long, value_enum, default_value = "execute", help_heading = "Input Options")]
48 mode: Mode,
49
50 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
53 parent_hash: Option<B256>,
54
55 #[arg(long, value_name = "ADDR", help_heading = "Explicit Value Overrides")]
57 fee_recipient: Option<Address>,
58
59 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
61 state_root: Option<B256>,
62
63 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
65 receipts_root: Option<B256>,
66
67 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
69 block_number: Option<u64>,
70
71 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
73 gas_limit: Option<u64>,
74
75 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
77 gas_used: Option<u64>,
78
79 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
81 timestamp: Option<u64>,
82
83 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
85 base_fee_per_gas: Option<u64>,
86
87 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
89 block_hash: Option<B256>,
90
91 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
93 blob_gas_used: Option<u64>,
94
95 #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
97 excess_blob_gas: Option<u64>,
98
99 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
101 parent_beacon_block_root: Option<B256>,
102
103 #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
105 requests_hash: Option<B256>,
106
107 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
110 invalidate_parent_hash: bool,
111
112 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
114 invalidate_state_root: bool,
115
116 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
118 invalidate_receipts_root: bool,
119
120 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
122 invalidate_gas_used: bool,
123
124 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
126 invalidate_block_number: bool,
127
128 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
130 invalidate_timestamp: bool,
131
132 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
134 invalidate_base_fee: bool,
135
136 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
138 invalidate_transactions: bool,
139
140 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
142 invalidate_block_hash: bool,
143
144 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
146 invalidate_withdrawals: bool,
147
148 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
150 invalidate_blob_gas_used: bool,
151
152 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
154 invalidate_excess_blob_gas: bool,
155
156 #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
158 invalidate_requests_hash: bool,
159
160 #[arg(long, default_value_t = false, help_heading = "Meta Flags")]
163 skip_hash_recalc: bool,
164
165 #[arg(long, default_value_t = false, help_heading = "Meta Flags")]
167 dry_run: bool,
168}
169
170#[derive(Debug, Clone, clap::ValueEnum)]
171enum Mode {
172 Execute,
175 Cast,
178 Json,
180}
181
182impl Command {
183 fn read_input(&self) -> Result<String> {
185 Ok(match &self.path {
186 Some(path) => reth_fs_util::read_to_string(path)?,
187 None => String::from_utf8(
188 BufReader::new(std::io::stdin()).bytes().collect::<Result<Vec<_>, _>>()?,
189 )?,
190 })
191 }
192
193 fn load_jwt_secret(&self) -> Result<Option<String>> {
195 match &self.jwt_secret {
196 Some(secret) => match std::fs::read_to_string(secret) {
197 Ok(contents) => Ok(Some(contents.trim().to_string())),
198 Err(_) => Ok(Some(secret.clone())),
199 },
200 None => Ok(None),
201 }
202 }
203
204 const fn build_invalidation_config(&self) -> InvalidationConfig {
206 InvalidationConfig {
207 parent_hash: self.parent_hash,
208 fee_recipient: self.fee_recipient,
209 state_root: self.state_root,
210 receipts_root: self.receipts_root,
211 logs_bloom: None,
212 prev_randao: None,
213 block_number: self.block_number,
214 gas_limit: self.gas_limit,
215 gas_used: self.gas_used,
216 timestamp: self.timestamp,
217 extra_data: None,
218 base_fee_per_gas: self.base_fee_per_gas,
219 block_hash: self.block_hash,
220 blob_gas_used: self.blob_gas_used,
221 excess_blob_gas: self.excess_blob_gas,
222 invalidate_parent_hash: self.invalidate_parent_hash,
223 invalidate_state_root: self.invalidate_state_root,
224 invalidate_receipts_root: self.invalidate_receipts_root,
225 invalidate_gas_used: self.invalidate_gas_used,
226 invalidate_block_number: self.invalidate_block_number,
227 invalidate_timestamp: self.invalidate_timestamp,
228 invalidate_base_fee: self.invalidate_base_fee,
229 invalidate_transactions: self.invalidate_transactions,
230 invalidate_block_hash: self.invalidate_block_hash,
231 invalidate_withdrawals: self.invalidate_withdrawals,
232 invalidate_blob_gas_used: self.invalidate_blob_gas_used,
233 invalidate_excess_blob_gas: self.invalidate_excess_blob_gas,
234 }
235 }
236
237 pub async fn execute(self, _ctx: CliContext) -> Result<()> {
239 let block_json = self.read_input()?;
240 let jwt_secret = self.load_jwt_secret()?;
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| tx.try_into_either::<OpTxEnvelope>())?
246 .into_consensus();
247
248 let config = self.build_invalidation_config();
249
250 let parent_beacon_block_root =
251 self.parent_beacon_block_root.or(block.header.parent_beacon_block_root);
252 let blob_versioned_hashes =
253 block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
254 let use_v4 = block.header.requests_hash.is_some();
255 let requests_hash = self.requests_hash.or(block.header.requests_hash);
256
257 let mut execution_payload = ExecutionPayload::from_block_slow(&block).0;
258
259 let changes = match &mut execution_payload {
260 ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
261 ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
262 ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
263 };
264
265 let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
266 if !skip_recalc {
267 let new_hash = match execution_payload.clone().into_block_raw() {
268 Ok(block) => block.header.hash_slow(),
269 Err(e) => {
270 eprintln!(
271 "Warning: Could not recalculate block hash: {e}. Using original hash."
272 );
273 match &execution_payload {
274 ExecutionPayload::V1(p) => p.block_hash,
275 ExecutionPayload::V2(p) => p.payload_inner.block_hash,
276 ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
277 }
278 }
279 };
280
281 match &mut execution_payload {
282 ExecutionPayload::V1(p) => p.block_hash = new_hash,
283 ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
284 ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
285 }
286 }
287
288 if self.dry_run {
289 println!("=== Dry Run ===");
290 println!("Changes that would be applied:");
291 for change in &changes {
292 println!(" - {}", change);
293 }
294 if changes.is_empty() {
295 println!(" (no changes)");
296 }
297 if skip_recalc {
298 println!(" - Block hash recalculation: SKIPPED");
299 } else {
300 println!(" - Block hash recalculation: PERFORMED");
301 }
302 println!("\nResulting payload JSON:");
303 let json = serde_json::to_string_pretty(&execution_payload)?;
304 println!("{}", json);
305 return Ok(());
306 }
307
308 let json_request = if use_v4 {
309 serde_json::to_string(&(
310 execution_payload,
311 blob_versioned_hashes,
312 parent_beacon_block_root,
313 requests_hash.unwrap_or_default(),
314 ))?
315 } else {
316 serde_json::to_string(&(
317 execution_payload,
318 blob_versioned_hashes,
319 parent_beacon_block_root,
320 ))?
321 };
322
323 match self.mode {
324 Mode::Execute => {
325 let mut command = std::process::Command::new("cast");
326 let method = if use_v4 { "engine_newPayloadV4" } else { "engine_newPayloadV3" };
327 command.arg("rpc").arg(method).arg("--raw");
328 if let Some(rpc_url) = self.rpc_url {
329 command.arg("--rpc-url").arg(rpc_url);
330 }
331 if let Some(secret) = &jwt_secret {
332 command.arg("--jwt-secret").arg(secret);
333 }
334
335 let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
336
337 process
338 .stdin
339 .take()
340 .ok_or_eyre("stdin not available")?
341 .write_all(json_request.as_bytes())?;
342
343 process.wait()?;
344 }
345 Mode::Cast => {
346 let mut cmd = format!(
347 "cast rpc engine_newPayloadV{} --raw '{}'",
348 self.new_payload_version, json_request
349 );
350
351 if let Some(rpc_url) = self.rpc_url {
352 cmd += &format!(" --rpc-url {rpc_url}");
353 }
354 if let Some(secret) = &jwt_secret {
355 cmd += &format!(" --jwt-secret {secret}");
356 }
357
358 println!("{cmd}");
359 }
360 Mode::Json => {
361 println!("{json_request}");
362 }
363 }
364
365 Ok(())
366 }
367}