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