1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6use alloy_consensus::{BlockHeader, Transaction, transaction::TxHashRef};
7use alloy_evm::block::BlockExecutionResult;
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_SECONDS: u64 = 3;
27
28pub const TEMPO_SHARED_GAS_DIVISOR: u64 = 10;
31
32pub const TEMPO_MAXIMUM_EXTRA_DATA_SIZE: usize = 10 * 1_024; #[derive(Debug, Clone)]
37pub struct TempoConsensus {
38 inner: EthBeaconConsensus<TempoChainSpec>,
40}
41
42impl TempoConsensus {
43 pub fn new(chain_spec: Arc<TempoChainSpec>) -> Self {
45 Self {
46 inner: EthBeaconConsensus::new(chain_spec)
47 .with_max_extra_data_size(TEMPO_MAXIMUM_EXTRA_DATA_SIZE),
48 }
49 }
50}
51
52impl HeaderValidator<TempoHeader> for TempoConsensus {
53 fn validate_header(&self, header: &SealedHeader<TempoHeader>) -> Result<(), ConsensusError> {
54 self.inner.validate_header(header)?;
55
56 let present_timestamp = std::time::SystemTime::now()
57 .duration_since(std::time::SystemTime::UNIX_EPOCH)
58 .expect("system time should never be before UNIX EPOCH")
59 .as_secs();
60
61 if header.timestamp() > present_timestamp + ALLOWED_FUTURE_BLOCK_TIME_SECONDS {
62 return Err(ConsensusError::TimestampIsInFuture {
63 timestamp: header.timestamp(),
64 present_timestamp,
65 });
66 }
67
68 if header.shared_gas_limit != header.gas_limit() / TEMPO_SHARED_GAS_DIVISOR {
69 return Err(ConsensusError::Other(
70 "Shared gas limit does not match header gas limit".to_string(),
71 ));
72 }
73
74 let expected_general_gas_limit = self.inner.chain_spec().general_gas_limit_at(
76 header.timestamp(),
77 header.gas_limit(),
78 header.shared_gas_limit,
79 );
80
81 if header.general_gas_limit != expected_general_gas_limit {
82 return Err(ConsensusError::Other(format!(
83 "General gas limit {} does not match expected {}",
84 header.general_gas_limit, expected_general_gas_limit
85 )));
86 }
87
88 if header.timestamp_millis_part >= 1000 {
90 return Err(ConsensusError::Other(
91 "Timestamp milliseconds part must be less than 1000".to_string(),
92 ));
93 }
94
95 Ok(())
96 }
97
98 fn validate_header_against_parent(
99 &self,
100 header: &SealedHeader<TempoHeader>,
101 parent: &SealedHeader<TempoHeader>,
102 ) -> Result<(), ConsensusError> {
103 validate_against_parent_hash_number(header.header(), parent)?;
104
105 validate_against_parent_gas_limit(header, parent, self.inner.chain_spec())?;
106
107 validate_against_parent_eip1559_base_fee(
108 header.header(),
109 parent.header(),
110 self.inner.chain_spec(),
111 )?;
112
113 if let Some(blob_params) = self
114 .inner
115 .chain_spec()
116 .blob_params_at_timestamp(header.timestamp())
117 {
118 validate_against_parent_4844(header.header(), parent.header(), blob_params)?;
119 }
120
121 if header.timestamp_millis() <= parent.timestamp_millis() {
122 return Err(ConsensusError::TimestampIsInPast {
123 parent_timestamp: parent.timestamp_millis(),
124 timestamp: header.timestamp_millis(),
125 });
126 }
127
128 Ok(())
129 }
130}
131
132impl Consensus<Block> for TempoConsensus {
133 fn validate_body_against_header(
134 &self,
135 body: &BlockBody,
136 header: &SealedHeader<TempoHeader>,
137 ) -> Result<(), ConsensusError> {
138 Consensus::<Block>::validate_body_against_header(&self.inner, body, header)
139 }
140
141 fn validate_block_pre_execution(
142 &self,
143 block: &SealedBlock<Block>,
144 ) -> Result<(), ConsensusError> {
145 let transactions = &block.body().transactions;
146
147 if let Some(tx) = transactions.iter().find(|&tx| {
148 tx.is_system_tx() && !tx.is_valid_system_tx(self.inner.chain_spec().chain().id())
149 }) {
150 return Err(ConsensusError::Other(format!(
151 "Invalid system transaction: {}",
152 tx.tx_hash()
153 )));
154 }
155
156 let end_of_block_system_txs = transactions
158 .get(transactions.len().saturating_sub(SYSTEM_TX_COUNT)..)
159 .map(|slice| {
160 slice
161 .iter()
162 .filter(|tx| tx.is_system_tx())
163 .collect::<Vec<&TempoTxEnvelope>>()
164 })
165 .unwrap_or_default();
166
167 if end_of_block_system_txs.len() != SYSTEM_TX_COUNT {
168 return Err(ConsensusError::Other(
169 "Block must contain end-of-block system txs".to_string(),
170 ));
171 }
172
173 for (tx, expected_to) in end_of_block_system_txs.into_iter().zip(SYSTEM_TX_ADDRESSES) {
175 if tx.to().unwrap_or_default() != expected_to {
176 return Err(ConsensusError::Other(
177 "Invalid end-of-block system tx order".to_string(),
178 ));
179 }
180 }
181
182 self.inner.validate_block_pre_execution(block)
183 }
184}
185
186impl FullConsensus<TempoPrimitives> for TempoConsensus {
187 fn validate_block_post_execution(
188 &self,
189 block: &RecoveredBlock<Block>,
190 result: &BlockExecutionResult<TempoReceipt>,
191 receipt_root_bloom: Option<ReceiptRootBloom>,
192 ) -> Result<(), ConsensusError> {
193 FullConsensus::<TempoPrimitives>::validate_block_post_execution(
194 &self.inner,
195 block,
196 result,
197 receipt_root_bloom,
198 )
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use alloy_consensus::{
206 Header, Signed, TxLegacy, constants::EMPTY_ROOT_HASH, proofs::calculate_transaction_root,
207 transaction::TxHashRef,
208 };
209 use alloy_genesis::Genesis;
210 use alloy_primitives::{Address, B256, Signature, TxKind, U256};
211 use reth_primitives_traits::SealedHeader;
212 use std::time::{SystemTime, UNIX_EPOCH};
213 use tempo_chainspec::{
214 hardfork::TempoHardfork,
215 spec::{ANDANTINO, TempoChainSpec},
216 };
217
218 fn current_timestamp() -> u64 {
219 SystemTime::now()
220 .duration_since(UNIX_EPOCH)
221 .unwrap()
222 .as_secs()
223 }
224
225 #[derive(Default)]
226 struct TestHeaderBuilder {
227 gas_limit: u64,
228 timestamp: u64,
229 timestamp_millis_part: u64,
230 number: u64,
231 parent_hash: B256,
232 shared_gas_limit: Option<u64>,
233 general_gas_limit: Option<u64>,
234 base_fee: Option<u64>,
235 }
236
237 impl TestHeaderBuilder {
238 fn gas_limit(mut self, gas_limit: u64) -> Self {
239 self.gas_limit = gas_limit;
240 self
241 }
242
243 fn timestamp(mut self, timestamp: u64) -> Self {
244 self.timestamp = timestamp;
245 self
246 }
247
248 fn timestamp_millis_part(mut self, millis: u64) -> Self {
249 self.timestamp_millis_part = millis;
250 self
251 }
252
253 fn number(mut self, number: u64) -> Self {
254 self.number = number;
255 self
256 }
257
258 fn parent_hash(mut self, hash: B256) -> Self {
259 self.parent_hash = hash;
260 self
261 }
262
263 fn shared_gas_limit(mut self, limit: u64) -> Self {
264 self.shared_gas_limit = Some(limit);
265 self
266 }
267
268 fn general_gas_limit(mut self, limit: u64) -> Self {
269 self.general_gas_limit = Some(limit);
270 self
271 }
272
273 fn base_fee(mut self, fee: u64) -> Self {
274 self.base_fee = Some(fee);
275 self
276 }
277
278 fn build(self) -> TempoHeader {
279 let shared_gas_limit = self
280 .shared_gas_limit
281 .unwrap_or(self.gas_limit / TEMPO_SHARED_GAS_DIVISOR);
282 let general_gas_limit = self
284 .general_gas_limit
285 .unwrap_or_else(|| (self.gas_limit - shared_gas_limit) / 2);
286
287 TempoHeader {
288 inner: Header {
289 gas_limit: self.gas_limit,
290 timestamp: self.timestamp,
291 number: self.number,
292 parent_hash: self.parent_hash,
293 base_fee_per_gas: Some(
294 self.base_fee
295 .unwrap_or(tempo_chainspec::spec::TEMPO_T0_BASE_FEE),
296 ),
297 withdrawals_root: Some(EMPTY_ROOT_HASH),
298 blob_gas_used: Some(0),
299 excess_blob_gas: Some(0),
300 parent_beacon_block_root: Some(B256::ZERO),
301 requests_hash: Some(B256::ZERO),
302 ..Default::default()
303 },
304 shared_gas_limit,
305 general_gas_limit,
306 timestamp_millis_part: self.timestamp_millis_part,
307 }
308 }
309 }
310
311 fn create_valid_block(header: TempoHeader, transactions: Vec<TempoTxEnvelope>) -> Block {
312 let transactions_root = calculate_transaction_root(&transactions);
313 let mut header = header;
314 header.inner.transactions_root = transactions_root;
315
316 Block {
317 header,
318 body: BlockBody {
319 transactions,
320 withdrawals: Some(Default::default()),
321 ..Default::default()
322 },
323 }
324 }
325
326 fn create_system_tx(chain_id: u64, to: Address) -> TempoTxEnvelope {
327 let tx = TxLegacy {
328 chain_id: Some(chain_id),
329 nonce: 0,
330 gas_price: 0,
331 gas_limit: 0,
332 to: TxKind::Call(to),
333 value: U256::ZERO,
334 input: Default::default(),
335 };
336 let signature = Signature::new(U256::ZERO, U256::ZERO, false);
337 TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, signature))
338 }
339
340 fn create_tx(chain_id: u64) -> TempoTxEnvelope {
341 let tx = TxLegacy {
342 chain_id: Some(chain_id),
343 nonce: 1,
344 gas_price: 1_000_000_000,
345 gas_limit: 21000,
346 to: TxKind::Call(Address::repeat_byte(0x42)),
347 value: U256::from(100),
348 input: Default::default(),
349 };
350 TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
351 }
352
353 #[test]
354 fn test_validate_header() {
355 let consensus = TempoConsensus::new(ANDANTINO.clone());
356 let header = TestHeaderBuilder::default()
357 .gas_limit(30_000_000)
358 .timestamp(current_timestamp())
359 .timestamp_millis_part(500)
360 .build();
361 let sealed = SealedHeader::seal_slow(header);
362
363 assert!(consensus.validate_header(&sealed).is_ok());
364 }
365
366 #[test]
367 fn test_validate_header_timestamp_in_the_future() {
368 let consensus = TempoConsensus::new(ANDANTINO.clone());
369 let future_timestamp = current_timestamp() + ALLOWED_FUTURE_BLOCK_TIME_SECONDS + 10;
370 let header = TestHeaderBuilder::default()
371 .gas_limit(30_000_000)
372 .timestamp(future_timestamp)
373 .timestamp_millis_part(500)
374 .build();
375 let sealed = SealedHeader::seal_slow(header);
376
377 let result = consensus.validate_header(&sealed);
378 assert!(
379 matches!(result, Err(ConsensusError::TimestampIsInFuture { timestamp, .. }) if timestamp == future_timestamp)
380 );
381 }
382
383 #[test]
384 fn test_validate_header_shared_gas_mismatch() {
385 let consensus = TempoConsensus::new(ANDANTINO.clone());
386 let header = TestHeaderBuilder::default()
387 .gas_limit(30_000_000)
388 .timestamp(current_timestamp())
389 .shared_gas_limit(999)
390 .build();
391 let sealed = SealedHeader::seal_slow(header);
392
393 let result = consensus.validate_header(&sealed);
394 let err = result.unwrap_err();
395 assert!(matches!(err, ConsensusError::Other(_)));
396 assert!(
397 err.to_string()
398 .contains("Shared gas limit does not match header gas limit")
399 );
400 }
401
402 #[test]
403 fn test_validate_header_general_gas_mismatch_pre_t1() {
404 let consensus = TempoConsensus::new(ANDANTINO.clone());
406 let gas_limit = 500_000_000u64;
407 let shared_gas_limit = gas_limit / TEMPO_SHARED_GAS_DIVISOR;
408 let header = TestHeaderBuilder::default()
411 .gas_limit(gas_limit)
412 .timestamp(current_timestamp())
413 .general_gas_limit(999)
414 .build();
415 let sealed = SealedHeader::seal_slow(header);
416
417 let result = consensus.validate_header(&sealed);
418 let err = result.unwrap_err();
419 assert!(matches!(err, ConsensusError::Other(_)));
420 assert!(
421 err.to_string().contains("General gas limit"),
422 "Expected error about general gas limit, got: {err}",
423 );
424
425 let expected_general_gas_limit = (gas_limit - shared_gas_limit) / 2;
427 let header = TestHeaderBuilder::default()
428 .gas_limit(gas_limit)
429 .timestamp(current_timestamp())
430 .general_gas_limit(expected_general_gas_limit)
431 .build();
432 let sealed = SealedHeader::seal_slow(header);
433 assert!(consensus.validate_header(&sealed).is_ok());
434 }
435
436 fn create_t1_chainspec() -> Arc<TempoChainSpec> {
438 let genesis_json = r#"{
439 "config": {
440 "chainId": 99999,
441 "homesteadBlock": 0,
442 "daoForkSupport": false,
443 "eip150Block": 0,
444 "eip155Block": 0,
445 "eip158Block": 0,
446 "byzantiumBlock": 0,
447 "constantinopleBlock": 0,
448 "petersburgBlock": 0,
449 "istanbulBlock": 0,
450 "berlinBlock": 0,
451 "londonBlock": 0,
452 "mergeNetsplitBlock": 0,
453 "shanghaiTime": 0,
454 "cancunTime": 0,
455 "pragueTime": 0,
456 "osakaTime": 0,
457 "terminalTotalDifficulty": 0,
458 "terminalTotalDifficultyPassed": true,
459 "epochLength": 21600,
460 "t0Time": 0,
461 "t1Time": 0
462 },
463 "nonce": "0x42",
464 "timestamp": "0x0",
465 "extraData": "0x",
466 "gasLimit": "0x1dcd6500",
467 "difficulty": "0x0",
468 "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
469 "coinbase": "0x0000000000000000000000000000000000000000",
470 "alloc": {}
471 }"#;
472 let genesis: Genesis = serde_json::from_str(genesis_json).unwrap();
473 Arc::new(TempoChainSpec::from_genesis(genesis))
474 }
475
476 #[test]
477 fn test_validate_header_general_gas_limit_t1() {
478 let chainspec = create_t1_chainspec();
480 let consensus = TempoConsensus::new(chainspec);
481 let gas_limit = 500_000_000u64;
482
483 let header = TestHeaderBuilder::default()
486 .gas_limit(gas_limit)
487 .timestamp(current_timestamp())
488 .general_gas_limit(999)
489 .build();
490 let sealed = SealedHeader::seal_slow(header);
491
492 let result = consensus.validate_header(&sealed);
493 let err = result.unwrap_err();
494 assert!(matches!(err, ConsensusError::Other(_)));
495 assert!(
496 err.to_string().contains("General gas limit"),
497 "Expected error about general gas limit, got: {err}",
498 );
499
500 let header = TestHeaderBuilder::default()
502 .gas_limit(gas_limit)
503 .timestamp(current_timestamp())
504 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
505 .build();
506 let sealed = SealedHeader::seal_slow(header);
507 assert!(consensus.validate_header(&sealed).is_ok());
508 }
509
510 #[test]
511 fn test_validate_header_timestamp_milli_gte_1000() {
512 let consensus = TempoConsensus::new(ANDANTINO.clone());
513
514 let header = TestHeaderBuilder::default()
516 .gas_limit(30_000_000)
517 .timestamp(current_timestamp())
518 .timestamp_millis_part(1000)
519 .build();
520 let sealed = SealedHeader::seal_slow(header);
521
522 let result = consensus.validate_header(&sealed);
523 let err = result.unwrap_err();
524 assert!(matches!(err, ConsensusError::Other(_)));
525 assert!(
526 err.to_string()
527 .contains("Timestamp milliseconds part must be less than 1000")
528 );
529
530 let header = TestHeaderBuilder::default()
532 .gas_limit(30_000_000)
533 .timestamp(current_timestamp())
534 .timestamp_millis_part(1001)
535 .build();
536 let sealed = SealedHeader::seal_slow(header);
537 let result = consensus.validate_header(&sealed);
538 let err = result.unwrap_err();
539 assert!(matches!(err, ConsensusError::Other(_)));
540 assert!(
541 err.to_string()
542 .contains("Timestamp milliseconds part must be less than 1000")
543 );
544 }
545
546 #[test]
547 fn test_validate_header_against_parent() {
548 let consensus = TempoConsensus::new(ANDANTINO.clone());
549 let parent_ts = current_timestamp() - 1;
550 let parent = TestHeaderBuilder::default()
551 .gas_limit(30_000_000)
552 .timestamp(parent_ts)
553 .number(1)
554 .timestamp_millis_part(500)
555 .build();
556 let parent_sealed = SealedHeader::seal_slow(parent);
557
558 let child = TestHeaderBuilder::default()
559 .gas_limit(30_000_000)
560 .timestamp(parent_ts + 1)
561 .timestamp_millis_part(600)
562 .number(2)
563 .parent_hash(parent_sealed.hash())
564 .build();
565 let child_sealed = SealedHeader::seal_slow(child);
566
567 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
568 assert!(result.is_ok());
569 }
570
571 #[test]
572 fn test_validate_header_against_parent_timestamp_not_increasing() {
573 let consensus = TempoConsensus::new(ANDANTINO.clone());
574 let parent_ts = current_timestamp();
575 let parent = TestHeaderBuilder::default()
576 .gas_limit(30_000_000)
577 .timestamp(parent_ts)
578 .timestamp_millis_part(500)
579 .build();
580 let parent_sealed = SealedHeader::seal_slow(parent);
581
582 let child = TestHeaderBuilder::default()
583 .gas_limit(30_000_000)
584 .timestamp(parent_ts)
585 .timestamp_millis_part(400)
586 .number(1)
587 .parent_hash(parent_sealed.hash())
588 .build();
589 let child_sealed = SealedHeader::seal_slow(child);
590
591 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
592 assert!(matches!(
593 result,
594 Err(ConsensusError::TimestampIsInPast { .. })
595 ));
596 }
597
598 #[test]
599 fn test_validate_header_against_parent_t1() {
600 use tempo_chainspec::spec::TEMPO_T1_BASE_FEE;
601
602 let chainspec = create_t1_chainspec();
603 let consensus = TempoConsensus::new(chainspec);
604
605 let parent_ts = current_timestamp() - 1;
606 let parent = TestHeaderBuilder::default()
607 .gas_limit(500_000_000)
608 .timestamp(parent_ts)
609 .number(1)
610 .timestamp_millis_part(500)
611 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
612 .base_fee(TEMPO_T1_BASE_FEE)
613 .build();
614 let parent_sealed = SealedHeader::seal_slow(parent);
615
616 let child = TestHeaderBuilder::default()
617 .gas_limit(500_000_000)
618 .timestamp(parent_ts + 1)
619 .timestamp_millis_part(600)
620 .number(2)
621 .parent_hash(parent_sealed.hash())
622 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
623 .base_fee(TEMPO_T1_BASE_FEE)
624 .build();
625 let child_sealed = SealedHeader::seal_slow(child);
626
627 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
628 assert!(result.is_ok(), "T1 validation failed: {result:?}");
629 }
630
631 #[test]
632 fn test_validate_header_against_parent_t1_wrong_base_fee() {
633 use tempo_chainspec::spec::{TEMPO_T0_BASE_FEE, TEMPO_T1_BASE_FEE};
634
635 let chainspec = create_t1_chainspec();
636 let consensus = TempoConsensus::new(chainspec);
637
638 let parent_ts = current_timestamp() - 1;
639 let parent = TestHeaderBuilder::default()
640 .gas_limit(500_000_000)
641 .timestamp(parent_ts)
642 .number(1)
643 .timestamp_millis_part(500)
644 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
645 .base_fee(TEMPO_T1_BASE_FEE)
646 .build();
647 let parent_sealed = SealedHeader::seal_slow(parent);
648
649 let child = TestHeaderBuilder::default()
651 .gas_limit(500_000_000)
652 .timestamp(parent_ts + 1)
653 .timestamp_millis_part(600)
654 .number(2)
655 .parent_hash(parent_sealed.hash())
656 .general_gas_limit(TempoHardfork::T1.general_gas_limit().unwrap())
657 .base_fee(TEMPO_T0_BASE_FEE)
658 .build();
659 let child_sealed = SealedHeader::seal_slow(child);
660
661 let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed);
662 assert!(
663 matches!(result, Err(ConsensusError::BaseFeeDiff(_))),
664 "Expected BaseFeeDiff error, got: {result:?}"
665 );
666 }
667
668 #[test]
669 fn test_validate_body_against_header() {
670 let consensus = TempoConsensus::new(ANDANTINO.clone());
671 let header = TestHeaderBuilder::default()
672 .gas_limit(30_000_000)
673 .timestamp(current_timestamp())
674 .build();
675 let sealed = SealedHeader::seal_slow(header);
676 let body = BlockBody {
677 withdrawals: Some(Default::default()),
678 ..Default::default()
679 };
680
681 assert!(
682 consensus
683 .validate_body_against_header(&body, &sealed)
684 .is_ok()
685 );
686 }
687
688 #[test]
689 fn test_validate_block_pre_execution() {
690 let consensus = TempoConsensus::new(ANDANTINO.clone());
691 let chain_id = ANDANTINO.chain().id();
692
693 let system_tx = create_system_tx(chain_id, SYSTEM_TX_ADDRESSES[0]);
694 let user_tx = create_tx(chain_id);
695
696 let header = TestHeaderBuilder::default()
697 .gas_limit(30_000_000)
698 .timestamp(current_timestamp())
699 .build();
700 let block = create_valid_block(header, vec![user_tx, system_tx]);
701 let sealed = reth_primitives_traits::SealedBlock::seal_slow(block);
702
703 assert!(consensus.validate_block_pre_execution(&sealed).is_ok());
704 }
705
706 #[test]
707 fn test_validate_block_pre_execution_invalid_system_tx() {
708 let consensus = TempoConsensus::new(ANDANTINO.clone());
709 let chain_id = ANDANTINO.chain().id();
710
711 let tx = TxLegacy {
712 chain_id: Some(chain_id),
713 nonce: 0,
714 gas_price: 1_000_000_000,
715 gas_limit: 21000,
716 to: TxKind::Call(Address::ZERO),
717 value: U256::ZERO,
718 input: Default::default(),
719 };
720 let signature = Signature::new(U256::ZERO, U256::ZERO, false);
721 let invalid_system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, signature));
722 let tx_hash = *invalid_system_tx.tx_hash();
723
724 let header = TestHeaderBuilder::default()
725 .gas_limit(30_000_000)
726 .timestamp(current_timestamp())
727 .build();
728 let block = create_valid_block(header, vec![invalid_system_tx]);
729 let sealed = SealedBlock::seal_slow(block);
730
731 let result = consensus.validate_block_pre_execution(&sealed);
732 let err = result.unwrap_err();
733 assert!(matches!(err, ConsensusError::Other(_)));
734 assert!(err.to_string().contains(&tx_hash.to_string()));
735 }
736
737 #[test]
738 fn test_validate_block_pre_execution_no_system_tx() {
739 let consensus = TempoConsensus::new(ANDANTINO.clone());
740 let chain_id = ANDANTINO.chain().id();
741
742 let user_tx = create_tx(chain_id);
743
744 let header = TestHeaderBuilder::default()
745 .gas_limit(30_000_000)
746 .timestamp(current_timestamp())
747 .build();
748 let block = create_valid_block(header, vec![user_tx]);
749 let sealed = SealedBlock::seal_slow(block);
750
751 let result = consensus.validate_block_pre_execution(&sealed);
752 let err = result.unwrap_err();
753 assert!(matches!(err, ConsensusError::Other(_)));
754 assert!(
755 err.to_string()
756 .contains("Block must contain end-of-block system txs")
757 );
758 }
759
760 #[test]
761 fn test_validate_body_against_header_bad_tx_root() {
762 let consensus = TempoConsensus::new(ANDANTINO.clone());
763 let header = TestHeaderBuilder::default()
764 .gas_limit(30_000_000)
765 .timestamp(current_timestamp())
766 .build();
767 let sealed = SealedHeader::seal_slow(header);
768
769 let chain_id = ANDANTINO.chain().id();
770 let user_tx = create_tx(chain_id);
771 let body = BlockBody {
772 transactions: vec![user_tx],
773 withdrawals: Some(Default::default()),
774 ..Default::default()
775 };
776
777 let result = consensus.validate_body_against_header(&body, &sealed);
778 assert!(
779 matches!(result, Err(ConsensusError::BodyTransactionRootDiff(_))),
780 "Expected BodyTransactionRootDiff error, got: {result:?}"
781 );
782 }
783
784 #[test]
785 fn test_validate_block_post_execution_bad_receipts() {
786 let consensus = TempoConsensus::new(ANDANTINO.clone());
787 let chain_id = ANDANTINO.chain().id();
788
789 let system_tx = create_system_tx(chain_id, SYSTEM_TX_ADDRESSES[0]);
790 let user_tx = create_tx(chain_id);
791
792 let header = TestHeaderBuilder::default()
793 .gas_limit(30_000_000)
794 .timestamp(current_timestamp())
795 .build();
796 let block = create_valid_block(header, vec![user_tx, system_tx]);
797 let recovered = RecoveredBlock::new_unhashed(block, vec![Address::ZERO, Address::ZERO]);
798
799 let receipt = TempoReceipt {
800 tx_type: tempo_primitives::TempoTxType::Legacy,
801 success: true,
802 cumulative_gas_used: 0,
803 logs: vec![],
804 };
805 let result = BlockExecutionResult {
806 receipts: vec![receipt],
807 requests: Default::default(),
808 gas_used: 0,
809 blob_gas_used: 0,
810 };
811
812 let err = consensus
813 .validate_block_post_execution(&recovered, &result, None)
814 .unwrap_err();
815 assert!(
816 matches!(err, ConsensusError::BodyReceiptRootDiff(_)),
817 "Expected BodyReceiptRootDiff error, got: {err:?}"
818 );
819 }
820
821 #[test]
822 fn test_validate_header_timestamp_exactly_at_boundary() {
823 let consensus = TempoConsensus::new(ANDANTINO.clone());
824 let boundary_timestamp = current_timestamp() + ALLOWED_FUTURE_BLOCK_TIME_SECONDS;
825 let header = TestHeaderBuilder::default()
826 .gas_limit(30_000_000)
827 .timestamp(boundary_timestamp)
828 .timestamp_millis_part(0)
829 .build();
830 let sealed = SealedHeader::seal_slow(header);
831
832 let result = consensus.validate_header(&sealed);
833 assert!(
834 result.is_ok(),
835 "Timestamp exactly at boundary should be accepted, got: {result:?}"
836 );
837 }
838
839 #[test]
840 fn test_validate_block_pre_execution_system_tx_out_of_order() {
841 let consensus = TempoConsensus::new(ANDANTINO.clone());
842 let chain_id = ANDANTINO.chain().id();
843
844 let wrong_addr = Address::repeat_byte(0xFF);
845 let system_tx = create_system_tx(chain_id, wrong_addr);
846
847 let header = TestHeaderBuilder::default()
848 .gas_limit(30_000_000)
849 .timestamp(current_timestamp())
850 .build();
851 let block = create_valid_block(header, vec![system_tx]);
852 let sealed = SealedBlock::seal_slow(block);
853
854 let result = consensus.validate_block_pre_execution(&sealed);
855 let err = result.unwrap_err();
856 assert!(matches!(err, ConsensusError::Other(_)));
857 assert!(
858 err.to_string()
859 .contains("Invalid end-of-block system tx order")
860 );
861 }
862}