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        if let Err(error) = verify_receipts_optimism(
98            header.receipts_root(),
99            header.logs_bloom(),
100            receipts,
101            chain_spec,
102            header.timestamp(),
103        ) {
104            let receipts = receipts
105                .iter()
106                .map(|r| Bytes::from(r.with_bloom_ref().encoded_2718()))
107                .collect::<Vec<_>>();
108            tracing::debug!(%error, ?receipts, "receipts verification failed");
109            return Err(error)
110        }
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    fn holocene_chainspec() -> Arc<OpChainSpec> {
187        let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone();
188        hardforks.insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(1800000000));
189        Arc::new(OpChainSpec {
190            inner: ChainSpec {
191                chain: BASE_SEPOLIA.inner.chain,
192                genesis: BASE_SEPOLIA.inner.genesis.clone(),
193                genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
194                paris_block_and_final_difficulty: Some((0, U256::from(0))),
195                hardforks,
196                base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
197                prune_delete_limit: 10000,
198                ..Default::default()
199            },
200        })
201    }
202
203    fn isthmus_chainspec() -> OpChainSpec {
204        let mut chainspec = BASE_SEPOLIA.as_ref().clone();
205        chainspec
206            .inner
207            .hardforks
208            .insert(OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(1800000000));
209        chainspec
210    }
211
212    #[test]
213    fn test_get_base_fee_pre_holocene() {
214        let op_chain_spec = BASE_SEPOLIA.clone();
215        let parent = Header {
216            base_fee_per_gas: Some(1),
217            gas_used: 15763614,
218            gas_limit: 144000000,
219            ..Default::default()
220        };
221        let base_fee =
222            reth_optimism_chainspec::OpChainSpec::next_block_base_fee(&op_chain_spec, &parent, 0);
223        assert_eq!(
224            base_fee.unwrap(),
225            op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
226        );
227    }
228
229    #[test]
230    fn test_get_base_fee_holocene_extra_data_not_set() {
231        let op_chain_spec = holocene_chainspec();
232        let parent = Header {
233            base_fee_per_gas: Some(1),
234            gas_used: 15763614,
235            gas_limit: 144000000,
236            timestamp: 1800000003,
237            extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
238            ..Default::default()
239        };
240        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
241            &op_chain_spec,
242            &parent,
243            1800000005,
244        );
245        assert_eq!(
246            base_fee.unwrap(),
247            op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
248        );
249    }
250
251    #[test]
252    fn test_get_base_fee_holocene_extra_data_set() {
253        let parent = Header {
254            base_fee_per_gas: Some(1),
255            gas_used: 15763614,
256            gas_limit: 144000000,
257            extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
258            timestamp: 1800000003,
259            ..Default::default()
260        };
261
262        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
263            &holocene_chainspec(),
264            &parent,
265            1800000005,
266        );
267        assert_eq!(
268            base_fee.unwrap(),
269            parent
270                .next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
271                .unwrap_or_default()
272        );
273    }
274
275    // <https://sepolia.basescan.org/block/19773628>
276    #[test]
277    fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
278        let parent = Header {
279            base_fee_per_gas: Some(507),
280            gas_used: 4847634,
281            gas_limit: 60000000,
282            extra_data: hex!("00000000fa0000000a").into(),
283            timestamp: 1735315544,
284            ..Default::default()
285        };
286
287        let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
288            &*BASE_SEPOLIA,
289            &parent,
290            1735315546,
291        )
292        .unwrap();
293        assert_eq!(base_fee, 507);
294    }
295
296    #[test]
297    fn body_against_header_isthmus() {
298        let chainspec = isthmus_chainspec();
299        let header = Header {
300            base_fee_per_gas: Some(507),
301            gas_used: 4847634,
302            gas_limit: 60000000,
303            extra_data: hex!("00000000fa0000000a").into(),
304            timestamp: 1800000000,
305            withdrawals_root: Some(b256!(
306                "0x611e1d75cbb77fa782d79485a8384e853bc92e56883c313a51e3f9feef9a9a71"
307            )),
308            ..Default::default()
309        };
310        let mut body = alloy_consensus::BlockBody::<OpTxEnvelope> {
311            transactions: vec![],
312            ommers: vec![],
313            withdrawals: Some(Default::default()),
314        };
315        validate_body_against_header_op(&chainspec, &body, &header).unwrap();
316
317        body.withdrawals.take();
318        validate_body_against_header_op(&chainspec, &body, &header).unwrap_err();
319    }
320}