@@ -38,12 +38,14 @@ use stacks_common::util::tests::TestFlag;
38
38
use crate :: chainstate:: burn:: db:: sortdb:: { SortitionDB , SortitionHandleConn } ;
39
39
use crate :: chainstate:: nakamoto:: miner:: NakamotoBlockBuilder ;
40
40
use crate :: chainstate:: nakamoto:: { NakamotoBlock , NakamotoChainState , NAKAMOTO_BLOCK_VERSION } ;
41
- use crate :: chainstate:: stacks:: db:: { StacksBlockHeaderTypes , StacksChainState } ;
41
+ use crate :: chainstate:: stacks:: db:: { StacksBlockHeaderTypes , StacksChainState , StacksHeaderInfo } ;
42
42
use crate :: chainstate:: stacks:: miner:: {
43
43
BlockBuilder , BlockLimitFunction , TransactionError , TransactionProblematic , TransactionResult ,
44
44
TransactionSkipped ,
45
45
} ;
46
- use crate :: chainstate:: stacks:: { Error as ChainError , StacksTransaction , TransactionPayload } ;
46
+ use crate :: chainstate:: stacks:: {
47
+ Error as ChainError , StacksTransaction , TenureChangeCause , TransactionPayload ,
48
+ } ;
47
49
use crate :: clarity_vm:: clarity:: Error as ClarityError ;
48
50
use crate :: core:: mempool:: ProposalCallbackReceiver ;
49
51
use crate :: net:: http:: {
@@ -536,6 +538,17 @@ impl NakamotoBlockProposal {
536
538
_ => None ,
537
539
} ) ;
538
540
541
+ let replay_tx_exhausted = self . validate_replay (
542
+ & parent_stacks_header,
543
+ tenure_change,
544
+ coinbase,
545
+ tenure_cause,
546
+ chainstate. mainnet ,
547
+ chainstate. chain_id ,
548
+ & chainstate. root_path . clone ( ) ,
549
+ & burn_dbconn,
550
+ ) ?;
551
+
539
552
let mut builder = NakamotoBlockBuilder :: new (
540
553
& parent_stacks_header,
541
554
& self . block . header . consensus_hash ,
@@ -550,109 +563,9 @@ impl NakamotoBlockProposal {
550
563
builder. load_tenure_info ( chainstate, & burn_dbconn, tenure_cause) ?;
551
564
let mut tenure_tx = builder. tenure_begin ( & burn_dbconn, & mut miner_tenure_info) ?;
552
565
553
- let mut replay_txs_maybe: Option < VecDeque < StacksTransaction > > =
554
- self . replay_txs . clone ( ) . map ( |txs| txs. into ( ) ) ;
555
-
556
- let mut replay_tx_exhausted = false ;
557
-
558
566
for ( i, tx) in self . block . txs . iter ( ) . enumerate ( ) {
559
567
let tx_len = tx. tx_len ( ) ;
560
568
561
- // If a list of replay transactions is set, this transaction must be the next
562
- // mineable transaction from this list.
563
- if let Some ( ref mut replay_txs) = replay_txs_maybe {
564
- loop {
565
- if matches ! (
566
- tx. payload,
567
- TransactionPayload :: TenureChange ( ..) | TransactionPayload :: Coinbase ( ..)
568
- ) {
569
- // Allow this to happen, tenure extend checks happen elsewhere.
570
- break ;
571
- }
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
- warn ! ( "Rejected block proposal. Block contains transactions beyond the replay set." ;
578
- "txid" => %tx. txid( ) ,
579
- "tx_index" => i,
580
- ) ;
581
- return Err ( BlockValidateRejectReason {
582
- reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
583
- reason : "Block contains transactions beyond the replay set" . into ( ) ,
584
- } ) ;
585
- } ;
586
- if replay_tx. txid ( ) == tx. txid ( ) {
587
- break ;
588
- }
589
-
590
- // The included tx doesn't match the next tx in the
591
- // replay set. Check to see if the tx is skipped because
592
- // it was unmineable.
593
- let tx_result = builder. try_mine_tx_with_len (
594
- & mut tenure_tx,
595
- & replay_tx,
596
- replay_tx. tx_len ( ) ,
597
- & BlockLimitFunction :: NO_LIMIT_HIT ,
598
- ASTRules :: PrecheckSize ,
599
- None ,
600
- ) ;
601
- match tx_result {
602
- TransactionResult :: Skipped ( TransactionSkipped { error, .. } )
603
- | TransactionResult :: ProcessingError ( TransactionError { error, .. } )
604
- | TransactionResult :: Problematic ( TransactionProblematic {
605
- error, ..
606
- } ) => {
607
- // The tx wasn't able to be mined. Check the underlying error, to
608
- // see if we should reject the block or allow the tx to be
609
- // dropped from the replay set.
610
-
611
- match error {
612
- ChainError :: CostOverflowError ( ..)
613
- | ChainError :: BlockTooBigError
614
- | ChainError :: ClarityError ( ClarityError :: CostError ( ..) ) => {
615
- // block limit reached; add tx back to replay set.
616
- // BUT we know that the block should have ended at this point, so
617
- // return an error.
618
- let txid = replay_tx. txid ( ) ;
619
- replay_txs. push_front ( replay_tx) ;
620
-
621
- warn ! ( "Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block." ;
622
- "error" => %error,
623
- "txid" => %txid,
624
- ) ;
625
-
626
- return Err ( BlockValidateRejectReason {
627
- reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
628
- reason : "Transaction is not in the replay set" . into ( ) ,
629
- } ) ;
630
- }
631
- _ => {
632
- // it's ok, drop it
633
- continue ;
634
- }
635
- }
636
- }
637
- TransactionResult :: Success ( _) => {
638
- // Tx should have been included
639
- warn ! ( "Rejected block proposal. Block doesn't contain replay transaction that should have been included." ;
640
- "block_txid" => %tx. txid( ) ,
641
- "block_tx_index" => i,
642
- "replay_txid" => %replay_tx. txid( ) ,
643
- ) ;
644
- return Err ( BlockValidateRejectReason {
645
- reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
646
- reason : "Transaction is not in the replay set" . into ( ) ,
647
- } ) ;
648
- }
649
- } ;
650
- }
651
- if replay_txs. is_empty ( ) {
652
- replay_tx_exhausted = true ;
653
- }
654
- }
655
-
656
569
let tx_result = builder. try_mine_tx_with_len (
657
570
& mut tenure_tx,
658
571
tx,
@@ -757,6 +670,189 @@ impl NakamotoBlockProposal {
757
670
hasher. finish ( )
758
671
} )
759
672
}
673
+
674
+ /// Validate the block against the replay set.
675
+ ///
676
+ /// Returns a boolean indicating whether this block exhausts the replay set.
677
+ ///
678
+ /// Returns `false` if there is no replay set.
679
+ pub fn validate_replay (
680
+ & self ,
681
+ parent_stacks_header : & StacksHeaderInfo ,
682
+ tenure_change : Option < & StacksTransaction > ,
683
+ coinbase : Option < & StacksTransaction > ,
684
+ tenure_cause : Option < TenureChangeCause > ,
685
+ mainnet : bool ,
686
+ chain_id : u32 ,
687
+ chainstate_path : & str ,
688
+ burn_dbconn : & SortitionHandleConn ,
689
+ ) -> Result < bool , BlockValidateRejectReason > {
690
+ let mut replay_txs_maybe: Option < VecDeque < StacksTransaction > > =
691
+ self . replay_txs . clone ( ) . map ( |txs| txs. into ( ) ) ;
692
+
693
+ let Some ( ref mut replay_txs) = replay_txs_maybe else {
694
+ return Ok ( false ) ;
695
+ } ;
696
+
697
+ let mut replay_builder = NakamotoBlockBuilder :: new (
698
+ & parent_stacks_header,
699
+ & self . block . header . consensus_hash ,
700
+ self . block . header . burn_spent ,
701
+ tenure_change,
702
+ coinbase,
703
+ self . block . header . pox_treatment . len ( ) ,
704
+ None ,
705
+ ) ?;
706
+ let ( mut replay_chainstate, _) =
707
+ StacksChainState :: open ( mainnet, chain_id, chainstate_path, None ) ?;
708
+ let mut replay_miner_tenure_info =
709
+ replay_builder. load_tenure_info ( & mut replay_chainstate, & burn_dbconn, tenure_cause) ?;
710
+ let mut replay_tenure_tx =
711
+ replay_builder. tenure_begin ( & burn_dbconn, & mut replay_miner_tenure_info) ?;
712
+
713
+ for ( i, tx) in self . block . txs . iter ( ) . enumerate ( ) {
714
+ let tx_len = tx. tx_len ( ) ;
715
+
716
+ // If a list of replay transactions is set, this transaction must be the next
717
+ // mineable transaction from this list.
718
+ loop {
719
+ if matches ! (
720
+ tx. payload,
721
+ TransactionPayload :: TenureChange ( ..) | TransactionPayload :: Coinbase ( ..)
722
+ ) {
723
+ // Allow this to happen, tenure extend checks happen elsewhere.
724
+ break ;
725
+ }
726
+ let Some ( replay_tx) = replay_txs. pop_front ( ) else {
727
+ // During transaction replay, we expect that the block only
728
+ // contains transactions from the replay set. Thus, if we're here,
729
+ // the block contains a transaction that is not in the replay set,
730
+ // and we should reject the block.
731
+ warn ! ( "Rejected block proposal. Block contains transactions beyond the replay set." ;
732
+ "txid" => %tx. txid( ) ,
733
+ "tx_index" => i,
734
+ ) ;
735
+ return Err ( BlockValidateRejectReason {
736
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
737
+ reason : "Block contains transactions beyond the replay set" . into ( ) ,
738
+ } ) ;
739
+ } ;
740
+ if replay_tx. txid ( ) == tx. txid ( ) {
741
+ break ;
742
+ }
743
+
744
+ // The included tx doesn't match the next tx in the
745
+ // replay set. Check to see if the tx is skipped because
746
+ // it was unmineable.
747
+ let tx_result = replay_builder. try_mine_tx_with_len (
748
+ & mut replay_tenure_tx,
749
+ & replay_tx,
750
+ replay_tx. tx_len ( ) ,
751
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
752
+ ASTRules :: PrecheckSize ,
753
+ None ,
754
+ ) ;
755
+ match tx_result {
756
+ TransactionResult :: Skipped ( TransactionSkipped { error, .. } )
757
+ | TransactionResult :: ProcessingError ( TransactionError { error, .. } )
758
+ | TransactionResult :: Problematic ( TransactionProblematic { error, .. } ) => {
759
+ // The tx wasn't able to be mined. Check the underlying error, to
760
+ // see if we should reject the block or allow the tx to be
761
+ // dropped from the replay set.
762
+
763
+ match error {
764
+ ChainError :: CostOverflowError ( ..)
765
+ | ChainError :: BlockTooBigError
766
+ | ChainError :: ClarityError ( ClarityError :: CostError ( ..) ) => {
767
+ // block limit reached; add tx back to replay set.
768
+ // BUT we know that the block should have ended at this point, so
769
+ // return an error.
770
+ let txid = replay_tx. txid ( ) ;
771
+ replay_txs. push_front ( replay_tx) ;
772
+
773
+ warn ! ( "Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block." ;
774
+ "error" => %error,
775
+ "txid" => %txid,
776
+ ) ;
777
+
778
+ return Err ( BlockValidateRejectReason {
779
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
780
+ reason : "Next replay tx exceeds cost limits, so should have been in the next block." . into ( ) ,
781
+ } ) ;
782
+ }
783
+ _ => {
784
+ info ! ( "During replay block validation, allowing problematic tx to be dropped" ;
785
+ "txid" => %replay_tx. txid( ) ,
786
+ "error" => %error,
787
+ ) ;
788
+ // it's ok, drop it
789
+ continue ;
790
+ }
791
+ }
792
+ }
793
+ TransactionResult :: Success ( _) => {
794
+ // Tx should have been included
795
+ warn ! ( "Rejected block proposal. Block doesn't contain replay transaction that should have been included." ;
796
+ "block_txid" => %tx. txid( ) ,
797
+ "block_tx_index" => i,
798
+ "replay_txid" => %replay_tx. txid( ) ,
799
+ ) ;
800
+ return Err ( BlockValidateRejectReason {
801
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
802
+ reason : "Transaction is not in the replay set" . into ( ) ,
803
+ } ) ;
804
+ }
805
+ } ;
806
+ }
807
+
808
+ // Apply the block's transaction to our block builder, but we don't
809
+ // actually care about the result - that happens in the main
810
+ // validation check.
811
+ let _tx_result = replay_builder. try_mine_tx_with_len (
812
+ & mut replay_tenure_tx,
813
+ tx,
814
+ tx_len,
815
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
816
+ ASTRules :: PrecheckSize ,
817
+ None ,
818
+ ) ;
819
+ }
820
+
821
+ let no_replay_txs_remaining = replay_txs. is_empty ( ) ;
822
+
823
+ // Now, we need to check if the remaining replay transactions are unmineable.
824
+ let only_unmineable_remaining = !replay_txs. is_empty ( )
825
+ && replay_txs. iter ( ) . all ( |tx| {
826
+ let tx_result = replay_builder. try_mine_tx_with_len (
827
+ & mut replay_tenure_tx,
828
+ & tx,
829
+ tx. tx_len ( ) ,
830
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
831
+ ASTRules :: PrecheckSize ,
832
+ None ,
833
+ ) ;
834
+ match tx_result {
835
+ TransactionResult :: Skipped ( TransactionSkipped { error, .. } )
836
+ | TransactionResult :: ProcessingError ( TransactionError { error, .. } )
837
+ | TransactionResult :: Problematic ( TransactionProblematic { error, .. } ) => {
838
+ // If it's just a cost error, it's not unmineable.
839
+ !matches ! (
840
+ error,
841
+ ChainError :: CostOverflowError ( ..)
842
+ | ChainError :: BlockTooBigError
843
+ | ChainError :: ClarityError ( ClarityError :: CostError ( ..) )
844
+ )
845
+ }
846
+ TransactionResult :: Success ( _) => {
847
+ // The tx could have been included, but wasn't. This is ok, but we
848
+ // haven't exhausted the replay set.
849
+ false
850
+ }
851
+ }
852
+ } ) ;
853
+
854
+ Ok ( no_replay_txs_remaining || only_unmineable_remaining)
855
+ }
760
856
}
761
857
762
858
#[ derive( Clone , Default ) ]
0 commit comments