1mod error;
4
5use alloy_consensus::{BlockHeader, Transaction, transaction::TxHashRef};
6use alloy_evm::block::BlockExecutionResult;
7pub use error::TempoConsensusError;
8use reth_chainspec::EthChainSpec;
9use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
10use reth_consensus_common::validation::{
11 validate_against_parent_4844, validate_against_parent_eip1559_base_fee,
12 validate_against_parent_gas_limit, validate_against_parent_hash_number,
13};
14use reth_ethereum_consensus::EthBeaconConsensus;
15use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader};
16use std::sync::Arc;
17use tempo_chainspec::{
18 hardfork::TempoHardforks,
19 spec::{SYSTEM_TX_ADDRESSES, SYSTEM_TX_COUNT, TempoChainSpec},
20};
21use tempo_primitives::{
22 Block, BlockBody, TempoHeader, TempoPrimitives, TempoReceipt, TempoTxEnvelope,
23};
24
25pub const ALLOWED_FUTURE_BLOCK_TIME_MILLIS: u64 = 0;
32
33pub const TEMPO_MAXIMUM_EXTRA_DATA_SIZE: usize = 10 * 1_024; #[derive(Debug, Clone)]
38pub struct TempoConsensus {
39 inner: EthBeaconConsensus<TempoChainSpec>,
41}
42
43impl TempoConsensus {
44 pub fn new(chain_spec: Arc<TempoChainSpec>) -> Self {
46 Self::new_with_bal_hashes(chain_spec, false)
47 }
48
49 pub fn new_with_bal_hashes(chain_spec: Arc<TempoChainSpec>, allow_bal_hashes: bool) -> Self {
51 Self {
52 inner: EthBeaconConsensus::new(chain_spec)
53 .with_max_extra_data_size(TEMPO_MAXIMUM_EXTRA_DATA_SIZE)
54 .with_allow_bal_hashes(allow_bal_hashes),
55 }
56 }
57
58 fn validate_header_with_timestamp_millis(
60 &self,
61 header: &SealedHeader<TempoHeader>,
62 present_timestamp_millis: u64,
63 ) -> Result<(), ConsensusError> {
64 self.inner.validate_header(header)?;
65
66 if header.timestamp_millis_part >= 1000 {
68 return Err(TempoConsensusError::InvalidTimestampMillisPart {
69 millis_part: header.timestamp_millis_part,
70 }
71 .into());
72 }
73
74 if header.timestamp_millis() > present_timestamp_millis + ALLOWED_FUTURE_BLOCK_TIME_MILLIS {
75 return Err(ConsensusError::TimestampIsInFuture {
76 timestamp: header.timestamp_millis(),
77 present_timestamp: present_timestamp_millis,
78 });
79 }
80
81 let expected_shared = self
82 .inner
83 .chain_spec()
84 .shared_gas_limit_at(header.timestamp(), header.gas_limit());
85 if header.shared_gas_limit != expected_shared {
86 return Err(TempoConsensusError::SharedGasLimitMismatch {
87 expected: expected_shared,
88 actual: header.shared_gas_limit,
89 }
90 .into());
91 }
92
93 let expected_general_gas_limit = self.inner.chain_spec().general_gas_limit_at(
95 header.timestamp(),
96 header.gas_limit(),
97 header.shared_gas_limit,
98 );
99
100 if header.general_gas_limit != expected_general_gas_limit {
101 return Err(TempoConsensusError::GeneralGasLimitMismatch {
102 expected: expected_general_gas_limit,
103 actual: header.general_gas_limit,
104 }
105 .into());
106 }
107
108 Ok(())
109 }
110}
111
112impl HeaderValidator<TempoHeader> for TempoConsensus {
113 fn validate_header(&self, header: &SealedHeader<TempoHeader>) -> Result<(), ConsensusError> {
114 let current_timestamp_millis = std::time::SystemTime::now()
115 .duration_since(std::time::SystemTime::UNIX_EPOCH)
116 .expect("system time should never be before UNIX EPOCH")
117 .as_millis() as u64;
118 self.validate_header_with_timestamp_millis(header, current_timestamp_millis)
119 }
120
121 fn validate_header_against_parent(
122 &self,
123 header: &SealedHeader<TempoHeader>,
124 parent: &SealedHeader<TempoHeader>,
125 ) -> Result<(), ConsensusError> {
126 validate_against_parent_hash_number(header.header(), parent)?;
127
128 validate_against_parent_gas_limit(header, parent, self.inner.chain_spec())?;
129
130 validate_against_parent_eip1559_base_fee(
131 header.header(),
132 parent.header(),
133 self.inner.chain_spec(),
134 )?;
135
136 if let Some(blob_params) = self
137 .inner
138 .chain_spec()
139 .blob_params_at_timestamp(header.timestamp())
140 {
141 validate_against_parent_4844(header.header(), parent.header(), blob_params)?;
142 }
143
144 if header.timestamp_millis() <= parent.timestamp_millis() {
145 return Err(ConsensusError::TimestampIsInPast {
146 parent_timestamp: parent.timestamp_millis(),
147 timestamp: header.timestamp_millis(),
148 });
149 }
150
151 Ok(())
152 }
153}
154
155impl Consensus<Block> for TempoConsensus {
156 fn validate_body_against_header(
157 &self,
158 body: &BlockBody,
159 header: &SealedHeader<TempoHeader>,
160 ) -> Result<(), ConsensusError> {
161 Consensus::<Block>::validate_body_against_header(&self.inner, body, header)
162 }
163
164 fn validate_block_pre_execution(
165 &self,
166 block: &SealedBlock<Block>,
167 ) -> Result<(), ConsensusError> {
168 let transactions = &block.body().transactions;
169
170 if let Some(tx) = transactions.iter().find(|&tx| {
171 tx.is_system_tx() && !tx.is_valid_system_tx(self.inner.chain_spec().chain().id())
172 }) {
173 return Err(TempoConsensusError::InvalidSystemTransaction {
174 tx_hash: *tx.tx_hash(),
175 }
176 .into());
177 }
178
179 let expected_system_tx_count = if self
180 .inner
181 .chain_spec()
182 .is_t4_active_at_timestamp(block.header().timestamp())
183 {
184 0
185 } else {
186 SYSTEM_TX_COUNT
187 };
188
189 let end_of_block_system_txs = transactions
191 .get(transactions.len().saturating_sub(expected_system_tx_count)..)
192 .map(|slice| {
193 slice
194 .iter()
195 .filter(|tx| tx.is_system_tx())
196 .collect::<Vec<&TempoTxEnvelope>>()
197 })
198 .unwrap_or_default();
199
200 if end_of_block_system_txs.len() != expected_system_tx_count {
201 return Err(TempoConsensusError::MissingEndOfBlockSystemTxs {
202 expected: expected_system_tx_count,
203 actual: end_of_block_system_txs.len(),
204 }
205 .into());
206 }
207
208 for (tx, expected_to) in end_of_block_system_txs.into_iter().zip(SYSTEM_TX_ADDRESSES) {
210 let actual_to = tx.to().unwrap_or_default();
211 if actual_to != expected_to {
212 return Err(TempoConsensusError::InvalidEndOfBlockSystemTxOrder {
213 expected: expected_to,
214 actual: actual_to,
215 }
216 .into());
217 }
218 }
219
220 self.inner.validate_block_pre_execution(block)
221 }
222
223 fn is_transient_error(&self, error: &ConsensusError) -> bool {
224 Consensus::<Block>::is_transient_error(&self.inner, error)
226 || matches!(error, ConsensusError::TimestampIsInFuture { .. })
227 }
228}
229
230impl FullConsensus<TempoPrimitives> for TempoConsensus {
231 fn validate_block_post_execution(
232 &self,
233 block: &RecoveredBlock<Block>,
234 result: &BlockExecutionResult<TempoReceipt>,
235 receipt_root_bloom: Option<ReceiptRootBloom>,
236 block_access_list_hash: Option<alloy_primitives::B256>,
237 ) -> Result<(), ConsensusError> {
238 FullConsensus::<TempoPrimitives>::validate_block_post_execution(
239 &self.inner,
240 block,
241 result,
242 receipt_root_bloom,
243 block_access_list_hash,
244 )
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use alloy_consensus::{
252 Header, Signed, TxLegacy, constants::EMPTY_ROOT_HASH, proofs::calculate_transaction_root,
253 transaction::TxHashRef,
254 };
255 use alloy_genesis::Genesis;
256 use alloy_primitives::{Address, B256, Signature, TxKind, U256};
257 use reth_primitives_traits::SealedHeader;
258 use std::time::{SystemTime, UNIX_EPOCH};
259 use tempo_chainspec::{
260 hardfork::TempoHardfork,
261 spec::{DEV, MODERATO, TempoChainSpec},
262 };
263
264 fn current_timestamp_millis() -> u64 {
265 SystemTime::now()
266 .duration_since(UNIX_EPOCH)
267 .unwrap()
268 .as_millis() as u64
269 }
270
271 #[derive(Default)]
272 struct TestHeaderBuilder {
273 gas_limit: u64,
274 timestamp: u64,
275 timestamp_millis_part: u64,
276 number: u64,
277 parent_hash: B256,
278 shared_gas_limit: Option<u64>,
279 general_gas_limit: Option<u64>,
280 base_fee: Option<u64>,
281 gas_used: u64,
282 }
283
284 impl TestHeaderBuilder {
285 fn gas_limit(mut self, gas_limit: u64) -> Self {
286 self.gas_limit = gas_limit;
287 self
288 }
289
290 fn timestamp_millis(mut self, timestamp: u64) -> Self {
291 self.timestamp = timestamp / 1000;
292 self.timestamp_millis_part = timestamp % 1000;
293 self
294 }
295
296 fn timestamp(mut self, timestamp: u64) -> Self {
297 self.timestamp = timestamp;
298 self
299 }
300
301 fn timestamp_millis_part(mut self, millis: u64) -> Self {
302 self.timestamp_millis_part = millis;
303 self
304 }
305
306 fn number(mut self, number: u64) -> Self {
307 self.number = number;
308 self
309 }
310
311 fn parent_hash(mut self, hash: B256) -> Self {
312 self.parent_hash = hash;
313 self
314 }
315
316 fn shared_gas_limit(mut self, limit: u64) -> Self {
317 self.shared_gas_limit = Some(limit);
318 self
319 }
320
321 fn general_gas_limit(mut self, limit: u64) -> Self {
322 self.general_gas_limit = Some(limit);
323 self
324 }
325
326 fn base_fee(mut self, fee: u64) -> Self {
327 self.base_fee = Some(fee);
328 self
329 }
330
331 fn gas_used(mut self, gas_used: u64) -> Self {
332 self.gas_used = gas_used;
333 self
334 }
335
336 fn build(self) -> TempoHeader {
337 let shared_gas_limit = self.shared_gas_limit.unwrap_or(0);
338 let general_gas_limit = self
340 .general_gas_limit
341 .unwrap_or(tempo_chainspec::spec::TEMPO_T1_GENERAL_GAS_LIMIT);
342
343 TempoHeader {
344 inner: Header {
345 gas_limit: self.gas_limit,
346 gas_used: self.gas_used,
347 timestamp: self.timestamp,
348 number: self.number,
349 parent_hash: self.parent_hash,
350 base_fee_per_gas: Some(
351 self.base_fee
352 .unwrap_or(tempo_chainspec::spec::TEMPO_T0_BASE_FEE),
353 ),
354 withdrawals_root: Some(EMPTY_ROOT_HASH),
355 blob_gas_used: Some(0),
356 excess_blob_gas: Some(0),
357 parent_beacon_block_root: Some(B256::ZERO),
358 requests_hash: Some(B256::ZERO),
359 ..Default::default()
360 },
361 shared_gas_limit,
362 general_gas_limit,
363 timestamp_millis_part: self.timestamp_millis_part,
364 ..Default::default()
365 }
366 }
367 }
368
369 fn create_valid_block(header: TempoHeader, transactions: Vec<TempoTxEnvelope>) -> Block {
370 let transactions_root = calculate_transaction_root(&transactions);
371 let mut header = header;
372 header.inner.transactions_root = transactions_root;
373
374 Block {
375 header,
376 body: BlockBody {
377 transactions,
378 withdrawals: Some(Default::default()),
379 ..Default::default()
380 },
381 }
382 }
383
384 fn create_system_tx(chain_id: u64, to: Address) -> TempoTxEnvelope {
385 let tx = TxLegacy {
386 chain_id: Some(chain_id),
387 nonce: 0,
388 gas_price: 0,
389 gas_limit: 0,
390 to: TxKind::Call(to),
391 value: U256::ZERO,
392 input: Default::default(),
393 };
394 let signature = Signature::new(U256::ZERO, U256::ZERO, false);
395 TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, signature))
396 }
397
398 fn create_tx(chain_id: u64) -> TempoTxEnvelope {
399 let tx = TxLegacy {
400 chain_id: Some(chain_id),
401 nonce: 1,
402 gas_price: 1_000_000_000,
403 gas_limit: 21000,
404 to: TxKind::Call(Address::repeat_byte(0x42)),
405 value: U256::from(100),
406 input: Default::default(),
407 };
408 TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
409 }
410
411 #[test]
412 fn test_validate_header() {
413 let consensus = TempoConsensus::new(MODERATO.clone());
414 let timestamp = current_timestamp_millis();
415 let header = TestHeaderBuilder::default()
416 .gas_limit(30_000_000)
417 .timestamp_millis(timestamp)
418 .shared_gas_limit(MODERATO.shared_gas_limit_at(timestamp / 1000, 30_000_000))
419 .build();
420 let sealed = SealedHeader::seal_slow(header);
421
422 assert!(consensus.validate_header(&sealed).is_ok());
423 }
424
425 #[test]
426 fn test_validate_header_shared_gas_mismatch() {
427 let consensus = TempoConsensus::new(MODERATO.clone());
428 let header = TestHeaderBuilder::default()
429 .gas_limit(30_000_000)
430 .timestamp_millis(current_timestamp_millis())
431 .shared_gas_limit(999)
432 .build();
433 let sealed = SealedHeader::seal_slow(header);
434
435 let result = consensus.validate_header(&sealed);
436 let err = result.unwrap_err();
437 assert!(
438 err.downcast_other_ref::<TempoConsensusError>()
439 .is_some_and(|e| matches!(e, TempoConsensusError::SharedGasLimitMismatch { .. })),
440 "Expected SharedGasLimitMismatch, got: {err:?}"
441 );
442 }
443
444 #[test]
445 fn test_validate_header_general_gas_mismatch_pre_t1() {
446 let consensus = TempoConsensus::new(create_pre_t1_chainspec());
448 let gas_limit = 500_000_000u64;
449 let shared_gas_limit = gas_limit / 10;
450 let header = TestHeaderBuilder::default()
452 .gas_limit(gas_limit)
453 .timestamp_millis(current_timestamp_millis())
454 .general_gas_limit(999)
455 .shared_gas_limit(shared_gas_limit)
456 .build();
457 let sealed = SealedHeader::seal_slow(header);
458
459 let result = consensus.validate_header(&sealed);
460 let err = result.unwrap_err();
461 assert!(
462 err.downcast_other_ref::<TempoConsensusError>()
463 .is_some_and(|e| matches!(e, TempoConsensusError::GeneralGasLimitMismatch { .. })),
464 "Expected GeneralGasLimitMismatch, got: {err:?}",
465 );
466
467 let expected_general_gas_limit = (gas_limit - shared_gas_limit) / 2;
469 let header = TestHeaderBuilder::default()
470 .gas_limit(gas_limit)
471 .timestamp_millis(current_timestamp_millis())
472 .general_gas_limit(expected_general_gas_limit)
473 .shared_gas_limit(shared_gas_limit)
474 .build();
475 let sealed = SealedHeader::seal_slow(header);
476 assert!(consensus.validate_header(&sealed).is_ok());
477 }
478
479 fn create_pre_t1_chainspec() -> Arc<TempoChainSpec> {
481 let genesis_json = r#"{
482 "config": {
483 "chainId": 99998,
484 "homesteadBlock": 0,
485 "daoForkSupport": false,
486 "eip150Block": 0,
487 "eip155Block": 0,
488 "eip158Block": 0,
489 "byzantiumBlock": 0,
490 "constantinopleBlock": 0,
491 "petersburgBlock": 0,
492 "istanbulBlock": 0,
493 "berlinBlock": 0,
494 "londonBlock": 0,
495 "mergeNetsplitBlock": 0,
496 "shanghaiTime": 0,
497 "cancunTime": 0,
498 "pragueTime": 0,
499 "osakaTime": 0,
500 "terminalTotalDifficulty": 0,
501 "terminalTotalDifficultyPassed": true,
502 "epochLength": 21600,
503 "t0Time": 0
504 },
505 "nonce": "0x42",
506 "timestamp": "0x0",
507 "extraData": "0x",
508 "gasLimit": "0x1dcd6500",
509 "difficulty": "0x0",
510 "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
511 "coinbase": "0x0000000000000000000000000000000000000000",
512 "alloc": {}
513 }"#;
514 let genesis: Genesis = serde_json::from_str(genesis_json).unwrap();
515 Arc::new(TempoChainSpec::from_genesis(genesis))
516 }
517
518 fn create_t1_chainspec() -> Arc<TempoChainSpec> {
520 let genesis_json = r#"{
521 "config": {
522 "chainId": 99999,
523 "homesteadBlock": 0,
524 "daoForkSupport": false,
525 "eip150Block": 0,
526 "eip155Block": 0,
527 "eip158Block": 0,
528 "byzantiumBlock": 0,
529 "constantinopleBlock": 0,
530 "petersburgBlock": 0,
531 "istanbulBlock": 0,
532 "berlinBlock": 0,
533 "londonBlock": 0,
534 "mergeNetsplitBlock": 0,
535 "shanghaiTime": 0,
536 "cancunTime": 0,
537 "pragueTime": 0,
538 "osakaTime": 0,
539 "terminalTotalDifficulty": 0,
540 "terminalTotalDifficultyPassed": true,
541 "epochLength": 21600,
542 "t0Time": 0,
543 "t1Time": 0
544 },
545 "nonce": "0x42",
546 "timestamp": "0x0",
547 "extraData": "0x",
548 "gasLimit": "0x1dcd6500",
549 "difficulty": "0x0",
550 "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
551 "coinbase": "0x0000000000000000000000000000000000000000",
552 "alloc": {}
553 }"#;
554 let genesis: Genesis = serde_json::from_str(genesis_json).unwrap();
555 Arc::new(TempoChainSpec::from_genesis(genesis))
556 }
557
558 fn create_t7_chainspec() -> Arc<TempoChainSpec> {
560 let genesis_json = r#"{
561 "config": {
562 "chainId": 100000,
563 "homesteadBlock": 0,
564 "daoForkSupport": false,
565 "eip150Block": 0,
566 "eip155Block": 0,
567 "eip158Block": 0,
568 "byzantiumBlock": 0,
569 "constantinopleBlock": 0,
570 "petersburgBlock": 0,
571 "istanbulBlock": 0,
572 "berlinBlock": 0,
573 "londonBlock": 0,
574 "mergeNetsplitBlock": 0,
575 "shanghaiTime": 0,
576 "cancunTime": 0,
577 "pragueTime": 0,
578 "osakaTime": 0,
579 "terminalTotalDifficulty": 0,
580 "terminalTotalDifficultyPassed": true,
581 "epochLength": 21600,
582 "t0Time": 0,
583 "t1Time": 0,
584 "t7Time": 10
585 },
586 "nonce": "0x42",
587 "timestamp": "0x0",
588 "extraData": "0x",
589 "gasLimit": "0x1dcd6500",
590 "difficulty": "0x0",
591 "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
592 "coinbase": "0x0000000000000000000000000000000000000000",
593 "alloc": {}
594 }"#;
595 let genesis: Genesis = serde_json::from_str(genesis_json).unwrap();
596 Arc::new(TempoChainSpec::from_genesis(genesis))
597 }
598
599 #[test]
600 fn test_validate_header_general_gas_limit_t1() {
601 let chainspec = create_t1_chainspec();
603 let consensus = TempoConsensus::new(chainspec);
604 let gas_limit = 500_000_000u64;
605
606 let header = TestHeaderBuilder::default()
609 .gas_limit(gas_limit)
610 .timestamp_millis(current_timestamp_millis())
611 .general_gas_limit(999)
612 .shared_gas_limit(50_000_000)
613 .build();
614 let sealed = SealedHeader::seal_slow(header);
615
616 let result = consensus.validate_header(&sealed);
617 let err = result.unwrap_err();
618 assert!(
619 err.downcast_other_ref::<TempoConsensusError>()
620 .is_some_and(|e| matches!(e, TempoConsensusError::GeneralGasLimitMismatch { .. })),
621 "Expected GeneralGasLimitMismatch, got: {err:?}",
622 );
623
624 let header = TestHeaderBuilder::default()
626 .gas_limit(gas_limit)
627 .timestamp_millis(current_timestamp_millis())
628 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
629 .shared_gas_limit(50_000_000)
630 .build();
631 let sealed = SealedHeader::seal_slow(header);
632 consensus.validate_header(&sealed).expect("should be valid");
633 }
634
635 #[test]
636 fn test_validate_header_timestamp_milli_gte_1000() {
637 let consensus = TempoConsensus::new(MODERATO.clone());
638
639 let current_timestamp_millis = 1000000999;
640
641 let header = TestHeaderBuilder::default()
643 .gas_limit(30_000_000)
644 .timestamp_millis(current_timestamp_millis)
645 .timestamp_millis_part(1000)
646 .build();
647 let sealed = SealedHeader::seal_slow(header);
648
649 let result =
650 consensus.validate_header_with_timestamp_millis(&sealed, current_timestamp_millis);
651 let err = result.unwrap_err();
652 assert!(
653 err.downcast_other_ref::<TempoConsensusError>()
654 .is_some_and(|e| matches!(
655 e,
656 TempoConsensusError::InvalidTimestampMillisPart { millis_part: 1000 }
657 )),
658 "Expected InvalidTimestampMillisPart, got: {err:?}"
659 );
660
661 let header = TestHeaderBuilder::default()
663 .gas_limit(30_000_000)
664 .timestamp_millis(current_timestamp_millis)
665 .timestamp_millis_part(1001)
666 .build();
667 let sealed = SealedHeader::seal_slow(header);
668 let result =
669 consensus.validate_header_with_timestamp_millis(&sealed, current_timestamp_millis);
670 let err = result.unwrap_err();
671 assert!(
672 err.downcast_other_ref::<TempoConsensusError>()
673 .is_some_and(|e| matches!(
674 e,
675 TempoConsensusError::InvalidTimestampMillisPart { millis_part: 1001 }
676 )),
677 "Expected InvalidTimestampMillisPart, got: {err:?}"
678 );
679 }
680
681 #[test]
682 fn test_validate_header_against_parent() {
683 use tempo_chainspec::spec::TEMPO_T1_BASE_FEE;
684
685 let consensus = TempoConsensus::new(MODERATO.clone());
686 let parent_ts = current_timestamp_millis() - 1;
687 let parent = TestHeaderBuilder::default()
688 .gas_limit(30_000_000)
689 .timestamp(parent_ts)
690 .number(1)
691 .timestamp_millis_part(500)
692 .base_fee(TEMPO_T1_BASE_FEE)
693 .build();
694 let parent_sealed = SealedHeader::seal_slow(parent);
695
696 let child = TestHeaderBuilder::default()
697 .gas_limit(30_000_000)
698 .timestamp(parent_ts + 1)
699 .timestamp_millis_part(600)
700 .number(2)
701 .base_fee(TEMPO_T1_BASE_FEE)
702 .parent_hash(parent_sealed.hash())
703 .build();
704 let child_sealed = SealedHeader::seal_slow(child);
705
706 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
707 assert!(result.is_ok());
708 }
709
710 #[test]
711 fn test_validate_header_against_parent_timestamp_not_increasing() {
712 use tempo_chainspec::spec::TEMPO_T1_BASE_FEE;
713
714 let consensus = TempoConsensus::new(MODERATO.clone());
715 let parent_ts = current_timestamp_millis();
716 let parent = TestHeaderBuilder::default()
717 .gas_limit(30_000_000)
718 .timestamp(parent_ts)
719 .timestamp_millis_part(500)
720 .base_fee(TEMPO_T1_BASE_FEE)
721 .build();
722 let parent_sealed = SealedHeader::seal_slow(parent);
723
724 let child = TestHeaderBuilder::default()
725 .gas_limit(30_000_000)
726 .timestamp(parent_ts)
727 .timestamp_millis_part(400)
728 .number(1)
729 .base_fee(TEMPO_T1_BASE_FEE)
730 .parent_hash(parent_sealed.hash())
731 .build();
732 let child_sealed = SealedHeader::seal_slow(child);
733
734 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
735 assert!(matches!(
736 result,
737 Err(ConsensusError::TimestampIsInPast { .. })
738 ));
739 }
740
741 #[test]
742 fn test_validate_header_against_parent_t1() {
743 use tempo_chainspec::spec::TEMPO_T1_BASE_FEE;
744
745 let chainspec = create_t1_chainspec();
746 let consensus = TempoConsensus::new(chainspec);
747
748 let parent_ts = current_timestamp_millis() - 1;
749 let parent = TestHeaderBuilder::default()
750 .gas_limit(500_000_000)
751 .timestamp(parent_ts)
752 .number(1)
753 .timestamp_millis_part(500)
754 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
755 .base_fee(TEMPO_T1_BASE_FEE)
756 .build();
757 let parent_sealed = SealedHeader::seal_slow(parent);
758
759 let child = TestHeaderBuilder::default()
760 .gas_limit(500_000_000)
761 .timestamp(parent_ts + 1)
762 .timestamp_millis_part(600)
763 .number(2)
764 .parent_hash(parent_sealed.hash())
765 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
766 .base_fee(TEMPO_T1_BASE_FEE)
767 .build();
768 let child_sealed = SealedHeader::seal_slow(child);
769
770 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
771 assert!(result.is_ok(), "T1 validation failed: {result:?}");
772 }
773
774 #[test]
775 fn test_validate_header_against_parent_t1_wrong_base_fee() {
776 use tempo_chainspec::spec::{TEMPO_T0_BASE_FEE, TEMPO_T1_BASE_FEE};
777
778 let chainspec = create_t1_chainspec();
779 let consensus = TempoConsensus::new(chainspec);
780
781 let parent_ts = current_timestamp_millis() - 1;
782 let parent = TestHeaderBuilder::default()
783 .gas_limit(500_000_000)
784 .timestamp(parent_ts)
785 .number(1)
786 .timestamp_millis_part(500)
787 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
788 .base_fee(TEMPO_T1_BASE_FEE)
789 .build();
790 let parent_sealed = SealedHeader::seal_slow(parent);
791
792 let child = TestHeaderBuilder::default()
794 .gas_limit(500_000_000)
795 .timestamp(parent_ts + 1)
796 .timestamp_millis_part(600)
797 .number(2)
798 .parent_hash(parent_sealed.hash())
799 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
800 .base_fee(TEMPO_T0_BASE_FEE)
801 .build();
802 let child_sealed = SealedHeader::seal_slow(child);
803
804 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
805 assert!(
806 matches!(result, Err(ConsensusError::BaseFeeDiff(_))),
807 "Expected BaseFeeDiff error, got: {result:?}"
808 );
809 }
810
811 #[test]
812 fn test_validate_header_against_parent_t7_dynamic_base_fee() {
813 use tempo_chainspec::spec::{TEMPO_T7_BASE_FEE_CAP, TEMPO_T7_BASE_FEE_FLOOR};
814
815 let chainspec = create_t7_chainspec();
816 let consensus = TempoConsensus::new(chainspec);
817
818 let parent = TestHeaderBuilder::default()
819 .gas_limit(500_000_000)
820 .timestamp(10)
821 .number(1)
822 .timestamp_millis_part(500)
823 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
824 .base_fee(TEMPO_T7_BASE_FEE_CAP)
825 .gas_used(0)
826 .build();
827 let parent_sealed = SealedHeader::seal_slow(parent);
828
829 let child = TestHeaderBuilder::default()
830 .gas_limit(500_000_000)
831 .timestamp(11)
832 .timestamp_millis_part(600)
833 .number(2)
834 .parent_hash(parent_sealed.hash())
835 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
836 .base_fee(TEMPO_T7_BASE_FEE_CAP * 7 / 8)
837 .build();
838 let child_sealed = SealedHeader::seal_slow(child);
839
840 assert!(
841 consensus
842 .validate_header_against_parent(&child_sealed, &parent_sealed)
843 .is_ok()
844 );
845
846 let bad_child = TestHeaderBuilder::default()
847 .gas_limit(500_000_000)
848 .timestamp(11)
849 .timestamp_millis_part(600)
850 .number(2)
851 .parent_hash(parent_sealed.hash())
852 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
853 .base_fee(TEMPO_T7_BASE_FEE_CAP)
854 .build();
855 let bad_child_sealed = SealedHeader::seal_slow(bad_child);
856 let result = consensus.validate_header_against_parent(&bad_child_sealed, &parent_sealed);
857 assert!(
858 matches!(result, Err(ConsensusError::BaseFeeDiff(_))),
859 "Expected BaseFeeDiff error, got: {result:?}"
860 );
861
862 let parent = TestHeaderBuilder::default()
863 .gas_limit(500_000_000)
864 .timestamp(10)
865 .number(1)
866 .timestamp_millis_part(500)
867 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
868 .base_fee(TEMPO_T7_BASE_FEE_FLOOR)
869 .gas_used(0)
870 .build();
871 let parent_sealed = SealedHeader::seal_slow(parent);
872 let child = TestHeaderBuilder::default()
873 .gas_limit(500_000_000)
874 .timestamp(11)
875 .timestamp_millis_part(600)
876 .number(2)
877 .parent_hash(parent_sealed.hash())
878 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
879 .base_fee(TEMPO_T7_BASE_FEE_FLOOR)
880 .build();
881 let child_sealed = SealedHeader::seal_slow(child);
882
883 assert!(
884 consensus
885 .validate_header_against_parent(&child_sealed, &parent_sealed)
886 .is_ok()
887 );
888 }
889
890 #[test]
891 fn test_validate_body_against_header() {
892 let consensus = TempoConsensus::new(MODERATO.clone());
893 let header = TestHeaderBuilder::default()
894 .gas_limit(30_000_000)
895 .timestamp(current_timestamp_millis())
896 .build();
897 let sealed = SealedHeader::seal_slow(header);
898 let body = BlockBody {
899 withdrawals: Some(Default::default()),
900 ..Default::default()
901 };
902
903 assert!(
904 consensus
905 .validate_body_against_header(&body, &sealed)
906 .is_ok()
907 );
908 }
909
910 #[test]
911 fn test_validate_block_pre_execution() {
912 let consensus = TempoConsensus::new(MODERATO.clone());
913 let chain_id = MODERATO.chain().id();
914
915 let system_tx = create_system_tx(chain_id, SYSTEM_TX_ADDRESSES[0]);
916 let user_tx = create_tx(chain_id);
917
918 let header = TestHeaderBuilder::default()
919 .gas_limit(30_000_000)
920 .timestamp(current_timestamp_millis())
921 .build();
922 let block = create_valid_block(header, vec![user_tx, system_tx]);
923 let sealed = reth_primitives_traits::SealedBlock::seal_slow(block);
924
925 assert!(consensus.validate_block_pre_execution(&sealed).is_ok());
926 }
927
928 #[test]
929 fn test_validate_block_pre_execution_invalid_system_tx() {
930 let consensus = TempoConsensus::new(MODERATO.clone());
931 let chain_id = MODERATO.chain().id();
932
933 let tx = TxLegacy {
934 chain_id: Some(chain_id),
935 nonce: 0,
936 gas_price: 1_000_000_000,
937 gas_limit: 21000,
938 to: TxKind::Call(Address::ZERO),
939 value: U256::ZERO,
940 input: Default::default(),
941 };
942 let signature = Signature::new(U256::ZERO, U256::ZERO, false);
943 let invalid_system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, signature));
944 let tx_hash = *invalid_system_tx.tx_hash();
945
946 let header = TestHeaderBuilder::default()
947 .gas_limit(30_000_000)
948 .timestamp(current_timestamp_millis())
949 .build();
950 let block = create_valid_block(header, vec![invalid_system_tx]);
951 let sealed = SealedBlock::seal_slow(block);
952
953 let result = consensus.validate_block_pre_execution(&sealed);
954 let err = result.unwrap_err();
955 assert!(
956 err.downcast_other_ref::<TempoConsensusError>()
957 .is_some_and(
958 |e| matches!(e, TempoConsensusError::InvalidSystemTransaction { tx_hash: h } if *h == tx_hash)
959 ),
960 "Expected InvalidSystemTransaction, got: {err:?}"
961 );
962 }
963
964 #[test]
965 fn test_validate_block_pre_execution_pre_t4_missing_system_tx() {
966 let consensus = TempoConsensus::new(MODERATO.clone());
967 let chain_id = MODERATO.chain().id();
968
969 let user_tx = create_tx(chain_id);
970
971 use tempo_chainspec::constants::moderato::MODERATO_T4_TIMESTAMP;
972
973 let header = TestHeaderBuilder::default()
974 .gas_limit(30_000_000)
975 .timestamp(MODERATO_T4_TIMESTAMP - 1)
976 .build();
977 let block = create_valid_block(header, vec![user_tx]);
978 let sealed = SealedBlock::seal_slow(block);
979
980 let result = consensus.validate_block_pre_execution(&sealed);
981 let err = result.unwrap_err();
982 assert!(
983 err.downcast_other_ref::<TempoConsensusError>()
984 .is_some_and(|e| matches!(
985 e,
986 TempoConsensusError::MissingEndOfBlockSystemTxs { .. }
987 )),
988 "Expected MissingEndOfBlockSystemTxs, got: {err:?}"
989 );
990 }
991
992 #[test]
993 fn test_validate_block_pre_execution_t4_allows_missing_system_tx() {
994 let consensus = TempoConsensus::new(DEV.clone());
995 let chain_id = DEV.chain().id();
996
997 let user_tx = create_tx(chain_id);
998
999 let header = TestHeaderBuilder::default()
1000 .gas_limit(30_000_000)
1001 .timestamp(0)
1002 .build();
1003 let block = create_valid_block(header, vec![user_tx]);
1004 let sealed = SealedBlock::seal_slow(block);
1005
1006 assert!(consensus.validate_block_pre_execution(&sealed).is_ok());
1007 }
1008
1009 #[test]
1010 fn test_validate_body_against_header_bad_tx_root() {
1011 let consensus = TempoConsensus::new(MODERATO.clone());
1012 let header = TestHeaderBuilder::default()
1013 .gas_limit(30_000_000)
1014 .timestamp(current_timestamp_millis())
1015 .build();
1016 let sealed = SealedHeader::seal_slow(header);
1017
1018 let chain_id = MODERATO.chain().id();
1019 let user_tx = create_tx(chain_id);
1020 let body = BlockBody {
1021 transactions: vec![user_tx],
1022 withdrawals: Some(Default::default()),
1023 ..Default::default()
1024 };
1025
1026 let result = consensus.validate_body_against_header(&body, &sealed);
1027 assert!(
1028 matches!(result, Err(ConsensusError::BodyTransactionRootDiff(_))),
1029 "Expected BodyTransactionRootDiff error, got: {result:?}"
1030 );
1031 }
1032
1033 #[test]
1034 fn test_validate_block_post_execution_bad_receipts() {
1035 let consensus = TempoConsensus::new(MODERATO.clone());
1036 let chain_id = MODERATO.chain().id();
1037
1038 let system_tx = create_system_tx(chain_id, SYSTEM_TX_ADDRESSES[0]);
1039 let user_tx = create_tx(chain_id);
1040
1041 let header = TestHeaderBuilder::default()
1042 .gas_limit(30_000_000)
1043 .timestamp(current_timestamp_millis())
1044 .build();
1045 let block = create_valid_block(header, vec![user_tx, system_tx]);
1046 let recovered = RecoveredBlock::new_unhashed(block, vec![Address::ZERO, Address::ZERO]);
1047
1048 let receipt = TempoReceipt {
1049 tx_type: tempo_primitives::TempoTxType::Legacy,
1050 success: true,
1051 cumulative_gas_used: 0,
1052 logs: vec![],
1053 };
1054 let result = BlockExecutionResult {
1055 receipts: vec![receipt],
1056 requests: Default::default(),
1057 gas_used: 0,
1058 blob_gas_used: 0,
1059 };
1060
1061 let err = consensus
1062 .validate_block_post_execution(&recovered, &result, None, None)
1063 .unwrap_err();
1064 assert!(
1065 matches!(err, ConsensusError::BodyReceiptRootDiff(_)),
1066 "Expected BodyReceiptRootDiff error, got: {err:?}"
1067 );
1068 }
1069
1070 #[test]
1071 fn test_validate_header_timestamp_exactly_at_boundary() {
1072 let consensus = TempoConsensus::new(MODERATO.clone());
1073 let boundary_timestamp = current_timestamp_millis() + ALLOWED_FUTURE_BLOCK_TIME_MILLIS;
1074 let header = TestHeaderBuilder::default()
1075 .gas_limit(30_000_000)
1076 .timestamp_millis(boundary_timestamp)
1077 .shared_gas_limit(MODERATO.shared_gas_limit_at(boundary_timestamp / 1000, 30_000_000))
1078 .build();
1079 let sealed = SealedHeader::seal_slow(header);
1080
1081 let result = consensus.validate_header(&sealed);
1082 assert!(
1083 result.is_ok(),
1084 "Timestamp exactly at boundary should be accepted, got: {result:?}"
1085 );
1086 }
1087
1088 #[test]
1089 fn test_timestamp_in_future_is_transient_error() {
1090 let consensus = TempoConsensus::new(MODERATO.clone());
1091 let err = ConsensusError::TimestampIsInFuture {
1092 timestamp: 2,
1093 present_timestamp: 1,
1094 };
1095
1096 assert!(Consensus::<Block>::is_transient_error(&consensus, &err));
1097
1098 let err = ConsensusError::TimestampIsInPast {
1099 parent_timestamp: 2,
1100 timestamp: 1,
1101 };
1102
1103 assert!(!Consensus::<Block>::is_transient_error(&consensus, &err));
1104 }
1105
1106 #[test]
1107 fn test_validate_block_pre_execution_system_tx_out_of_order() {
1108 let consensus = TempoConsensus::new(MODERATO.clone());
1109 let chain_id = MODERATO.chain().id();
1110
1111 let wrong_addr = Address::repeat_byte(0xFF);
1112 let system_tx = create_system_tx(chain_id, wrong_addr);
1113
1114 use tempo_chainspec::constants::moderato::MODERATO_T4_TIMESTAMP;
1115
1116 let header = TestHeaderBuilder::default()
1117 .gas_limit(30_000_000)
1118 .timestamp(MODERATO_T4_TIMESTAMP - 1)
1119 .build();
1120 let block = create_valid_block(header, vec![system_tx]);
1121 let sealed = SealedBlock::seal_slow(block);
1122
1123 let result = consensus.validate_block_pre_execution(&sealed);
1124 let err = result.unwrap_err();
1125 assert!(
1126 err.downcast_other_ref::<TempoConsensusError>()
1127 .is_some_and(|e| matches!(
1128 e,
1129 TempoConsensusError::InvalidEndOfBlockSystemTxOrder { .. }
1130 )),
1131 "Expected InvalidEndOfBlockSystemTxOrder, got: {err:?}"
1132 );
1133 }
1134}