reth_optimism_payload_builder/
payload.rs
1use 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
25pub use op_alloy_rpc_types_engine::OpPayloadAttributes;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct OpPayloadBuilderAttributes<T> {
31 pub payload_attributes: EthPayloadBuilderAttributes,
33 pub no_tx_pool: bool,
35 pub transactions: Vec<WithEncoded<T>>,
38 pub gas_limit: Option<u64>,
40 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 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 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#[derive(Debug, Clone)]
158pub struct OpBuiltPayload<N: NodePrimitives = OpPrimitives> {
159 pub(crate) id: PayloadId,
161 pub(crate) block: Arc<SealedBlock<N::Block>>,
163 pub(crate) executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
165 pub(crate) fees: U256,
167}
168
169impl<N: NodePrimitives> OpBuiltPayload<N> {
172 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 pub const fn id(&self) -> PayloadId {
184 self.id
185 }
186
187 pub fn block(&self) -> &SealedBlock<N::Block> {
189 &self.block
190 }
191
192 pub const fn fees(&self) -> U256 {
194 self.fees
195 }
196
197 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
223impl<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
237impl<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 should_override_builder: false,
281 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 should_override_builder: false,
319 blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
321 parent_beacon_block_root,
322 execution_requests: vec![],
323 }
324 }
325}
326
327pub(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 let tx_hash = keccak256(tx);
361 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 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 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}