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
@@ -88,9 +98,10 @@ define_u8_enum![ValidateRejectCode {
88
98
UnknownParent = 4 ,
89
99
NonCanonicalTenure = 5 ,
90
100
NoSuchTenure = 6 ,
91
- InvalidParentBlock = 7 ,
92
- InvalidTimestamp = 8 ,
93
- NetworkChainMismatch = 9
101
+ InvalidTransactionReplay = 7 ,
102
+ InvalidParentBlock = 8 ,
103
+ InvalidTimestamp = 9 ,
104
+ NetworkChainMismatch = 10
94
105
} ] ;
95
106
96
107
pub static TOO_MANY_REQUESTS_STATUS : u16 = 429 ;
@@ -210,6 +221,8 @@ pub struct NakamotoBlockProposal {
210
221
pub block : NakamotoBlock ,
211
222
/// Identifies which chain block is for (Mainnet, Testnet, etc.)
212
223
pub chain_id : u32 ,
224
+ /// Optional transaction replay set
225
+ pub replay_txs : Option < Vec < StacksTransaction > > ,
213
226
}
214
227
215
228
impl NakamotoBlockProposal {
@@ -375,6 +388,11 @@ impl NakamotoBlockProposal {
375
388
/// - Miner signature is valid
376
389
/// - Validation of transactions by executing them agains current chainstate.
377
390
/// This is resource intensive, and therefore done only if previous checks pass
391
+ ///
392
+ /// During transaction replay, we also check that the block only contains the unmined
393
+ /// transactions that need to be replayed, up until either:
394
+ /// - The set of transactions that must be replayed is exhausted
395
+ /// - A cost limit is hit
378
396
pub fn validate (
379
397
& self ,
380
398
sortdb : & SortitionDB ,
@@ -544,8 +562,88 @@ impl NakamotoBlockProposal {
544
562
builder. load_tenure_info ( chainstate, & burn_dbconn, tenure_cause) ?;
545
563
let mut tenure_tx = builder. tenure_begin ( & burn_dbconn, & mut miner_tenure_info) ?;
546
564
565
+ let mut replay_txs_maybe: Option < VecDeque < StacksTransaction > > =
566
+ self . replay_txs . clone ( ) . map ( |txs| txs. into ( ) ) ;
567
+
547
568
for ( i, tx) in self . block . txs . iter ( ) . enumerate ( ) {
548
569
let tx_len = tx. tx_len ( ) ;
570
+
571
+ // If a list of replay transactions is set, this transaction must be the next
572
+ // mineable transaction from this list.
573
+ if let Some ( ref mut replay_txs) = replay_txs_maybe {
574
+ loop {
575
+ let Some ( replay_tx) = replay_txs. pop_front ( ) else {
576
+ // During transaction replay, we expect that the block only
577
+ // contains transactions from the replay set. Thus, if we're here,
578
+ // the block contains a transaction that is not in the replay set,
579
+ // and we should reject the block.
580
+ return Err ( BlockValidateRejectReason {
581
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
582
+ reason : "Transaction is not in the replay set" . into ( ) ,
583
+ } ) ;
584
+ } ;
585
+ if replay_tx. txid ( ) == tx. txid ( ) {
586
+ break ;
587
+ }
588
+
589
+ // The included tx doesn't match the next tx in the
590
+ // replay set. Check to see if the tx is skipped because
591
+ // it was unmineable.
592
+ let tx_result = builder. try_mine_tx_with_len (
593
+ & mut tenure_tx,
594
+ & replay_tx,
595
+ replay_tx. tx_len ( ) ,
596
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
597
+ ASTRules :: PrecheckSize ,
598
+ None ,
599
+ ) ;
600
+ match tx_result {
601
+ TransactionResult :: Skipped ( TransactionSkipped { error, .. } )
602
+ | TransactionResult :: ProcessingError ( TransactionError { error, .. } )
603
+ | TransactionResult :: Problematic ( TransactionProblematic {
604
+ error, ..
605
+ } ) => {
606
+ // The tx wasn't able to be mined. Check the underlying error, to
607
+ // see if we should reject the block or allow the tx to be
608
+ // dropped from the replay set.
609
+
610
+ match error {
611
+ ChainError :: CostOverflowError ( ..)
612
+ | ChainError :: BlockTooBigError
613
+ | ChainError :: ClarityError ( ClarityError :: CostError ( ..) ) => {
614
+ // block limit reached; add tx back to replay set.
615
+ // BUT we know that the block should have ended at this point, so
616
+ // return an error.
617
+ let txid = replay_tx. txid ( ) ;
618
+ replay_txs. push_front ( replay_tx) ;
619
+
620
+ warn ! ( "Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block." ;
621
+ "error" => %error,
622
+ "txid" => %txid,
623
+ ) ;
624
+
625
+ return Err ( BlockValidateRejectReason {
626
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
627
+ reason : "Transaction is not in the replay set" . into ( ) ,
628
+ } ) ;
629
+ }
630
+ _ => {
631
+ // it's ok, drop it
632
+ continue ;
633
+ }
634
+ }
635
+ }
636
+ TransactionResult :: Success ( _) => {
637
+ // Tx should have been included
638
+ return Err ( BlockValidateRejectReason {
639
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
640
+ reason : "Transaction is not in the replay set" . into ( ) ,
641
+ } ) ;
642
+ }
643
+ } ;
644
+ }
645
+ }
646
+
549
647
let tx_result = builder. try_mine_tx_with_len (
550
648
& mut tenure_tx,
551
649
tx,
0 commit comments