op_alloy_rpc_types_engine/payload/
v4.rs

1//! Optimism execution payload envelope V3.
2
3use alloc::vec::Vec;
4use alloy_consensus::Block;
5use alloy_eips::{Decodable2718, eip4895::Withdrawal};
6use alloy_primitives::{Address, B256, Bloom, Bytes, U256};
7use alloy_rpc_types_engine::{
8    BlobsBundleV1, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, PayloadError,
9};
10
11/// The Opstack execution payload for `newPayloadV4` of the engine API introduced with isthmus.
12/// See also <https://specs.optimism.io/protocol/isthmus/exec-engine.html#engine_newpayloadv4-api>
13#[derive(Clone, Debug, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
16pub struct OpExecutionPayloadV4 {
17    /// L1 execution payload
18    #[cfg_attr(feature = "serde", serde(flatten))]
19    pub payload_inner: ExecutionPayloadV3,
20    /// OP-Stack Isthmus specific field:
21    /// instead of computing the root from a withdrawals list, set it directly.
22    /// The "withdrawals" list attribute must be non-nil but empty.
23    pub withdrawals_root: B256,
24}
25
26impl OpExecutionPayloadV4 {
27    /// Converts [`ExecutionPayloadV3`] to [`OpExecutionPayloadV4`] using the given L2 withdrawals
28    /// root.
29    ///
30    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
31    pub const fn from_v3_with_withdrawals_root(
32        payload: ExecutionPayloadV3,
33        withdrawals_root: B256,
34    ) -> Self {
35        Self { withdrawals_root, payload_inner: payload }
36    }
37
38    /// Converts [`OpExecutionPayloadV4`] to [`Block`].
39    ///
40    /// This performs the same conversion as the underlying V3 payload, but inserts the L2
41    /// withdrawals root.
42    ///
43    /// See also [`ExecutionPayloadV3::try_into_block`].
44    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
45        let mut base_block = self.payload_inner.try_into_block()?;
46
47        // overwrite l1 withdrawals root with l2 withdrawals root
48        base_block.header.withdrawals_root = Some(self.withdrawals_root);
49
50        Ok(base_block)
51    }
52}
53
54#[cfg(feature = "std")]
55impl ssz::Decode for OpExecutionPayloadV4 {
56    fn is_ssz_fixed_len() -> bool {
57        false
58    }
59
60    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
61        let mut builder = ssz::SszDecoderBuilder::new(bytes);
62
63        builder.register_type::<B256>()?;
64        builder.register_type::<Address>()?;
65        builder.register_type::<B256>()?;
66        builder.register_type::<B256>()?;
67        builder.register_type::<Bloom>()?;
68        builder.register_type::<B256>()?;
69        builder.register_type::<u64>()?;
70        builder.register_type::<u64>()?;
71        builder.register_type::<u64>()?;
72        builder.register_type::<u64>()?;
73        builder.register_type::<Bytes>()?;
74        builder.register_type::<U256>()?;
75        builder.register_type::<B256>()?;
76        builder.register_type::<Vec<Bytes>>()?;
77        builder.register_type::<Vec<Withdrawal>>()?;
78        builder.register_type::<u64>()?;
79        builder.register_type::<u64>()?;
80        builder.register_type::<B256>()?;
81
82        let mut decoder = builder.build()?;
83
84        Ok(Self {
85            payload_inner: ExecutionPayloadV3 {
86                payload_inner: ExecutionPayloadV2 {
87                    payload_inner: ExecutionPayloadV1 {
88                        parent_hash: decoder.decode_next()?,
89                        fee_recipient: decoder.decode_next()?,
90                        state_root: decoder.decode_next()?,
91                        receipts_root: decoder.decode_next()?,
92                        logs_bloom: decoder.decode_next()?,
93                        prev_randao: decoder.decode_next()?,
94                        block_number: decoder.decode_next()?,
95                        gas_limit: decoder.decode_next()?,
96                        gas_used: decoder.decode_next()?,
97                        timestamp: decoder.decode_next()?,
98                        extra_data: decoder.decode_next()?,
99                        base_fee_per_gas: decoder.decode_next()?,
100                        block_hash: decoder.decode_next()?,
101                        transactions: decoder.decode_next()?,
102                    },
103                    withdrawals: decoder.decode_next()?,
104                },
105                blob_gas_used: decoder.decode_next()?,
106                excess_blob_gas: decoder.decode_next()?,
107            },
108            withdrawals_root: decoder.decode_next()?,
109        })
110    }
111}
112
113#[cfg(feature = "std")]
114impl ssz::Encode for OpExecutionPayloadV4 {
115    fn is_ssz_fixed_len() -> bool {
116        false
117    }
118
119    fn ssz_append(&self, buf: &mut Vec<u8>) {
120        let offset = <B256 as ssz::Encode>::ssz_fixed_len() * 6
121            + <Address as ssz::Encode>::ssz_fixed_len()
122            + <Bloom as ssz::Encode>::ssz_fixed_len()
123            + <u64 as ssz::Encode>::ssz_fixed_len() * 6
124            + <U256 as ssz::Encode>::ssz_fixed_len()
125            + ssz::BYTES_PER_LENGTH_OFFSET * 3;
126
127        let mut encoder = ssz::SszEncoder::container(buf, offset);
128
129        encoder.append(&self.payload_inner.payload_inner.payload_inner.parent_hash);
130        encoder.append(&self.payload_inner.payload_inner.payload_inner.fee_recipient);
131        encoder.append(&self.payload_inner.payload_inner.payload_inner.state_root);
132        encoder.append(&self.payload_inner.payload_inner.payload_inner.receipts_root);
133        encoder.append(&self.payload_inner.payload_inner.payload_inner.logs_bloom);
134        encoder.append(&self.payload_inner.payload_inner.payload_inner.prev_randao);
135        encoder.append(&self.payload_inner.payload_inner.payload_inner.block_number);
136        encoder.append(&self.payload_inner.payload_inner.payload_inner.gas_limit);
137        encoder.append(&self.payload_inner.payload_inner.payload_inner.gas_used);
138        encoder.append(&self.payload_inner.payload_inner.payload_inner.timestamp);
139        encoder.append(&self.payload_inner.payload_inner.payload_inner.extra_data);
140        encoder.append(&self.payload_inner.payload_inner.payload_inner.base_fee_per_gas);
141        encoder.append(&self.payload_inner.payload_inner.payload_inner.block_hash);
142        encoder.append(&self.payload_inner.payload_inner.payload_inner.transactions);
143        encoder.append(&self.payload_inner.payload_inner.withdrawals);
144        encoder.append(&self.payload_inner.blob_gas_used);
145        encoder.append(&self.payload_inner.excess_blob_gas);
146        encoder.append(&self.withdrawals_root);
147
148        encoder.finalize();
149    }
150
151    fn ssz_bytes_len(&self) -> usize {
152        <ExecutionPayloadV3 as ssz::Encode>::ssz_bytes_len(&self.payload_inner)
153            + <B256 as ssz::Encode>::ssz_fixed_len()
154    }
155}
156
157/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
158/// V4.
159///
160/// See also:
161/// [Optimism execution payload envelope v4] <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/exec-engine.md#engine_getpayloadv4>
162#[derive(Clone, Debug, PartialEq, Eq)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
165pub struct OpExecutionPayloadEnvelopeV4 {
166    /// Execution payload V4
167    pub execution_payload: OpExecutionPayloadV4,
168    /// The expected value to be received by the feeRecipient in wei
169    pub block_value: U256,
170    /// The blobs, commitments, and proofs associated with the executed payload.
171    pub blobs_bundle: BlobsBundleV1,
172    /// Introduced in V3, this represents a suggestion from the execution layer if the payload
173    /// should be used instead of an externally provided one.
174    pub should_override_builder: bool,
175    /// Ecotone parent beacon block root
176    pub parent_beacon_block_root: B256,
177    /// A list of opaque [EIP-7685][eip7685] requests.
178    ///
179    /// [eip7685]: https://eips.ethereum.org/EIPS/eip-7685
180    pub execution_requests: Vec<Bytes>,
181}
182
183#[cfg(test)]
184#[cfg(feature = "serde")]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn serde_roundtrip_execution_payload_envelope_v4() {
190        // modified execution payload envelope v3 with empty deposit, withdrawal, and consolidation
191        // requests.
192        let response = r#"{"executionPayload":{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[],"blobGasUsed":"0x0","excessBlobGas":"0x0","withdrawalsRoot":"0x123400000000000000000000000000000000000000000000000000000000babe"},"blockValue":"0x0","blobsBundle":{"commitments":[],"proofs":[],"blobs":[]},"shouldOverrideBuilder":false,"parentBeaconBlockRoot":"0xdead00000000000000000000000000000000000000000000000000000000beef","executionRequests":["0xdeadbeef"]}"#;
193        let envelope: OpExecutionPayloadEnvelopeV4 = serde_json::from_str(response).unwrap();
194        assert_eq!(serde_json::to_string(&envelope).unwrap(), response);
195    }
196}