@@ -48,7 +48,9 @@ use crate::chainstate::nakamoto::miner::NakamotoBlockBuilder;
48
48
use crate :: chainstate:: nakamoto:: { NakamotoBlock , NakamotoChainState , NAKAMOTO_BLOCK_VERSION } ;
49
49
use crate :: chainstate:: stacks:: db:: blocks:: MINIMUM_TX_FEE_RATE_PER_BYTE ;
50
50
use crate :: chainstate:: stacks:: db:: { StacksBlockHeaderTypes , StacksChainState } ;
51
- use crate :: chainstate:: stacks:: miner:: { BlockBuilder , BlockLimitFunction , TransactionResult } ;
51
+ use crate :: chainstate:: stacks:: miner:: {
52
+ BlockBuilder , BlockLimitFunction , TransactionError , TransactionResult ,
53
+ } ;
52
54
use crate :: chainstate:: stacks:: {
53
55
Error as ChainError , StacksBlock , StacksBlockHeader , StacksTransaction , TransactionPayload ,
54
56
} ;
@@ -76,6 +78,11 @@ pub static TEST_VALIDATE_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFla
76
78
/// Artificial delay to add to block validation.
77
79
pub static TEST_VALIDATE_DELAY_DURATION_SECS : LazyLock < TestFlag < u64 > > =
78
80
LazyLock :: new ( TestFlag :: default) ;
81
+ #[ cfg( any( test, feature = "testing" ) ) ]
82
+ /// Mock for the set of transactions that must be replayed
83
+ pub static TEST_REPLAY_TRANSACTIONS : LazyLock <
84
+ TestFlag < std:: collections:: VecDeque < StacksTransaction > > ,
85
+ > = LazyLock :: new ( TestFlag :: default) ;
79
86
80
87
// This enum is used to supply a `reason_code` for validation
81
88
// rejection responses. This is serialized as an enum with string
@@ -87,7 +94,8 @@ define_u8_enum![ValidateRejectCode {
87
94
ChainstateError = 3 ,
88
95
UnknownParent = 4 ,
89
96
NonCanonicalTenure = 5 ,
90
- NoSuchTenure = 6
97
+ NoSuchTenure = 6 ,
98
+ InvalidTransactionReplay = 7
91
99
} ] ;
92
100
93
101
pub static TOO_MANY_REQUESTS_STATUS : u16 = 429 ;
@@ -372,6 +380,11 @@ impl NakamotoBlockProposal {
372
380
/// - Miner signature is valid
373
381
/// - Validation of transactions by executing them agains current chainstate.
374
382
/// This is resource intensive, and therefore done only if previous checks pass
383
+ ///
384
+ /// During transaction replay, we also check that the block only contains the unmined
385
+ /// transactions that need to be replayed, up until either:
386
+ /// - The set of transactions that must be replayed is exhausted
387
+ /// - A cost limit is hit
375
388
pub fn validate (
376
389
& self ,
377
390
sortdb : & SortitionDB ,
@@ -541,8 +554,70 @@ impl NakamotoBlockProposal {
541
554
builder. load_tenure_info ( chainstate, & burn_dbconn, tenure_cause) ?;
542
555
let mut tenure_tx = builder. tenure_begin ( & burn_dbconn, & mut miner_tenure_info) ?;
543
556
557
+ // TODO: get replay set from stackerdb
558
+ #[ cfg( any( test, feature = "testing" ) ) ]
559
+ let mut replay_txs_maybe = TEST_REPLAY_TRANSACTIONS . 0 . lock ( ) . unwrap ( ) . clone ( ) ;
560
+ #[ cfg( not( any( test, feature = "testing" ) ) ) ]
561
+ let mut replay_txs_maybe = None ;
562
+
544
563
for ( i, tx) in self . block . txs . iter ( ) . enumerate ( ) {
545
564
let tx_len = tx. tx_len ( ) ;
565
+
566
+ // Check if tx is in the replay set, if required
567
+ if let Some ( ref mut replay_txs) = replay_txs_maybe {
568
+ loop {
569
+ let Some ( replay_tx) = replay_txs. pop_front ( ) else {
570
+ return Err ( BlockValidateRejectReason {
571
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
572
+ reason : "Transaction is not in the replay set" . into ( ) ,
573
+ } ) ;
574
+ } ;
575
+ if replay_tx. txid ( ) == tx. txid ( ) {
576
+ break ;
577
+ }
578
+ let tx_result = builder. try_mine_tx_with_len (
579
+ & mut tenure_tx,
580
+ & replay_tx,
581
+ replay_tx. tx_len ( ) ,
582
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
583
+ ASTRules :: PrecheckSize ,
584
+ None ,
585
+ ) ;
586
+ match tx_result {
587
+ TransactionResult :: ProcessingError ( e) => {
588
+ // The tx wasn't able to be mined. Check `TransactionError`, to
589
+ // see if we should error or allow the tx to be dropped from the replay set.
590
+
591
+ // TODO: handle TransactionError cases
592
+ match e. error {
593
+ ChainError :: BlockCostExceeded => {
594
+ // block limit reached; add tx back to replay set.
595
+ // BUT we know that the block should have ended at this point, so
596
+ // return an error.
597
+ replay_txs. push_front ( replay_tx) ;
598
+
599
+ return Err ( BlockValidateRejectReason {
600
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
601
+ reason : "Transaction is not in the replay set" . into ( ) ,
602
+ } ) ;
603
+ }
604
+ _ => {
605
+ // it's ok, drop it
606
+ continue ;
607
+ }
608
+ }
609
+ }
610
+ _ => {
611
+ // Tx should have been included
612
+ return Err ( BlockValidateRejectReason {
613
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
614
+ reason : "Transaction is not in the replay set" . into ( ) ,
615
+ } ) ;
616
+ }
617
+ } ;
618
+ }
619
+ }
620
+
546
621
let tx_result = builder. try_mine_tx_with_len (
547
622
& mut tenure_tx,
548
623
tx,
0 commit comments