Skip to main content

reth_bench/bench/
send_payload.rs

1use super::helpers::{fetch_block_access_list, load_jwt_secret, read_input};
2use alloy_consensus::TxEnvelope;
3use alloy_primitives::Bytes;
4use alloy_provider::{
5    network::{AnyNetwork, AnyRpcBlock},
6    RootProvider,
7};
8use alloy_rpc_client::ClientBuilder;
9use alloy_rpc_types_engine::ExecutionPayload;
10use clap::Parser;
11use eyre::{OptionExt, Result};
12use reth_cli_runner::CliContext;
13use std::io::Write;
14
15/// Command for generating and sending an `engine_newPayload` request constructed from an RPC
16/// block.
17#[derive(Debug, Parser)]
18pub struct Command {
19    /// Path to the json file to parse. If not specified, stdin will be used.
20    #[arg(short, long)]
21    path: Option<String>,
22
23    /// The engine RPC url to use.
24    #[arg(
25        short,
26        long,
27        // Required if `mode` is `execute` or `cast`.
28        required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
29        // If `mode` is not specified, then `execute` is used, so we need to require it.
30        required_unless_present("mode")
31    )]
32    rpc_url: Option<String>,
33
34    /// The JWT secret to use. Can be either a path to a file containing the secret or the secret
35    /// itself.
36    #[arg(short, long)]
37    jwt_secret: Option<String>,
38
39    #[arg(long, default_value_t = 3)]
40    new_payload_version: u8,
41
42    /// The mode to use.
43    #[arg(long, value_enum, default_value = "execute")]
44    mode: Mode,
45}
46
47#[derive(Debug, Clone, clap::ValueEnum)]
48enum Mode {
49    /// Execute the `cast` command. This works with blocks of any size, because it pipes the
50    /// payload into the `cast` command.
51    Execute,
52    /// Print the `cast` command. Caution: this may not work with large blocks because of the
53    /// command length limit.
54    Cast,
55    /// Print the JSON payload. Can be piped into `cast` command if the block is small enough.
56    Json,
57}
58
59impl Command {
60    /// Execute the generate payload command
61    pub async fn execute(self, _ctx: CliContext) -> Result<()> {
62        // Load block
63        let block_json = read_input(self.path.as_deref())?;
64
65        // Load JWT secret
66        let jwt_secret = load_jwt_secret(self.jwt_secret.as_deref())?;
67
68        // Parse the block
69        let block = serde_json::from_str::<AnyRpcBlock>(&block_json)?
70            .into_inner()
71            .map_header(|header| header.map(|h| h.into_header_with_defaults()))
72            .try_map_transactions(|tx| -> eyre::Result<TxEnvelope> {
73                tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type"))
74            })?
75            .into_consensus();
76
77        let use_v4 = block.header.requests_hash.is_some();
78        let use_v5 = block.header.block_access_list_hash.is_some();
79
80        // Extract parent beacon block root
81        let parent_beacon_block_root = block.header.parent_beacon_block_root;
82
83        // Extract blob versioned hashes
84        let blob_versioned_hashes =
85            block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
86
87        // V5 payloads must carry the full RLP-encoded block access list, not just the hash stored
88        // in the header.
89        let execution_payload = if use_v5 {
90            let encoded_bal = self.fetch_encoded_block_access_list(block.header.number).await?;
91            ExecutionPayload::from_block_slow_with_bal(&block, encoded_bal).0
92        } else {
93            ExecutionPayload::from_block_slow(&block).0
94        };
95
96        // Create JSON request data
97        let json_request = if use_v4 {
98            serde_json::to_string(&(
99                execution_payload,
100                blob_versioned_hashes,
101                parent_beacon_block_root,
102                block.header.requests_hash.unwrap_or_default(),
103            ))?
104        } else {
105            serde_json::to_string(&(
106                execution_payload,
107                blob_versioned_hashes,
108                parent_beacon_block_root,
109            ))?
110        };
111
112        // Print output or execute command
113        match self.mode {
114            Mode::Execute => {
115                // Create cast command
116                let mut command = std::process::Command::new("cast");
117                let method = if use_v5 {
118                    "engine_newPayloadV5"
119                } else if use_v4 {
120                    "engine_newPayloadV4"
121                } else {
122                    "engine_newPayloadV3"
123                };
124                command.arg("rpc").arg(method).arg("--raw");
125                if let Some(rpc_url) = self.rpc_url {
126                    command.arg("--rpc-url").arg(rpc_url);
127                }
128                if let Some(secret) = &jwt_secret {
129                    command.arg("--jwt-secret").arg(secret);
130                }
131
132                // Start cast process
133                let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
134
135                // Write to cast's stdin
136                process
137                    .stdin
138                    .take()
139                    .ok_or_eyre("stdin not available")?
140                    .write_all(json_request.as_bytes())?;
141
142                // Wait for cast to finish
143                process.wait()?;
144            }
145            Mode::Cast => {
146                let mut cmd = format!(
147                    "cast rpc engine_newPayloadV{} --raw '{}'",
148                    self.new_payload_version, json_request
149                );
150
151                if let Some(rpc_url) = self.rpc_url {
152                    cmd += &format!(" --rpc-url {rpc_url}");
153                }
154                if let Some(secret) = &jwt_secret {
155                    cmd += &format!(" --jwt-secret {secret}");
156                }
157
158                println!("{cmd}");
159            }
160            Mode::Json => {
161                println!("{json_request}");
162            }
163        }
164
165        Ok(())
166    }
167
168    async fn fetch_encoded_block_access_list(&self, block_number: u64) -> Result<Bytes> {
169        let rpc_url = self
170            .rpc_url
171            .as_deref()
172            .ok_or_eyre("--rpc-url is required to fetch the block access list for V5 payloads")?;
173        let client = ClientBuilder::default()
174            .layer(alloy_transport::layers::RetryBackoffLayer::new(10, 800, u64::MAX))
175            .http(rpc_url.parse()?);
176        let provider = RootProvider::<AnyNetwork>::new(client);
177        let bal = fetch_block_access_list(&provider, block_number).await?;
178        Ok(alloy_rlp::encode(bal).into())
179    }
180}