reth_bench/bench/
send_payload.rs

1use alloy_provider::network::AnyRpcBlock;
2use alloy_rpc_types_engine::ExecutionPayload;
3use clap::Parser;
4use eyre::{OptionExt, Result};
5use op_alloy_consensus::OpTxEnvelope;
6use reth_cli_runner::CliContext;
7use std::io::{BufReader, Read, Write};
8
9/// Command for generating and sending an `engine_newPayload` request constructed from an RPC
10/// block.
11#[derive(Debug, Parser)]
12pub struct Command {
13    /// Path to the json file to parse. If not specified, stdin will be used.
14    #[arg(short, long)]
15    path: Option<String>,
16
17    /// The engine RPC url to use.
18    #[arg(
19        short,
20        long,
21        // Required if `mode` is `execute` or `cast`.
22        required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
23        // If `mode` is not specified, then `execute` is used, so we need to require it.
24        required_unless_present("mode")
25    )]
26    rpc_url: Option<String>,
27
28    /// The JWT secret to use. Can be either a path to a file containing the secret or the secret
29    /// itself.
30    #[arg(short, long)]
31    jwt_secret: Option<String>,
32
33    #[arg(long, default_value_t = 3)]
34    new_payload_version: u8,
35
36    /// The mode to use.
37    #[arg(long, value_enum, default_value = "execute")]
38    mode: Mode,
39}
40
41#[derive(Debug, Clone, clap::ValueEnum)]
42enum Mode {
43    /// Execute the `cast` command. This works with blocks of any size, because it pipes the
44    /// payload into the `cast` command.
45    Execute,
46    /// Print the `cast` command. Caution: this may not work with large blocks because of the
47    /// command length limit.
48    Cast,
49    /// Print the JSON payload. Can be piped into `cast` command if the block is small enough.
50    Json,
51}
52
53impl Command {
54    /// Read input from either a file or stdin
55    fn read_input(&self) -> Result<String> {
56        Ok(match &self.path {
57            Some(path) => reth_fs_util::read_to_string(path)?,
58            None => String::from_utf8(
59                BufReader::new(std::io::stdin()).bytes().collect::<Result<Vec<_>, _>>()?,
60            )?,
61        })
62    }
63
64    /// Load JWT secret from either a file or use the provided string directly
65    fn load_jwt_secret(&self) -> Result<Option<String>> {
66        match &self.jwt_secret {
67            Some(secret) => {
68                // Try to read as file first
69                match std::fs::read_to_string(secret) {
70                    Ok(contents) => Ok(Some(contents.trim().to_string())),
71                    // If file read fails, use the string directly
72                    Err(_) => Ok(Some(secret.clone())),
73                }
74            }
75            None => Ok(None),
76        }
77    }
78
79    /// Execute the generate payload command
80    pub async fn execute(self, _ctx: CliContext) -> Result<()> {
81        // Load block
82        let block_json = self.read_input()?;
83
84        // Load JWT secret
85        let jwt_secret = self.load_jwt_secret()?;
86
87        // Parse the block
88        let block = serde_json::from_str::<AnyRpcBlock>(&block_json)?
89            .into_inner()
90            .map_header(|header| header.map(|h| h.into_header_with_defaults()))
91            .try_map_transactions(|tx| {
92                // try to convert unknowns into op type so that we can also support optimism
93                tx.try_into_either::<OpTxEnvelope>()
94            })?
95            .into_consensus();
96
97        // Extract parent beacon block root
98        let parent_beacon_block_root = block.header.parent_beacon_block_root;
99
100        // Extract blob versioned hashes
101        let blob_versioned_hashes =
102            block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
103
104        // Convert to execution payload
105        let execution_payload = ExecutionPayload::from_block_slow(&block).0;
106
107        // Create JSON request data
108        let json_request = serde_json::to_string(&(
109            execution_payload,
110            blob_versioned_hashes,
111            parent_beacon_block_root,
112        ))?;
113
114        // Print output or execute command
115        match self.mode {
116            Mode::Execute => {
117                // Create cast command
118                let mut command = std::process::Command::new("cast");
119                command.arg("rpc").arg("engine_newPayloadV3").arg("--raw");
120                if let Some(rpc_url) = self.rpc_url {
121                    command.arg("--rpc-url").arg(rpc_url);
122                }
123                if let Some(secret) = &jwt_secret {
124                    command.arg("--jwt-secret").arg(secret);
125                }
126
127                // Start cast process
128                let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
129
130                // Write to cast's stdin
131                process
132                    .stdin
133                    .take()
134                    .ok_or_eyre("stdin not available")?
135                    .write_all(json_request.as_bytes())?;
136
137                // Wait for cast to finish
138                process.wait()?;
139            }
140            Mode::Cast => {
141                let mut cmd = format!(
142                    "cast rpc engine_newPayloadV{} --raw '{}'",
143                    self.new_payload_version, json_request
144                );
145
146                if let Some(rpc_url) = self.rpc_url {
147                    cmd += &format!(" --rpc-url {}", rpc_url);
148                }
149                if let Some(secret) = &jwt_secret {
150                    cmd += &format!(" --jwt-secret {}", secret);
151                }
152
153                println!("{cmd}");
154            }
155            Mode::Json => {
156                println!("{json_request}");
157            }
158        }
159
160        Ok(())
161    }
162}