reth_optimism_consensus/validation/
mod.rs

1//! Verification of blocks w.r.t. Optimism hardforks.
2
3pub mod canyon;
4pub mod isthmus;
5
6// Re-export the decode_holocene_base_fee function for compatibility
7pub use reth_optimism_chainspec::decode_holocene_base_fee;
8
9use crate::proof::calculate_receipt_root_optimism;
10use alloc::vec::Vec;
11use alloy_consensus::{BlockHeader, TxReceipt, EMPTY_OMMER_ROOT_HASH};
12use alloy_eips::Encodable2718;
13use alloy_primitives::{Bloom, Bytes, B256};
14use alloy_trie::EMPTY_ROOT_HASH;
15use reth_consensus::ConsensusError;
16use reth_optimism_forks::OpHardforks;
17use reth_optimism_primitives::DepositReceipt;
18use reth_primitives_traits::{receipt::gas_spent_by_transactions, BlockBody, GotExpected};
19
20/// Ensures the block response data matches the header.
21///
22/// This ensures the body response items match the header's hashes:
23///   - ommer hash
24///   - transaction root
25///   - withdrawals root: the body's withdrawals root must only match the header's before isthmus
26pub fn validate_body_against_header_op<B, H>(
27    chain_spec: impl OpHardforks,
28    body: &B,
29    header: &H,
30) -> Result<(), ConsensusError>
31where
32    B: BlockBody,
33    H: reth_primitives_traits::BlockHeader,
34{
35    let ommers_hash = body.calculate_ommers_root();
36    if Some(header.ommers_hash()) != ommers_hash {
37        return Err(ConsensusError::BodyOmmersHashDiff(
38            GotExpected {
39                got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
40                expected: header.ommers_hash(),
41            }
42            .into(),
43        ))
44    }
45
46    let tx_root = body.calculate_tx_root();
47    if header.transactions_root() != tx_root {
48        return Err(ConsensusError::BodyTransactionRootDiff(
49            GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
50        ))
51    }
52
53    match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
54        (Some(header_withdrawals_root), Some(withdrawals_root)) => {
55            // after isthmus, the withdrawals root field is repurposed and no longer mirrors the
56            // withdrawals root computed from the body
57            if chain_spec.is_isthmus_active_at_timestamp(header.timestamp()) {
58                // After isthmus we only ensure that the body has empty withdrawals
59                if withdrawals_root != EMPTY_ROOT_HASH {
60                    return Err(ConsensusError::BodyWithdrawalsRootDiff(
61                        GotExpected { got: withdrawals_root, expected: EMPTY_ROOT_HASH }.into(),
62                    ))
63                }
64            } else {
65                // before isthmus we ensure that the header root matches the body
66                if withdrawals_root != header_withdrawals_root {
67                    return Err(ConsensusError::BodyWithdrawalsRootDiff(
68                        GotExpected { got: withdrawals_root, expected: header_withdrawals_root }
69                            .into(),
70                    ))
71                }
72            }
73        }
74        (None, None) => {
75            // this is ok because we assume the fork is not active in this case
76        }
77        _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
78    }
79
80    Ok(())
81}
82
83/// Validate a block with regard to execution results:
84///
85/// - Compares the receipts root in the block header to the block body
86/// - Compares the gas used in the block header to the actual gas usage after execution
87pub fn validate_block_post_execution<R: DepositReceipt>(
88    header: impl BlockHeader,
89    chain_spec: impl OpHardforks,
90    receipts: &[R],
91) -> Result<(), ConsensusError> {
92    // Before Byzantium, receipts contained state root that would mean that expensive
93    // operation as hashing that is required for state root got calculated in every
94    // transaction This was replaced with is_success flag.
95    // See more about EIP here: https://eips.ethereum.org/EIPS/eip-658
96    if chain_spec.is_byzantium_active_at_block(header.number()) &&
97        let Err(error) = verify_receipts_optimism(
98            header.receipts_root(),
99            header.logs_bloom(),
100            receipts,
101            chain_spec,
102            header.timestamp(),
103        )
104    {
105        let receipts = receipts
106            .iter()
107            .map(|r| Bytes::from(r.with_bloom_ref().encoded_2718()))
108            .collect::<Vec<_>>();
109        tracing::debug!(%error, ?receipts, "receipts verification failed");
110        return Err(error)
111    }
112
113    // Check if gas used matches the value set in header.
114    let cumulative_gas_used =
115        receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or(0);
116    if header.gas_used() != cumulative_gas_used {
117        return Err(ConsensusError::BlockGasUsed {
118            gas: GotExpected { got: cumulative_gas_used, expected: header.gas_used() },
119            gas_spent_by_tx: gas_spent_by_transactions(receipts),
120        })
121    }
122
123    Ok(())
124}
125
126/// Verify the calculated receipts root against the expected receipts root.
127fn verify_receipts_optimism<R: DepositReceipt>(
128    expected_receipts_root: B256,
129    expected_logs_bloom: Bloom,
130    receipts: &[R],
131    chain_spec: impl OpHardforks,
132    timestamp: u64,
133) -> Result<(), ConsensusError> {
134    // Calculate receipts root.
135    let receipts_with_bloom = receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
136    let receipts_root =
137        calculate_receipt_root_optimism(&receipts_with_bloom, chain_spec, timestamp);
138
139    // Calculate header logs bloom.
140    let logs_bloom = receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());
141
142    compare_receipts_root_and_logs_bloom(
143        receipts_root,
144        logs_bloom,
145        expected_receipts_root,
146        expected_logs_bloom,
147    )?;
148
149    Ok(())
150}
151
152/// Compare the calculated receipts root with the expected receipts root, also compare
153/// the calculated logs bloom with the expected logs bloom.
154fn compare_receipts_root_and_logs_bloom(
155    calculated_receipts_root: B256,
156    calculated_logs_bloom: Bloom,
157    expected_receipts_root: B256,
158    expected_logs_bloom: Bloom,
159) -> Result<(), ConsensusError> {
160    if calculated_receipts_root != expected_receipts_root {
161        return Err(ConsensusError::BodyReceiptRootDiff(
162            GotExpected { got: calculated_receipts_root, expected: expected_receipts_root }.into(),
163        ))
164    }
165
166    if calculated_logs_bloom != expected_logs_bloom {
167        return Err(ConsensusError::BodyBloomLogDiff(
168            GotExpected { got: calculated_logs_bloom, expected: expected_logs_bloom }.into(),
169        ))
170    }
171
172    Ok(())
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use alloy_consensus::Header;
179    use alloy_primitives::{b256, hex, Bytes, U256};
180    use op_alloy_consensus::OpTxEnvelope;
181    use reth_chainspec::{BaseFeeParams, ChainSpec, EthChainSpec, ForkCondition, Hardfork};
182    use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
183    use reth_optimism_forks::{OpHardfork, BASE_SEPOLIA_HARDFORKS};
184    use std::sync::Arc;
185
186    const JOVIAN_TIMESTAMP: u64 = 1900000000;
187    const BLOCK_TIME_SECONDS: u64 = 2;
188
189    fn holocene_chainspec() -> Arc<OpChainSpec> {
190        let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone();
191        hardforks.insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(1800000000));
192        Arc::new(OpChainSpec {
193            inner: ChainSpec {
194                chain: BASE_SEPOLIA.inner.chain,
195                genesis: BASE_SEPOLIA.inner.genesis.clone(),
196                genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
197                paris_block_and_final_difficulty: Some((0, U256::from(0))),
198                hardforks,
199                base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
200                prune_delete_limit: 10000,
201                ..Default::default()
202            },
203        })
204    }
205
206    fn isthmus_chainspec() -> OpChainSpec {
207        let mut chainspec = BASE_SEPOLIA.as_ref().clone();
208        chainspec
209            .inner
210            .hardforks
211            .insert(OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(1800000000));
212        chainspec
213    }
214
215    fn jovian_chainspec() -> OpChainSpec {
216        let mut chainspec = BASE_SEPOLIA.as_ref().clone();
217        chainspec
218            .inner
219            .hardforks
220            .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(1900000000));
221        chainspec
222    }
223
224    #[test]
225    fn test_get_base_fee_pre_holocene() {
226        let op_chain_spec = BASE_SEPOLIA.clone();
227        let parent = Header {
228            base_fee_per_gas: Some(1),
229            gas_used: 15763614,
230            gas_limit: 144000000,
231            ..Default::default()
232        };
233        let base_fee =
234            reth_optimism_chainspec::OpChainSpec::next_block_base_fee(&op_chain_spec, &parent, 0);
235        assert_eq!(
236            base_fee.unwrap(),
237            op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
238        );
239    }
240
241    #[test]
242    fn test_get_base_fee_holocene_extra_data_not_set() {
243        let op_chain_spec = holocene_chainspec();
244        let parent = Header {
245            base_fee_per_gas: Some(1),
246            gas_used: 15763614,
247            gas_limit: 144000000,
248            timestamp: 1800000003,
249            extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
250            ..Default::default()
251        };
252        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
253            &op_chain_spec,
254            &parent,
255            1800000005,
256        );
257        assert_eq!(
258            base_fee.unwrap(),
259            op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
260        );
261    }
262
263    #[test]
264    fn test_get_base_fee_holocene_extra_data_set() {
265        let parent = Header {
266            base_fee_per_gas: Some(1),
267            gas_used: 15763614,
268            gas_limit: 144000000,
269            extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
270            timestamp: 1800000003,
271            ..Default::default()
272        };
273
274        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
275            &holocene_chainspec(),
276            &parent,
277            1800000005,
278        );
279        assert_eq!(
280            base_fee.unwrap(),
281            parent
282                .next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
283                .unwrap_or_default()
284        );
285    }
286
287    // <https://sepolia.basescan.org/block/19773628>
288    #[test]
289    fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
290        let parent = Header {
291            base_fee_per_gas: Some(507),
292            gas_used: 4847634,
293            gas_limit: 60000000,
294            extra_data: hex!("00000000fa0000000a").into(),
295            timestamp: 1735315544,
296            ..Default::default()
297        };
298
299        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
300            &*BASE_SEPOLIA,
301            &parent,
302            1735315546,
303        )
304        .unwrap();
305        assert_eq!(base_fee, 507);
306    }
307
308    #[test]
309    fn test_get_base_fee_holocene_extra_data_set_and_min_base_fee_set() {
310        const MIN_BASE_FEE: u64 = 10;
311
312        let mut extra_data = Vec::new();
313        // eip1559 params
314        extra_data.append(&mut hex!("00000000fa0000000a").to_vec());
315        // min base fee
316        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
317        let extra_data = Bytes::from(extra_data);
318
319        let parent = Header {
320            base_fee_per_gas: Some(507),
321            gas_used: 4847634,
322            gas_limit: 60000000,
323            extra_data,
324            timestamp: 1735315544,
325            ..Default::default()
326        };
327
328        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
329            &*BASE_SEPOLIA,
330            &parent,
331            1735315546,
332        );
333        assert_eq!(base_fee, None);
334    }
335
336    /// The version byte for Jovian is 1.
337    const JOVIAN_EXTRA_DATA_VERSION_BYTE: u8 = 1;
338
339    #[test]
340    fn test_get_base_fee_jovian_extra_data_and_min_base_fee_not_set() {
341        let op_chain_spec = jovian_chainspec();
342
343        let mut extra_data = Vec::new();
344        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
345        // eip1559 params
346        extra_data.append(&mut [0_u8; 8].to_vec());
347        let extra_data = Bytes::from(extra_data);
348
349        let parent = Header {
350            base_fee_per_gas: Some(1),
351            gas_used: 15763614,
352            gas_limit: 144000000,
353            timestamp: JOVIAN_TIMESTAMP,
354            extra_data,
355            ..Default::default()
356        };
357        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
358            &op_chain_spec,
359            &parent,
360            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
361        );
362        assert_eq!(base_fee, None);
363    }
364
365    /// After Jovian, the next block base fee cannot be less than the minimum base fee.
366    #[test]
367    fn test_get_base_fee_jovian_default_extra_data_and_min_base_fee() {
368        const CURR_BASE_FEE: u64 = 1;
369        const MIN_BASE_FEE: u64 = 10;
370
371        let mut extra_data = Vec::new();
372        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
373        // eip1559 params
374        extra_data.append(&mut [0_u8; 8].to_vec());
375        // min base fee
376        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
377        let extra_data = Bytes::from(extra_data);
378
379        let op_chain_spec = jovian_chainspec();
380        let parent = Header {
381            base_fee_per_gas: Some(CURR_BASE_FEE),
382            gas_used: 15763614,
383            gas_limit: 144000000,
384            timestamp: JOVIAN_TIMESTAMP,
385            extra_data,
386            ..Default::default()
387        };
388        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
389            &op_chain_spec,
390            &parent,
391            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
392        );
393        assert_eq!(base_fee, Some(MIN_BASE_FEE));
394    }
395
396    /// After Jovian, the next block base fee cannot be less than the minimum base fee.
397    #[test]
398    fn test_jovian_min_base_fee_cannot_decrease() {
399        const MIN_BASE_FEE: u64 = 10;
400
401        let mut extra_data = Vec::new();
402        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
403        // eip1559 params
404        extra_data.append(&mut [0_u8; 8].to_vec());
405        // min base fee
406        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
407        let extra_data = Bytes::from(extra_data);
408
409        let op_chain_spec = jovian_chainspec();
410
411        // If we're currently at the minimum base fee, the next block base fee cannot decrease.
412        let parent = Header {
413            base_fee_per_gas: Some(MIN_BASE_FEE),
414            gas_used: 10,
415            gas_limit: 144000000,
416            timestamp: JOVIAN_TIMESTAMP,
417            extra_data: extra_data.clone(),
418            ..Default::default()
419        };
420        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
421            &op_chain_spec,
422            &parent,
423            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
424        );
425        assert_eq!(base_fee, Some(MIN_BASE_FEE));
426
427        // The next block can increase the base fee
428        let parent = Header {
429            base_fee_per_gas: Some(MIN_BASE_FEE),
430            gas_used: 144000000,
431            gas_limit: 144000000,
432            timestamp: JOVIAN_TIMESTAMP,
433            extra_data,
434            ..Default::default()
435        };
436        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
437            &op_chain_spec,
438            &parent,
439            JOVIAN_TIMESTAMP + 2 * BLOCK_TIME_SECONDS,
440        );
441        assert_eq!(base_fee, Some(MIN_BASE_FEE + 1));
442    }
443
444    #[test]
445    fn test_jovian_base_fee_can_decrease_if_above_min_base_fee() {
446        const MIN_BASE_FEE: u64 = 10;
447
448        let mut extra_data = Vec::new();
449        extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
450        // eip1559 params
451        extra_data.append(&mut [0_u8; 8].to_vec());
452        // min base fee
453        extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
454        let extra_data = Bytes::from(extra_data);
455
456        let op_chain_spec = jovian_chainspec();
457
458        let parent = Header {
459            base_fee_per_gas: Some(100 * MIN_BASE_FEE),
460            gas_used: 10,
461            gas_limit: 144000000,
462            timestamp: JOVIAN_TIMESTAMP,
463            extra_data,
464            ..Default::default()
465        };
466        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
467            &op_chain_spec,
468            &parent,
469            JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
470        )
471        .unwrap();
472        assert_eq!(
473            base_fee,
474            op_chain_spec
475                .inner
476                .next_block_base_fee(&parent, JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS)
477                .unwrap()
478        );
479    }
480
481    #[test]
482    fn body_against_header_isthmus() {
483        let chainspec = isthmus_chainspec();
484        let header = Header {
485            base_fee_per_gas: Some(507),
486            gas_used: 4847634,
487            gas_limit: 60000000,
488            extra_data: hex!("00000000fa0000000a").into(),
489            timestamp: 1800000000,
490            withdrawals_root: Some(b256!(
491                "0x611e1d75cbb77fa782d79485a8384e853bc92e56883c313a51e3f9feef9a9a71"
492            )),
493            ..Default::default()
494        };
495        let mut body = alloy_consensus::BlockBody::<OpTxEnvelope> {
496            transactions: vec![],
497            ommers: vec![],
498            withdrawals: Some(Default::default()),
499        };
500        validate_body_against_header_op(&chainspec, &body, &header).unwrap();
501
502        body.withdrawals.take();
503        validate_body_against_header_op(&chainspec, &body, &header).unwrap_err();
504    }
505}