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