Skip to main content

reth_bench/bench/
helpers.rs

1//! Common helpers for reth-bench commands.
2
3use crate::valid_payload::call_forkchoice_updated;
4use eyre::Result;
5use std::{
6    io::{BufReader, Read},
7    time::Duration,
8};
9
10/// Read input from either a file path or stdin.
11pub(crate) fn read_input(path: Option<&str>) -> Result<String> {
12    Ok(match path {
13        Some(path) => reth_fs_util::read_to_string(path)?,
14        None => String::from_utf8(
15            BufReader::new(std::io::stdin()).bytes().collect::<Result<Vec<_>, _>>()?,
16        )?,
17    })
18}
19
20/// Load JWT secret from either a file or use the provided string directly.
21pub(crate) fn load_jwt_secret(jwt_secret: Option<&str>) -> Result<Option<String>> {
22    match jwt_secret {
23        Some(secret) => {
24            // Try to read as file first
25            match std::fs::read_to_string(secret) {
26                Ok(contents) => Ok(Some(contents.trim().to_string())),
27                // If file read fails, use the string directly
28                Err(_) => Ok(Some(secret.to_string())),
29            }
30        }
31        None => Ok(None),
32    }
33}
34
35/// Parses a gas limit value with optional suffix: K for thousand, M for million, G for billion.
36///
37/// Examples: "30000000", "30M", "1G", "2G"
38pub(crate) fn parse_gas_limit(s: &str) -> eyre::Result<u64> {
39    let s = s.trim();
40    if s.is_empty() {
41        return Err(eyre::eyre!("empty value"));
42    }
43
44    let (num_str, multiplier) = if let Some(prefix) = s.strip_suffix(['G', 'g']) {
45        (prefix, 1_000_000_000u64)
46    } else if let Some(prefix) = s.strip_suffix(['M', 'm']) {
47        (prefix, 1_000_000u64)
48    } else if let Some(prefix) = s.strip_suffix(['K', 'k']) {
49        (prefix, 1_000u64)
50    } else {
51        (s, 1u64)
52    };
53
54    let base: u64 = num_str.trim().parse()?;
55    base.checked_mul(multiplier).ok_or_else(|| eyre::eyre!("value overflow"))
56}
57
58/// Parses a duration string, treating bare integers as milliseconds.
59///
60/// Accepts either a `humantime` duration string (e.g. `"100ms"`, `"2s"`) or a plain
61/// integer which is interpreted as milliseconds (e.g. `"400"` → 400ms).
62pub(crate) fn parse_duration(s: &str) -> eyre::Result<Duration> {
63    match humantime::parse_duration(s) {
64        Ok(d) => Ok(d),
65        Err(_) => {
66            let millis: u64 =
67                s.trim().parse().map_err(|_| eyre::eyre!("invalid duration: {s:?}"))?;
68            Ok(Duration::from_millis(millis))
69        }
70    }
71}
72
73use alloy_consensus::Header;
74use alloy_eips::eip4844::kzg_to_versioned_hash;
75use alloy_primitives::{Address, B256};
76use alloy_provider::{ext::EngineApi, network::AnyNetwork, RootProvider};
77use alloy_rpc_types_engine::{
78    CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ForkchoiceState,
79    PayloadAttributes, PayloadId,
80};
81use eyre::OptionExt;
82use reth_chainspec::{ChainSpec, EthereumHardforks};
83use reth_node_api::EngineApiMessageVersion;
84use tracing::debug;
85
86/// Prepared payload request data for triggering block building.
87pub(crate) struct PayloadRequest {
88    /// The payload attributes for the new block.
89    pub(crate) attributes: PayloadAttributes,
90    /// The forkchoice state pointing to the parent block.
91    pub(crate) forkchoice_state: ForkchoiceState,
92    /// The engine API version for FCU calls.
93    pub(crate) fcu_version: EngineApiMessageVersion,
94    /// The getPayload version to use (1-5).
95    pub(crate) get_payload_version: u8,
96    /// The newPayload version to use.
97    pub(crate) new_payload_version: EngineApiMessageVersion,
98}
99
100/// Prepare payload attributes and forkchoice state for a new block.
101pub(crate) fn prepare_payload_request(
102    chain_spec: &ChainSpec,
103    timestamp: u64,
104    parent_hash: B256,
105) -> PayloadRequest {
106    let shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
107    let cancun_active = chain_spec.is_cancun_active_at_timestamp(timestamp);
108    let prague_active = chain_spec.is_prague_active_at_timestamp(timestamp);
109    let osaka_active = chain_spec.is_osaka_active_at_timestamp(timestamp);
110
111    // FCU version: V3 for Cancun+Prague+Osaka, V2 for Shanghai, V1 otherwise
112    let fcu_version = if cancun_active {
113        EngineApiMessageVersion::V3
114    } else if shanghai_active {
115        EngineApiMessageVersion::V2
116    } else {
117        EngineApiMessageVersion::V1
118    };
119
120    // getPayload version: 5 for Osaka, 4 for Prague, 3 for Cancun, 2 for Shanghai, 1 otherwise
121    // newPayload version: 4 for Prague+Osaka (no V5), 3 for Cancun, 2 for Shanghai, 1 otherwise
122    let (get_payload_version, new_payload_version) = if osaka_active {
123        (5, EngineApiMessageVersion::V4) // Osaka uses getPayloadV5 but newPayloadV4
124    } else if prague_active {
125        (4, EngineApiMessageVersion::V4)
126    } else if cancun_active {
127        (3, EngineApiMessageVersion::V3)
128    } else if shanghai_active {
129        (2, EngineApiMessageVersion::V2)
130    } else {
131        (1, EngineApiMessageVersion::V1)
132    };
133
134    PayloadRequest {
135        attributes: PayloadAttributes {
136            timestamp,
137            prev_randao: B256::ZERO,
138            suggested_fee_recipient: Address::ZERO,
139            withdrawals: shanghai_active.then(Vec::new),
140            parent_beacon_block_root: cancun_active.then_some(B256::ZERO),
141        },
142        forkchoice_state: ForkchoiceState {
143            head_block_hash: parent_hash,
144            safe_block_hash: parent_hash,
145            finalized_block_hash: parent_hash,
146        },
147        fcu_version,
148        get_payload_version,
149        new_payload_version,
150    }
151}
152
153/// Trigger payload building via FCU and retrieve the built payload.
154///
155/// This sends a forkchoiceUpdated with payload attributes to start building,
156/// then calls getPayload to retrieve the result.
157pub(crate) async fn build_payload(
158    provider: &RootProvider<AnyNetwork>,
159    request: PayloadRequest,
160) -> eyre::Result<(ExecutionPayload, ExecutionPayloadSidecar)> {
161    let fcu_result = call_forkchoice_updated(
162        provider,
163        request.fcu_version,
164        request.forkchoice_state,
165        Some(request.attributes.clone()),
166    )
167    .await?;
168
169    let payload_id =
170        fcu_result.payload_id.ok_or_eyre("Payload builder did not return a payload id")?;
171
172    get_payload_with_sidecar(
173        provider,
174        request.get_payload_version,
175        payload_id,
176        request.attributes.parent_beacon_block_root,
177    )
178    .await
179}
180
181/// Convert an RPC block to a consensus header and block hash.
182pub(crate) fn rpc_block_to_header(block: alloy_provider::network::AnyRpcBlock) -> (Header, B256) {
183    let block_hash = block.header.hash;
184    let header = block.header.inner.clone().into_header_with_defaults();
185    (header, block_hash)
186}
187
188/// Compute versioned hashes from KZG commitments.
189fn versioned_hashes_from_commitments(
190    commitments: &[alloy_primitives::FixedBytes<48>],
191) -> Vec<B256> {
192    commitments.iter().map(|c| kzg_to_versioned_hash(c.as_ref())).collect()
193}
194
195/// Fetch an execution payload using the appropriate engine API version.
196pub(crate) async fn get_payload_with_sidecar(
197    provider: &RootProvider<AnyNetwork>,
198    version: u8,
199    payload_id: PayloadId,
200    parent_beacon_block_root: Option<B256>,
201) -> eyre::Result<(ExecutionPayload, ExecutionPayloadSidecar)> {
202    debug!(target: "reth-bench", get_payload_version = ?version, ?payload_id, "Sending getPayload");
203
204    match version {
205        1 => {
206            let payload = provider.get_payload_v1(payload_id).await?;
207            Ok((ExecutionPayload::V1(payload), ExecutionPayloadSidecar::none()))
208        }
209        2 => {
210            let envelope = provider.get_payload_v2(payload_id).await?;
211            let payload = match envelope.execution_payload {
212                alloy_rpc_types_engine::ExecutionPayloadFieldV2::V1(p) => ExecutionPayload::V1(p),
213                alloy_rpc_types_engine::ExecutionPayloadFieldV2::V2(p) => ExecutionPayload::V2(p),
214            };
215            Ok((payload, ExecutionPayloadSidecar::none()))
216        }
217        3 => {
218            let envelope = provider.get_payload_v3(payload_id).await?;
219            let versioned_hashes =
220                versioned_hashes_from_commitments(&envelope.blobs_bundle.commitments);
221            let cancun_fields = CancunPayloadFields {
222                parent_beacon_block_root: parent_beacon_block_root
223                    .ok_or_eyre("parent_beacon_block_root required for V3")?,
224                versioned_hashes,
225            };
226            Ok((
227                ExecutionPayload::V3(envelope.execution_payload),
228                ExecutionPayloadSidecar::v3(cancun_fields),
229            ))
230        }
231        4 => {
232            let envelope = provider.get_payload_v4(payload_id).await?;
233            Ok(envelope.into_payload_and_sidecar(
234                parent_beacon_block_root.ok_or_eyre("parent_beacon_block_root required for V4")?,
235            ))
236        }
237        5 => {
238            let envelope = provider.get_payload_v5(payload_id).await?;
239            Ok(envelope.into_payload_and_sidecar(
240                parent_beacon_block_root.ok_or_eyre("parent_beacon_block_root required for V5")?,
241            ))
242        }
243        _ => panic!("This tool does not support getPayload versions past v5"),
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_parse_gas_limit_plain_number() {
253        assert_eq!(parse_gas_limit("30000000").unwrap(), 30_000_000);
254        assert_eq!(parse_gas_limit("1").unwrap(), 1);
255        assert_eq!(parse_gas_limit("0").unwrap(), 0);
256    }
257
258    #[test]
259    fn test_parse_gas_limit_k_suffix() {
260        assert_eq!(parse_gas_limit("1K").unwrap(), 1_000);
261        assert_eq!(parse_gas_limit("30k").unwrap(), 30_000);
262        assert_eq!(parse_gas_limit("100K").unwrap(), 100_000);
263    }
264
265    #[test]
266    fn test_parse_gas_limit_m_suffix() {
267        assert_eq!(parse_gas_limit("1M").unwrap(), 1_000_000);
268        assert_eq!(parse_gas_limit("30m").unwrap(), 30_000_000);
269        assert_eq!(parse_gas_limit("100M").unwrap(), 100_000_000);
270    }
271
272    #[test]
273    fn test_parse_gas_limit_g_suffix() {
274        assert_eq!(parse_gas_limit("1G").unwrap(), 1_000_000_000);
275        assert_eq!(parse_gas_limit("2g").unwrap(), 2_000_000_000);
276        assert_eq!(parse_gas_limit("10G").unwrap(), 10_000_000_000);
277    }
278
279    #[test]
280    fn test_parse_gas_limit_with_whitespace() {
281        assert_eq!(parse_gas_limit(" 1G ").unwrap(), 1_000_000_000);
282        assert_eq!(parse_gas_limit("2 M").unwrap(), 2_000_000);
283    }
284
285    #[test]
286    fn test_parse_gas_limit_errors() {
287        assert!(parse_gas_limit("").is_err());
288        assert!(parse_gas_limit("abc").is_err());
289        assert!(parse_gas_limit("G").is_err());
290        assert!(parse_gas_limit("-1G").is_err());
291    }
292
293    #[test]
294    fn test_parse_duration_with_unit() {
295        assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
296        assert_eq!(parse_duration("2s").unwrap(), Duration::from_secs(2));
297        assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
298    }
299
300    #[test]
301    fn test_parse_duration_bare_millis() {
302        assert_eq!(parse_duration("400").unwrap(), Duration::from_millis(400));
303        assert_eq!(parse_duration("0").unwrap(), Duration::from_millis(0));
304        assert_eq!(parse_duration("1000").unwrap(), Duration::from_millis(1000));
305    }
306
307    #[test]
308    fn test_parse_duration_errors() {
309        assert!(parse_duration("abc").is_err());
310        assert!(parse_duration("").is_err());
311    }
312}