reth_optimism_consensus/validation/
mod.rs

1//! Verification of blocks w.r.t. Optimism hardforks.
2
3pub mod canyon;
4pub mod isthmus;
5
6use crate::proof::calculate_receipt_root_optimism;
7use alloc::vec::Vec;
8use alloy_consensus::{BlockHeader, TxReceipt, EMPTY_OMMER_ROOT_HASH};
9use alloy_primitives::{Bloom, B256};
10use alloy_trie::EMPTY_ROOT_HASH;
11use op_alloy_consensus::{decode_holocene_extra_data, EIP1559ParamError};
12use reth_chainspec::{BaseFeeParams, EthChainSpec};
13use reth_consensus::ConsensusError;
14use reth_optimism_forks::OpHardforks;
15use reth_optimism_primitives::DepositReceipt;
16use reth_primitives_traits::{receipt::gas_spent_by_transactions, BlockBody, GotExpected};
17
18/// Ensures the block response data matches the header.
19///
20/// This ensures the body response items match the header's hashes:
21///   - ommer hash
22///   - transaction root
23///   - withdrawals root: the body's withdrawals root must only match the header's before isthmus
24pub fn validate_body_against_header_op<B, H>(
25    chain_spec: impl OpHardforks,
26    body: &B,
27    header: &H,
28) -> Result<(), ConsensusError>
29where
30    B: BlockBody,
31    H: reth_primitives_traits::BlockHeader,
32{
33    let ommers_hash = body.calculate_ommers_root();
34    if Some(header.ommers_hash()) != ommers_hash {
35        return Err(ConsensusError::BodyOmmersHashDiff(
36            GotExpected {
37                got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
38                expected: header.ommers_hash(),
39            }
40            .into(),
41        ))
42    }
43
44    let tx_root = body.calculate_tx_root();
45    if header.transactions_root() != tx_root {
46        return Err(ConsensusError::BodyTransactionRootDiff(
47            GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
48        ))
49    }
50
51    match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
52        (Some(header_withdrawals_root), Some(withdrawals_root)) => {
53            // after isthmus, the withdrawals root field is repurposed and no longer mirrors the
54            // withdrawals root computed from the body
55            if chain_spec.is_isthmus_active_at_timestamp(header.timestamp()) {
56                // After isthmus we only ensure that the body has empty withdrawals
57                if withdrawals_root != EMPTY_ROOT_HASH {
58                    return Err(ConsensusError::BodyWithdrawalsRootDiff(
59                        GotExpected { got: withdrawals_root, expected: EMPTY_ROOT_HASH }.into(),
60                    ))
61                }
62            } else {
63                // before isthmus we ensure that the header root matches the body
64                if withdrawals_root != header_withdrawals_root {
65                    return Err(ConsensusError::BodyWithdrawalsRootDiff(
66                        GotExpected { got: withdrawals_root, expected: header_withdrawals_root }
67                            .into(),
68                    ))
69                }
70            }
71        }
72        (None, None) => {
73            // this is ok because we assume the fork is not active in this case
74        }
75        _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
76    }
77
78    Ok(())
79}
80
81/// Validate a block with regard to execution results:
82///
83/// - Compares the receipts root in the block header to the block body
84/// - Compares the gas used in the block header to the actual gas usage after execution
85pub fn validate_block_post_execution<R: DepositReceipt>(
86    header: impl BlockHeader,
87    chain_spec: impl OpHardforks,
88    receipts: &[R],
89) -> Result<(), ConsensusError> {
90    // Before Byzantium, receipts contained state root that would mean that expensive
91    // operation as hashing that is required for state root got calculated in every
92    // transaction This was replaced with is_success flag.
93    // See more about EIP here: https://eips.ethereum.org/EIPS/eip-658
94    if chain_spec.is_byzantium_active_at_block(header.number()) {
95        if let Err(error) = verify_receipts_optimism(
96            header.receipts_root(),
97            header.logs_bloom(),
98            receipts,
99            chain_spec,
100            header.timestamp(),
101        ) {
102            tracing::debug!(%error, ?receipts, "receipts verification failed");
103            return Err(error)
104        }
105    }
106
107    // Check if gas used matches the value set in header.
108    let cumulative_gas_used =
109        receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or(0);
110    if header.gas_used() != cumulative_gas_used {
111        return Err(ConsensusError::BlockGasUsed {
112            gas: GotExpected { got: cumulative_gas_used, expected: header.gas_used() },
113            gas_spent_by_tx: gas_spent_by_transactions(receipts),
114        })
115    }
116
117    Ok(())
118}
119
120/// Verify the calculated receipts root against the expected receipts root.
121fn verify_receipts_optimism<R: DepositReceipt>(
122    expected_receipts_root: B256,
123    expected_logs_bloom: Bloom,
124    receipts: &[R],
125    chain_spec: impl OpHardforks,
126    timestamp: u64,
127) -> Result<(), ConsensusError> {
128    // Calculate receipts root.
129    let receipts_with_bloom = receipts.iter().cloned().map(Into::into).collect::<Vec<_>>();
130    let receipts_root =
131        calculate_receipt_root_optimism(&receipts_with_bloom, chain_spec, timestamp);
132
133    // Calculate header logs bloom.
134    let logs_bloom = receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom());
135
136    compare_receipts_root_and_logs_bloom(
137        receipts_root,
138        logs_bloom,
139        expected_receipts_root,
140        expected_logs_bloom,
141    )?;
142
143    Ok(())
144}
145
146/// Compare the calculated receipts root with the expected receipts root, also compare
147/// the calculated logs bloom with the expected logs bloom.
148fn compare_receipts_root_and_logs_bloom(
149    calculated_receipts_root: B256,
150    calculated_logs_bloom: Bloom,
151    expected_receipts_root: B256,
152    expected_logs_bloom: Bloom,
153) -> Result<(), ConsensusError> {
154    if calculated_receipts_root != expected_receipts_root {
155        return Err(ConsensusError::BodyReceiptRootDiff(
156            GotExpected { got: calculated_receipts_root, expected: expected_receipts_root }.into(),
157        ))
158    }
159
160    if calculated_logs_bloom != expected_logs_bloom {
161        return Err(ConsensusError::BodyBloomLogDiff(
162            GotExpected { got: calculated_logs_bloom, expected: expected_logs_bloom }.into(),
163        ))
164    }
165
166    Ok(())
167}
168
169/// Extracts the Holocene 1599 parameters from the encoded extra data from the parent header.
170///
171/// Caution: Caller must ensure that holocene is active in the parent header.
172///
173/// See also [Base fee computation](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#base-fee-computation)
174pub fn decode_holocene_base_fee(
175    chain_spec: impl EthChainSpec + OpHardforks,
176    parent: impl BlockHeader,
177    timestamp: u64,
178) -> Result<u64, EIP1559ParamError> {
179    let (elasticity, denominator) = decode_holocene_extra_data(parent.extra_data())?;
180    let base_fee_params = if elasticity == 0 && denominator == 0 {
181        chain_spec.base_fee_params_at_timestamp(timestamp)
182    } else {
183        BaseFeeParams::new(denominator as u128, elasticity as u128)
184    };
185
186    Ok(parent.next_block_base_fee(base_fee_params).unwrap_or_default())
187}
188
189/// Read from parent to determine the base fee for the next block
190///
191/// See also [Base fee computation](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#base-fee-computation)
192pub fn next_block_base_fee(
193    chain_spec: impl EthChainSpec + OpHardforks,
194    parent: impl BlockHeader,
195    timestamp: u64,
196) -> Result<u64, EIP1559ParamError> {
197    // If we are in the Holocene, we need to use the base fee params
198    // from the parent block's extra data.
199    // Else, use the base fee params (default values) from chainspec
200    if chain_spec.is_holocene_active_at_timestamp(parent.timestamp()) {
201        Ok(decode_holocene_base_fee(chain_spec, parent, timestamp)?)
202    } else {
203        Ok(parent
204            .next_block_base_fee(chain_spec.base_fee_params_at_timestamp(timestamp))
205            .unwrap_or_default())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use alloy_consensus::Header;
213    use alloy_primitives::{b256, hex, Bytes, U256};
214    use op_alloy_consensus::OpTxEnvelope;
215    use reth_chainspec::{ChainSpec, ForkCondition, Hardfork};
216    use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
217    use reth_optimism_forks::{OpHardfork, BASE_SEPOLIA_HARDFORKS};
218    use std::sync::Arc;
219
220    fn holocene_chainspec() -> Arc<OpChainSpec> {
221        let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone();
222        hardforks.insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(1800000000));
223        Arc::new(OpChainSpec {
224            inner: ChainSpec {
225                chain: BASE_SEPOLIA.inner.chain,
226                genesis: BASE_SEPOLIA.inner.genesis.clone(),
227                genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
228                paris_block_and_final_difficulty: Some((0, U256::from(0))),
229                hardforks,
230                base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
231                prune_delete_limit: 10000,
232                ..Default::default()
233            },
234        })
235    }
236
237    fn isthmus_chainspec() -> OpChainSpec {
238        let mut chainspec = BASE_SEPOLIA.as_ref().clone();
239        chainspec
240            .inner
241            .hardforks
242            .insert(OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(1800000000));
243        chainspec
244    }
245
246    #[test]
247    fn test_get_base_fee_pre_holocene() {
248        let op_chain_spec = BASE_SEPOLIA.clone();
249        let parent = Header {
250            base_fee_per_gas: Some(1),
251            gas_used: 15763614,
252            gas_limit: 144000000,
253            ..Default::default()
254        };
255        let base_fee = next_block_base_fee(&op_chain_spec, &parent, 0);
256        assert_eq!(
257            base_fee.unwrap(),
258            parent
259                .next_block_base_fee(op_chain_spec.base_fee_params_at_timestamp(0))
260                .unwrap_or_default()
261        );
262    }
263
264    #[test]
265    fn test_get_base_fee_holocene_extra_data_not_set() {
266        let op_chain_spec = holocene_chainspec();
267        let parent = Header {
268            base_fee_per_gas: Some(1),
269            gas_used: 15763614,
270            gas_limit: 144000000,
271            timestamp: 1800000003,
272            extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
273            ..Default::default()
274        };
275        let base_fee = next_block_base_fee(&op_chain_spec, &parent, 1800000005);
276        assert_eq!(
277            base_fee.unwrap(),
278            parent
279                .next_block_base_fee(op_chain_spec.base_fee_params_at_timestamp(0))
280                .unwrap_or_default()
281        );
282    }
283
284    #[test]
285    fn test_get_base_fee_holocene_extra_data_set() {
286        let parent = Header {
287            base_fee_per_gas: Some(1),
288            gas_used: 15763614,
289            gas_limit: 144000000,
290            extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
291            timestamp: 1800000003,
292            ..Default::default()
293        };
294
295        let base_fee = next_block_base_fee(holocene_chainspec(), &parent, 1800000005);
296        assert_eq!(
297            base_fee.unwrap(),
298            parent
299                .next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
300                .unwrap_or_default()
301        );
302    }
303
304    // <https://sepolia.basescan.org/block/19773628>
305    #[test]
306    fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
307        let parent = Header {
308            base_fee_per_gas: Some(507),
309            gas_used: 4847634,
310            gas_limit: 60000000,
311            extra_data: hex!("00000000fa0000000a").into(),
312            timestamp: 1735315544,
313            ..Default::default()
314        };
315
316        let base_fee = next_block_base_fee(&*BASE_SEPOLIA, &parent, 1735315546).unwrap();
317        assert_eq!(base_fee, 507);
318    }
319
320    #[test]
321    fn body_against_header_isthmus() {
322        let chainspec = isthmus_chainspec();
323        let header = Header {
324            base_fee_per_gas: Some(507),
325            gas_used: 4847634,
326            gas_limit: 60000000,
327            extra_data: hex!("00000000fa0000000a").into(),
328            timestamp: 1800000000,
329            withdrawals_root: Some(b256!(
330                "0x611e1d75cbb77fa782d79485a8384e853bc92e56883c313a51e3f9feef9a9a71"
331            )),
332            ..Default::default()
333        };
334        let mut body = alloy_consensus::BlockBody::<OpTxEnvelope> {
335            transactions: vec![],
336            ommers: vec![],
337            withdrawals: Some(Default::default()),
338        };
339        validate_body_against_header_op(&chainspec, &body, &header).unwrap();
340
341        body.withdrawals.take();
342        validate_body_against_header_op(&chainspec, &body, &header).unwrap_err();
343    }
344}