Skip to content

Commit f3f889f

Browse files
Merge pull request #6699 from Jiloc/feat/add-setup-contracts-to-contract-consensus-test
feat: add setup contracts to contract consensus test
2 parents 6e33578 + cdfacf2 commit f3f889f

File tree

3 files changed

+324
-27
lines changed

3 files changed

+324
-27
lines changed

stackslib/src/chainstate/tests/consensus.rs

Lines changed: 178 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use clarity::types::{EpochList, StacksEpoch, StacksEpochId};
2323
use clarity::util::hash::{Hash160, MerkleTree, Sha512Trunc256Sum};
2424
use clarity::util::secp256k1::MessageSignature;
2525
use clarity::vm::costs::ExecutionCost;
26-
use clarity::vm::types::PrincipalData;
26+
use clarity::vm::types::{PrincipalData, ResponseData};
2727
use clarity::vm::{ClarityVersion, Value as ClarityValue};
2828
use serde::{Deserialize, Serialize, Serializer};
2929
use 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
/// ```
15481662
macro_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
}
15751684
pub(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
15781730
pub struct ConsensusUtils;
15791731

0 commit comments

Comments
 (0)