reth_optimism_payload_builder/
payload.rs

1//! Payload related types
2
3use std::{fmt::Debug, sync::Arc};
4
5use alloy_consensus::Block;
6use alloy_eips::{
7    eip1559::BaseFeeParams, eip2718::Decodable2718, eip4895::Withdrawals, eip7685::Requests,
8};
9use alloy_primitives::{keccak256, Address, Bytes, B256, B64, U256};
10use alloy_rlp::Encodable;
11use alloy_rpc_types_engine::{
12    BlobsBundleV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadFieldV2, ExecutionPayloadV1,
13    ExecutionPayloadV3, PayloadId,
14};
15use op_alloy_consensus::{encode_holocene_extra_data, EIP1559ParamError};
16use op_alloy_rpc_types_engine::{
17    OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4,
18};
19use reth_chain_state::ExecutedBlockWithTrieUpdates;
20use reth_optimism_primitives::OpPrimitives;
21use reth_payload_builder::EthPayloadBuilderAttributes;
22use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes};
23use reth_primitives_traits::{NodePrimitives, SealedBlock, SignedTransaction, WithEncoded};
24
25/// Re-export for use in downstream arguments.
26pub use op_alloy_rpc_types_engine::OpPayloadAttributes;
27
28/// Optimism Payload Builder Attributes
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct OpPayloadBuilderAttributes<T> {
31    /// Inner ethereum payload builder attributes
32    pub payload_attributes: EthPayloadBuilderAttributes,
33    /// `NoTxPool` option for the generated payload
34    pub no_tx_pool: bool,
35    /// Decoded transactions and the original EIP-2718 encoded bytes as received in the payload
36    /// attributes.
37    pub transactions: Vec<WithEncoded<T>>,
38    /// The gas limit for the generated payload
39    pub gas_limit: Option<u64>,
40    /// EIP-1559 parameters for the generated payload
41    pub eip_1559_params: Option<B64>,
42}
43
44impl<T> Default for OpPayloadBuilderAttributes<T> {
45    fn default() -> Self {
46        Self {
47            payload_attributes: Default::default(),
48            no_tx_pool: Default::default(),
49            gas_limit: Default::default(),
50            eip_1559_params: Default::default(),
51            transactions: Default::default(),
52        }
53    }
54}
55
56impl<T> OpPayloadBuilderAttributes<T> {
57    /// Extracts the `eip1559` parameters for the payload.
58    pub fn get_holocene_extra_data(
59        &self,
60        default_base_fee_params: BaseFeeParams,
61    ) -> Result<Bytes, EIP1559ParamError> {
62        self.eip_1559_params
63            .map(|params| encode_holocene_extra_data(params, default_base_fee_params))
64            .ok_or(EIP1559ParamError::NoEIP1559Params)?
65    }
66}
67
68impl<T: Decodable2718 + Send + Sync + Debug> PayloadBuilderAttributes
69    for OpPayloadBuilderAttributes<T>
70{
71    type RpcPayloadAttributes = OpPayloadAttributes;
72    type Error = alloy_rlp::Error;
73
74    /// Creates a new payload builder for the given parent block and the attributes.
75    ///
76    /// Derives the unique [`PayloadId`] for the given parent and attributes
77    fn try_new(
78        parent: B256,
79        attributes: OpPayloadAttributes,
80        version: u8,
81    ) -> Result<Self, Self::Error> {
82        let id = payload_id_optimism(&parent, &attributes, version);
83
84        let transactions = attributes
85            .transactions
86            .unwrap_or_default()
87            .into_iter()
88            .map(|data| {
89                let mut buf = data.as_ref();
90                let tx = Decodable2718::decode_2718(&mut buf).map_err(alloy_rlp::Error::from)?;
91
92                if !buf.is_empty() {
93                    return Err(alloy_rlp::Error::UnexpectedLength);
94                }
95
96                Ok(WithEncoded::new(data, tx))
97            })
98            .collect::<Result<_, _>>()?;
99
100        let payload_attributes = EthPayloadBuilderAttributes {
101            id,
102            parent,
103            timestamp: attributes.payload_attributes.timestamp,
104            suggested_fee_recipient: attributes.payload_attributes.suggested_fee_recipient,
105            prev_randao: attributes.payload_attributes.prev_randao,
106            withdrawals: attributes.payload_attributes.withdrawals.unwrap_or_default().into(),
107            parent_beacon_block_root: attributes.payload_attributes.parent_beacon_block_root,
108        };
109
110        Ok(Self {
111            payload_attributes,
112            no_tx_pool: attributes.no_tx_pool.unwrap_or_default(),
113            transactions,
114            gas_limit: attributes.gas_limit,
115            eip_1559_params: attributes.eip_1559_params,
116        })
117    }
118
119    fn payload_id(&self) -> PayloadId {
120        self.payload_attributes.id
121    }
122
123    fn parent(&self) -> B256 {
124        self.payload_attributes.parent
125    }
126
127    fn timestamp(&self) -> u64 {
128        self.payload_attributes.timestamp
129    }
130
131    fn parent_beacon_block_root(&self) -> Option<B256> {
132        self.payload_attributes.parent_beacon_block_root
133    }
134
135    fn suggested_fee_recipient(&self) -> Address {
136        self.payload_attributes.suggested_fee_recipient
137    }
138
139    fn prev_randao(&self) -> B256 {
140        self.payload_attributes.prev_randao
141    }
142
143    fn withdrawals(&self) -> &Withdrawals {
144        &self.payload_attributes.withdrawals
145    }
146}
147
148impl<OpTransactionSigned> From<EthPayloadBuilderAttributes>
149    for OpPayloadBuilderAttributes<OpTransactionSigned>
150{
151    fn from(value: EthPayloadBuilderAttributes) -> Self {
152        Self { payload_attributes: value, ..Default::default() }
153    }
154}
155
156/// Contains the built payload.
157#[derive(Debug, Clone)]
158pub struct OpBuiltPayload<N: NodePrimitives = OpPrimitives> {
159    /// Identifier of the payload
160    pub(crate) id: PayloadId,
161    /// Sealed block
162    pub(crate) block: Arc<SealedBlock<N::Block>>,
163    /// Block execution data for the payload, if any.
164    pub(crate) executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
165    /// The fees of the block
166    pub(crate) fees: U256,
167}
168
169// === impl BuiltPayload ===
170
171impl<N: NodePrimitives> OpBuiltPayload<N> {
172    /// Initializes the payload with the given initial block.
173    pub const fn new(
174        id: PayloadId,
175        block: Arc<SealedBlock<N::Block>>,
176        fees: U256,
177        executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
178    ) -> Self {
179        Self { id, block, fees, executed_block }
180    }
181
182    /// Returns the identifier of the payload.
183    pub const fn id(&self) -> PayloadId {
184        self.id
185    }
186
187    /// Returns the built block(sealed)
188    pub fn block(&self) -> &SealedBlock<N::Block> {
189        &self.block
190    }
191
192    /// Fees of the block
193    pub const fn fees(&self) -> U256 {
194        self.fees
195    }
196
197    /// Converts the value into [`SealedBlock`].
198    pub fn into_sealed_block(self) -> SealedBlock<N::Block> {
199        Arc::unwrap_or_clone(self.block)
200    }
201}
202
203impl<N: NodePrimitives> BuiltPayload for OpBuiltPayload<N> {
204    type Primitives = N;
205
206    fn block(&self) -> &SealedBlock<N::Block> {
207        self.block()
208    }
209
210    fn fees(&self) -> U256 {
211        self.fees
212    }
213
214    fn executed_block(&self) -> Option<ExecutedBlockWithTrieUpdates<N>> {
215        self.executed_block.clone()
216    }
217
218    fn requests(&self) -> Option<Requests> {
219        None
220    }
221}
222
223// V1 engine_getPayloadV1 response
224impl<T, N> From<OpBuiltPayload<N>> for ExecutionPayloadV1
225where
226    T: SignedTransaction,
227    N: NodePrimitives<Block = Block<T>>,
228{
229    fn from(value: OpBuiltPayload<N>) -> Self {
230        Self::from_block_unchecked(
231            value.block().hash(),
232            &Arc::unwrap_or_clone(value.block).into_block(),
233        )
234    }
235}
236
237// V2 engine_getPayloadV2 response
238impl<T, N> From<OpBuiltPayload<N>> for ExecutionPayloadEnvelopeV2
239where
240    T: SignedTransaction,
241    N: NodePrimitives<Block = Block<T>>,
242{
243    fn from(value: OpBuiltPayload<N>) -> Self {
244        let OpBuiltPayload { block, fees, .. } = value;
245
246        Self {
247            block_value: fees,
248            execution_payload: ExecutionPayloadFieldV2::from_block_unchecked(
249                block.hash(),
250                &Arc::unwrap_or_clone(block).into_block(),
251            ),
252        }
253    }
254}
255
256impl<T, N> From<OpBuiltPayload<N>> for OpExecutionPayloadEnvelopeV3
257where
258    T: SignedTransaction,
259    N: NodePrimitives<Block = Block<T>>,
260{
261    fn from(value: OpBuiltPayload<N>) -> Self {
262        let OpBuiltPayload { block, fees, .. } = value;
263
264        let parent_beacon_block_root = block.parent_beacon_block_root.unwrap_or_default();
265
266        Self {
267            execution_payload: ExecutionPayloadV3::from_block_unchecked(
268                block.hash(),
269                &Arc::unwrap_or_clone(block).into_block(),
270            ),
271            block_value: fees,
272            // From the engine API spec:
273            //
274            // > Client software **MAY** use any heuristics to decide whether to set
275            // `shouldOverrideBuilder` flag or not. If client software does not implement any
276            // heuristic this flag **SHOULD** be set to `false`.
277            //
278            // Spec:
279            // <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
280            should_override_builder: false,
281            // No blobs for OP.
282            blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
283            parent_beacon_block_root,
284        }
285    }
286}
287
288impl<T, N> From<OpBuiltPayload<N>> for OpExecutionPayloadEnvelopeV4
289where
290    T: SignedTransaction,
291    N: NodePrimitives<Block = Block<T>>,
292{
293    fn from(value: OpBuiltPayload<N>) -> Self {
294        let OpBuiltPayload { block, fees, .. } = value;
295
296        let parent_beacon_block_root = block.parent_beacon_block_root.unwrap_or_default();
297
298        let l2_withdrawals_root = block.withdrawals_root.unwrap_or_default();
299        let payload_v3 = ExecutionPayloadV3::from_block_unchecked(
300            block.hash(),
301            &Arc::unwrap_or_clone(block).into_block(),
302        );
303
304        Self {
305            execution_payload: OpExecutionPayloadV4::from_v3_with_withdrawals_root(
306                payload_v3,
307                l2_withdrawals_root,
308            ),
309            block_value: fees,
310            // From the engine API spec:
311            //
312            // > Client software **MAY** use any heuristics to decide whether to set
313            // `shouldOverrideBuilder` flag or not. If client software does not implement any
314            // heuristic this flag **SHOULD** be set to `false`.
315            //
316            // Spec:
317            // <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
318            should_override_builder: false,
319            // No blobs for OP.
320            blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
321            parent_beacon_block_root,
322            execution_requests: vec![],
323        }
324    }
325}
326
327/// Generates the payload id for the configured payload from the [`OpPayloadAttributes`].
328///
329/// Returns an 8-byte identifier by hashing the payload components with sha256 hash.
330pub(crate) fn payload_id_optimism(
331    parent: &B256,
332    attributes: &OpPayloadAttributes,
333    payload_version: u8,
334) -> PayloadId {
335    use sha2::Digest;
336    let mut hasher = sha2::Sha256::new();
337    hasher.update(parent.as_slice());
338    hasher.update(&attributes.payload_attributes.timestamp.to_be_bytes()[..]);
339    hasher.update(attributes.payload_attributes.prev_randao.as_slice());
340    hasher.update(attributes.payload_attributes.suggested_fee_recipient.as_slice());
341    if let Some(withdrawals) = &attributes.payload_attributes.withdrawals {
342        let mut buf = Vec::new();
343        withdrawals.encode(&mut buf);
344        hasher.update(buf);
345    }
346
347    if let Some(parent_beacon_block) = attributes.payload_attributes.parent_beacon_block_root {
348        hasher.update(parent_beacon_block);
349    }
350
351    let no_tx_pool = attributes.no_tx_pool.unwrap_or_default();
352    if no_tx_pool || attributes.transactions.as_ref().is_some_and(|txs| !txs.is_empty()) {
353        hasher.update([no_tx_pool as u8]);
354        let txs_len = attributes.transactions.as_ref().map(|txs| txs.len()).unwrap_or_default();
355        hasher.update(&txs_len.to_be_bytes()[..]);
356        if let Some(txs) = &attributes.transactions {
357            for tx in txs {
358                // we have to just hash the bytes here because otherwise we would need to decode
359                // the transactions here which really isn't ideal
360                let tx_hash = keccak256(tx);
361                // maybe we can try just taking the hash and not decoding
362                hasher.update(tx_hash)
363            }
364        }
365    }
366
367    if let Some(gas_limit) = attributes.gas_limit {
368        hasher.update(gas_limit.to_be_bytes());
369    }
370
371    if let Some(eip_1559_params) = attributes.eip_1559_params {
372        hasher.update(eip_1559_params.as_slice());
373    }
374
375    let mut out = hasher.finalize();
376    out[0] = payload_version;
377    PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length"))
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::OpPayloadAttributes;
384    use alloy_primitives::{address, b256, bytes, FixedBytes};
385    use alloy_rpc_types_engine::PayloadAttributes;
386    use reth_optimism_primitives::OpTransactionSigned;
387    use reth_payload_primitives::EngineApiMessageVersion;
388    use std::str::FromStr;
389
390    #[test]
391    fn test_payload_id_parity_op_geth() {
392        // INFO rollup_boost::server:received fork_choice_updated_v3 from builder and l2_client
393        // payload_id_builder="0x6ef26ca02318dcf9" payload_id_l2="0x03d2dae446d2a86a"
394        let expected =
395            PayloadId::new(FixedBytes::<8>::from_str("0x03d2dae446d2a86a").unwrap().into());
396        let attrs = OpPayloadAttributes {
397            payload_attributes: PayloadAttributes {
398                timestamp: 1728933301,
399                prev_randao: b256!("0x9158595abbdab2c90635087619aa7042bbebe47642dfab3c9bfb934f6b082765"),
400                suggested_fee_recipient: address!("0x4200000000000000000000000000000000000011"),
401                withdrawals: Some([].into()),
402                parent_beacon_block_root: b256!("0x8fe0193b9bf83cb7e5a08538e494fecc23046aab9a497af3704f4afdae3250ff").into(),
403            },
404            transactions: Some([bytes!("7ef8f8a0dc19cfa777d90980e4875d0a548a881baaa3f83f14d1bc0d3038bc329350e54194deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000f424000000000000000000000000300000000670d6d890000000000000125000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000014bf9181db6e381d4384bbf69c48b0ee0eed23c6ca26143c6d2544f9d39997a590000000000000000000000007f83d659683caf2767fd3c720981d51f5bc365bc")].into()),
405            no_tx_pool: None,
406            gas_limit: Some(30000000),
407            eip_1559_params: None,
408        };
409
410        // Reth's `PayloadId` should match op-geth's `PayloadId`. This fails
411        assert_eq!(
412            expected,
413            payload_id_optimism(
414                &b256!("0x3533bf30edaf9505d0810bf475cbe4e5f4b9889904b9845e83efdeab4e92eb1e"),
415                &attrs,
416                EngineApiMessageVersion::V3 as u8
417            )
418        );
419    }
420
421    #[test]
422    fn test_get_extra_data_post_holocene() {
423        let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
424            OpPayloadBuilderAttributes {
425                eip_1559_params: Some(B64::from_str("0x0000000800000008").unwrap()),
426                ..Default::default()
427            };
428        let extra_data = attributes.get_holocene_extra_data(BaseFeeParams::new(80, 60));
429        assert_eq!(extra_data.unwrap(), Bytes::copy_from_slice(&[0, 0, 0, 0, 8, 0, 0, 0, 8]));
430    }
431
432    #[test]
433    fn test_get_extra_data_post_holocene_default() {
434        let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
435            OpPayloadBuilderAttributes { eip_1559_params: Some(B64::ZERO), ..Default::default() };
436        let extra_data = attributes.get_holocene_extra_data(BaseFeeParams::new(80, 60));
437        assert_eq!(extra_data.unwrap(), Bytes::copy_from_slice(&[0, 0, 0, 0, 80, 0, 0, 0, 60]));
438    }
439}