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