reth_optimism_consensus/validation/
mod.rs1pub mod canyon;
4pub mod isthmus;
5
6pub 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
20pub 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 if chain_spec.is_isthmus_active_at_timestamp(header.timestamp()) {
58 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 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 }
77 _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
78 }
79
80 Ok(())
81}
82
83pub fn validate_block_post_execution<R: DepositReceipt>(
88 header: impl BlockHeader,
89 chain_spec: impl OpHardforks,
90 receipts: &[R],
91) -> Result<(), ConsensusError> {
92 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 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
126fn 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 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 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
152fn 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 #[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}