reth_optimism_payload_builder/
payload.rs

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