reth_bench/bench/
helpers.rs

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