Skip to main content

reth_bench/bench/send_invalid_payload/
mod.rs

1//! Command for sending invalid payloads to test Engine API rejection.
2
3mod invalidation;
4use invalidation::InvalidationConfig;
5
6use super::helpers::{load_jwt_secret, read_input};
7use alloy_primitives::{Address, B256};
8use alloy_provider::network::AnyRpcBlock;
9use alloy_rpc_types_engine::ExecutionPayload;
10use clap::Parser;
11use eyre::{OptionExt, Result};
12use op_alloy_consensus::OpTxEnvelope;
13use reth_cli_runner::CliContext;
14use std::io::Write;
15
16/// Command for generating and sending an invalid `engine_newPayload` request.
17///
18/// Takes a valid block and modifies fields to make it invalid for testing
19/// Engine API rejection behavior. Block hash is recalculated after modifications
20/// unless `--invalidate-block-hash` or `--skip-hash-recalc` is used.
21#[derive(Debug, Parser)]
22pub struct Command {
23    // ==================== Input Options ====================
24    /// Path to the JSON file containing the block. If not specified, stdin will be used.
25    #[arg(short, long, help_heading = "Input Options")]
26    path: Option<String>,
27
28    /// The engine RPC URL to use.
29    #[arg(
30        short,
31        long,
32        help_heading = "Input Options",
33        required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
34        required_unless_present("mode")
35    )]
36    rpc_url: Option<String>,
37
38    /// The JWT secret to use. Can be either a path to a file containing the secret or the secret
39    /// itself.
40    #[arg(short, long, help_heading = "Input Options")]
41    jwt_secret: Option<String>,
42
43    /// The newPayload version to use (3 or 4).
44    #[arg(long, default_value_t = 3, help_heading = "Input Options")]
45    new_payload_version: u8,
46
47    /// The output mode to use.
48    #[arg(long, value_enum, default_value = "execute", help_heading = "Input Options")]
49    mode: Mode,
50
51    // ==================== Explicit Value Overrides ====================
52    /// Override the parent hash with a specific value.
53    #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
54    parent_hash: Option<B256>,
55
56    /// Override the fee recipient (coinbase) with a specific address.
57    #[arg(long, value_name = "ADDR", help_heading = "Explicit Value Overrides")]
58    fee_recipient: Option<Address>,
59
60    /// Override the state root with a specific value.
61    #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
62    state_root: Option<B256>,
63
64    /// Override the receipts root with a specific value.
65    #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
66    receipts_root: Option<B256>,
67
68    /// Override the block number with a specific value.
69    #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
70    block_number: Option<u64>,
71
72    /// Override the gas limit with a specific value.
73    #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
74    gas_limit: Option<u64>,
75
76    /// Override the gas used with a specific value.
77    #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
78    gas_used: Option<u64>,
79
80    /// Override the timestamp with a specific value.
81    #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
82    timestamp: Option<u64>,
83
84    /// Override the base fee per gas with a specific value.
85    #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
86    base_fee_per_gas: Option<u64>,
87
88    /// Override the block hash with a specific value (skips hash recalculation).
89    #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
90    block_hash: Option<B256>,
91
92    /// Override the blob gas used with a specific value.
93    #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
94    blob_gas_used: Option<u64>,
95
96    /// Override the excess blob gas with a specific value.
97    #[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
98    excess_blob_gas: Option<u64>,
99
100    /// Override the parent beacon block root with a specific value.
101    #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
102    parent_beacon_block_root: Option<B256>,
103
104    /// Override the requests hash with a specific value (EIP-7685).
105    #[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
106    requests_hash: Option<B256>,
107
108    // ==================== Auto-Invalidation Flags ====================
109    /// Invalidate the parent hash by setting it to a random value.
110    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
111    invalidate_parent_hash: bool,
112
113    /// Invalidate the state root by setting it to a random value.
114    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
115    invalidate_state_root: bool,
116
117    /// Invalidate the receipts root by setting it to a random value.
118    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
119    invalidate_receipts_root: bool,
120
121    /// Invalidate the gas used by setting it to an incorrect value.
122    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
123    invalidate_gas_used: bool,
124
125    /// Invalidate the block number by setting it to an incorrect value.
126    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
127    invalidate_block_number: bool,
128
129    /// Invalidate the timestamp by setting it to an incorrect value.
130    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
131    invalidate_timestamp: bool,
132
133    /// Invalidate the base fee by setting it to an incorrect value.
134    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
135    invalidate_base_fee: bool,
136
137    /// Invalidate the transactions by modifying them.
138    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
139    invalidate_transactions: bool,
140
141    /// Invalidate the block hash by not recalculating it after modifications.
142    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
143    invalidate_block_hash: bool,
144
145    /// Invalidate the withdrawals by modifying them.
146    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
147    invalidate_withdrawals: bool,
148
149    /// Invalidate the blob gas used by setting it to an incorrect value.
150    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
151    invalidate_blob_gas_used: bool,
152
153    /// Invalidate the excess blob gas by setting it to an incorrect value.
154    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
155    invalidate_excess_blob_gas: bool,
156
157    /// Invalidate the requests hash by setting it to a random value (EIP-7685).
158    #[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
159    invalidate_requests_hash: bool,
160
161    // ==================== Meta Flags ====================
162    /// Skip block hash recalculation after modifications.
163    #[arg(long, default_value_t = false, help_heading = "Meta Flags")]
164    skip_hash_recalc: bool,
165
166    /// Print what would be done without actually sending the payload.
167    #[arg(long, default_value_t = false, help_heading = "Meta Flags")]
168    dry_run: bool,
169}
170
171#[derive(Debug, Clone, clap::ValueEnum)]
172enum Mode {
173    /// Execute the `cast` command. This works with blocks of any size, because it pipes the
174    /// payload into the `cast` command.
175    Execute,
176    /// Print the `cast` command. Caution: this may not work with large blocks because of the
177    /// command length limit.
178    Cast,
179    /// Print the JSON payload. Can be piped into `cast` command if the block is small enough.
180    Json,
181}
182
183impl Command {
184    /// Build `InvalidationConfig` from command flags
185    const fn build_invalidation_config(&self) -> InvalidationConfig {
186        InvalidationConfig {
187            parent_hash: self.parent_hash,
188            fee_recipient: self.fee_recipient,
189            state_root: self.state_root,
190            receipts_root: self.receipts_root,
191            logs_bloom: None,
192            prev_randao: None,
193            block_number: self.block_number,
194            gas_limit: self.gas_limit,
195            gas_used: self.gas_used,
196            timestamp: self.timestamp,
197            extra_data: None,
198            base_fee_per_gas: self.base_fee_per_gas,
199            block_hash: self.block_hash,
200            blob_gas_used: self.blob_gas_used,
201            excess_blob_gas: self.excess_blob_gas,
202            invalidate_parent_hash: self.invalidate_parent_hash,
203            invalidate_state_root: self.invalidate_state_root,
204            invalidate_receipts_root: self.invalidate_receipts_root,
205            invalidate_gas_used: self.invalidate_gas_used,
206            invalidate_block_number: self.invalidate_block_number,
207            invalidate_timestamp: self.invalidate_timestamp,
208            invalidate_base_fee: self.invalidate_base_fee,
209            invalidate_transactions: self.invalidate_transactions,
210            invalidate_block_hash: self.invalidate_block_hash,
211            invalidate_withdrawals: self.invalidate_withdrawals,
212            invalidate_blob_gas_used: self.invalidate_blob_gas_used,
213            invalidate_excess_blob_gas: self.invalidate_excess_blob_gas,
214        }
215    }
216
217    /// Execute the command
218    pub async fn execute(self, _ctx: CliContext) -> Result<()> {
219        let block_json = read_input(self.path.as_deref())?;
220        let jwt_secret = load_jwt_secret(self.jwt_secret.as_deref())?;
221
222        let block = serde_json::from_str::<AnyRpcBlock>(&block_json)?
223            .into_inner()
224            .map_header(|header| header.map(|h| h.into_header_with_defaults()))
225            .try_map_transactions(|tx| tx.try_into_either::<OpTxEnvelope>())?
226            .into_consensus();
227
228        let config = self.build_invalidation_config();
229
230        let parent_beacon_block_root =
231            self.parent_beacon_block_root.or(block.header.parent_beacon_block_root);
232        let blob_versioned_hashes =
233            block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
234        let use_v4 = block.header.requests_hash.is_some();
235        let requests_hash = self.requests_hash.or(block.header.requests_hash);
236
237        let mut execution_payload = ExecutionPayload::from_block_slow(&block).0;
238
239        let changes = match &mut execution_payload {
240            ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
241            ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
242            ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
243        };
244
245        let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
246        if !skip_recalc {
247            let new_hash = match execution_payload.clone().into_block_raw() {
248                Ok(block) => block.header.hash_slow(),
249                Err(e) => {
250                    eprintln!(
251                        "Warning: Could not recalculate block hash: {e}. Using original hash."
252                    );
253                    match &execution_payload {
254                        ExecutionPayload::V1(p) => p.block_hash,
255                        ExecutionPayload::V2(p) => p.payload_inner.block_hash,
256                        ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
257                    }
258                }
259            };
260
261            match &mut execution_payload {
262                ExecutionPayload::V1(p) => p.block_hash = new_hash,
263                ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
264                ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
265            }
266        }
267
268        if self.dry_run {
269            println!("=== Dry Run ===");
270            println!("Changes that would be applied:");
271            for change in &changes {
272                println!("  - {}", change);
273            }
274            if changes.is_empty() {
275                println!("  (no changes)");
276            }
277            if skip_recalc {
278                println!("  - Block hash recalculation: SKIPPED");
279            } else {
280                println!("  - Block hash recalculation: PERFORMED");
281            }
282            println!("\nResulting payload JSON:");
283            let json = serde_json::to_string_pretty(&execution_payload)?;
284            println!("{}", json);
285            return Ok(());
286        }
287
288        let json_request = if use_v4 {
289            serde_json::to_string(&(
290                execution_payload,
291                blob_versioned_hashes,
292                parent_beacon_block_root,
293                requests_hash.unwrap_or_default(),
294            ))?
295        } else {
296            serde_json::to_string(&(
297                execution_payload,
298                blob_versioned_hashes,
299                parent_beacon_block_root,
300            ))?
301        };
302
303        match self.mode {
304            Mode::Execute => {
305                let mut command = std::process::Command::new("cast");
306                let method = if use_v4 { "engine_newPayloadV4" } else { "engine_newPayloadV3" };
307                command.arg("rpc").arg(method).arg("--raw");
308                if let Some(rpc_url) = self.rpc_url {
309                    command.arg("--rpc-url").arg(rpc_url);
310                }
311                if let Some(secret) = &jwt_secret {
312                    command.arg("--jwt-secret").arg(secret);
313                }
314
315                let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
316
317                process
318                    .stdin
319                    .take()
320                    .ok_or_eyre("stdin not available")?
321                    .write_all(json_request.as_bytes())?;
322
323                process.wait()?;
324            }
325            Mode::Cast => {
326                let mut cmd = format!(
327                    "cast rpc engine_newPayloadV{} --raw '{}'",
328                    self.new_payload_version, json_request
329                );
330
331                if let Some(rpc_url) = self.rpc_url {
332                    cmd += &format!(" --rpc-url {rpc_url}");
333                }
334                if let Some(secret) = &jwt_secret {
335                    cmd += &format!(" --jwt-secret {secret}");
336                }
337
338                println!("{cmd}");
339            }
340            Mode::Json => {
341                println!("{json_request}");
342            }
343        }
344
345        Ok(())
346    }
347}