kona_engine/
attributes.rs

1//! Contains a utility method to check if attributes match a block.
2
3use alloy_eips::{Decodable2718, eip1559::BaseFeeParams};
4use alloy_network::TransactionResponse;
5use alloy_primitives::{Address, B256, Bytes};
6use alloy_rpc_types_eth::{Block, BlockTransactions, Withdrawals};
7use kona_genesis::RollupConfig;
8use kona_protocol::OpAttributesWithParent;
9use op_alloy_consensus::{EIP1559ParamError, OpTxEnvelope, decode_holocene_extra_data};
10use op_alloy_rpc_types::Transaction;
11
12/// Result of validating payload attributes against an execution layer block.
13///
14/// Used to verify that proposed payload attributes match the actual executed block,
15/// ensuring consistency between the rollup derivation process and execution layer.
16/// Validation includes withdrawals, transactions, fees, and other block properties.
17///
18/// # Examples
19///
20/// ```rust,ignore
21/// use kona_engine::AttributesMatch;
22/// use kona_genesis::RollupConfig;
23/// use kona_protocol::OpAttributesWithParent;
24///
25/// let config = RollupConfig::default();
26/// let match_result = AttributesMatch::check_withdrawals(&config, &attributes, &block);
27///
28/// if match_result.is_match() {
29///     println!("Attributes are valid for this block");
30/// }
31/// ```
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AttributesMatch {
34    /// The payload attributes are consistent with the block.
35    Match,
36    /// The attributes do not match the block (contains mismatch details).
37    Mismatch(AttributesMismatch),
38}
39
40impl AttributesMatch {
41    /// Returns true if the attributes match the block.
42    pub const fn is_match(&self) -> bool {
43        matches!(self, Self::Match)
44    }
45
46    /// Returns true if the attributes do not match the block.
47    pub const fn is_mismatch(&self) -> bool {
48        matches!(self, Self::Mismatch(_))
49    }
50
51    /// Checks that withdrawals for a block and attributes match.
52    pub fn check_withdrawals(
53        config: &RollupConfig,
54        attributes: &OpAttributesWithParent,
55        block: &Block<Transaction>,
56    ) -> Self {
57        let attr_withdrawals = attributes.inner().payload_attributes.withdrawals.as_ref();
58        let attr_withdrawals = attr_withdrawals.map(|w| Withdrawals::new(w.to_vec()));
59        let block_withdrawals = block.withdrawals.as_ref();
60
61        if config.is_canyon_active(block.header.timestamp) {
62            // In canyon, the withdrawals list should be some and empty
63            if attr_withdrawals.is_none_or(|w| !w.is_empty()) {
64                return Self::Mismatch(AttributesMismatch::CanyonWithdrawalsNotEmpty);
65            }
66            if block_withdrawals.is_none_or(|w| !w.is_empty()) {
67                return Self::Mismatch(AttributesMismatch::CanyonWithdrawalsNotEmpty);
68            }
69            if !config.is_isthmus_active(block.header.timestamp) {
70                // In canyon, the withdrawals root should be set to the empty value
71                let empty_hash = alloy_consensus::EMPTY_ROOT_HASH;
72                if block.header.inner.withdrawals_root != Some(empty_hash) {
73                    return Self::Mismatch(AttributesMismatch::CanyonNotEmptyHash);
74                }
75            }
76        } else {
77            // In bedrock, the withdrawals list should be None
78            if attr_withdrawals.is_some() {
79                return Self::Mismatch(AttributesMismatch::BedrockWithdrawals);
80            }
81        }
82
83        if config.is_isthmus_active(block.header.timestamp) {
84            // In isthmus, the withdrawals root must be set
85            if block.header.inner.withdrawals_root.is_none() {
86                return Self::Mismatch(AttributesMismatch::IsthmusMissingWithdrawalsRoot);
87            }
88        }
89
90        Self::Match
91    }
92
93    /// Checks the attributes and block transaction list for consolidation.
94    /// We start by checking that there are the same number of transactions in both the attribute
95    /// payload and the block. Then we compare their contents
96    fn check_transactions(attributes_txs: &[Bytes], block: &Block<Transaction>) -> Self {
97        // Before checking the number of transactions, we have to make sure that the block
98        // has the right transactions format. We need to have access to the
99        // full transactions to be able to compare their contents.
100        let block_txs = match block.transactions {
101            BlockTransactions::Hashes(_) | BlockTransactions::Full(_)
102                if attributes_txs.is_empty() && block.transactions.is_empty() =>
103            {
104                // We early return when both attributes and blocks are empty. This is for ergonomics
105                // because the default [`BlockTransactions`] format is
106                // [`BlockTransactions::Hash`], which may cause
107                // the [`BlockTransactions`] format check to fail right below. We may want to be a
108                // bit more flexible and not reject the hash format if both the
109                // attributes and the block are empty.
110                return Self::Match;
111            }
112            BlockTransactions::Uncle => {
113                // This can never be uncle transactions
114                error!(
115                    "Invalid format for the block transactions. The `Uncle` transaction format is not relevant in that context and should not get used here. This is a bug"
116                );
117
118                return AttributesMismatch::MalformedBlockTransactions.into();
119            }
120            BlockTransactions::Hashes(_) => {
121                // We can't have hash transactions with non empty blocks
122                error!(
123                    "Invalid format for the block transactions. The `Hash` transaction format is not relevant in that context and should not get used here. This is a bug."
124                );
125
126                return AttributesMismatch::MalformedBlockTransactions.into();
127            }
128            BlockTransactions::Full(ref block_txs) => block_txs,
129        };
130
131        let attributes_txs_len = attributes_txs.len();
132        let block_txs_len = block_txs.len();
133
134        if attributes_txs_len != block_txs_len {
135            return AttributesMismatch::TransactionLen(attributes_txs_len, block_txs_len).into();
136        }
137
138        // Then we need to check that the content of the encoded transactions match
139        // Note that it is safe to zip both iterators because we checked their length
140        // beforehand.
141        for (attr_tx_bytes, block_tx) in attributes_txs.iter().zip(block_txs) {
142            trace!(
143                target: "engine",
144                ?attr_tx_bytes,
145                block_tx_hash = %block_tx.tx_hash(),
146                "Checking attributes transaction against block transaction",
147            );
148            // Let's try to deserialize the attributes transaction
149            let Ok(attr_tx) = OpTxEnvelope::decode_2718(&mut &attr_tx_bytes[..]) else {
150                error!(
151                    "Impossible to deserialize transaction from attributes. If we have stored these attributes it means the transactions where well formatted. This is a bug"
152                );
153
154                return AttributesMismatch::MalformedAttributesTransaction.into();
155            };
156
157            if &attr_tx != block_tx.inner.inner.inner() {
158                warn!(target: "engine", ?attr_tx, ?block_tx, "Transaction mismatch in derived attributes");
159                return AttributesMismatch::TransactionContent(attr_tx.tx_hash(), block_tx.tx_hash())
160                    .into()
161            }
162        }
163
164        Self::Match
165    }
166
167    /// Validates and compares EIP1559 parameters for consolidation.
168    fn check_eip1559(
169        config: &RollupConfig,
170        attributes: &OpAttributesWithParent,
171        block: &Block<Transaction>,
172    ) -> Self {
173        // We can assume that the EIP-1559 params are set iff holocene is active.
174        // Note here that we don't need to check for the attributes length because of type-safety.
175        let (ae, ad): (u128, u128) = match attributes.inner().decode_eip_1559_params() {
176            None => {
177                // Holocene is active but the eip1559 are not set. This is a bug!
178                // Note: we checked the timestamp match above, so we can assume that both the
179                // attributes and the block have the same stamps
180                if config.is_holocene_active(block.header.timestamp) {
181                    error!(
182                        "EIP1559 parameters for attributes not set while holocene is active. This is a bug"
183                    );
184                    return AttributesMismatch::MissingAttributesEIP1559.into();
185                }
186
187                // If the attributes are not specified, that means we can just early return.
188                return Self::Match;
189            }
190            Some((0, e)) if e != 0 => {
191                error!(
192                    "Holocene EIP1559 params cannot have a 0 denominator unless elasticity is also 0. This is a bug"
193                );
194                return AttributesMismatch::InvalidEIP1559ParamsCombination.into();
195            }
196            // We need to translate (0, 0) parameters to pre-holocene protocol constants.
197            // Since holocene is supposed to be active, canyon should be as well. We take the canyon
198            // base fee params.
199            Some((0, 0)) => {
200                let BaseFeeParams { max_change_denominator, elasticity_multiplier } =
201                    config.chain_op_config.as_canyon_base_fee_params();
202
203                (elasticity_multiplier, max_change_denominator)
204            }
205            Some((ae, ad)) => (ae.into(), ad.into()),
206        };
207
208        // We decode the extra data stemming from the block header.
209        let (be, bd): (u128, u128) = match decode_holocene_extra_data(&block.header.extra_data) {
210            Ok((be, bd)) => (be.into(), bd.into()),
211            Err(EIP1559ParamError::NoEIP1559Params) => {
212                error!(
213                    "EIP1559 parameters for the block not set while holocene is active. This is a bug"
214                );
215                return AttributesMismatch::MissingBlockEIP1559.into();
216            }
217            Err(EIP1559ParamError::InvalidVersion(v)) => {
218                error!(
219                    version = v,
220                    "The version in the extra data EIP1559 payload is incorrect. Should be 0. This is a bug",
221                );
222                return AttributesMismatch::InvalidExtraDataVersion.into();
223            }
224            Err(e) => {
225                error!(err = ?e, "An unknown extra data decoding error occurred. This is a bug",);
226
227                return AttributesMismatch::UnknownExtraDataDecodingError(e).into();
228            }
229        };
230
231        // We now have to check that both parameters match
232        if ae != be || ad != bd {
233            return AttributesMismatch::EIP1559Parameters(
234                BaseFeeParams { max_change_denominator: ad, elasticity_multiplier: ae },
235                BaseFeeParams { max_change_denominator: bd, elasticity_multiplier: be },
236            )
237            .into()
238        }
239
240        Self::Match
241    }
242
243    /// Checks if the specified [`OpAttributesWithParent`] matches the specified [`Block`].
244    /// Returns [`AttributesMatch::Match`] if they match, otherwise returns
245    /// [`AttributesMatch::Mismatch`].
246    pub fn check(
247        config: &RollupConfig,
248        attributes: &OpAttributesWithParent,
249        block: &Block<Transaction>,
250    ) -> Self {
251        if attributes.parent.block_info.hash != block.header.inner.parent_hash {
252            return AttributesMismatch::ParentHash(
253                attributes.parent.block_info.hash,
254                block.header.inner.parent_hash,
255            )
256            .into();
257        }
258
259        if attributes.inner().payload_attributes.timestamp != block.header.inner.timestamp {
260            return AttributesMismatch::Timestamp(
261                attributes.inner().payload_attributes.timestamp,
262                block.header.inner.timestamp,
263            )
264            .into();
265        }
266
267        let mix_hash = block.header.inner.mix_hash;
268        if attributes.inner().payload_attributes.prev_randao != mix_hash {
269            return AttributesMismatch::PrevRandao(
270                attributes.inner().payload_attributes.prev_randao,
271                mix_hash,
272            )
273            .into();
274        }
275
276        // Let's extract the list of attribute transactions
277        let default_vec = vec![];
278        let attributes_txs =
279            attributes.inner().transactions.as_ref().map_or_else(|| &default_vec, |attrs| attrs);
280
281        // Check transactions
282        if let mismatch @ Self::Mismatch(_) = Self::check_transactions(attributes_txs, block) {
283            return mismatch
284        }
285
286        let Some(gas_limit) = attributes.inner().gas_limit else {
287            return AttributesMismatch::MissingAttributesGasLimit.into();
288        };
289
290        if gas_limit != block.header.inner.gas_limit {
291            return AttributesMismatch::GasLimit(gas_limit, block.header.inner.gas_limit).into();
292        }
293
294        if let Self::Mismatch(m) = Self::check_withdrawals(config, attributes, block) {
295            return m.into();
296        }
297
298        if attributes.inner().payload_attributes.parent_beacon_block_root !=
299            block.header.inner.parent_beacon_block_root
300        {
301            return AttributesMismatch::ParentBeaconBlockRoot(
302                attributes.inner().payload_attributes.parent_beacon_block_root,
303                block.header.inner.parent_beacon_block_root,
304            )
305            .into();
306        }
307
308        if attributes.inner().payload_attributes.suggested_fee_recipient !=
309            block.header.inner.beneficiary
310        {
311            return AttributesMismatch::FeeRecipient(
312                attributes.inner().payload_attributes.suggested_fee_recipient,
313                block.header.inner.beneficiary,
314            )
315            .into();
316        }
317
318        // Check the EIP-1559 parameters in a separate helper method
319        if let m @ Self::Mismatch(_) = Self::check_eip1559(config, attributes, block) {
320            return m;
321        }
322
323        Self::Match
324    }
325}
326
327/// An enum over the type of mismatch between [`OpAttributesWithParent`]
328/// and a [`Block`].
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum AttributesMismatch {
331    /// The parent hash of the block does not match the parent hash of the attributes.
332    ParentHash(B256, B256),
333    /// The timestamp of the block does not match the timestamp of the attributes.
334    Timestamp(u64, u64),
335    /// The prev randao of the block does not match the prev randao of the attributes.
336    PrevRandao(B256, B256),
337    /// The block contains malformed transactions. This is a bug - the transaction format
338    /// should be checked before the consolidation step.
339    MalformedBlockTransactions,
340    /// There is a malformed transaction inside the attributes. This is a bug - the transaction
341    /// format should be checked before the consolidation step.
342    MalformedAttributesTransaction,
343    /// A mismatch in the number of transactions contained in the attributes and the block.
344    TransactionLen(usize, usize),
345    /// A mismatch in the content of some transactions contained in the attributes and the block.
346    TransactionContent(B256, B256),
347    /// The EIP1559 payload for the [`OpAttributesWithParent`] is missing when holocene is active.
348    MissingAttributesEIP1559,
349    /// The EIP1559 payload for the block is missing when holocene is active.
350    MissingBlockEIP1559,
351    /// The version in the extra data EIP1559 payload is incorrect. Should be 0.
352    InvalidExtraDataVersion,
353    /// An unknown extra data decoding error occurred.
354    UnknownExtraDataDecodingError(EIP1559ParamError),
355    /// Holocene EIP1559 params cannot have a 0 denominator unless elasticity is also 0
356    InvalidEIP1559ParamsCombination,
357    /// The EIP1559 base fee parameters of the attributes and the block don't match
358    EIP1559Parameters(BaseFeeParams, BaseFeeParams),
359    /// Transactions mismatch.
360    Transactions(u64, u64),
361    /// The gas limit of the block does not match the gas limit of the attributes.
362    GasLimit(u64, u64),
363    /// The gas limit for the [`OpAttributesWithParent`] is missing.
364    MissingAttributesGasLimit,
365    /// The fee recipient of the block does not match the fee recipient of the attributes.
366    FeeRecipient(Address, Address),
367    /// A mismatch in the parent beacon block root.
368    ParentBeaconBlockRoot(Option<B256>, Option<B256>),
369    /// After the canyon hardfork, withdrawals cannot be empty.
370    CanyonWithdrawalsNotEmpty,
371    /// After the canyon hardfork, the withdrawals root must be the empty hash.
372    CanyonNotEmptyHash,
373    /// In the bedrock hardfork, the attributes must has empty withdrawals.
374    BedrockWithdrawals,
375    /// In the isthmus hardfork, the withdrawals root must be set.
376    IsthmusMissingWithdrawalsRoot,
377}
378
379impl From<AttributesMismatch> for AttributesMatch {
380    fn from(mismatch: AttributesMismatch) -> Self {
381        Self::Mismatch(mismatch)
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::AttributesMismatch::EIP1559Parameters;
389    use alloy_consensus::EMPTY_ROOT_HASH;
390    use alloy_primitives::{Bytes, FixedBytes, address, b256};
391    use alloy_rpc_types_eth::BlockTransactions;
392    use arbitrary::{Arbitrary, Unstructured};
393    use kona_protocol::{BlockInfo, L2BlockInfo};
394    use kona_registry::ROLLUP_CONFIGS;
395    use op_alloy_consensus::encode_holocene_extra_data;
396    use op_alloy_rpc_types_engine::OpPayloadAttributes;
397
398    fn default_attributes() -> OpAttributesWithParent {
399        OpAttributesWithParent {
400            inner: OpPayloadAttributes::default(),
401            parent: L2BlockInfo::default(),
402            derived_from: Some(BlockInfo::default()),
403            is_last_in_span: true,
404        }
405    }
406
407    fn default_rollup_config() -> &'static RollupConfig {
408        let opm = 10;
409        ROLLUP_CONFIGS.get(&opm).expect("default rollup config should exist")
410    }
411
412    #[test]
413    fn test_attributes_match_parent_hash_mismatch() {
414        let cfg = default_rollup_config();
415        let attributes = default_attributes();
416        let mut block = Block::<Transaction>::default();
417        block.header.inner.parent_hash =
418            b256!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
419        let check = AttributesMatch::check(cfg, &attributes, &block);
420        let expected: AttributesMatch = AttributesMismatch::ParentHash(
421            attributes.parent.block_info.hash,
422            block.header.inner.parent_hash,
423        )
424        .into();
425        assert_eq!(check, expected);
426        assert!(check.is_mismatch());
427    }
428
429    #[test]
430    fn test_attributes_match_check_timestamp() {
431        let cfg = default_rollup_config();
432        let attributes = default_attributes();
433        let mut block = Block::<Transaction>::default();
434        block.header.inner.timestamp = 1234567890;
435        let check = AttributesMatch::check(cfg, &attributes, &block);
436        let expected: AttributesMatch = AttributesMismatch::Timestamp(
437            attributes.inner().payload_attributes.timestamp,
438            block.header.inner.timestamp,
439        )
440        .into();
441        assert_eq!(check, expected);
442        assert!(check.is_mismatch());
443    }
444
445    #[test]
446    fn test_attributes_match_check_prev_randao() {
447        let cfg = default_rollup_config();
448        let attributes = default_attributes();
449        let mut block = Block::<Transaction>::default();
450        block.header.inner.mix_hash =
451            b256!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
452        let check = AttributesMatch::check(cfg, &attributes, &block);
453        let expected: AttributesMatch = AttributesMismatch::PrevRandao(
454            attributes.inner().payload_attributes.prev_randao,
455            block.header.inner.mix_hash,
456        )
457        .into();
458        assert_eq!(check, expected);
459        assert!(check.is_mismatch());
460    }
461
462    #[test]
463    fn test_attributes_match_missing_gas_limit() {
464        let cfg = default_rollup_config();
465        let attributes = default_attributes();
466        let mut block = Block::<Transaction>::default();
467        block.header.inner.gas_limit = 123456;
468        let check = AttributesMatch::check(cfg, &attributes, &block);
469        let expected: AttributesMatch = AttributesMismatch::MissingAttributesGasLimit.into();
470        assert_eq!(check, expected);
471        assert!(check.is_mismatch());
472    }
473
474    #[test]
475    fn test_attributes_match_check_gas_limit() {
476        let cfg = default_rollup_config();
477        let mut attributes = default_attributes();
478        attributes.inner.gas_limit = Some(123457);
479        let mut block = Block::<Transaction>::default();
480        block.header.inner.gas_limit = 123456;
481        let check = AttributesMatch::check(cfg, &attributes, &block);
482        let expected: AttributesMatch = AttributesMismatch::GasLimit(
483            attributes.inner().gas_limit.unwrap_or_default(),
484            block.header.inner.gas_limit,
485        )
486        .into();
487        assert_eq!(check, expected);
488        assert!(check.is_mismatch());
489    }
490
491    #[test]
492    fn test_attributes_match_check_parent_beacon_block_root() {
493        let cfg = default_rollup_config();
494        let mut attributes = default_attributes();
495        attributes.inner.gas_limit = Some(0);
496        attributes.inner.payload_attributes.parent_beacon_block_root =
497            Some(b256!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"));
498        let block = Block::<Transaction>::default();
499        let check = AttributesMatch::check(cfg, &attributes, &block);
500        let expected: AttributesMatch = AttributesMismatch::ParentBeaconBlockRoot(
501            attributes.inner().payload_attributes.parent_beacon_block_root,
502            block.header.inner.parent_beacon_block_root,
503        )
504        .into();
505        assert_eq!(check, expected);
506        assert!(check.is_mismatch());
507    }
508
509    #[test]
510    fn test_attributes_match_check_fee_recipient() {
511        let cfg = default_rollup_config();
512        let mut attributes = default_attributes();
513        attributes.inner.gas_limit = Some(0);
514        let mut block = Block::<Transaction>::default();
515        block.header.inner.beneficiary = address!("1234567890abcdef1234567890abcdef12345678");
516        let check = AttributesMatch::check(cfg, &attributes, &block);
517        let expected: AttributesMatch = AttributesMismatch::FeeRecipient(
518            attributes.inner().payload_attributes.suggested_fee_recipient,
519            block.header.inner.beneficiary,
520        )
521        .into();
522        assert_eq!(check, expected);
523        assert!(check.is_mismatch());
524    }
525
526    fn generate_txs(num_txs: usize) -> Vec<Transaction> {
527        // Simulate some random data
528        let mut data = vec![0; 1024];
529        let mut rng = rand::rng();
530
531        (0..num_txs)
532            .map(|_| {
533                rand::Rng::fill(&mut rng, &mut data[..]);
534
535                // Create unstructured data with the random bytes
536                let u = Unstructured::new(&data);
537
538                // Generate a random instance of MyStruct
539                Transaction::arbitrary_take_rest(u).expect("Impossible to generate arbitrary tx")
540            })
541            .collect()
542    }
543
544    fn test_transactions_match_helper() -> (OpAttributesWithParent, Block<Transaction>) {
545        const NUM_TXS: usize = 10;
546
547        let transactions = generate_txs(NUM_TXS);
548        let mut attributes = default_attributes();
549        attributes.inner.gas_limit = Some(0);
550        attributes.inner.transactions = Some(
551            transactions
552                .iter()
553                .map(|tx| {
554                    use alloy_eips::Encodable2718;
555                    let mut buf = vec![];
556                    tx.inner.inner.inner().encode_2718(&mut buf);
557                    Bytes::from(buf)
558                })
559                .collect::<Vec<_>>(),
560        );
561
562        let block = Block::<Transaction> {
563            transactions: BlockTransactions::Full(transactions),
564            ..Default::default()
565        };
566
567        (attributes, block)
568    }
569
570    #[test]
571    fn test_attributes_match_check_transactions() {
572        let cfg = default_rollup_config();
573        let (attributes, block) = test_transactions_match_helper();
574        let check = AttributesMatch::check(cfg, &attributes, &block);
575        assert_eq!(check, AttributesMatch::Match);
576    }
577
578    #[test]
579    fn test_attributes_mismatch_check_transactions_len() {
580        let cfg = default_rollup_config();
581        let (mut attributes, block) = test_transactions_match_helper();
582        attributes.inner = OpPayloadAttributes {
583            transactions: attributes.inner.transactions.map(|mut txs| {
584                txs.pop();
585                txs
586            }),
587            ..attributes.inner
588        };
589
590        let block_txs_len = block.transactions.len();
591
592        let expected: AttributesMatch =
593            AttributesMismatch::TransactionLen(block_txs_len - 1, block_txs_len).into();
594
595        let check = AttributesMatch::check(cfg, &attributes, &block);
596        assert_eq!(check, expected);
597        assert!(check.is_mismatch());
598    }
599
600    #[test]
601    fn test_attributes_mismatch_check_transaction_content() {
602        let cfg = default_rollup_config();
603        let (attributes, mut block) = test_transactions_match_helper();
604        let BlockTransactions::Full(block_txs) = &mut block.transactions else {
605            unreachable!("The helper should build a full list of transactions")
606        };
607
608        let first_tx = block_txs.last().unwrap().clone();
609        let first_tx_hash = first_tx.tx_hash();
610
611        // We set the last tx to be the same as the first transaction.
612        // Since the transactions are generated randomly and there are more than one transaction,
613        // there is a very high likelihood that any pair of transactions is distinct.
614        let last_tx = block_txs.first_mut().unwrap();
615        let last_tx_hash = last_tx.tx_hash();
616        *last_tx = first_tx;
617
618        let expected: AttributesMatch =
619            AttributesMismatch::TransactionContent(last_tx_hash, first_tx_hash).into();
620
621        let check = AttributesMatch::check(cfg, &attributes, &block);
622        assert_eq!(check, expected);
623        assert!(check.is_mismatch());
624    }
625
626    /// Checks the edge case where the attributes array is empty.
627    #[test]
628    fn test_attributes_mismatch_empty_tx_attributes() {
629        let cfg = default_rollup_config();
630        let (mut attributes, block) = test_transactions_match_helper();
631        attributes.inner = OpPayloadAttributes { transactions: None, ..attributes.inner };
632
633        let block_txs_len = block.transactions.len();
634
635        let expected: AttributesMatch = AttributesMismatch::TransactionLen(0, block_txs_len).into();
636
637        let check = AttributesMatch::check(cfg, &attributes, &block);
638        assert_eq!(check, expected);
639        assert!(check.is_mismatch());
640    }
641
642    /// Checks the edge case where the transactions contained in the block have the wrong
643    /// format.
644    #[test]
645    fn test_block_transactions_wrong_format() {
646        let cfg = default_rollup_config();
647        let (attributes, mut block) = test_transactions_match_helper();
648        block.transactions = BlockTransactions::Uncle;
649
650        let expected: AttributesMatch = AttributesMismatch::MalformedBlockTransactions.into();
651
652        let check = AttributesMatch::check(cfg, &attributes, &block);
653        assert_eq!(check, expected);
654        assert!(check.is_mismatch());
655    }
656
657    /// Checks the edge case where the transactions contained in the attributes have the wrong
658    /// format.
659    #[test]
660    fn test_attributes_transactions_wrong_format() {
661        let cfg = default_rollup_config();
662        let (mut attributes, block) = test_transactions_match_helper();
663        let txs = attributes.inner.transactions.as_mut().unwrap();
664        let first_tx_bytes = txs.first_mut().unwrap();
665        *first_tx_bytes = Bytes::copy_from_slice(&[0, 1, 2]);
666
667        let expected: AttributesMatch = AttributesMismatch::MalformedAttributesTransaction.into();
668
669        let check = AttributesMatch::check(cfg, &attributes, &block);
670        assert_eq!(check, expected);
671        assert!(check.is_mismatch());
672    }
673
674    // Test that the check pass if the transactions obtained from the attributes have the format
675    // `Some(vec![])`, ie an empty vector inside a `Some` option.
676    #[test]
677    fn test_attributes_and_block_transactions_empty() {
678        let cfg = default_rollup_config();
679        let (mut attributes, mut block) = test_transactions_match_helper();
680
681        attributes.inner = OpPayloadAttributes { transactions: Some(vec![]), ..attributes.inner };
682
683        block.transactions = BlockTransactions::Full(vec![]);
684
685        let check = AttributesMatch::check(cfg, &attributes, &block);
686        assert_eq!(check, AttributesMatch::Match);
687
688        // Edge case: if the block transactions and the payload attributes are empty, we can also
689        // use the hash format (this is the default value of `BlockTransactions`).
690        attributes.inner = OpPayloadAttributes { transactions: None, ..attributes.inner };
691        block.transactions = BlockTransactions::Hashes(vec![]);
692
693        let check = AttributesMatch::check(cfg, &attributes, &block);
694        assert_eq!(check, AttributesMatch::Match);
695    }
696
697    // Edge case: if the payload attributes has the format `Some(vec![])`, we can still
698    // use the hash format.
699    #[test]
700    fn test_attributes_and_block_transactions_empty_hash_format() {
701        let cfg = default_rollup_config();
702        let (mut attributes, mut block) = test_transactions_match_helper();
703
704        attributes.inner = OpPayloadAttributes { transactions: Some(vec![]), ..attributes.inner };
705
706        block.transactions = BlockTransactions::Hashes(vec![]);
707
708        let check = AttributesMatch::check(cfg, &attributes, &block);
709        assert_eq!(check, AttributesMatch::Match);
710    }
711
712    // Test that the check fails if the block format is incorrect and the attributes are empty
713    #[test]
714    fn test_attributes_empty_and_block_uncle() {
715        let cfg = default_rollup_config();
716        let (mut attributes, mut block) = test_transactions_match_helper();
717
718        attributes.inner = OpPayloadAttributes { transactions: Some(vec![]), ..attributes.inner };
719
720        block.transactions = BlockTransactions::Uncle;
721
722        let expected: AttributesMatch = AttributesMismatch::MalformedBlockTransactions.into();
723
724        let check = AttributesMatch::check(cfg, &attributes, &block);
725        assert_eq!(check, expected);
726    }
727
728    fn eip1559_test_setup() -> (RollupConfig, OpAttributesWithParent, Block<Transaction>) {
729        let mut cfg = default_rollup_config().clone();
730
731        // We need to activate holocene to make sure it works! We set the activation time to zero to
732        // make sure that it is activated by default.
733        cfg.hardforks.holocene_time = Some(0);
734
735        let mut attributes = default_attributes();
736        attributes.inner.gas_limit = Some(0);
737        // For canyon and above we need to specify the withdrawals
738        attributes.inner.payload_attributes.withdrawals = Some(vec![]);
739
740        // For canyon and above we also need to specify the withdrawal headers
741        let block = Block {
742            withdrawals: Some(Withdrawals(vec![])),
743            header: alloy_rpc_types_eth::Header {
744                inner: alloy_consensus::Header {
745                    withdrawals_root: Some(EMPTY_ROOT_HASH),
746                    ..Default::default()
747                },
748                ..Default::default()
749            },
750            ..Default::default()
751        };
752
753        (cfg, attributes, block)
754    }
755
756    /// Ensures that we have to set the EIP1559 parameters for holocene and above.
757    #[test]
758    fn test_eip1559_parameters_not_specified_holocene() {
759        let (cfg, attributes, block) = eip1559_test_setup();
760
761        let check = AttributesMatch::check(&cfg, &attributes, &block);
762        assert_eq!(check, AttributesMatch::Mismatch(AttributesMismatch::MissingAttributesEIP1559));
763        assert!(check.is_mismatch());
764    }
765
766    /// Ensures that we have to set the EIP1559 parameters for holocene and above.
767    #[test]
768    fn test_eip1559_parameters_specified_attributes_but_not_block() {
769        let (cfg, mut attributes, block) = eip1559_test_setup();
770
771        attributes.inner.eip_1559_params = Some(Default::default());
772
773        let check = AttributesMatch::check(&cfg, &attributes, &block);
774        assert_eq!(check, AttributesMatch::Mismatch(AttributesMismatch::MissingBlockEIP1559));
775        assert!(check.is_mismatch());
776    }
777
778    /// Check that, when the eip1559 params are specified and empty, the check fails because we
779    /// fallback on canyon params for the attributes but not for the block (edge case).
780    #[test]
781    fn test_eip1559_parameters_specified_both_and_empty() {
782        let (cfg, mut attributes, mut block) = eip1559_test_setup();
783
784        attributes.inner.eip_1559_params = Some(Default::default());
785        block.header.extra_data = vec![0; 9].into();
786
787        let check = AttributesMatch::check(&cfg, &attributes, &block);
788        assert_eq!(
789            check,
790            AttributesMatch::Mismatch(EIP1559Parameters(
791                BaseFeeParams { max_change_denominator: 250, elasticity_multiplier: 6 },
792                BaseFeeParams { max_change_denominator: 0, elasticity_multiplier: 0 }
793            ))
794        );
795        assert!(check.is_mismatch());
796    }
797
798    #[test]
799    fn test_eip1559_parameters_empty_for_attr_only() {
800        let (cfg, mut attributes, mut block) = eip1559_test_setup();
801
802        attributes.inner.eip_1559_params = Some(Default::default());
803        block.header.extra_data = encode_holocene_extra_data(
804            Default::default(),
805            BaseFeeParams { max_change_denominator: 250, elasticity_multiplier: 6 },
806        )
807        .unwrap();
808
809        let check = AttributesMatch::check(&cfg, &attributes, &block);
810        assert_eq!(check, AttributesMatch::Match);
811        assert!(check.is_match());
812    }
813
814    #[test]
815    fn test_eip1559_parameters_custom_values_match() {
816        let (cfg, mut attributes, mut block) = eip1559_test_setup();
817
818        let eip1559_extra_params = encode_holocene_extra_data(
819            Default::default(),
820            BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 },
821        )
822        .unwrap();
823        let eip1559_params: FixedBytes<8> =
824            eip1559_extra_params.clone().split_off(1).as_ref().try_into().unwrap();
825
826        attributes.inner.eip_1559_params = Some(eip1559_params);
827        block.header.extra_data = eip1559_extra_params;
828
829        let check = AttributesMatch::check(&cfg, &attributes, &block);
830        assert_eq!(check, AttributesMatch::Match);
831        assert!(check.is_match());
832    }
833
834    #[test]
835    fn test_eip1559_parameters_custom_values_mismatch() {
836        let (cfg, mut attributes, mut block) = eip1559_test_setup();
837
838        let eip1559_extra_params = encode_holocene_extra_data(
839            Default::default(),
840            BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 },
841        )
842        .unwrap();
843
844        let eip1559_params: FixedBytes<8> = encode_holocene_extra_data(
845            Default::default(),
846            BaseFeeParams { max_change_denominator: 99, elasticity_multiplier: 2 },
847        )
848        .unwrap()
849        .split_off(1)
850        .as_ref()
851        .try_into()
852        .unwrap();
853
854        attributes.inner.eip_1559_params = Some(eip1559_params);
855        block.header.extra_data = eip1559_extra_params;
856
857        let check = AttributesMatch::check(&cfg, &attributes, &block);
858        assert_eq!(
859            check,
860            AttributesMatch::Mismatch(AttributesMismatch::EIP1559Parameters(
861                BaseFeeParams { max_change_denominator: 99, elasticity_multiplier: 2 },
862                BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 }
863            ))
864        );
865        assert!(check.is_mismatch());
866    }
867
868    /// Edge case: if the elasticity multiplier is 0, the max change denominator cannot be 0 as well
869    #[test]
870    fn test_eip1559_parameters_combination_mismatch() {
871        let (cfg, mut attributes, mut block) = eip1559_test_setup();
872
873        let eip1559_extra_params = encode_holocene_extra_data(
874            Default::default(),
875            BaseFeeParams { max_change_denominator: 5, elasticity_multiplier: 0 },
876        )
877        .unwrap();
878        let eip1559_params: FixedBytes<8> =
879            eip1559_extra_params.clone().split_off(1).as_ref().try_into().unwrap();
880
881        attributes.inner.eip_1559_params = Some(eip1559_params);
882        block.header.extra_data = eip1559_extra_params;
883
884        let check = AttributesMatch::check(&cfg, &attributes, &block);
885        assert_eq!(
886            check,
887            AttributesMatch::Mismatch(AttributesMismatch::InvalidEIP1559ParamsCombination)
888        );
889        assert!(check.is_mismatch());
890    }
891
892    /// Check that the version of the extra block data must be zero.
893    #[test]
894    fn test_eip1559_parameters_invalid_version() {
895        let (cfg, mut attributes, mut block) = eip1559_test_setup();
896
897        let eip1559_extra_params = encode_holocene_extra_data(
898            Default::default(),
899            BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 },
900        )
901        .unwrap();
902        let eip1559_params: FixedBytes<8> =
903            eip1559_extra_params.clone().split_off(1).as_ref().try_into().unwrap();
904
905        let mut raw_extra_params_bytes = eip1559_extra_params.to_vec();
906        raw_extra_params_bytes[0] = 10;
907
908        attributes.inner.eip_1559_params = Some(eip1559_params);
909        block.header.extra_data = raw_extra_params_bytes.into();
910
911        let check = AttributesMatch::check(&cfg, &attributes, &block);
912        assert_eq!(check, AttributesMatch::Mismatch(AttributesMismatch::InvalidExtraDataVersion));
913        assert!(check.is_mismatch());
914    }
915
916    /// The default parameters can't overflow the u32 byte representation of the base fee params!
917    #[test]
918    fn test_eip1559_default_param_cant_overflow() {
919        let (mut cfg, mut attributes, mut block) = eip1559_test_setup();
920        cfg.chain_op_config.eip1559_denominator_canyon = u64::MAX;
921        cfg.chain_op_config.eip1559_elasticity = u64::MAX;
922
923        attributes.inner.eip_1559_params = Some(Default::default());
924        block.header.extra_data = vec![0; 9].into();
925
926        let check = AttributesMatch::check(&cfg, &attributes, &block);
927
928        // Note that in this case we *always* have a mismatch because there isn't enough bytes in
929        // the default representation of the extra params to represent a u128
930        assert_eq!(
931            check,
932            AttributesMatch::Mismatch(EIP1559Parameters(
933                BaseFeeParams {
934                    max_change_denominator: u64::MAX as u128,
935                    elasticity_multiplier: u64::MAX as u128
936                },
937                BaseFeeParams { max_change_denominator: 0, elasticity_multiplier: 0 }
938            ))
939        );
940        assert!(check.is_mismatch());
941    }
942
943    #[test]
944    fn test_attributes_match() {
945        let cfg = default_rollup_config();
946        let mut attributes = default_attributes();
947        attributes.inner.gas_limit = Some(0);
948        let block = Block::<Transaction>::default();
949        let check = AttributesMatch::check(cfg, &attributes, &block);
950        assert_eq!(check, AttributesMatch::Match);
951        assert!(check.is_match());
952    }
953}