14
14
// You should have received a copy of the GNU General Public License
15
15
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16
16
17
+ use std:: collections:: VecDeque ;
17
18
use std:: io:: { Read , Write } ;
18
19
#[ cfg( any( test, feature = "testing" ) ) ]
19
20
use std:: sync:: LazyLock ;
@@ -48,10 +49,14 @@ use crate::chainstate::nakamoto::miner::NakamotoBlockBuilder;
48
49
use crate :: chainstate:: nakamoto:: { NakamotoBlock , NakamotoChainState , NAKAMOTO_BLOCK_VERSION } ;
49
50
use crate :: chainstate:: stacks:: db:: blocks:: MINIMUM_TX_FEE_RATE_PER_BYTE ;
50
51
use crate :: chainstate:: stacks:: db:: { StacksBlockHeaderTypes , StacksChainState } ;
51
- use crate :: chainstate:: stacks:: miner:: { BlockBuilder , BlockLimitFunction , TransactionResult } ;
52
+ use crate :: chainstate:: stacks:: miner:: {
53
+ BlockBuilder , BlockLimitFunction , TransactionError , TransactionProblematic , TransactionResult ,
54
+ TransactionSkipped ,
55
+ } ;
52
56
use crate :: chainstate:: stacks:: {
53
57
Error as ChainError , StacksBlock , StacksBlockHeader , StacksTransaction , TransactionPayload ,
54
58
} ;
59
+ use crate :: clarity_vm:: clarity:: Error as ClarityError ;
55
60
use crate :: core:: mempool:: { MemPoolDB , ProposalCallbackReceiver } ;
56
61
use crate :: cost_estimates:: FeeRateEstimate ;
57
62
use crate :: net:: http:: {
@@ -76,6 +81,11 @@ pub static TEST_VALIDATE_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFla
76
81
/// Artificial delay to add to block validation.
77
82
pub static TEST_VALIDATE_DELAY_DURATION_SECS : LazyLock < TestFlag < u64 > > =
78
83
LazyLock :: new ( TestFlag :: default) ;
84
+ #[ cfg( any( test, feature = "testing" ) ) ]
85
+ /// Mock for the set of transactions that must be replayed
86
+ pub static TEST_REPLAY_TRANSACTIONS : LazyLock <
87
+ TestFlag < std:: collections:: VecDeque < StacksTransaction > > ,
88
+ > = LazyLock :: new ( TestFlag :: default) ;
79
89
80
90
// This enum is used to supply a `reason_code` for validation
81
91
// rejection responses. This is serialized as an enum with string
@@ -87,7 +97,8 @@ define_u8_enum![ValidateRejectCode {
87
97
ChainstateError = 3 ,
88
98
UnknownParent = 4 ,
89
99
NonCanonicalTenure = 5 ,
90
- NoSuchTenure = 6
100
+ NoSuchTenure = 6 ,
101
+ InvalidTransactionReplay = 7
91
102
} ] ;
92
103
93
104
pub static TOO_MANY_REQUESTS_STATUS : u16 = 429 ;
@@ -207,6 +218,8 @@ pub struct NakamotoBlockProposal {
207
218
pub block : NakamotoBlock ,
208
219
/// Identifies which chain block is for (Mainnet, Testnet, etc.)
209
220
pub chain_id : u32 ,
221
+ /// Optional transaction replay set
222
+ pub replay_txs : Option < Vec < StacksTransaction > > ,
210
223
}
211
224
212
225
impl NakamotoBlockProposal {
@@ -372,6 +385,11 @@ impl NakamotoBlockProposal {
372
385
/// - Miner signature is valid
373
386
/// - Validation of transactions by executing them agains current chainstate.
374
387
/// This is resource intensive, and therefore done only if previous checks pass
388
+ ///
389
+ /// During transaction replay, we also check that the block only contains the unmined
390
+ /// transactions that need to be replayed, up until either:
391
+ /// - The set of transactions that must be replayed is exhausted
392
+ /// - A cost limit is hit
375
393
pub fn validate (
376
394
& self ,
377
395
sortdb : & SortitionDB ,
@@ -541,8 +559,88 @@ impl NakamotoBlockProposal {
541
559
builder. load_tenure_info ( chainstate, & burn_dbconn, tenure_cause) ?;
542
560
let mut tenure_tx = builder. tenure_begin ( & burn_dbconn, & mut miner_tenure_info) ?;
543
561
562
+ let mut replay_txs_maybe: Option < VecDeque < StacksTransaction > > =
563
+ self . replay_txs . clone ( ) . map ( |txs| txs. into ( ) ) ;
564
+
544
565
for ( i, tx) in self . block . txs . iter ( ) . enumerate ( ) {
545
566
let tx_len = tx. tx_len ( ) ;
567
+
568
+ // If a list of replay transactions is set, this transaction must be the next
569
+ // mineable transaction from this list.
570
+ if let Some ( ref mut replay_txs) = replay_txs_maybe {
571
+ loop {
572
+ let Some ( replay_tx) = replay_txs. pop_front ( ) else {
573
+ // During transaction replay, we expect that the block only
574
+ // contains transactions from the replay set. Thus, if we're here,
575
+ // the block contains a transaction that is not in the replay set,
576
+ // and we should reject the block.
577
+ return Err ( BlockValidateRejectReason {
578
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
579
+ reason : "Transaction is not in the replay set" . into ( ) ,
580
+ } ) ;
581
+ } ;
582
+ if replay_tx. txid ( ) == tx. txid ( ) {
583
+ break ;
584
+ }
585
+
586
+ // The included tx doesn't match the next tx in the
587
+ // replay set. Check to see if the tx is skipped because
588
+ // it was unmineable.
589
+ let tx_result = builder. try_mine_tx_with_len (
590
+ & mut tenure_tx,
591
+ & replay_tx,
592
+ replay_tx. tx_len ( ) ,
593
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
594
+ ASTRules :: PrecheckSize ,
595
+ None ,
596
+ ) ;
597
+ match tx_result {
598
+ TransactionResult :: Skipped ( TransactionSkipped { error, .. } )
599
+ | TransactionResult :: ProcessingError ( TransactionError { error, .. } )
600
+ | TransactionResult :: Problematic ( TransactionProblematic {
601
+ error, ..
602
+ } ) => {
603
+ // The tx wasn't able to be mined. Check the underlying error, to
604
+ // see if we should reject the block or allow the tx to be
605
+ // dropped from the replay set.
606
+
607
+ match error {
608
+ ChainError :: CostOverflowError ( ..)
609
+ | ChainError :: BlockTooBigError
610
+ | ChainError :: ClarityError ( ClarityError :: CostError ( ..) ) => {
611
+ // block limit reached; add tx back to replay set.
612
+ // BUT we know that the block should have ended at this point, so
613
+ // return an error.
614
+ let txid = replay_tx. txid ( ) ;
615
+ replay_txs. push_front ( replay_tx) ;
616
+
617
+ warn ! ( "Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block." ;
618
+ "error" => %error,
619
+ "txid" => %txid,
620
+ ) ;
621
+
622
+ return Err ( BlockValidateRejectReason {
623
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
624
+ reason : "Transaction is not in the replay set" . into ( ) ,
625
+ } ) ;
626
+ }
627
+ _ => {
628
+ // it's ok, drop it
629
+ continue ;
630
+ }
631
+ }
632
+ }
633
+ TransactionResult :: Success ( _) => {
634
+ // Tx should have been included
635
+ return Err ( BlockValidateRejectReason {
636
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
637
+ reason : "Transaction is not in the replay set" . into ( ) ,
638
+ } ) ;
639
+ }
640
+ } ;
641
+ }
642
+ }
643
+
546
644
let tx_result = builder. try_mine_tx_with_len (
547
645
& mut tenure_tx,
548
646
tx,
0 commit comments