Skip to content

Commit 6069c36

Browse files
Split ConsensusTest's chain logic out to underlying ConsensusChain and use in ContractConsensusTest
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent 0e71860 commit 6069c36

File tree

1 file changed

+131
-88
lines changed

1 file changed

+131
-88
lines changed

stackslib/src/chainstate/tests/consensus.rs

Lines changed: 131 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)