@@ -23,7 +23,7 @@ use clarity::types::{EpochList, StacksEpoch, StacksEpochId};
2323use clarity:: util:: hash:: { Hash160 , MerkleTree , Sha512Trunc256Sum } ;
2424use clarity:: util:: secp256k1:: MessageSignature ;
2525use clarity:: vm:: costs:: ExecutionCost ;
26- use clarity:: vm:: types:: PrincipalData ;
26+ use clarity:: vm:: types:: { PrincipalData , ResponseData } ;
2727use clarity:: vm:: { ClarityVersion , Value as ClarityValue } ;
2828use serde:: { Deserialize , Serialize , Serializer } ;
2929use stacks_common:: bitvec:: BitVec ;
@@ -984,6 +984,8 @@ pub struct ContractConsensusTest<'a> {
984984 chain : ConsensusChain < ' a > ,
985985 /// Address of the contract deployer (the test faucet).
986986 contract_addr : StacksAddress ,
987+ /// Mapping of epoch → list of prerequisite contracts to deploy.
988+ setup_contracts_per_epoch : HashMap < StacksEpochId , Vec < SetupContract > > ,
987989 /// Mapping of epoch → list of `(contract_name, ClarityVersion)` deployed in that epoch.
988990 /// Multiple versions may exist per epoch (e.g., Clarity 1, 2, 3 in Epoch 3.0).
989991 contract_deploys_per_epoch : HashMap < StacksEpochId , Vec < ( String , ClarityVersion ) > > ,
@@ -1019,6 +1021,7 @@ impl ContractConsensusTest<'_> {
10191021 /// * `contract_code` - Clarity source code of the contract
10201022 /// * `function_name` - Contract function to test
10211023 /// * `function_args` - Arguments passed to `function_name` on every call
1024+ /// * `setup_contracts` - Contracts that must be deployed before epoch-specific logic runs
10221025 ///
10231026 /// # Panics
10241027 ///
@@ -1034,6 +1037,7 @@ impl ContractConsensusTest<'_> {
10341037 contract_code : & str ,
10351038 function_name : & str ,
10361039 function_args : & [ ClarityValue ] ,
1040+ setup_contracts : & [ SetupContract ] ,
10371041 ) -> Self {
10381042 assert ! (
10391043 !deploy_epochs. is_empty( ) ,
@@ -1044,22 +1048,60 @@ impl ContractConsensusTest<'_> {
10441048 call_epochs. iter( ) . all( |e| e >= min_deploy_epoch) ,
10451049 "All call epochs must be >= the minimum deploy epoch"
10461050 ) ;
1047-
1051+ assert ! (
1052+ setup_contracts
1053+ . iter( )
1054+ . all( |c| c. deploy_epoch. is_none( ) || c. deploy_epoch. unwrap( ) >= * min_deploy_epoch) ,
1055+ "All setup contracts must have a deploy epoch >= the minimum deploy epoch"
1056+ ) ;
10481057 // Build epoch_blocks map based on deploy and call epochs
10491058 let mut num_blocks_per_epoch: HashMap < StacksEpochId , u64 > = HashMap :: new ( ) ;
10501059 let mut contract_deploys_per_epoch: HashMap < StacksEpochId , Vec < ( String , ClarityVersion ) > > =
10511060 HashMap :: new ( ) ;
10521061 let mut contract_calls_per_epoch: HashMap < StacksEpochId , Vec < String > > = HashMap :: new ( ) ;
10531062 let mut contract_names = vec ! [ ] ;
1063+ let mut setup_contracts_per_epoch: HashMap < StacksEpochId , Vec < SetupContract > > =
1064+ HashMap :: new ( ) ;
1065+
1066+ let mut epoch_candidates: BTreeSet < StacksEpochId > = deploy_epochs. iter ( ) . copied ( ) . collect ( ) ;
1067+ epoch_candidates. extend ( call_epochs. iter ( ) . copied ( ) ) ;
1068+ let default_setup_epoch = * epoch_candidates
1069+ . iter ( )
1070+ . next ( )
1071+ . expect ( "deploy_epochs guarantees at least one epoch" ) ;
1072+
1073+ for contract in setup_contracts {
1074+ // Deploy the setup contracts in the first epoch if not specified.
1075+ let deploy_epoch = contract. deploy_epoch . unwrap_or ( default_setup_epoch) ;
1076+ // Get the default Clarity version for the epoch of the contract if not specified.
1077+ let clarity_version = contract. clarity_version . or_else ( || {
1078+ if deploy_epoch < StacksEpochId :: Epoch21 {
1079+ None
1080+ } else {
1081+ Some ( ClarityVersion :: default_for_epoch ( deploy_epoch) )
1082+ }
1083+ } ) ;
1084+ let mut contract = contract. clone ( ) ;
1085+ contract. deploy_epoch = Some ( deploy_epoch) ;
1086+ contract. clarity_version = clarity_version;
1087+ setup_contracts_per_epoch
1088+ . entry ( deploy_epoch)
1089+ . or_default ( )
1090+ . push ( contract) ;
1091+ }
10541092
10551093 // Combine and sort unique epochs
1056- let all_epochs: BTreeSet < StacksEpochId > =
1057- deploy_epochs . iter ( ) . chain ( call_epochs ) . cloned ( ) . collect ( ) ;
1094+ let mut all_epochs: BTreeSet < StacksEpochId > = epoch_candidates ;
1095+ all_epochs . extend ( setup_contracts_per_epoch . keys ( ) . copied ( ) ) ;
10581096
10591097 // Precompute contract names and block counts
10601098 for epoch in & all_epochs {
10611099 let mut num_blocks = 0 ;
10621100
1101+ if let Some ( contracts) = setup_contracts_per_epoch. get ( epoch) {
1102+ num_blocks += contracts. len ( ) as u64 ;
1103+ }
1104+
10631105 if deploy_epochs. contains ( epoch) {
10641106 let clarity_versions = clarity_versions_for_epoch ( * epoch) ;
10651107 let epoch_name = format ! ( "Epoch{}" , epoch. to_string( ) . replace( '.' , "_" ) ) ;
@@ -1102,6 +1144,7 @@ impl ContractConsensusTest<'_> {
11021144 contract_code : contract_code. to_string ( ) ,
11031145 function_name : function_name. to_string ( ) ,
11041146 function_args : function_args. to_vec ( ) ,
1147+ setup_contracts_per_epoch,
11051148 all_epochs,
11061149 }
11071150 }
@@ -1134,6 +1177,62 @@ impl ContractConsensusTest<'_> {
11341177 result
11351178 }
11361179
1180+ /// Deploys prerequisite contracts scheduled for the given epoch.
1181+ /// Panics if the deployment fails.
1182+ fn deploy_setup_contracts ( & mut self , epoch : StacksEpochId ) {
1183+ let Some ( contracts) = self . setup_contracts_per_epoch . get ( & epoch) . cloned ( ) else {
1184+ return ;
1185+ } ;
1186+
1187+ let is_naka_block = epoch. uses_nakamoto_blocks ( ) ;
1188+ contracts. into_iter ( ) . for_each ( |contract| {
1189+ self . chain . consume_pre_naka_prepare_phase ( ) ;
1190+ let result = self . append_tx_block (
1191+ & TestTxSpec :: ContractDeploy {
1192+ sender : & FAUCET_PRIV_KEY ,
1193+ name : & contract. name ,
1194+ code : & contract. code ,
1195+ clarity_version : contract. clarity_version ,
1196+ } ,
1197+ is_naka_block,
1198+ ) ;
1199+ match result {
1200+ ExpectedResult :: Success ( ref output) => {
1201+ assert_eq ! (
1202+ output. transactions. len( ) ,
1203+ 1 ,
1204+ "Expected 1 transaction for setup contract {}, got {}" ,
1205+ contract. name,
1206+ output. transactions. len( )
1207+ ) ;
1208+ let tx_output = & output. transactions . first ( ) . unwrap ( ) ;
1209+ assert_eq ! (
1210+ tx_output. return_type,
1211+ ClarityValue :: Response ( ResponseData {
1212+ committed: true ,
1213+ data: Box :: new( ClarityValue :: Bool ( true ) ) ,
1214+ } ) ,
1215+ "Setup contract {} failed to deploy: got {:?}" ,
1216+ contract. name,
1217+ tx_output
1218+ ) ;
1219+ assert ! (
1220+ tx_output. vm_error. is_none( ) ,
1221+ "Expected no VM error for setup contract {}, got {:?}" ,
1222+ contract. name,
1223+ tx_output. vm_error
1224+ ) ;
1225+ }
1226+ ExpectedResult :: Failure ( error) => {
1227+ panic ! (
1228+ "Setup contract {} deployment failed: {error:?}" ,
1229+ contract. name
1230+ ) ;
1231+ }
1232+ }
1233+ } ) ;
1234+ }
1235+
11371236 /// Deploys all contract versions scheduled for the given epoch.
11381237 ///
11391238 /// For each Clarity version supported in the epoch:
@@ -1251,6 +1350,9 @@ impl ContractConsensusTest<'_> {
12511350 . test_chainstate
12521351 . advance_into_epoch ( & private_key, epoch) ;
12531352
1353+ // Differently from the deploy_contracts and call_contracts functions, setup contracts are expected to succeed.
1354+ // Their receipt is not relevant to the test.
1355+ self . deploy_setup_contracts ( epoch) ;
12541356 results. extend ( self . deploy_contracts ( epoch) ) ;
12551357 results. extend ( self . call_contracts ( epoch) ) ;
12561358 }
@@ -1464,8 +1566,9 @@ impl TestTxFactory {
14641566/// * `contract_code` — The Clarity source code for the contract.
14651567/// * `function_name` — The public function to call.
14661568/// * `function_args` — Function arguments, provided as a slice of [`ClarityValue`].
1467- /// * `deploy_epochs` — *(optional)* Epochs in which to deploy the contract. Defaults to all epochs ≥ 3 .0.
1569+ /// * `deploy_epochs` — *(optional)* Epochs in which to deploy the contract. Defaults to all epochs ≥ 2 .0.
14681570/// * `call_epochs` — *(optional)* Epochs in which to call the function. Defaults to [`EPOCHS_TO_TEST`].
1571+ /// * `setup_contracts` — *(optional)* Slice of [`SetupContract`] values to deploy once before the main contract logic.
14691572///
14701573/// # Example
14711574///
@@ -1474,9 +1577,15 @@ impl TestTxFactory {
14741577/// fn test_my_contract_call_consensus() {
14751578/// contract_call_consensus_test!(
14761579/// contract_name: "my-contract",
1477- /// contract_code: "(define-public (get-message) (ok \"hello\"))",
1580+ /// contract_code: "
1581+ /// (define-public (get-message)
1582+ /// (contract-call? .dependency.foo))",
14781583/// function_name: "get-message",
14791584/// function_args: &[],
1585+ /// setup_contracts: &[SetupContract::new(
1586+ /// "dependency",
1587+ /// "(define-public (foo) (ok \"hello\"))",
1588+ /// )],
14801589/// );
14811590/// }
14821591/// ```
@@ -1488,6 +1597,7 @@ macro_rules! contract_call_consensus_test {
14881597 function_args: $function_args: expr,
14891598 $( deploy_epochs: $deploy_epochs: expr, ) ?
14901599 $( call_epochs: $call_epochs: expr, ) ?
1600+ $( setup_contracts: $setup_contracts: expr, ) ?
14911601 ) => {
14921602 {
14931603 // Handle deploy_epochs parameter (default to all epochs >= 2.0 if not provided)
@@ -1497,6 +1607,8 @@ macro_rules! contract_call_consensus_test {
14971607 // Handle call_epochs parameter (default to EPOCHS_TO_TEST if not provided)
14981608 let call_epochs = $crate:: chainstate:: tests:: consensus:: EPOCHS_TO_TEST ;
14991609 $( let call_epochs = $call_epochs; ) ?
1610+ let setup_contracts: & [ $crate:: chainstate:: tests:: consensus:: SetupContract ] = & [ ] ;
1611+ $( let setup_contracts = $setup_contracts; ) ?
15001612 let contract_test = $crate:: chainstate:: tests:: consensus:: ContractConsensusTest :: new(
15011613 function_name!( ) ,
15021614 vec![ ] ,
@@ -1506,6 +1618,7 @@ macro_rules! contract_call_consensus_test {
15061618 $contract_code,
15071619 $function_name,
15081620 $function_args,
1621+ setup_contracts,
15091622 ) ;
15101623 let result = contract_test. run( ) ;
15111624 insta:: assert_ron_snapshot!( result) ;
@@ -1532,6 +1645,7 @@ pub(crate) use contract_call_consensus_test;
15321645/// * `contract_name` — Name of the contract being tested.
15331646/// * `contract_code` — The Clarity source code of the contract.
15341647/// * `deploy_epochs` — *(optional)* Epochs in which to deploy the contract. Defaults to [`EPOCHS_TO_TEST`].
1648+ /// * `setup_contracts` — *(optional)* Slice of [`SetupContract`] values to deploy before the main contract.
15351649///
15361650/// # Example
15371651///
@@ -1546,34 +1660,72 @@ pub(crate) use contract_call_consensus_test;
15461660/// }
15471661/// ```
15481662macro_rules! contract_deploy_consensus_test {
1549- // Handle the case where deploy_epochs is not provided
15501663 (
15511664 contract_name: $contract_name: expr,
15521665 contract_code: $contract_code: expr,
1666+ $( deploy_epochs: $deploy_epochs: expr, ) ?
1667+ $( setup_contracts: $setup_contracts: expr, ) ?
15531668 ) => {
1554- contract_deploy_consensus_test!(
1555- contract_name: $contract_name,
1556- contract_code: $contract_code,
1557- deploy_epochs: $crate:: chainstate:: tests:: consensus:: EPOCHS_TO_TEST ,
1558- ) ;
1559- } ;
1560- (
1561- contract_name: $contract_name: expr,
1562- contract_code: $contract_code: expr,
1563- deploy_epochs: $deploy_epochs: expr,
1564- ) => {
1565- $crate:: chainstate:: tests:: consensus:: contract_call_consensus_test!(
1566- contract_name: $contract_name,
1567- contract_code: $contract_code,
1568- function_name: "" , // No function calls, just deploys
1569- function_args: & [ ] , // No function calls, just deploys
1570- deploy_epochs: $deploy_epochs,
1571- call_epochs: & [ ] , // No function calls, just deploys
1572- ) ;
1669+ {
1670+ let deploy_epochs = $crate:: chainstate:: tests:: consensus:: EPOCHS_TO_TEST ;
1671+ $( let deploy_epochs = $deploy_epochs; ) ?
1672+ $crate:: chainstate:: tests:: consensus:: contract_call_consensus_test!(
1673+ contract_name: $contract_name,
1674+ contract_code: $contract_code,
1675+ function_name: "" , // No function calls, just deploys
1676+ function_args: & [ ] , // No function calls, just deploys
1677+ deploy_epochs: deploy_epochs,
1678+ call_epochs: & [ ] , // No function calls, just deploys
1679+ $( setup_contracts: $setup_contracts, ) ?
1680+ ) ;
1681+ }
15731682 } ;
15741683}
15751684pub ( crate ) use contract_deploy_consensus_test;
15761685
1686+ /// Contract deployment that must occur before `contract_call_consensus_test!` or `contract_deploy_consensus_test!` runs its own logic.
1687+ ///
1688+ /// These setups are useful when the primary contract references other contracts (traits, functions, etc.)
1689+ /// that need to exist ahead of time with deterministic names and versions.
1690+ #[ derive( Clone , Debug ) ]
1691+ pub struct SetupContract {
1692+ /// Contract name that should be deployed (no macro suffixes applied).
1693+ pub name : String ,
1694+ /// Source code for the supporting contract.
1695+ pub code : String ,
1696+ /// Optional Clarity version for this contract.
1697+ pub clarity_version : Option < ClarityVersion > ,
1698+ /// Optional epoch for this contract.
1699+ pub deploy_epoch : Option < StacksEpochId > ,
1700+ }
1701+
1702+ impl SetupContract {
1703+ /// Creates a new SetupContract with default deployment settings.
1704+ ///
1705+ /// By default, the contract will deploy in the first epoch used by the test and with the
1706+ /// default Clarity version for that epoch.
1707+ pub fn new ( name : impl Into < String > , code : impl Into < String > ) -> Self {
1708+ Self {
1709+ name : name. into ( ) ,
1710+ code : code. into ( ) ,
1711+ clarity_version : None ,
1712+ deploy_epoch : None ,
1713+ }
1714+ }
1715+
1716+ /// Override the epoch where this setup contract should deploy.
1717+ pub fn with_epoch ( mut self , epoch : StacksEpochId ) -> Self {
1718+ self . deploy_epoch = Some ( epoch) ;
1719+ self
1720+ }
1721+
1722+ /// Override the Clarity version used to deploy this setup contract.
1723+ pub fn with_clarity_version ( mut self , version : ClarityVersion ) -> Self {
1724+ self . clarity_version = Some ( version) ;
1725+ self
1726+ }
1727+ }
1728+
15771729// Just a namespace for utilities for writing consensus tests
15781730pub struct ConsensusUtils ;
15791731
0 commit comments