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, encode_jovian_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 pub min_base_fee: Option<u64>,
49}
50
51impl<T> Default for OpPayloadBuilderAttributes<T> {
52 fn default() -> Self {
53 Self {
54 payload_attributes: Default::default(),
55 no_tx_pool: Default::default(),
56 gas_limit: Default::default(),
57 eip_1559_params: Default::default(),
58 transactions: Default::default(),
59 min_base_fee: Default::default(),
60 }
61 }
62}
63
64impl<T> OpPayloadBuilderAttributes<T> {
65 pub fn get_holocene_extra_data(
68 &self,
69 default_base_fee_params: BaseFeeParams,
70 ) -> Result<Bytes, EIP1559ParamError> {
71 self.eip_1559_params
72 .map(|params| encode_holocene_extra_data(params, default_base_fee_params))
73 .ok_or(EIP1559ParamError::NoEIP1559Params)?
74 }
75
76 pub fn get_jovian_extra_data(
79 &self,
80 default_base_fee_params: BaseFeeParams,
81 ) -> Result<Bytes, EIP1559ParamError> {
82 let min_base_fee = self.min_base_fee.ok_or(EIP1559ParamError::MinBaseFeeNotSet)?;
83 self.eip_1559_params
84 .map(|params| encode_jovian_extra_data(params, default_base_fee_params, min_base_fee))
85 .ok_or(EIP1559ParamError::NoEIP1559Params)?
86 }
87}
88
89impl<T: Decodable2718 + Send + Sync + Debug + Unpin + 'static> PayloadBuilderAttributes
90 for OpPayloadBuilderAttributes<T>
91{
92 type RpcPayloadAttributes = OpPayloadAttributes;
93 type Error = alloy_rlp::Error;
94
95 fn try_new(
99 parent: B256,
100 attributes: OpPayloadAttributes,
101 version: u8,
102 ) -> Result<Self, Self::Error> {
103 let id = payload_id_optimism(&parent, &attributes, version);
104
105 let transactions = attributes
106 .transactions
107 .unwrap_or_default()
108 .into_iter()
109 .map(|data| {
110 Decodable2718::decode_2718_exact(data.as_ref()).map(|tx| WithEncoded::new(data, tx))
111 })
112 .collect::<Result<_, _>>()?;
113
114 let payload_attributes = EthPayloadBuilderAttributes {
115 id,
116 parent,
117 timestamp: attributes.payload_attributes.timestamp,
118 suggested_fee_recipient: attributes.payload_attributes.suggested_fee_recipient,
119 prev_randao: attributes.payload_attributes.prev_randao,
120 withdrawals: attributes.payload_attributes.withdrawals.unwrap_or_default().into(),
121 parent_beacon_block_root: attributes.payload_attributes.parent_beacon_block_root,
122 };
123
124 Ok(Self {
125 payload_attributes,
126 no_tx_pool: attributes.no_tx_pool.unwrap_or_default(),
127 transactions,
128 gas_limit: attributes.gas_limit,
129 eip_1559_params: attributes.eip_1559_params,
130 min_base_fee: attributes.min_base_fee,
131 })
132 }
133
134 fn payload_id(&self) -> PayloadId {
135 self.payload_attributes.id
136 }
137
138 fn parent(&self) -> B256 {
139 self.payload_attributes.parent
140 }
141
142 fn timestamp(&self) -> u64 {
143 self.payload_attributes.timestamp
144 }
145
146 fn parent_beacon_block_root(&self) -> Option<B256> {
147 self.payload_attributes.parent_beacon_block_root
148 }
149
150 fn suggested_fee_recipient(&self) -> Address {
151 self.payload_attributes.suggested_fee_recipient
152 }
153
154 fn prev_randao(&self) -> B256 {
155 self.payload_attributes.prev_randao
156 }
157
158 fn withdrawals(&self) -> &Withdrawals {
159 &self.payload_attributes.withdrawals
160 }
161}
162
163impl<OpTransactionSigned> From<EthPayloadBuilderAttributes>
164 for OpPayloadBuilderAttributes<OpTransactionSigned>
165{
166 fn from(value: EthPayloadBuilderAttributes) -> Self {
167 Self { payload_attributes: value, ..Default::default() }
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct OpBuiltPayload<N: NodePrimitives = OpPrimitives> {
174 pub(crate) id: PayloadId,
176 pub(crate) block: Arc<SealedBlock<N::Block>>,
178 pub(crate) executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
180 pub(crate) fees: U256,
182}
183
184impl<N: NodePrimitives> OpBuiltPayload<N> {
187 pub const fn new(
189 id: PayloadId,
190 block: Arc<SealedBlock<N::Block>>,
191 fees: U256,
192 executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
193 ) -> Self {
194 Self { id, block, fees, executed_block }
195 }
196
197 pub const fn id(&self) -> PayloadId {
199 self.id
200 }
201
202 pub fn block(&self) -> &SealedBlock<N::Block> {
204 &self.block
205 }
206
207 pub const fn fees(&self) -> U256 {
209 self.fees
210 }
211
212 pub fn into_sealed_block(self) -> SealedBlock<N::Block> {
214 Arc::unwrap_or_clone(self.block)
215 }
216}
217
218impl<N: NodePrimitives> BuiltPayload for OpBuiltPayload<N> {
219 type Primitives = N;
220
221 fn block(&self) -> &SealedBlock<N::Block> {
222 self.block()
223 }
224
225 fn fees(&self) -> U256 {
226 self.fees
227 }
228
229 fn executed_block(&self) -> Option<ExecutedBlockWithTrieUpdates<N>> {
230 self.executed_block.clone()
231 }
232
233 fn requests(&self) -> Option<Requests> {
234 None
235 }
236}
237
238impl<T, N> From<OpBuiltPayload<N>> for ExecutionPayloadV1
240where
241 T: SignedTransaction,
242 N: NodePrimitives<Block = Block<T>>,
243{
244 fn from(value: OpBuiltPayload<N>) -> Self {
245 Self::from_block_unchecked(
246 value.block().hash(),
247 &Arc::unwrap_or_clone(value.block).into_block(),
248 )
249 }
250}
251
252impl<T, N> From<OpBuiltPayload<N>> for ExecutionPayloadEnvelopeV2
254where
255 T: SignedTransaction,
256 N: NodePrimitives<Block = Block<T>>,
257{
258 fn from(value: OpBuiltPayload<N>) -> Self {
259 let OpBuiltPayload { block, fees, .. } = value;
260
261 Self {
262 block_value: fees,
263 execution_payload: ExecutionPayloadFieldV2::from_block_unchecked(
264 block.hash(),
265 &Arc::unwrap_or_clone(block).into_block(),
266 ),
267 }
268 }
269}
270
271impl<T, N> From<OpBuiltPayload<N>> for OpExecutionPayloadEnvelopeV3
272where
273 T: SignedTransaction,
274 N: NodePrimitives<Block = Block<T>>,
275{
276 fn from(value: OpBuiltPayload<N>) -> Self {
277 let OpBuiltPayload { block, fees, .. } = value;
278
279 let parent_beacon_block_root = block.parent_beacon_block_root.unwrap_or_default();
280
281 Self {
282 execution_payload: ExecutionPayloadV3::from_block_unchecked(
283 block.hash(),
284 &Arc::unwrap_or_clone(block).into_block(),
285 ),
286 block_value: fees,
287 should_override_builder: false,
296 blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
298 parent_beacon_block_root,
299 }
300 }
301}
302
303impl<T, N> From<OpBuiltPayload<N>> for OpExecutionPayloadEnvelopeV4
304where
305 T: SignedTransaction,
306 N: NodePrimitives<Block = Block<T>>,
307{
308 fn from(value: OpBuiltPayload<N>) -> Self {
309 let OpBuiltPayload { block, fees, .. } = value;
310
311 let parent_beacon_block_root = block.parent_beacon_block_root.unwrap_or_default();
312
313 let l2_withdrawals_root = block.withdrawals_root.unwrap_or_default();
314 let payload_v3 = ExecutionPayloadV3::from_block_unchecked(
315 block.hash(),
316 &Arc::unwrap_or_clone(block).into_block(),
317 );
318
319 Self {
320 execution_payload: OpExecutionPayloadV4::from_v3_with_withdrawals_root(
321 payload_v3,
322 l2_withdrawals_root,
323 ),
324 block_value: fees,
325 should_override_builder: false,
334 blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
336 parent_beacon_block_root,
337 execution_requests: vec![],
338 }
339 }
340}
341
342pub fn payload_id_optimism(
346 parent: &B256,
347 attributes: &OpPayloadAttributes,
348 payload_version: u8,
349) -> PayloadId {
350 use sha2::Digest;
351 let mut hasher = sha2::Sha256::new();
352 hasher.update(parent.as_slice());
353 hasher.update(&attributes.payload_attributes.timestamp.to_be_bytes()[..]);
354 hasher.update(attributes.payload_attributes.prev_randao.as_slice());
355 hasher.update(attributes.payload_attributes.suggested_fee_recipient.as_slice());
356 if let Some(withdrawals) = &attributes.payload_attributes.withdrawals {
357 let mut buf = Vec::new();
358 withdrawals.encode(&mut buf);
359 hasher.update(buf);
360 }
361
362 if let Some(parent_beacon_block) = attributes.payload_attributes.parent_beacon_block_root {
363 hasher.update(parent_beacon_block);
364 }
365
366 let no_tx_pool = attributes.no_tx_pool.unwrap_or_default();
367 if no_tx_pool || attributes.transactions.as_ref().is_some_and(|txs| !txs.is_empty()) {
368 hasher.update([no_tx_pool as u8]);
369 let txs_len = attributes.transactions.as_ref().map(|txs| txs.len()).unwrap_or_default();
370 hasher.update(&txs_len.to_be_bytes()[..]);
371 if let Some(txs) = &attributes.transactions {
372 for tx in txs {
373 let tx_hash = keccak256(tx);
376 hasher.update(tx_hash)
378 }
379 }
380 }
381
382 if let Some(gas_limit) = attributes.gas_limit {
383 hasher.update(gas_limit.to_be_bytes());
384 }
385
386 if let Some(eip_1559_params) = attributes.eip_1559_params {
387 hasher.update(eip_1559_params.as_slice());
388 }
389
390 let mut out = hasher.finalize();
391 out[0] = payload_version;
392 PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length"))
393}
394
395impl<H, T, ChainSpec> BuildNextEnv<OpPayloadBuilderAttributes<T>, H, ChainSpec>
396 for OpNextBlockEnvAttributes
397where
398 H: BlockHeader,
399 T: SignedTransaction,
400 ChainSpec: EthChainSpec + OpHardforks,
401{
402 fn build_next_env(
403 attributes: &OpPayloadBuilderAttributes<T>,
404 parent: &SealedHeader<H>,
405 chain_spec: &ChainSpec,
406 ) -> Result<Self, PayloadBuilderError> {
407 let extra_data = if chain_spec.is_jovian_active_at_timestamp(attributes.timestamp()) {
408 attributes
409 .get_jovian_extra_data(
410 chain_spec.base_fee_params_at_timestamp(attributes.timestamp()),
411 )
412 .map_err(PayloadBuilderError::other)?
413 } else if chain_spec.is_holocene_active_at_timestamp(attributes.timestamp()) {
414 attributes
415 .get_holocene_extra_data(
416 chain_spec.base_fee_params_at_timestamp(attributes.timestamp()),
417 )
418 .map_err(PayloadBuilderError::other)?
419 } else {
420 Default::default()
421 };
422
423 Ok(Self {
424 timestamp: attributes.timestamp(),
425 suggested_fee_recipient: attributes.suggested_fee_recipient(),
426 prev_randao: attributes.prev_randao(),
427 gas_limit: attributes.gas_limit.unwrap_or_else(|| parent.gas_limit()),
428 parent_beacon_block_root: attributes.parent_beacon_block_root(),
429 extra_data,
430 })
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::OpPayloadAttributes;
438 use alloy_primitives::{address, b256, bytes, FixedBytes};
439 use alloy_rpc_types_engine::PayloadAttributes;
440 use reth_optimism_primitives::OpTransactionSigned;
441 use reth_payload_primitives::EngineApiMessageVersion;
442 use std::str::FromStr;
443
444 #[test]
445 fn test_payload_id_parity_op_geth() {
446 let expected =
449 PayloadId::new(FixedBytes::<8>::from_str("0x03d2dae446d2a86a").unwrap().into());
450 let attrs = OpPayloadAttributes {
451 payload_attributes: PayloadAttributes {
452 timestamp: 1728933301,
453 prev_randao: b256!("0x9158595abbdab2c90635087619aa7042bbebe47642dfab3c9bfb934f6b082765"),
454 suggested_fee_recipient: address!("0x4200000000000000000000000000000000000011"),
455 withdrawals: Some([].into()),
456 parent_beacon_block_root: b256!("0x8fe0193b9bf83cb7e5a08538e494fecc23046aab9a497af3704f4afdae3250ff").into(),
457 },
458 transactions: Some([bytes!("7ef8f8a0dc19cfa777d90980e4875d0a548a881baaa3f83f14d1bc0d3038bc329350e54194deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000f424000000000000000000000000300000000670d6d890000000000000125000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000014bf9181db6e381d4384bbf69c48b0ee0eed23c6ca26143c6d2544f9d39997a590000000000000000000000007f83d659683caf2767fd3c720981d51f5bc365bc")].into()),
459 no_tx_pool: None,
460 gas_limit: Some(30000000),
461 eip_1559_params: None,
462 min_base_fee: None,
463 };
464
465 assert_eq!(
467 expected,
468 payload_id_optimism(
469 &b256!("0x3533bf30edaf9505d0810bf475cbe4e5f4b9889904b9845e83efdeab4e92eb1e"),
470 &attrs,
471 EngineApiMessageVersion::V3 as u8
472 )
473 );
474 }
475
476 #[test]
477 fn test_get_extra_data_post_holocene() {
478 let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
479 OpPayloadBuilderAttributes {
480 eip_1559_params: Some(B64::from_str("0x0000000800000008").unwrap()),
481 ..Default::default()
482 };
483 let extra_data = attributes.get_holocene_extra_data(BaseFeeParams::new(80, 60));
484 assert_eq!(extra_data.unwrap(), Bytes::copy_from_slice(&[0, 0, 0, 0, 8, 0, 0, 0, 8]));
485 }
486
487 #[test]
488 fn test_get_extra_data_post_holocene_default() {
489 let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
490 OpPayloadBuilderAttributes { eip_1559_params: Some(B64::ZERO), ..Default::default() };
491 let extra_data = attributes.get_holocene_extra_data(BaseFeeParams::new(80, 60));
492 assert_eq!(extra_data.unwrap(), Bytes::copy_from_slice(&[0, 0, 0, 0, 80, 0, 0, 0, 60]));
493 }
494
495 #[test]
496 fn test_get_extra_data_post_jovian() {
497 let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
498 OpPayloadBuilderAttributes {
499 eip_1559_params: Some(B64::from_str("0x0000000800000008").unwrap()),
500 min_base_fee: Some(10),
501 ..Default::default()
502 };
503 let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60));
504 assert_eq!(
505 extra_data.unwrap(),
506 Bytes::copy_from_slice(&[1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 10])
509 );
510 }
511
512 #[test]
513 fn test_get_extra_data_post_jovian_default() {
514 let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
515 OpPayloadBuilderAttributes {
516 eip_1559_params: Some(B64::ZERO),
517 min_base_fee: Some(10),
518 ..Default::default()
519 };
520 let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60));
521 assert_eq!(
522 extra_data.unwrap(),
523 Bytes::copy_from_slice(&[1, 0, 0, 0, 80, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 10])
526 );
527 }
528
529 #[test]
530 fn test_get_extra_data_post_jovian_no_base_fee() {
531 let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
532 OpPayloadBuilderAttributes {
533 eip_1559_params: Some(B64::ZERO),
534 min_base_fee: None,
535 ..Default::default()
536 };
537 let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60));
538 assert_eq!(extra_data.unwrap_err(), EIP1559ParamError::MinBaseFeeNotSet);
539 }
540}