@@ -26,8 +26,8 @@ use alloy_evm::{
2626 block:: { CommitChanges , ExecutableTx , InternalBlockExecutionError } ,
2727 FromTxWithEncoded , RecoveredTx ,
2828} ;
29- use revm:: context:: result:: ExecutionResult ;
30- use seismic_alloy_consensus:: InputDecryptionElements ;
29+ use revm:: context:: { result:: ExecutionResult , BlockEnv } ;
30+ use seismic_alloy_consensus:: { InputDecryptionElements , SeismicValidationError } ;
3131
3232type SeismicBlockExecutionCtx < ' a > = EthBlockExecutionCtx < ' a > ;
3333
6565 }
6666}
6767
68+ /// Rejects Seismic transactions whose `expires_at_block` is before the current block.
69+ /// Non-Seismic transaction types are passed through without validation.
70+ fn validate_expiration (
71+ tx : & impl InputDecryptionElements ,
72+ block : & BlockEnv ,
73+ ) -> Result < ( ) , BlockExecutionError > {
74+ if let Ok ( elements) = tx. get_decryption_elements ( ) {
75+ let current_block: u64 = block. number . saturating_to ( ) ;
76+ if current_block > elements. expires_at_block {
77+ return Err ( InternalBlockExecutionError :: SeismicValidationFailed (
78+ SeismicValidationError :: TransactionExpired {
79+ current_block,
80+ expires_at_block : elements. expires_at_block ,
81+ } ,
82+ )
83+ . into ( ) ) ;
84+ }
85+ }
86+ Ok ( ( ) )
87+ }
88+
6889impl < ' db , DB , E , Spec , R > BlockExecutor for SeismicBlockExecutor < ' _ , E , Spec , R >
6990where
7091 DB : Database + ' db ,
@@ -91,16 +112,14 @@ where
91112 tx : impl ExecutableTx < Self > ,
92113 f : impl FnOnce ( & ExecutionResult < <Self :: Evm as Evm >:: HaltReason > ) -> CommitChanges ,
93114 ) -> Result < Option < u64 > , BlockExecutionError > {
94- // Convert from ExecutableTx<Self> to R::Transaction,
95- // which has the InputDecryptionElements bound
96115 let receipt_tx: & <R as ReceiptBuilder >:: Transaction = RecoveredTx :: tx ( & tx) ;
116+ validate_expiration ( receipt_tx, self . evm ( ) . block ( ) ) ?;
97117
98118 let signer = RecoveredTx :: signer ( & tx) ;
99119 let plaintext_base = receipt_tx
100120 . plaintext_copy ( & self . purpose_keys . tx_io_sk , * signer)
101121 . map_err ( |e| InternalBlockExecutionError :: FailedToDecryptSeismicTx ( e) ) ?;
102122
103- // call inner
104123 let recovered = Recovered :: new_unchecked ( plaintext_base, * signer) ;
105124 self . inner . execute_transaction_with_commit_condition ( & recovered, f)
106125 }
@@ -110,16 +129,14 @@ where
110129 tx : impl ExecutableTx < Self > ,
111130 f : impl FnOnce ( & ExecutionResult < <Self :: Evm as Evm >:: HaltReason > ) ,
112131 ) -> Result < u64 , BlockExecutionError > {
113- // Convert from ExecutableTx<Self> to R::Transaction,
114- // which has the InputDecryptionElements bound
115132 let receipt_tx: & <R as ReceiptBuilder >:: Transaction = RecoveredTx :: tx ( & tx) ;
133+ validate_expiration ( receipt_tx, self . evm ( ) . block ( ) ) ?;
116134
117135 let signer = RecoveredTx :: signer ( & tx) ;
118136 let plaintext_base = receipt_tx
119137 . plaintext_copy ( & self . purpose_keys . tx_io_sk , * signer)
120138 . map_err ( |e| InternalBlockExecutionError :: FailedToDecryptSeismicTx ( e) ) ?;
121139
122- // call inner
123140 let recovered = Recovered :: new_unchecked ( plaintext_base, * signer) ;
124141 self . inner . execute_transaction_with_result_closure ( & recovered, f)
125142 }
@@ -339,16 +356,11 @@ mod tests {
339356 return tx_envelope;
340357 }
341358
342- fn sample_seismic_tx < ' a > ( setup : & SetupTest < ' a > , plaintext : & str ) -> TxSeismic {
343- let seismic_elements = TxSeismicElements {
344- encryption_pubkey : setup. encryption_pubkey ,
345- encryption_nonce : U96 :: from_be_slice ( & setup. encryption_nonce . 0 ) ,
346- message_version : 0 ,
347- recent_block_hash : alloy_primitives:: B256 :: from_slice ( & [ 1u8 ; 32 ] ) ,
348- expires_at_block : 1000000 ,
349- signed_read : false ,
350- } ;
351-
359+ fn sample_seismic_tx_with_elements < ' a > (
360+ setup : & SetupTest < ' a > ,
361+ plaintext : & str ,
362+ seismic_elements : TxSeismicElements ,
363+ ) -> TxSeismic {
352364 let pt_bytes = Bytes :: from ( plaintext. as_bytes ( ) . to_vec ( ) ) ;
353365
354366 let tx_metadata = TxSeismicMetadata {
@@ -378,6 +390,18 @@ mod tests {
378390 }
379391 }
380392
393+ fn sample_seismic_tx < ' a > ( setup : & SetupTest < ' a > , plaintext : & str ) -> TxSeismic {
394+ let seismic_elements = TxSeismicElements {
395+ encryption_pubkey : setup. encryption_pubkey ,
396+ encryption_nonce : U96 :: from_be_slice ( & setup. encryption_nonce . 0 ) ,
397+ message_version : 0 ,
398+ recent_block_hash : alloy_primitives:: B256 :: from_slice ( & [ 1u8 ; 32 ] ) ,
399+ expires_at_block : 1000000 ,
400+ signed_read : false ,
401+ } ;
402+ sample_seismic_tx_with_elements ( setup, plaintext, seismic_elements)
403+ }
404+
381405 #[ test]
382406 fn test_transaction_decryption_in_executor ( ) {
383407 let db = InMemoryDB :: default ( ) ;
@@ -425,4 +449,116 @@ mod tests {
425449 let result = executor. execute_transaction ( recovered) ;
426450 assert ! ( result. is_err( ) , "expected transaction to fail, but got: {:?}" , result) ;
427451 }
452+
453+ #[ test]
454+ fn test_expired_tx_rejected ( ) {
455+ let db = InMemoryDB :: default ( ) ;
456+ let mut state = StateBuilder :: new_with_database ( db) . build ( ) ;
457+
458+ let setup = setup_test ( & mut state) ;
459+
460+ // Set block number to 100 so the tx with expires_at_block=50 is expired
461+ let mut block_env = BlockEnv :: default ( ) ;
462+ block_env. number = U256 :: from ( 100 ) ;
463+
464+ let evm = setup. evm_factory . create_evm (
465+ & mut state,
466+ EvmEnv :: new ( CfgEnv :: new_with_spec ( SeismicSpecId :: MERCURY ) , block_env) ,
467+ ) ;
468+ let mut executor = setup. executor_factory . create_executor ( evm, setup. ctx . clone ( ) ) ;
469+
470+ let plaintext = "hello world" ;
471+ let seismic_elements = TxSeismicElements {
472+ encryption_pubkey : setup. encryption_pubkey ,
473+ encryption_nonce : U96 :: from_be_slice ( & setup. encryption_nonce . 0 ) ,
474+ message_version : 0 ,
475+ recent_block_hash : alloy_primitives:: B256 :: from_slice ( & [ 1u8 ; 32 ] ) ,
476+ expires_at_block : 50 ,
477+ signed_read : false ,
478+ } ;
479+ let tx_seismic = sample_seismic_tx_with_elements ( & setup, plaintext, seismic_elements) ;
480+ let tx_envelope = get_tx_envelope ( & setup, tx_seismic) ;
481+ let recovered = Recovered :: new_unchecked ( & tx_envelope, setup. signer ) ;
482+
483+ let result = executor. execute_transaction ( recovered) ;
484+ assert ! (
485+ result. is_err( ) ,
486+ "expired transaction should be rejected, but it was accepted"
487+ ) ;
488+ }
489+
490+ #[ test]
491+ fn test_tx_at_exact_expiry_block_accepted ( ) {
492+ let db = InMemoryDB :: default ( ) ;
493+ let mut state = StateBuilder :: new_with_database ( db) . build ( ) ;
494+
495+ let setup = setup_test ( & mut state) ;
496+
497+ // Set block number exactly equal to expires_at_block (should still be valid)
498+ let mut block_env = BlockEnv :: default ( ) ;
499+ block_env. number = U256 :: from ( 100 ) ;
500+
501+ let evm = setup. evm_factory . create_evm (
502+ & mut state,
503+ EvmEnv :: new ( CfgEnv :: new_with_spec ( SeismicSpecId :: MERCURY ) , block_env) ,
504+ ) ;
505+ let mut executor = setup. executor_factory . create_executor ( evm, setup. ctx . clone ( ) ) ;
506+
507+ let plaintext = "hello world" ;
508+ let seismic_elements = TxSeismicElements {
509+ encryption_pubkey : setup. encryption_pubkey ,
510+ encryption_nonce : U96 :: from_be_slice ( & setup. encryption_nonce . 0 ) ,
511+ message_version : 0 ,
512+ recent_block_hash : alloy_primitives:: B256 :: from_slice ( & [ 1u8 ; 32 ] ) ,
513+ expires_at_block : 100 ,
514+ signed_read : false ,
515+ } ;
516+ let tx_seismic = sample_seismic_tx_with_elements ( & setup, plaintext, seismic_elements) ;
517+ let tx_envelope = get_tx_envelope ( & setup, tx_seismic) ;
518+ let recovered = Recovered :: new_unchecked ( & tx_envelope, setup. signer ) ;
519+
520+ let result = executor. execute_transaction ( recovered) ;
521+ assert ! (
522+ result. is_ok( ) ,
523+ "transaction at exact expiry block should be accepted, got: {:?}" ,
524+ result
525+ ) ;
526+ }
527+
528+ #[ test]
529+ fn test_tx_one_block_past_expiry_rejected ( ) {
530+ let db = InMemoryDB :: default ( ) ;
531+ let mut state = StateBuilder :: new_with_database ( db) . build ( ) ;
532+
533+ let setup = setup_test ( & mut state) ;
534+
535+ // Set block number to one past expires_at_block
536+ let mut block_env = BlockEnv :: default ( ) ;
537+ block_env. number = U256 :: from ( 101 ) ;
538+
539+ let evm = setup. evm_factory . create_evm (
540+ & mut state,
541+ EvmEnv :: new ( CfgEnv :: new_with_spec ( SeismicSpecId :: MERCURY ) , block_env) ,
542+ ) ;
543+ let mut executor = setup. executor_factory . create_executor ( evm, setup. ctx . clone ( ) ) ;
544+
545+ let plaintext = "hello world" ;
546+ let seismic_elements = TxSeismicElements {
547+ encryption_pubkey : setup. encryption_pubkey ,
548+ encryption_nonce : U96 :: from_be_slice ( & setup. encryption_nonce . 0 ) ,
549+ message_version : 0 ,
550+ recent_block_hash : alloy_primitives:: B256 :: from_slice ( & [ 1u8 ; 32 ] ) ,
551+ expires_at_block : 100 ,
552+ signed_read : false ,
553+ } ;
554+ let tx_seismic = sample_seismic_tx_with_elements ( & setup, plaintext, seismic_elements) ;
555+ let tx_envelope = get_tx_envelope ( & setup, tx_seismic) ;
556+ let recovered = Recovered :: new_unchecked ( & tx_envelope, setup. signer ) ;
557+
558+ let result = executor. execute_transaction ( recovered) ;
559+ assert ! (
560+ result. is_err( ) ,
561+ "transaction one block past expiry should be rejected, but it was accepted"
562+ ) ;
563+ }
428564}
0 commit comments