@@ -225,13 +225,29 @@ pub struct TestBlock {
225225 pub transactions : Vec < StacksTransaction > ,
226226}
227227
228- /// Represents a consensus test with chainstate.
229- pub struct ConsensusTest < ' a > {
230- pub chain : TestChainstate < ' a > ,
228+ /// Manages a `TestChainstate` tailored for consensus-rule verification.
229+ ///
230+ /// Initialises the chain with enough burn-chain blocks per epoch to run the requested Stacks blocks.
231+ ///
232+ /// Provides high-level helpers for:
233+ /// - Appending Nakamoto or pre-Nakamoto blocks
234+ pub struct ConsensusChain < ' a > {
235+ pub test_chainstate : TestChainstate < ' a > ,
231236}
232237
233- impl ConsensusTest < ' _ > {
234- /// Creates a new `ConsensusTest` with the given test name, initial balances, and epoch blocks.
238+ impl ConsensusChain < ' _ > {
239+ /// Creates a new `ConsensusChain`.
240+ ///
241+ /// # Arguments
242+ ///
243+ /// * `test_name` – identifier used for logging / snapshot names / database names
244+ /// * `initial_balances` – `(principal, amount)` pairs that receive an initial STX balance
245+ /// * `num_blocks_per_epoch` – how many **Stacks** blocks must fit into each epoch
246+ ///
247+ /// # Panics
248+ ///
249+ /// * If `Epoch10` is requested (unsupported)
250+ /// * If any requested epoch is given `0` blocks
235251 pub fn new (
236252 test_name : & str ,
237253 initial_balances : Vec < ( PrincipalData , u64 ) > ,
@@ -257,8 +273,8 @@ impl ConsensusTest<'_> {
257273 let ( epochs, first_burnchain_height) =
258274 Self :: calculate_epochs ( & boot_plan. pox_constants , num_blocks_per_epoch) ;
259275 boot_plan = boot_plan. with_epochs ( epochs) ;
260- let chain = boot_plan. to_chainstate ( None , Some ( first_burnchain_height) ) ;
261- Self { chain }
276+ let test_chainstate = boot_plan. to_chainstate ( None , Some ( first_burnchain_height) ) ;
277+ Self { test_chainstate }
262278 }
263279
264280 /// Calculates a valid [`EpochList`] and starting burnchain height for the test harness.
@@ -473,8 +489,8 @@ impl ConsensusTest<'_> {
473489 fn append_nakamoto_block ( & mut self , block : TestBlock ) -> ExpectedResult {
474490 debug ! ( "--------- Running block {block:?} ---------" ) ;
475491 let ( nakamoto_block, _block_size) = self . construct_nakamoto_block ( block) ;
476- let mut sortdb = self . chain . sortdb . take ( ) . unwrap ( ) ;
477- let mut stacks_node = self . chain . stacks_node . take ( ) . unwrap ( ) ;
492+ let mut sortdb = self . test_chainstate . sortdb . take ( ) . unwrap ( ) ;
493+ let mut stacks_node = self . test_chainstate . stacks_node . take ( ) . unwrap ( ) ;
478494 let chain_tip =
479495 NakamotoChainState :: get_canonical_block_header ( stacks_node. chainstate . db ( ) , & sortdb)
480496 . unwrap ( )
@@ -488,9 +504,9 @@ impl ConsensusTest<'_> {
488504 let res = TestStacksNode :: process_pushed_next_ready_block (
489505 & mut stacks_node,
490506 & mut sortdb,
491- & mut self . chain . miner ,
507+ & mut self . test_chainstate . miner ,
492508 & chain_tip. consensus_hash ,
493- & mut self . chain . coord ,
509+ & mut self . test_chainstate . coord ,
494510 nakamoto_block. clone ( ) ,
495511 ) ;
496512 debug ! (
@@ -499,8 +515,8 @@ impl ConsensusTest<'_> {
499515 ) ;
500516 let remapped_result = res. map ( |receipt| receipt. unwrap ( ) ) ;
501517 // Restore chainstate for the next block
502- self . chain . sortdb = Some ( sortdb) ;
503- self . chain . stacks_node = Some ( stacks_node) ;
518+ self . test_chainstate . sortdb = Some ( sortdb) ;
519+ self . test_chainstate . stacks_node = Some ( stacks_node) ;
504520 ExpectedResult :: create_from ( remapped_result, expected_marf)
505521 }
506522
@@ -519,29 +535,30 @@ impl ConsensusTest<'_> {
519535 /// A [`ExpectedResult`] with the outcome of the block processing.
520536 fn append_pre_nakamoto_block ( & mut self , block : TestBlock ) -> ExpectedResult {
521537 debug ! ( "--------- Running Pre-Nakamoto block {block:?} ---------" ) ;
522- let ( ch, bh) =
523- SortitionDB :: get_canonical_stacks_chain_tip_hash ( self . chain . sortdb_ref ( ) . conn ( ) )
524- . unwrap ( ) ;
538+ let ( ch, bh) = SortitionDB :: get_canonical_stacks_chain_tip_hash (
539+ self . test_chainstate . sortdb_ref ( ) . conn ( ) ,
540+ )
541+ . unwrap ( ) ;
525542 let tip_id = StacksBlockId :: new ( & ch, & bh) ;
526543 let ( burn_ops, stacks_block, microblocks) = self
527- . chain
544+ . test_chainstate
528545 . make_pre_nakamoto_tenure_with_txs ( & block. transactions ) ;
529- let ( _, _, consensus_hash) = self . chain . next_burnchain_block ( burn_ops) ;
546+ let ( _, _, consensus_hash) = self . test_chainstate . next_burnchain_block ( burn_ops) ;
530547
531548 debug ! (
532549 "--------- Processing Pre-Nakamoto block ---------" ;
533550 "block" => ?stacks_block
534551 ) ;
535552
536- let mut stacks_node = self . chain . stacks_node . take ( ) . unwrap ( ) ;
537- let mut sortdb = self . chain . sortdb . take ( ) . unwrap ( ) ;
553+ let mut stacks_node = self . test_chainstate . stacks_node . take ( ) . unwrap ( ) ;
554+ let mut sortdb = self . test_chainstate . sortdb . take ( ) . unwrap ( ) ;
538555 let expected_marf = stacks_block. header . state_index_root ;
539556 let res = TestStacksNode :: process_pre_nakamoto_next_ready_block (
540557 & mut stacks_node,
541558 & mut sortdb,
542- & mut self . chain . miner ,
559+ & mut self . test_chainstate . miner ,
543560 & ch,
544- & mut self . chain . coord ,
561+ & mut self . test_chainstate . coord ,
545562 & stacks_block,
546563 & microblocks,
547564 ) ;
@@ -563,8 +580,8 @@ impl ConsensusTest<'_> {
563580 receipt
564581 } ) ;
565582 // Restore chainstate for the next block
566- self . chain . sortdb = Some ( sortdb) ;
567- self . chain . stacks_node = Some ( stacks_node) ;
583+ self . test_chainstate . sortdb = Some ( sortdb) ;
584+ self . test_chainstate . stacks_node = Some ( stacks_node) ;
568585 ExpectedResult :: create_from ( remapped_result, expected_marf)
569586 }
570587
@@ -589,57 +606,22 @@ impl ConsensusTest<'_> {
589606 }
590607 }
591608
592- /// Executes a full test plan by processing blocks across multiple epochs.
593- ///
594- /// This function serves as the primary test runner. It iterates through the
595- /// provided epochs in chronological order, automatically advancing the
596- /// chainstate to the start of each epoch. It then processes all [`TestBlock`]'s
597- /// associated with that epoch and collects their results.
598- ///
599- /// # Arguments
600- ///
601- /// * `epoch_blocks` - A map where keys are [`StacksEpochId`]s and values are the
602- /// sequence of blocks to be executed during that epoch.
603- ///
604- /// # Returns
605- ///
606- /// A `Vec<ExpectedResult>` with the outcome of each block for snapshot testing.
607- pub fn run (
608- mut self ,
609- epoch_blocks : HashMap < StacksEpochId , Vec < TestBlock > > ,
610- ) -> Vec < ExpectedResult > {
611- let mut sorted_epochs: Vec < _ > = epoch_blocks. clone ( ) . into_iter ( ) . collect ( ) ;
612- sorted_epochs. sort_by_key ( |( epoch_id, _) | * epoch_id) ;
613-
614- let mut results = vec ! [ ] ;
615-
616- for ( epoch, blocks) in sorted_epochs {
617- debug ! (
618- "--------- Processing epoch {epoch:?} with {} blocks ---------" ,
619- blocks. len( )
620- ) ;
621- // Use the miner key to prevent messing with FAUCET nonces.
622- let miner_key = self . chain . miner . nakamoto_miner_key ( ) ;
623- self . chain . advance_into_epoch ( & miner_key, epoch) ;
624-
625- for block in blocks {
626- results. push ( self . append_block ( block, epoch. uses_nakamoto_blocks ( ) ) ) ;
627- }
628- }
629- results
630- }
631-
632609 /// Constructs a Nakamoto block with the given [`TestBlock`] configuration.
633610 fn construct_nakamoto_block ( & mut self , test_block : TestBlock ) -> ( NakamotoBlock , usize ) {
634611 let chain_tip = NakamotoChainState :: get_canonical_block_header (
635- self . chain . stacks_node . as_ref ( ) . unwrap ( ) . chainstate . db ( ) ,
636- self . chain . sortdb . as_ref ( ) . unwrap ( ) ,
612+ self . test_chainstate
613+ . stacks_node
614+ . as_ref ( )
615+ . unwrap ( )
616+ . chainstate
617+ . db ( ) ,
618+ self . test_chainstate . sortdb . as_ref ( ) . unwrap ( ) ,
637619 )
638620 . unwrap ( )
639621 . unwrap ( ) ;
640- let cycle = self . chain . get_reward_cycle ( ) ;
622+ let cycle = self . test_chainstate . get_reward_cycle ( ) ;
641623 let burn_spent = SortitionDB :: get_block_snapshot_consensus (
642- self . chain . sortdb_ref ( ) . conn ( ) ,
624+ self . test_chainstate . sortdb_ref ( ) . conn ( ) ,
643625 & chain_tip. consensus_hash ,
644626 )
645627 . unwrap ( )
@@ -680,8 +662,13 @@ impl ConsensusTest<'_> {
680662 Err ( _) => TrieHash :: from_bytes ( & [ 0 ; 32 ] ) . unwrap ( ) ,
681663 } ;
682664
683- self . chain . miner . sign_nakamoto_block ( & mut block) ;
684- let mut signers = self . chain . config . test_signers . clone ( ) . unwrap_or_default ( ) ;
665+ self . test_chainstate . miner . sign_nakamoto_block ( & mut block) ;
666+ let mut signers = self
667+ . test_chainstate
668+ . config
669+ . test_signers
670+ . clone ( )
671+ . unwrap_or_default ( ) ;
685672 signers. sign_nakamoto_block ( & mut block, cycle) ;
686673 let block_len = block. serialize_to_vec ( ) . len ( ) ;
687674 ( block, block_len)
@@ -700,8 +687,8 @@ impl ConsensusTest<'_> {
700687 block_time : u64 ,
701688 block_txs : & [ StacksTransaction ] ,
702689 ) -> Result < TrieHash , String > {
703- let node = self . chain . stacks_node . as_mut ( ) . unwrap ( ) ;
704- let sortdb = self . chain . sortdb . as_ref ( ) . unwrap ( ) ;
690+ let node = self . test_chainstate . stacks_node . as_mut ( ) . unwrap ( ) ;
691+ let sortdb = self . test_chainstate . sortdb . as_ref ( ) . unwrap ( ) ;
705692 let burndb_conn = sortdb. index_handle_at_tip ( ) ;
706693 let chainstate = & mut node. chainstate ;
707694
@@ -761,6 +748,69 @@ impl ConsensusTest<'_> {
761748 }
762749}
763750
751+ /// A complete consensus test that drives a `ConsensusChain` through a series of epochs.
752+ ///
753+ /// It stores the blocks to execute per epoch and runs them in chronological order,
754+ /// producing a vector of `ExpectedResult` suitable for snapshot testing.
755+ pub struct ConsensusTest < ' a > {
756+ pub chain : ConsensusChain < ' a > ,
757+ epoch_blocks : HashMap < StacksEpochId , Vec < TestBlock > > ,
758+ }
759+
760+ impl ConsensusTest < ' _ > {
761+ /// Constructs a `ConsensusTest` from a map of **epoch → blocks**.
762+ ///
763+ /// The map is converted into `num_blocks_per_epoch` for chain initialisation.
764+ pub fn new (
765+ test_name : & str ,
766+ initial_balances : Vec < ( PrincipalData , u64 ) > ,
767+ epoch_blocks : HashMap < StacksEpochId , Vec < TestBlock > > ,
768+ ) -> Self {
769+ let mut num_blocks_per_epoch = HashMap :: new ( ) ;
770+ for ( epoch, blocks) in & epoch_blocks {
771+ num_blocks_per_epoch. insert ( * epoch, blocks. len ( ) as u64 ) ;
772+ }
773+ Self {
774+ chain : ConsensusChain :: new ( test_name, initial_balances, num_blocks_per_epoch) ,
775+ epoch_blocks,
776+ }
777+ }
778+
779+ /// Executes a full test plan by processing blocks across multiple epochs.
780+ ///
781+ /// This function serves as the primary test runner. It iterates through the
782+ /// provided epochs in chronological order, automatically advancing the
783+ /// chainstate to the start of each epoch. It then processes all [`TestBlock`]'s
784+ /// associated with that epoch and collects their results.
785+ ///
786+ /// # Returns
787+ ///
788+ /// A `Vec<ExpectedResult>` with the outcome of each block for snapshot testing.
789+ pub fn run ( mut self ) -> Vec < ExpectedResult > {
790+ let mut sorted_epochs: Vec < _ > = self . epoch_blocks . clone ( ) . into_iter ( ) . collect ( ) ;
791+ sorted_epochs. sort_by_key ( |( epoch_id, _) | * epoch_id) ;
792+
793+ let mut results = vec ! [ ] ;
794+
795+ for ( epoch, blocks) in sorted_epochs {
796+ debug ! (
797+ "--------- Processing epoch {epoch:?} with {} blocks ---------" ,
798+ blocks. len( )
799+ ) ;
800+ // Use the miner key to prevent messing with FAUCET nonces.
801+ let miner_key = self . chain . test_chainstate . miner . nakamoto_miner_key ( ) ;
802+ self . chain
803+ . test_chainstate
804+ . advance_into_epoch ( & miner_key, epoch) ;
805+
806+ for block in blocks {
807+ results. push ( self . chain . append_block ( block, epoch. uses_nakamoto_blocks ( ) ) ) ;
808+ }
809+ }
810+ results
811+ }
812+ }
813+
764814/// A high-level test harness for running consensus-critical smart contract tests.
765815///
766816/// This struct enables end-to-end testing of Clarity smart contracts under varying epoch conditions,
@@ -772,7 +822,7 @@ impl ConsensusTest<'_> {
772822/// - Snapshot testing of execution outcomes via [`ExpectedResult`]
773823///
774824/// It integrates:
775- /// - [`ConsensusTest `] for chain simulation and block production
825+ /// - [`ConsensusChain `] for chain simulation and block production
776826/// - [`TestTxFactory`] for deterministic transaction generation
777827///
778828/// NOTE: The **majority of logic and state computation occurs during construction to enable a deterministic TestChainstate** (`new()`):
@@ -784,7 +834,7 @@ struct ContractConsensusTest<'a> {
784834 /// Factory for generating signed, nonce-managed transactions.
785835 tx_factory : TestTxFactory ,
786836 /// Underlying chainstate used for block execution and consensus checks.
787- consensus_test : ConsensusTest < ' a > ,
837+ chain : ConsensusChain < ' a > ,
788838 /// Address of the contract deployer (the test faucet).
789839 contract_addr : StacksAddress ,
790840 /// Mapping of epoch → list of `(contract_name, ClarityVersion)` deployed in that epoch.
@@ -898,7 +948,7 @@ impl ContractConsensusTest<'_> {
898948
899949 Self {
900950 tx_factory : TestTxFactory :: new ( CHAIN_ID_TESTNET ) ,
901- consensus_test : ConsensusTest :: new ( test_name, initial_balances, num_blocks_per_epoch) ,
951+ chain : ConsensusChain :: new ( test_name, initial_balances, num_blocks_per_epoch) ,
902952 contract_addr : to_addr ( & FAUCET_PRIV_KEY ) ,
903953 contract_deploys_per_epoch,
904954 contract_calls_per_epoch,
@@ -928,7 +978,7 @@ impl ContractConsensusTest<'_> {
928978 transactions : vec ! [ tx] ,
929979 } ;
930980
931- let result = self . consensus_test . append_block ( block, is_naka_block) ;
981+ let result = self . chain . append_block ( block, is_naka_block) ;
932982
933983 if let ExpectedResult :: Success ( _) = result {
934984 self . tx_factory . increase_nonce_for_tx ( tx_spec) ;
@@ -1045,11 +1095,11 @@ impl ContractConsensusTest<'_> {
10451095 // Process epochs in order
10461096 for epoch in self . all_epochs . clone ( ) {
10471097 // Use the miner as the sender to prevent messing with the block transaction nonces of the deployer/callers
1048- let private_key = self . consensus_test . chain . miner . nakamoto_miner_key ( ) ;
1098+ let private_key = self . chain . test_chainstate . miner . nakamoto_miner_key ( ) ;
10491099
10501100 // Advance the chain into the target epoch
1051- self . consensus_test
1052- . chain
1101+ self . chain
1102+ . test_chainstate
10531103 . advance_into_epoch ( & private_key, epoch) ;
10541104
10551105 results. extend ( self . deploy_contracts ( epoch) ) ;
@@ -1382,14 +1432,11 @@ fn test_append_empty_blocks() {
13821432 transactions: vec![ ] ,
13831433 } ] ;
13841434 let mut epoch_blocks = HashMap :: new ( ) ;
1385- let mut num_blocks_per_epoch = HashMap :: new ( ) ;
13861435 for epoch in EPOCHS_TO_TEST {
13871436 epoch_blocks. insert ( * epoch, empty_test_blocks. clone ( ) ) ;
1388- num_blocks_per_epoch. insert ( * epoch, 1 ) ;
13891437 }
13901438
1391- let result =
1392- ConsensusTest :: new ( function_name ! ( ) , vec ! [ ] , num_blocks_per_epoch) . run ( epoch_blocks) ;
1439+ let result = ConsensusTest :: new ( function_name ! ( ) , vec ! [ ] , epoch_blocks) . run ( ) ;
13931440 insta:: assert_ron_snapshot!( result) ;
13941441}
13951442
@@ -1414,7 +1461,6 @@ fn test_append_stx_transfers_success() {
14141461
14151462 // build transactions per epoch, incrementing nonce per sender
14161463 let mut epoch_blocks = HashMap :: new ( ) ;
1417- let mut num_blocks_per_epoch = HashMap :: new ( ) ;
14181464 let mut nonces = vec ! [ 0u64 ; sender_privks. len( ) ] ; // track nonce per sender
14191465
14201466 for epoch in EPOCHS_TO_TEST {
@@ -1434,13 +1480,10 @@ fn test_append_stx_transfers_success() {
14341480 tx
14351481 } )
14361482 . collect ( ) ;
1437-
1438- num_blocks_per_epoch. insert ( * epoch, 1 ) ;
14391483 epoch_blocks. insert ( * epoch, vec ! [ TestBlock { transactions } ] ) ;
14401484 }
14411485
1442- let result = ConsensusTest :: new ( function_name ! ( ) , initial_balances, num_blocks_per_epoch)
1443- . run ( epoch_blocks) ;
1486+ let result = ConsensusTest :: new ( function_name ! ( ) , initial_balances, epoch_blocks) . run ( ) ;
14441487 insta:: assert_ron_snapshot!( result) ;
14451488}
14461489
0 commit comments