reth_optimism_payload_builder/
payload.rs1use 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
29pub use op_alloy_rpc_types_engine::OpPayloadAttributes;
31use reth_optimism_primitives::OpPrimitives;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct OpPayloadBuilderAttributes<T> {
36 pub payload_attributes: EthPayloadBuilderAttributes,
38 pub no_tx_pool: bool,
40 pub transactions: Vec<WithEncoded<T>>,
43 pub gas_limit: Option<u64>,
45 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 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 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#[derive(Debug, Clone)]
163pub struct OpBuiltPayload<N: NodePrimitives = OpPrimitives> {
164 pub(crate) id: PayloadId,
166 pub(crate) block: Arc<SealedBlock<N::Block>>,
168 pub(crate) executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
170 pub(crate) fees: U256,
172}
173
174impl<N: NodePrimitives> OpBuiltPayload<N> {
177 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 pub const fn id(&self) -> PayloadId {
189 self.id
190 }
191
192 pub fn block(&self) -> &SealedBlock<N::Block> {
194 &self.block
195 }
196
197 pub const fn fees(&self) -> U256 {
199 self.fees
200 }
201
202 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
228impl<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
242impl<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 should_override_builder: false,
286 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 should_override_builder: false,
324 blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
326 parent_beacon_block_root,
327 execution_requests: vec![],
328 }
329 }
330}
331
332pub 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 let tx_hash = keccak256(tx);
366 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 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 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}