@@ -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,190 @@ 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
+ // if let Some(ref mut replay_txs) = replay_txs_maybe {
719
+ loop {
720
+ if matches ! (
721
+ tx. payload,
722
+ TransactionPayload :: TenureChange ( ..) | TransactionPayload :: Coinbase ( ..)
723
+ ) {
724
+ // Allow this to happen, tenure extend checks happen elsewhere.
725
+ break ;
726
+ }
727
+ let Some ( replay_tx) = replay_txs. pop_front ( ) else {
728
+ // During transaction replay, we expect that the block only
729
+ // contains transactions from the replay set. Thus, if we're here,
730
+ // the block contains a transaction that is not in the replay set,
731
+ // and we should reject the block.
732
+ warn ! ( "Rejected block proposal. Block contains transactions beyond the replay set." ;
733
+ "txid" => %tx. txid( ) ,
734
+ "tx_index" => i,
735
+ ) ;
736
+ return Err ( BlockValidateRejectReason {
737
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
738
+ reason : "Block contains transactions beyond the replay set" . into ( ) ,
739
+ } ) ;
740
+ } ;
741
+ if replay_tx. txid ( ) == tx. txid ( ) {
742
+ break ;
743
+ }
744
+
745
+ // The included tx doesn't match the next tx in the
746
+ // replay set. Check to see if the tx is skipped because
747
+ // it was unmineable.
748
+ let tx_result = replay_builder. try_mine_tx_with_len (
749
+ & mut replay_tenure_tx,
750
+ & replay_tx,
751
+ replay_tx. tx_len ( ) ,
752
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
753
+ ASTRules :: PrecheckSize ,
754
+ None ,
755
+ ) ;
756
+ match tx_result {
757
+ TransactionResult :: Skipped ( TransactionSkipped { error, .. } )
758
+ | TransactionResult :: ProcessingError ( TransactionError { error, .. } )
759
+ | TransactionResult :: Problematic ( TransactionProblematic { error, .. } ) => {
760
+ // The tx wasn't able to be mined. Check the underlying error, to
761
+ // see if we should reject the block or allow the tx to be
762
+ // dropped from the replay set.
763
+
764
+ match error {
765
+ ChainError :: CostOverflowError ( ..)
766
+ | ChainError :: BlockTooBigError
767
+ | ChainError :: ClarityError ( ClarityError :: CostError ( ..) ) => {
768
+ // block limit reached; add tx back to replay set.
769
+ // BUT we know that the block should have ended at this point, so
770
+ // return an error.
771
+ let txid = replay_tx. txid ( ) ;
772
+ replay_txs. push_front ( replay_tx) ;
773
+
774
+ warn ! ( "Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block." ;
775
+ "error" => %error,
776
+ "txid" => %txid,
777
+ ) ;
778
+
779
+ return Err ( BlockValidateRejectReason {
780
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
781
+ reason : "Transaction is not in the replay set" . into ( ) ,
782
+ } ) ;
783
+ }
784
+ _ => {
785
+ info ! ( "During replay block validation, allowing problematic tx to be dropped" ;
786
+ "txid" => %replay_tx. txid( ) ,
787
+ "error" => %error,
788
+ ) ;
789
+ // it's ok, drop it
790
+ continue ;
791
+ }
792
+ }
793
+ }
794
+ TransactionResult :: Success ( _) => {
795
+ // Tx should have been included
796
+ warn ! ( "Rejected block proposal. Block doesn't contain replay transaction that should have been included." ;
797
+ "block_txid" => %tx. txid( ) ,
798
+ "block_tx_index" => i,
799
+ "replay_txid" => %replay_tx. txid( ) ,
800
+ ) ;
801
+ return Err ( BlockValidateRejectReason {
802
+ reason_code : ValidateRejectCode :: InvalidTransactionReplay ,
803
+ reason : "Transaction is not in the replay set" . into ( ) ,
804
+ } ) ;
805
+ }
806
+ } ;
807
+ }
808
+
809
+ // Apply the block's transaction to our block builder, but we don't
810
+ // actually care about the result - that happens in the main
811
+ // validation check.
812
+ let _tx_result = replay_builder. try_mine_tx_with_len (
813
+ & mut replay_tenure_tx,
814
+ tx,
815
+ tx_len,
816
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
817
+ ASTRules :: PrecheckSize ,
818
+ None ,
819
+ ) ;
820
+ }
821
+
822
+ let no_replay_txs_remaining = replay_txs. is_empty ( ) ;
823
+
824
+ // Now, we need to check if the remaining replay transactions are unmineable.
825
+ let only_unmineable_remaining = replay_txs. is_empty ( )
826
+ || replay_txs. iter ( ) . all ( |tx| {
827
+ let tx_result = replay_builder. try_mine_tx_with_len (
828
+ & mut replay_tenure_tx,
829
+ & tx,
830
+ tx. tx_len ( ) ,
831
+ & BlockLimitFunction :: NO_LIMIT_HIT ,
832
+ ASTRules :: PrecheckSize ,
833
+ None ,
834
+ ) ;
835
+ match tx_result {
836
+ TransactionResult :: Skipped ( TransactionSkipped { error, .. } )
837
+ | TransactionResult :: ProcessingError ( TransactionError { error, .. } )
838
+ | TransactionResult :: Problematic ( TransactionProblematic { error, .. } ) => {
839
+ // If it's just a cost error, it's not unmineable.
840
+ !matches ! (
841
+ error,
842
+ ChainError :: CostOverflowError ( ..)
843
+ | ChainError :: BlockTooBigError
844
+ | ChainError :: ClarityError ( ClarityError :: CostError ( ..) )
845
+ )
846
+ }
847
+ TransactionResult :: Success ( _) => {
848
+ // The tx could have been included, but wasn't. This is ok, but we
849
+ // haven't exhausted the replay set.
850
+ false
851
+ }
852
+ }
853
+ } ) ;
854
+
855
+ Ok ( no_replay_txs_remaining || only_unmineable_remaining)
856
+ }
760
857
}
761
858
762
859
#[ derive( Clone , Default ) ]
0 commit comments