arb_payload/
lib.rs

1//! Arbitrum payload and engine types.
2//!
3//! Defines the payload attributes, built payload, payload types, and engine
4//! types used by the engine API and block construction pipeline.
5
6use std::{marker::PhantomData, sync::Arc};
7
8use alloy_eips::{
9    eip4895::{Withdrawal, Withdrawals},
10    eip7685::Requests,
11};
12use alloy_primitives::{Address, B256, Bytes, U256};
13use alloy_rpc_types_engine::{
14    BlobsBundleV1, BlobsBundleV2, ExecutionData, ExecutionPayload as AlloyExecutionPayload,
15    ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadEnvelopeV4,
16    ExecutionPayloadEnvelopeV5, ExecutionPayloadEnvelopeV6, ExecutionPayloadFieldV2,
17    ExecutionPayloadV1, ExecutionPayloadV3, PayloadAttributes as AlloyPayloadAttributes, PayloadId,
18};
19use arb_primitives::ArbPrimitives;
20use reth_engine_primitives::EngineTypes;
21use reth_payload_primitives::{
22    BuiltPayload, PayloadAttributes as PayloadAttributesTrait, PayloadBuilderAttributes,
23    PayloadTypes,
24};
25use reth_primitives_traits::{NodePrimitives, SealedBlock};
26use serde::{Deserialize, Serialize};
27
28/// Type alias for the Arbitrum block type.
29pub type ArbBlock = <ArbPrimitives as NodePrimitives>::Block;
30
31// ── Payload Attributes ────────────────────────────────────────────────────────
32
33/// Arbitrum-specific payload attributes extending the standard engine API.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct ArbPayloadAttributes {
37    /// Standard Ethereum payload attributes.
38    #[serde(flatten)]
39    pub inner: AlloyPayloadAttributes,
40    /// Sequencer transactions for this block.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub transactions: Option<Vec<Bytes>>,
43    /// Whether to exclude the transaction pool.
44    #[serde(default)]
45    pub no_tx_pool: bool,
46}
47
48impl PayloadAttributesTrait for ArbPayloadAttributes {
49    fn timestamp(&self) -> u64 {
50        self.inner.timestamp
51    }
52
53    fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
54        self.inner.withdrawals.as_ref()
55    }
56
57    fn parent_beacon_block_root(&self) -> Option<B256> {
58        self.inner.parent_beacon_block_root
59    }
60}
61
62// ── Payload Builder Attributes ────────────────────────────────────────────────
63
64/// Builder attributes for constructing Arbitrum payloads.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct ArbPayloadBuilderAttributes {
67    /// Payload identifier.
68    pub id: PayloadId,
69    /// Parent block hash.
70    pub parent: B256,
71    /// Target timestamp.
72    pub timestamp: u64,
73    /// Fee recipient address.
74    pub suggested_fee_recipient: Address,
75    /// Randomness value.
76    pub prev_randao: B256,
77    /// Withdrawals to include.
78    pub withdrawals: Withdrawals,
79    /// Parent beacon block root.
80    pub parent_beacon_block_root: Option<B256>,
81    /// Whether to exclude the transaction pool.
82    pub no_tx_pool: bool,
83    /// Forced transactions from the sequencer.
84    pub transactions: Vec<Bytes>,
85}
86
87impl PayloadBuilderAttributes for ArbPayloadBuilderAttributes {
88    type RpcPayloadAttributes = ArbPayloadAttributes;
89    type Error = PayloadIdComputeError;
90
91    fn try_new(
92        parent: B256,
93        attributes: ArbPayloadAttributes,
94        _version: u8,
95    ) -> Result<Self, Self::Error> {
96        let id = arb_payload_id(&parent, &attributes);
97        Ok(Self {
98            id,
99            parent,
100            timestamp: attributes.inner.timestamp,
101            suggested_fee_recipient: attributes.inner.suggested_fee_recipient,
102            prev_randao: attributes.inner.prev_randao,
103            withdrawals: attributes.inner.withdrawals.unwrap_or_default().into(),
104            parent_beacon_block_root: attributes.inner.parent_beacon_block_root,
105            no_tx_pool: attributes.no_tx_pool,
106            transactions: attributes.transactions.unwrap_or_default(),
107        })
108    }
109
110    fn payload_id(&self) -> PayloadId {
111        self.id
112    }
113
114    fn parent(&self) -> B256 {
115        self.parent
116    }
117
118    fn timestamp(&self) -> u64 {
119        self.timestamp
120    }
121
122    fn parent_beacon_block_root(&self) -> Option<B256> {
123        self.parent_beacon_block_root
124    }
125
126    fn suggested_fee_recipient(&self) -> Address {
127        self.suggested_fee_recipient
128    }
129
130    fn prev_randao(&self) -> B256 {
131        self.prev_randao
132    }
133
134    fn withdrawals(&self) -> &Withdrawals {
135        &self.withdrawals
136    }
137}
138
139// ── Payload ID Computation ────────────────────────────────────────────────────
140
141/// Error when computing payload IDs (infallible in practice).
142#[derive(Debug, Clone, Copy, thiserror::Error)]
143#[error("payload id computation failed")]
144pub struct PayloadIdComputeError;
145
146/// Compute a unique payload ID from the parent hash and attributes.
147pub fn arb_payload_id(parent: &B256, attributes: &ArbPayloadAttributes) -> PayloadId {
148    use alloy_rlp::Encodable;
149    use sha2::Digest;
150
151    let mut hasher = sha2::Sha256::new();
152    hasher.update(parent.as_slice());
153    hasher.update(attributes.inner.timestamp.to_be_bytes());
154    hasher.update(attributes.inner.prev_randao.as_slice());
155    hasher.update(attributes.inner.suggested_fee_recipient.as_slice());
156    if let Some(withdrawals) = &attributes.inner.withdrawals {
157        let mut buf = Vec::new();
158        withdrawals.encode(&mut buf);
159        hasher.update(buf);
160    }
161    if let Some(root) = attributes.inner.parent_beacon_block_root {
162        hasher.update(root);
163    }
164    // Include Arbitrum-specific fields in the payload ID.
165    if attributes.no_tx_pool {
166        hasher.update([1u8]);
167    }
168    if let Some(txs) = &attributes.transactions {
169        for tx in txs {
170            hasher.update(tx.as_ref());
171        }
172    }
173
174    let out = hasher.finalize();
175    PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length"))
176}
177
178// ── Built Payload ─────────────────────────────────────────────────────────────
179
180/// A built Arbitrum payload ready to be sealed.
181#[derive(Debug, Clone)]
182pub struct ArbBuiltPayload {
183    /// Payload identifier.
184    pub id: PayloadId,
185    /// The sealed block.
186    pub block: Arc<SealedBlock<ArbBlock>>,
187    /// Total fees collected.
188    pub fees: U256,
189    /// Execution requests, if any.
190    pub requests: Option<Requests>,
191}
192
193impl ArbBuiltPayload {
194    /// Create a new built payload.
195    pub fn new(id: PayloadId, block: Arc<SealedBlock<ArbBlock>>, fees: U256) -> Self {
196        Self {
197            id,
198            block,
199            fees,
200            requests: None,
201        }
202    }
203
204    /// Set execution requests on this payload.
205    pub fn with_requests(mut self, requests: Option<Requests>) -> Self {
206        self.requests = requests;
207        self
208    }
209}
210
211impl BuiltPayload for ArbBuiltPayload {
212    type Primitives = ArbPrimitives;
213
214    fn block(&self) -> &SealedBlock<ArbBlock> {
215        &self.block
216    }
217
218    fn fees(&self) -> U256 {
219        self.fees
220    }
221
222    fn requests(&self) -> Option<Requests> {
223        self.requests.clone()
224    }
225}
226
227// ── Conversion Error ──────────────────────────────────────────────────────────
228
229/// Error when converting built payloads to envelope types.
230#[derive(Debug, Clone, thiserror::Error)]
231#[error("payload conversion failed")]
232pub struct ArbPayloadConversionError;
233
234// ── From/TryFrom for execution payload envelopes ──────────────────────────────
235
236// V1
237impl From<ArbBuiltPayload> for ExecutionPayloadV1 {
238    fn from(value: ArbBuiltPayload) -> Self {
239        Self::from_block_unchecked(
240            value.block.hash(),
241            &Arc::unwrap_or_clone(value.block).into_block(),
242        )
243    }
244}
245
246// V2
247impl From<ArbBuiltPayload> for ExecutionPayloadEnvelopeV2 {
248    fn from(value: ArbBuiltPayload) -> Self {
249        let ArbBuiltPayload { block, fees, .. } = value;
250        Self {
251            block_value: fees,
252            execution_payload: ExecutionPayloadFieldV2::from_block_unchecked(
253                block.hash(),
254                &Arc::unwrap_or_clone(block).into_block(),
255            ),
256        }
257    }
258}
259
260// V3
261impl TryFrom<ArbBuiltPayload> for ExecutionPayloadEnvelopeV3 {
262    type Error = ArbPayloadConversionError;
263
264    fn try_from(value: ArbBuiltPayload) -> Result<Self, Self::Error> {
265        let ArbBuiltPayload { block, fees, .. } = value;
266        Ok(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            should_override_builder: false,
273            blobs_bundle: BlobsBundleV1::empty(),
274        })
275    }
276}
277
278// V4
279impl TryFrom<ArbBuiltPayload> for ExecutionPayloadEnvelopeV4 {
280    type Error = ArbPayloadConversionError;
281
282    fn try_from(value: ArbBuiltPayload) -> Result<Self, Self::Error> {
283        let requests = value.requests.clone().unwrap_or_default();
284        let v3: ExecutionPayloadEnvelopeV3 = value.try_into()?;
285        Ok(Self {
286            execution_requests: requests,
287            envelope_inner: v3,
288        })
289    }
290}
291
292// V5
293impl TryFrom<ArbBuiltPayload> for ExecutionPayloadEnvelopeV5 {
294    type Error = ArbPayloadConversionError;
295
296    fn try_from(value: ArbBuiltPayload) -> Result<Self, Self::Error> {
297        let ArbBuiltPayload {
298            block,
299            fees,
300            requests,
301            ..
302        } = value;
303        Ok(Self {
304            execution_payload: ExecutionPayloadV3::from_block_unchecked(
305                block.hash(),
306                &Arc::unwrap_or_clone(block).into_block(),
307            ),
308            block_value: fees,
309            should_override_builder: false,
310            blobs_bundle: BlobsBundleV2::empty(),
311            execution_requests: requests.unwrap_or_default(),
312        })
313    }
314}
315
316// V6
317impl TryFrom<ArbBuiltPayload> for ExecutionPayloadEnvelopeV6 {
318    type Error = ArbPayloadConversionError;
319
320    fn try_from(_value: ArbBuiltPayload) -> Result<Self, Self::Error> {
321        Err(ArbPayloadConversionError)
322    }
323}
324
325// ── Payload Types ─────────────────────────────────────────────────────────────
326
327/// Payload types for the Arbitrum engine.
328#[derive(Debug, Default, Clone, Serialize, Deserialize)]
329#[non_exhaustive]
330pub struct ArbPayloadTypes;
331
332impl PayloadTypes for ArbPayloadTypes {
333    type ExecutionData = ExecutionData;
334    type BuiltPayload = ArbBuiltPayload;
335    type PayloadAttributes = ArbPayloadAttributes;
336    type PayloadBuilderAttributes = ArbPayloadBuilderAttributes;
337
338    fn block_to_payload(
339        block: SealedBlock<
340            <<Self::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
341        >,
342    ) -> Self::ExecutionData {
343        let (payload, sidecar) =
344            AlloyExecutionPayload::from_block_unchecked(block.hash(), &block.into_block());
345        ExecutionData { payload, sidecar }
346    }
347}
348
349// ── Engine Types ──────────────────────────────────────────────────────────────
350
351/// Engine types for the Arbitrum consensus engine.
352#[derive(Debug, Default, Clone, Serialize, Deserialize)]
353#[non_exhaustive]
354pub struct ArbEngineTypes<T: PayloadTypes = ArbPayloadTypes> {
355    _marker: PhantomData<T>,
356}
357
358impl<T: PayloadTypes<ExecutionData = ExecutionData>> PayloadTypes for ArbEngineTypes<T>
359where
360    T::BuiltPayload: BuiltPayload<Primitives: NodePrimitives<Block = ArbBlock>>,
361{
362    type ExecutionData = T::ExecutionData;
363    type BuiltPayload = T::BuiltPayload;
364    type PayloadAttributes = T::PayloadAttributes;
365    type PayloadBuilderAttributes = T::PayloadBuilderAttributes;
366
367    fn block_to_payload(
368        block: SealedBlock<
369            <<Self::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
370        >,
371    ) -> Self::ExecutionData {
372        T::block_to_payload(block)
373    }
374}
375
376impl<T> EngineTypes for ArbEngineTypes<T>
377where
378    T: PayloadTypes<ExecutionData = ExecutionData>,
379    T::BuiltPayload: BuiltPayload<Primitives: NodePrimitives<Block = ArbBlock>>
380        + TryInto<ExecutionPayloadV1>
381        + TryInto<ExecutionPayloadEnvelopeV2>
382        + TryInto<ExecutionPayloadEnvelopeV3>
383        + TryInto<ExecutionPayloadEnvelopeV4>
384        + TryInto<ExecutionPayloadEnvelopeV5>
385        + TryInto<ExecutionPayloadEnvelopeV6>,
386{
387    type ExecutionPayloadEnvelopeV1 = ExecutionPayloadV1;
388    type ExecutionPayloadEnvelopeV2 = ExecutionPayloadEnvelopeV2;
389    type ExecutionPayloadEnvelopeV3 = ExecutionPayloadEnvelopeV3;
390    type ExecutionPayloadEnvelopeV4 = ExecutionPayloadEnvelopeV4;
391    type ExecutionPayloadEnvelopeV5 = ExecutionPayloadEnvelopeV5;
392    type ExecutionPayloadEnvelopeV6 = ExecutionPayloadEnvelopeV6;
393}