diff --git a/changelog.d/7341-cost-voting-epoch-4-0-gate.changed b/changelog.d/7341-cost-voting-epoch-4-0-gate.changed
new file mode 100644
index 00000000000..abb315646b5
--- /dev/null
+++ b/changelog.d/7341-cost-voting-epoch-4-0-gate.changed
@@ -0,0 +1 @@
+Added `.cost-voting` disablement gate beginning from epoch 4.0
diff --git a/clarity/src/vm/costs/mod.rs b/clarity/src/vm/costs/mod.rs
index 9efcab817cd..9e14915e88c 100644
--- a/clarity/src/vm/costs/mod.rs
+++ b/clarity/src/vm/costs/mod.rs
@@ -953,18 +953,29 @@ impl TrackerData {
))
})?;
+ // TODO(cost-voting): Remove after epoch 4.0 activation (when cost-voting deactivates), and
+ // use `CostStateSummary::empty()` directly instead.
+ let state_summary = if epoch_id.supports_cost_voting_contract() {
+ // If cost-voting is active, load and apply the current cost function configuration from
+ // the chain tip, applying any changes that have been voted on but not yet applied.
+ load_cost_functions(self.mainnet, clarity_db, apply_updates).map_err(|e| {
+ let result = clarity_db
+ .roll_back()
+ .map_err(|e| CostErrors::Expect(e.to_string()));
+ match result {
+ Ok(_) => e,
+ Err(rollback_err) => rollback_err,
+ }
+ })?
+ } else {
+ // Cost-voting is retired at Epoch 4.0: every cost function uses boot defaults.
+ CostStateSummary::empty()
+ };
+
let CostStateSummary {
contract_call_circuits,
mut cost_function_references,
- } = load_cost_functions(self.mainnet, clarity_db, apply_updates).map_err(|e| {
- let result = clarity_db
- .roll_back()
- .map_err(|e| CostErrors::Expect(e.to_string()));
- match result {
- Ok(_) => e,
- Err(rollback_err) => rollback_err,
- }
- })?;
+ } = state_summary;
self.contract_call_circuits = contract_call_circuits;
diff --git a/clarity/src/vm/tests/epoch_gating.rs b/clarity/src/vm/tests/epoch_gating.rs
index 902bdbb26a8..2faf3ae86db 100644
--- a/clarity/src/vm/tests/epoch_gating.rs
+++ b/clarity/src/vm/tests/epoch_gating.rs
@@ -12,7 +12,7 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use stacks_common::types::StacksEpochId;
+use stacks_common::types::{StacksEpochId, StacksEpochRangeTestExt as _};
use crate::vm::ClarityVersion;
@@ -21,7 +21,7 @@ use crate::vm::ClarityVersion;
#[test]
fn test_default_for_epoch_is_monotonic() {
// No Clarity in Epoch10.
- let clarity_epochs = &StacksEpochId::since(StacksEpochId::Epoch20);
+ let clarity_epochs = (StacksEpochId::Epoch20..).as_slice();
for window in clarity_epochs.windows(2) {
let earlier = ClarityVersion::default_for_epoch(window[0]);
let later = ClarityVersion::default_for_epoch(window[1]);
diff --git a/stacks-common/src/types/mod.rs b/stacks-common/src/types/mod.rs
index 9ed0a014c5d..f86afc73e3f 100644
--- a/stacks-common/src/types/mod.rs
+++ b/stacks-common/src/types/mod.rs
@@ -17,6 +17,8 @@
use std::cmp::Ordering;
use std::fmt;
use std::io::{Read, Write};
+#[cfg(any(test, feature = "testing"))]
+use std::ops::{Bound, RangeBounds};
use std::ops::{Deref, DerefMut, Index, IndexMut};
use std::str::FromStr;
use std::sync::LazyLock;
@@ -596,6 +598,12 @@ impl StacksEpochId {
}
}
+ /// Whether or not this epoch supports the cost-voting contract (SIP-006), which is
+ /// disabled from Epoch 4.0 (SIP-044).
+ pub fn supports_cost_voting_contract(&self) -> bool {
+ self < &StacksEpochId::Epoch40
+ }
+
/// Returns true for epochs which use Nakamoto blocks. These blocks use a
/// different header format than the previous Stacks blocks, which among
/// other changes includes a Stacks-specific timestamp.
@@ -814,34 +822,84 @@ impl StacksEpochId {
StacksEpochId::Epoch40 => PEER_VERSION_EPOCH_4_0,
}
}
+}
- #[cfg(any(test, feature = "testing"))]
- pub fn since(epoch: StacksEpochId) -> &'static [StacksEpochId] {
- let idx = Self::ALL
+/// Test-only helper functions for `StacksEpochId`.
+///
+/// These functions rely on the [`StacksEpochId::ALL`] array of all defined epochs and are only
+/// intended to be used in testing as they may return variants that are not yet active according to
+/// [`StacksEpochId::RELEASE_LATEST_EPOCH`].
+#[cfg(any(test, feature = "testing"))]
+impl StacksEpochId {
+ /// Gets the index of the provided `epoch` within the [`ALL`](StacksEpochId::ALL) array of
+ /// defined epochs.
+ fn index_of(epoch: Self) -> usize {
+ Self::ALL
.iter()
.position(|&e| e == epoch)
- .expect("epoch not found in ALL");
+ .expect("epoch not found in ALL")
+ }
- &Self::ALL[idx..]
+ /// Returns all [`StacksEpochId`] variants after the provided `epoch` (exclusive).
+ ///
+ /// Provided as a helper function since this can't be expressed as a range (there's no
+ /// "excluded" start-bound syntax).
+ ///
+ /// Useful for iterating over all epochs _after_ a specific epoch, when the next epoch may not
+ /// yet be known or defined (e.g. in tests that want to assert an invariant for all future
+ /// [`StacksEpochId`] variants).
+ pub fn all_after(epoch: Self) -> impl Iterator- {
+ (Bound::Excluded(epoch), Bound::Unbounded).iter().copied()
}
- /// Returns all [`StacksEpochId`] from `start` to `end`, both inclusive.
- #[cfg(any(test, feature = "testing"))]
- pub fn between(start: StacksEpochId, end: StacksEpochId) -> &'static [StacksEpochId] {
- let start_idx = Self::ALL
- .iter()
- .position(|&e| e == start)
- .expect("start epoch not found in ALL");
- let end_idx = Self::ALL
- .iter()
- .position(|&e| e == end)
- .expect("end epoch not found in ALL");
- assert!(start_idx <= end_idx, "start epoch must be <= end epoch");
+ /// Returns the first (lowest) [`StacksEpochId`] variant.
+ pub const fn first() -> StacksEpochId {
+ Self::ALL[0]
+ }
+
+ /// Returns the last (highest) defined [`StacksEpochId`] variant.
+ pub const fn last() -> StacksEpochId {
+ Self::ALL[Self::ALL.len() - 1]
+ }
+}
+
+/// Extension methods for iterating over standard Rust range bounds of [`StacksEpochId`].
+/// Note: When `Step` stabilizes, this can be refactored.
+#[cfg(any(test, feature = "testing"))]
+pub trait StacksEpochRangeTestExt: RangeBounds + Sized {
+ /// Iterates by reference over all [`StacksEpochId`] variants in this range.
+ ///
+ /// Forgiving: behaves like standard `Range` iterators in that `start >= end` results in an
+ /// empty iterator.
+ fn iter(&self) -> std::slice::Iter<'static, StacksEpochId> {
+ let start = match self.start_bound() {
+ Bound::Included(epoch) => StacksEpochId::index_of(*epoch),
+ Bound::Excluded(epoch) => StacksEpochId::index_of(*epoch) + 1,
+ Bound::Unbounded => 0,
+ };
+
+ let end = match self.end_bound() {
+ Bound::Included(epoch) => StacksEpochId::index_of(*epoch) + 1,
+ Bound::Excluded(epoch) => StacksEpochId::index_of(*epoch),
+ Bound::Unbounded => StacksEpochId::ALL.len(),
+ };
- &Self::ALL[start_idx..=end_idx]
+ // Yield an empty slice if end <= start, mirroring standard Rust behavior.
+ let end = end.max(start);
+ StacksEpochId::ALL[start..end].iter()
+ }
+
+ /// Returns a slice of all [`StacksEpochId`] variants in this range.
+ fn as_slice(&self) -> &'static [StacksEpochId] {
+ self.iter().as_slice()
}
}
+/// Implement [`StacksEpochRangeTestExt`] for [`StacksEpochId`] ranges (e.g.
+/// `StacksEpochId::Epoch10..=StacksEpochId::Epoch23`).
+#[cfg(any(test, feature = "testing"))]
+impl StacksEpochRangeTestExt for R where R: RangeBounds {}
+
impl PartialOrd for StacksAddress {
fn partial_cmp(&self, other: &StacksAddress) -> Option {
Some(self.cmp(other))
diff --git a/stacks-common/src/types/tests.rs b/stacks-common/src/types/tests.rs
index f5b8274f577..89bff743494 100644
--- a/stacks-common/src/types/tests.rs
+++ b/stacks-common/src/types/tests.rs
@@ -14,12 +14,119 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+use std::ops::Bound;
use std::str::FromStr;
use super::{
set_test_coinbase_schedule, CoinbaseInterval, StacksEpochId, COINBASE_INTERVALS_MAINNET,
COINBASE_INTERVALS_TESTNET,
};
+use crate::types::StacksEpochRangeTestExt;
+
+#[test]
+fn test_epoch40_cost_voting_contract_support_gate() {
+ // Up to (excluding) Epoch 4.0 = enabled.
+ for epoch in (..StacksEpochId::Epoch40).iter() {
+ assert!(
+ epoch.supports_cost_voting_contract(),
+ "cost-voting gate must remain active before Epoch 4.0 for {epoch}"
+ );
+ }
+
+ // Epoch 4.0 and onward = disabled.
+ for epoch in (StacksEpochId::Epoch40..).iter() {
+ assert!(
+ !epoch.supports_cost_voting_contract(),
+ "cost-voting gate must be inactive from Epoch 4.0 onward for {epoch}"
+ );
+ }
+}
+
+#[test]
+fn test_epoch_range_ext_iter() {
+ // Alias to keep the below cleaner
+ fn epoch_index(epoch: StacksEpochId) -> usize {
+ StacksEpochId::index_of(epoch)
+ }
+
+ // Full range is effectively equivalent to the ALL constant.
+ assert_eq!((..).as_slice(), StacksEpochId::ALL);
+
+ // Start = inclusive, end = unbounded
+ assert_eq!(
+ (StacksEpochId::Epoch32..).as_slice(),
+ &StacksEpochId::ALL[epoch_index(StacksEpochId::Epoch32)..]
+ );
+
+ // Start = unbounded, end = exclusive
+ assert_eq!(
+ (..StacksEpochId::Epoch21).as_slice(),
+ &[
+ StacksEpochId::Epoch10,
+ StacksEpochId::Epoch20,
+ StacksEpochId::Epoch2_05
+ ]
+ );
+
+ // Start = unbounded, end = inclusive
+ assert_eq!(
+ (..=StacksEpochId::Epoch21).as_slice(),
+ &[
+ StacksEpochId::Epoch10,
+ StacksEpochId::Epoch20,
+ StacksEpochId::Epoch2_05,
+ StacksEpochId::Epoch21
+ ]
+ );
+
+ // Start = inclusive, end = exclusive
+ assert_eq!(
+ (StacksEpochId::Epoch20..StacksEpochId::Epoch22).as_slice(),
+ &[
+ StacksEpochId::Epoch20,
+ StacksEpochId::Epoch2_05,
+ StacksEpochId::Epoch21
+ ]
+ );
+
+ // Start = inclusive, end = inclusive
+ assert_eq!(
+ (StacksEpochId::Epoch20..=StacksEpochId::Epoch22).as_slice(),
+ &[
+ StacksEpochId::Epoch20,
+ StacksEpochId::Epoch2_05,
+ StacksEpochId::Epoch21,
+ StacksEpochId::Epoch22
+ ]
+ );
+
+ // Start = exclusive, end = unbounded (a'la `all_after`).
+ let expected = &StacksEpochId::ALL[epoch_index(StacksEpochId::Epoch32) + 1..];
+ assert_eq!(
+ (Bound::Excluded(StacksEpochId::Epoch32), Bound::Unbounded).as_slice(),
+ expected
+ );
+ let all_after: Vec<_> = StacksEpochId::all_after(StacksEpochId::Epoch32).collect();
+ assert_eq!(&all_after, expected);
+
+ // Single element range with exclusive end bound should yield empty.
+ assert_eq!(
+ (StacksEpochId::Epoch23..StacksEpochId::Epoch23).as_slice(),
+ &[]
+ );
+
+ // Single element range with inclusive end bound should yield that item.
+ assert_eq!(
+ (StacksEpochId::Epoch23..=StacksEpochId::Epoch23).as_slice(),
+ &[StacksEpochId::Epoch23]
+ );
+
+ // Reversed range should yield empty.
+ assert_eq!(
+ (StacksEpochId::Epoch22..StacksEpochId::Epoch20).as_slice(),
+ &[]
+ );
+}
#[test]
fn test_stacks_epoch_id_display_fromstr_tryfrom_roundtrip() {
diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs
index 582171d1aa0..45cb8e5c812 100644
--- a/stackslib/src/chainstate/tests/consensus.rs
+++ b/stackslib/src/chainstate/tests/consensus.rs
@@ -19,7 +19,7 @@ use clarity::boot_util::boot_code_addr;
use clarity::codec::StacksMessageCodec;
use clarity::consts::{CHAIN_ID_TESTNET, STACKS_EPOCH_MAX};
use clarity::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey, TrieHash};
-use clarity::types::{EpochList, StacksEpoch, StacksEpochId};
+use clarity::types::{EpochList, StacksEpoch, StacksEpochId, StacksEpochRangeTestExt as _};
use clarity::util::hash::{Hash160, MerkleTree, Sha512Trunc256Sum};
use clarity::util::secp256k1::MessageSignature;
use clarity::vm::costs::ExecutionCost;
@@ -72,14 +72,14 @@ pub fn max_tested_epoch() -> StacksEpochId {
.expect("EPOCHS_TO_TEST must contain at least one epoch")
}
-/// Like [`StacksEpochId::since`], but clamped to [`max_tested_epoch`]. Tests use
-/// this to build deploy/call epoch ranges so that excluded epochs are never
-/// deployed in or called in. Deploying or calling in an excluded epoch would
-/// otherwise keep that epoch's results (marf hashes, costs) in the snapshots and
-/// reintroduce the churn that excluding it is meant to avoid.
+/// Like a `(start..)` range, but clamped to [`max_tested_epoch`].
+///
+/// Tests use this to build deploy/call epoch ranges so that excluded epochs are never used, which
+/// otherwise would keep that epoch's results (marf hashes, costs) in the snapshots and reintroduce
+/// the churn that excluding it is meant to avoid.
pub fn tested_epochs_since(start: StacksEpochId) -> Vec {
let max = max_tested_epoch();
- StacksEpochId::since(start)
+ (start..)
.iter()
.copied()
.filter(|epoch| *epoch <= max)
diff --git a/stackslib/src/chainstate/tests/consensus_unit_tests.rs b/stackslib/src/chainstate/tests/consensus_unit_tests.rs
index 4c9932e1337..218736f7551 100644
--- a/stackslib/src/chainstate/tests/consensus_unit_tests.rs
+++ b/stackslib/src/chainstate/tests/consensus_unit_tests.rs
@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use clarity::types::StacksEpochId;
+use clarity::types::{StacksEpochId, StacksEpochRangeTestExt as _};
use clarity::vm::{ClarityVersion, Value};
use crate::chainstate::tests::consensus::{
@@ -25,7 +25,7 @@ fn test_example_1_cdeploy() {
let report = contract_deploy_consensus_unit_test!(
contract_name: "map_empty",
contract_code: "(map + (list) (list 10 20))",
- deploy_epochs: StacksEpochId::since(StacksEpochId::Epoch20),
+ deploy_epochs: (StacksEpochId::Epoch20..).as_slice(),
clarity_versions: ClarityVersion::ALL,
);
@@ -57,7 +57,7 @@ fn test_example_2_ccall() {
",
function_name: "trigger",
function_args: &[],
- deploy_epochs: StacksEpochId::since(StacksEpochId::Epoch20),
+ deploy_epochs: (StacksEpochId::Epoch20..).as_slice(),
clarity_versions: ClarityVersion::ALL,
);
diff --git a/stackslib/src/chainstate/tests/runtime_analysis_tests.rs b/stackslib/src/chainstate/tests/runtime_analysis_tests.rs
index d6e66595a10..85eb9b6accf 100644
--- a/stackslib/src/chainstate/tests/runtime_analysis_tests.rs
+++ b/stackslib/src/chainstate/tests/runtime_analysis_tests.rs
@@ -17,7 +17,7 @@
use std::collections::HashMap;
-use clarity::types::StacksEpochId;
+use clarity::types::{StacksEpochId, StacksEpochRangeTestExt as _};
#[allow(unused_imports)]
use clarity::vm::analysis::RuntimeCheckErrorKind;
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, MAX_TYPE_DEPTH};
@@ -613,7 +613,7 @@ fn runtime_check_error_kind_type_error_ccall() {
(get-shares u999 .pool))",
function_name: "trigger-error",
function_args: &[],
- deploy_epochs: StacksEpochId::between(StacksEpochId::Epoch20, StacksEpochId::Epoch33),
+ deploy_epochs: (StacksEpochId::Epoch20..=StacksEpochId::Epoch33).as_slice(),
call_epochs: &[StacksEpochId::Epoch33],
setup_contracts: &[contract_1, contract_2],
);
diff --git a/stackslib/src/chainstate/tests/runtime_tests.rs b/stackslib/src/chainstate/tests/runtime_tests.rs
index 7c4ef09aa86..6b77b00494a 100644
--- a/stackslib/src/chainstate/tests/runtime_tests.rs
+++ b/stackslib/src/chainstate/tests/runtime_tests.rs
@@ -18,7 +18,7 @@
use std::collections::HashMap;
use clarity::types::chainstate::{StacksPrivateKey, StacksPublicKey};
-use clarity::types::StacksEpochId;
+use clarity::types::{StacksEpochId, StacksEpochRangeTestExt as _};
use clarity::vm::errors::RuntimeError;
use clarity::vm::types::{PrincipalData, ResponseData};
use clarity::vm::{max_call_stack_depth_for_epoch, ClarityVersion, Value as ClarityValue};
@@ -682,7 +682,7 @@ fn unknown_block_header_hash_fork() {
)",
function_name: "trigger",
function_args: &[],
- deploy_epochs: StacksEpochId::between(StacksEpochId::Epoch20, StacksEpochId::Epoch33),
+ deploy_epochs: (StacksEpochId::Epoch20..=StacksEpochId::Epoch33).as_slice(),
call_epochs: &[StacksEpochId::Epoch33],
);
}
@@ -708,7 +708,7 @@ fn bad_block_hash() {
)",
function_name: "trigger",
function_args: &[],
- deploy_epochs: StacksEpochId::between(StacksEpochId::Epoch20, StacksEpochId::Epoch33),
+ deploy_epochs: (StacksEpochId::Epoch20..=StacksEpochId::Epoch33).as_slice(),
call_epochs: &[StacksEpochId::Epoch33],
);
}
diff --git a/stackslib/src/clarity_vm/clarity.rs b/stackslib/src/clarity_vm/clarity.rs
index 5d3e44961b2..50f3451f3b0 100644
--- a/stackslib/src/clarity_vm/clarity.rs
+++ b/stackslib/src/clarity_vm/clarity.rs
@@ -1037,6 +1037,77 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> {
Ok(boot_code_account)
}
+ /// Prepares a [`StacksTransaction`] with a [`SmartContract`](TransactionPayload::SmartContract)
+ /// payload for instantiating a boot contract, but does not execute it.
+ ///
+ /// Sets the transaction's auth to be the boot code account and the transaction version based on
+ /// this instance's `mainnet` field.
+ fn make_boot_code_smart_contract_tx(
+ &mut self,
+ contract_name: &str,
+ code_body: &str,
+ clarity_version: Option,
+ ) -> Result<(StacksAccount, StacksTransaction), ClarityError> {
+ let boot_code_account = self.get_boot_code_account()?;
+ let tx_version = if self.mainnet {
+ TransactionVersion::Mainnet
+ } else {
+ TransactionVersion::Testnet
+ };
+
+ let boot_code_auth = boot_code_tx_auth(boot_code_addr(self.mainnet));
+ let payload = TransactionPayload::SmartContract(
+ TransactionSmartContract {
+ name: ContractName::try_from(contract_name.to_string())
+ .expect("FATAL: invalid boot-code contract name"),
+ code_body: StacksString::from_str(code_body)
+ .expect("FATAL: invalid boot code body"),
+ },
+ clarity_version,
+ );
+
+ Ok((
+ boot_code_account,
+ StacksTransaction::new(tx_version, boot_code_auth, payload),
+ ))
+ }
+
+ /// Instantiates a boot contract by:
+ ///
+ /// 1. Preparing a [`StacksTransaction`] with the appropriate version, payload and auth,
+ /// 2. Executing it using the boot account within a cost-free transaction, and
+ /// 3. Asserting the receipt for success.
+ ///
+ /// Panics if any of the above steps fail.
+ fn instantiate_boot_contract(
+ &mut self,
+ contract_name: &str,
+ code_body: &str,
+ clarity_version: Option,
+ ) -> Result {
+ let contract_id = boot_code_id(contract_name, self.mainnet);
+
+ let (boot_code_account, contract_tx) =
+ self.make_boot_code_smart_contract_tx(contract_name, code_body, clarity_version)?;
+
+ let receipt = self.as_free_transaction(|tx_conn| {
+ info!("Instantiate {} contract", &contract_id);
+ StacksChainState::process_transaction_payload(
+ tx_conn,
+ &contract_tx,
+ &boot_code_account,
+ None,
+ )
+ .expect("FATAL: Failed to process boot contract initialization")
+ });
+
+ if receipt.result != Value::okay_true() || receipt.post_condition_aborted {
+ panic!("FATAL: Failure processing {contract_id} contract initialization: {receipt:#?}");
+ }
+
+ Ok(receipt)
+ }
+
pub fn initialize_epoch_2_05(&mut self) -> Result {
// use the `using!` statement to ensure that the old cost_tracker is placed
// back in all branches after initialization
@@ -2002,6 +2073,27 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> {
})
}
+ /// Instantiates epoch 4.0's `costs-5` cost contract using the [`COSTS_5_NAME`] and
+ /// [`BOOT_CODE_COSTS_5`] constants.
+ ///
+ /// Returns an error if this instance's epoch is not already set to
+ /// [`Epoch40`](StacksEpochId::Epoch40).
+ ///
+ /// Used both by epoch 4.0 initialization and by tests which need to instantiate the costs-5
+ /// contract without full epoch 4.0 initialization.
+ pub(super) fn instantiate_epoch_4_0_cost_contract(
+ &mut self,
+ ) -> Result {
+ if self.epoch != StacksEpochId::Epoch40 {
+ return Err(ClarityError::BadTransaction(format!(
+ "Epoch 4.0 cost contract initialization requires Epoch 4.0 rules; current epoch is {}",
+ self.epoch
+ )));
+ }
+
+ self.instantiate_boot_contract(COSTS_5_NAME, BOOT_CODE_COSTS_5, None)
+ }
+
pub fn initialize_epoch_4_0(&mut self) -> Result, ClarityError> {
// use the `using!` statement to ensure that the old cost_tracker is placed
// back in all branches after initialization
@@ -2036,70 +2128,21 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> {
.expect("PANIC: PoX-5 first reward cycle begins *before* first burn block height")
+ 1;
- let boot_code_account = self
- .get_boot_code_account()
- .expect("FATAL: did not get boot account");
-
- let mainnet = self.mainnet;
- let tx_version = if mainnet {
- TransactionVersion::Mainnet
- } else {
- TransactionVersion::Testnet
- };
-
- let boot_code_address = boot_code_addr(mainnet);
- let boot_code_auth = boot_code_tx_auth(boot_code_address);
-
/////////////////// .costs-5 ////////////////////////
- let costs_5_contract_id = boot_code_id(COSTS_5_NAME, mainnet);
- let payload = TransactionPayload::SmartContract(
- TransactionSmartContract {
- name: ContractName::try_from(COSTS_5_NAME)
- .expect("FATAL: invalid boot-code contract name"),
- code_body: StacksString::from_str(BOOT_CODE_COSTS_5)
- .expect("FATAL: invalid boot code body"),
- },
- None,
- );
-
- let costs_5_contract_tx =
- StacksTransaction::new(tx_version, boot_code_auth.clone(), payload);
-
- let costs_5_initialization_receipt = self.as_transaction(|tx_conn| {
- debug!("Instantiate {} contract", &costs_5_contract_id);
- let receipt = StacksChainState::process_transaction_payload(
- tx_conn,
- &costs_5_contract_tx,
- &boot_code_account,
- None,
- )
- .expect("FATAL: Failed to process .costs-5 contract initialization");
- receipt
- });
-
- if costs_5_initialization_receipt.result != Value::okay_true()
- || costs_5_initialization_receipt.post_condition_aborted
- {
- panic!(
- "FATAL: Failure processing .costs-5 contract initialization: {:#?}",
- &costs_5_initialization_receipt
- );
- }
+ let costs_5_initialization_receipt = self
+ .instantiate_epoch_4_0_cost_contract()
+ .expect("FATAL: Failed to initialize Epoch 4.0 cost contract");
/////////////////// .pox-5 ////////////////////////
- let pox_5_contract_id = boot_code_id(POX_5_NAME, mainnet);
- let pox_5_code = make_pox_5_body(mainnet);
- let payload = TransactionPayload::SmartContract(
- TransactionSmartContract {
- name: ContractName::try_from(POX_5_NAME)
- .expect("FATAL: invalid boot-code contract name"),
- code_body: StacksString::from_str(&pox_5_code)
- .expect("FATAL: invalid boot code body"),
- },
- Some(ClarityVersion::Clarity6),
- );
-
- let pox_5_contract_tx = StacksTransaction::new(tx_version, boot_code_auth, payload);
+ let pox_5_contract_id = boot_code_id(POX_5_NAME, self.mainnet);
+ let pox_5_code = make_pox_5_body(self.mainnet);
+ let (boot_code_account, pox_5_contract_tx) = self
+ .make_boot_code_smart_contract_tx(
+ POX_5_NAME,
+ &pox_5_code,
+ Some(ClarityVersion::Clarity6),
+ )
+ .expect("FATAL: Failed to construct .pox-5 contract initialization");
let pox_5_initialization_receipt = self.as_transaction(|tx_conn| {
debug!("Instantiate {} contract", &pox_5_contract_id);
diff --git a/stackslib/src/clarity_vm/tests/costs.rs b/stackslib/src/clarity_vm/tests/costs.rs
index 7796007ac3f..7e2b782c0fc 100644
--- a/stackslib/src/clarity_vm/tests/costs.rs
+++ b/stackslib/src/clarity_vm/tests/costs.rs
@@ -22,6 +22,7 @@ use clarity::vm::costs::{
DefaultVersion, ExecutionCost, LimitedCostTracker, COSTS_1_NAME, COSTS_2_NAME, COSTS_3_NAME,
COSTS_4_NAME,
};
+use clarity::vm::database::clarity_db::ClarityDatabase;
use clarity::vm::errors::VmExecutionError;
use clarity::vm::events::StacksTransactionEvent;
use clarity::vm::functions::NativeFunctions;
@@ -215,10 +216,21 @@ fn execute_transaction(
env.execute_transaction(issuer, None, contract_identifier.clone(), tx, args)
}
-fn with_owned_env(epoch: StacksEpochId, use_mainnet: bool, to_do: F) -> R
-where
- F: Fn(OwnedEnvironment) -> R,
-{
+// Epochs which deploy boot contracts needed for costs tests.
+const COST_TEST_SETUP_EPOCHS: [StacksEpochId; 4] = [
+ StacksEpochId::Epoch2_05,
+ StacksEpochId::Epoch21,
+ StacksEpochId::Epoch33,
+ StacksEpochId::Epoch40,
+];
+
+fn next_test_block_id(block_id_byte: &mut u8) -> StacksBlockId {
+ let block_id = StacksBlockId([*block_id_byte; 32]);
+ *block_id_byte += 1;
+ block_id
+}
+
+fn new_cost_test_clarity_instance(use_mainnet: bool) -> (ClarityInstance, StacksBlockId, u8) {
let marf_kv = MarfedKV::temporary();
let chain_id = test_only_mainnet_to_chain_id(use_mainnet);
let mut clarity_instance = ClarityInstance::new(use_mainnet, chain_id, marf_kv);
@@ -233,52 +245,74 @@ where
)
.commit_block();
- let mut tip = first_block.clone();
+ (clarity_instance, first_block, 1)
+}
- if epoch >= StacksEpochId::Epoch2_05 {
- let burn_state_db = generate_test_burn_state_db(StacksEpochId::Epoch2_05);
- let next_block = StacksBlockId([1; 32]);
- let mut clarity_conn =
- clarity_instance.begin_block(&tip, &next_block, &TEST_HEADER_DB, &burn_state_db);
- clarity_conn.initialize_epoch_2_05().unwrap();
- clarity_conn.commit_block();
- tip = next_block.clone();
+fn setup_cost_test_epoch(
+ clarity_instance: &mut ClarityInstance,
+ tip: &mut StacksBlockId,
+ block_id_byte: &mut u8,
+ setup_epoch: StacksEpochId,
+) {
+ let burn_state_db = generate_test_burn_state_db(setup_epoch);
+ let next_block = next_test_block_id(block_id_byte);
+ let mut clarity_conn =
+ clarity_instance.begin_block(tip, &next_block, &TEST_HEADER_DB, &burn_state_db);
+
+ match setup_epoch {
+ StacksEpochId::Epoch2_05 => {
+ clarity_conn.initialize_epoch_2_05().unwrap();
+ }
+ StacksEpochId::Epoch21 => {
+ clarity_conn.initialize_epoch_2_1().unwrap();
+ }
+ StacksEpochId::Epoch33 => {
+ clarity_conn.initialize_epoch_3_3().unwrap();
+ }
+ StacksEpochId::Epoch40 => {
+ // `initialize_epoch_4_0()` also deploys PoX-5, which requires the chainstate
+ // test harness's sBTC stubs. Cost tests only need the costs-5 contract.
+ clarity_conn.set_epoch_for_testing(StacksEpochId::Epoch40);
+ clarity_conn.instantiate_epoch_4_0_cost_contract().unwrap();
+ }
+ _ => unreachable!(
+ "COST_TEST_SETUP_EPOCHS only contains epochs which instantiate boot contracts needed for costs tests"
+ ),
}
- if epoch >= StacksEpochId::Epoch21 {
- let burn_state_db = generate_test_burn_state_db(StacksEpochId::Epoch21);
- let next_block = StacksBlockId([2; 32]);
- let mut clarity_conn =
- clarity_instance.begin_block(&tip, &next_block, &TEST_HEADER_DB, &burn_state_db);
- clarity_conn.initialize_epoch_2_1().unwrap();
- clarity_conn.commit_block();
- tip = next_block.clone();
- }
+ clarity_conn.commit_block();
+ *tip = next_block;
+}
- if epoch >= StacksEpochId::Epoch30 {
- let burn_state_db = generate_test_burn_state_db(StacksEpochId::Epoch30);
- let next_block = StacksBlockId([3; 32]);
- let mut clarity_conn =
- clarity_instance.begin_block(&tip, &next_block, &TEST_HEADER_DB, &burn_state_db);
- clarity_conn.initialize_epoch_3_0().unwrap();
- clarity_conn.commit_block();
- tip = next_block.clone();
- }
+fn setup_cost_test_epochs_through(
+ clarity_instance: &mut ClarityInstance,
+ tip: &mut StacksBlockId,
+ block_id_byte: &mut u8,
+ epoch: StacksEpochId,
+) {
+ for setup_epoch in COST_TEST_SETUP_EPOCHS {
+ if epoch < setup_epoch {
+ break;
+ }
- if epoch >= StacksEpochId::Epoch33 {
- let burn_state_db = generate_test_burn_state_db(StacksEpochId::Epoch33);
- let next_block = StacksBlockId([4; 32]);
- let mut clarity_conn =
- clarity_instance.begin_block(&tip, &next_block, &TEST_HEADER_DB, &burn_state_db);
- clarity_conn.initialize_epoch_3_3().unwrap();
- clarity_conn.commit_block();
- tip = next_block.clone();
+ setup_cost_test_epoch(clarity_instance, tip, block_id_byte, setup_epoch);
}
+}
+
+fn with_owned_env(epoch: StacksEpochId, use_mainnet: bool, to_do: F) -> R
+where
+ F: Fn(OwnedEnvironment) -> R,
+{
+ let (mut clarity_instance, mut tip, mut block_id_byte) =
+ new_cost_test_clarity_instance(use_mainnet);
+
+ setup_cost_test_epochs_through(&mut clarity_instance, &mut tip, &mut block_id_byte, epoch);
let mut marf_kv = clarity_instance.destroy();
let burn_state_db = generate_test_burn_state_db(epoch);
- let mut store = marf_kv.begin(&tip, &StacksBlockId([5; 32]));
+ let final_block = next_test_block_id(&mut block_id_byte);
+ let mut store = marf_kv.begin(&tip, &final_block);
to_do(OwnedEnvironment::new_max_limit(
store.as_clarity_db(&TEST_HEADER_DB, &burn_state_db),
@@ -287,6 +321,228 @@ where
))
}
+fn cost_voting_contract(use_mainnet: bool) -> &'static QualifiedContractIdentifier {
+ if use_mainnet {
+ &COST_VOTING_MAINNET_CONTRACT
+ } else {
+ &COST_VOTING_TESTNET_CONTRACT
+ }
+}
+
+struct CostProposal {
+ function_contract: PrincipalData,
+ function_name: &'static str,
+ cost_function_contract: PrincipalData,
+ cost_function_name: &'static str,
+}
+
+fn cost_proposal(
+ function_contract: impl Into,
+ function_name: &'static str,
+ cost_function_contract: impl Into,
+ cost_function_name: &'static str,
+) -> CostProposal {
+ CostProposal {
+ function_contract: function_contract.into(),
+ function_name,
+ cost_function_contract: cost_function_contract.into(),
+ cost_function_name,
+ }
+}
+
+fn write_confirmed_cost_proposals(
+ db: &mut ClarityDatabase,
+ use_mainnet: bool,
+ proposal_start_id: usize,
+ confirmed_proposal_count: usize,
+ proposals: impl IntoIterator
- ,
+) {
+ let voting_contract_to_use = cost_voting_contract(use_mainnet);
+ db.set_variable_unknown_descriptor(
+ voting_contract_to_use,
+ "confirmed-proposal-count",
+ Value::UInt(confirmed_proposal_count as u128),
+ )
+ .unwrap();
+
+ let epoch = db.get_clarity_epoch_version().unwrap();
+ for (ix, proposal) in proposals.into_iter().enumerate() {
+ let value = format!(
+ "{{ function-contract: '{},
+ function-name: \"{}\",
+ cost-function-contract: '{},
+ cost-function-name: \"{}\",
+ confirmed-height: u1 }}",
+ proposal.function_contract,
+ proposal.function_name,
+ proposal.cost_function_contract,
+ proposal.cost_function_name
+ );
+ db.set_entry_unknown_descriptor(
+ voting_contract_to_use,
+ "confirmed-proposals",
+ execute_on_network(
+ &format!("{{ confirmed-id: u{} }}", ix + proposal_start_id),
+ use_mainnet,
+ ),
+ execute_on_network(&value, use_mainnet),
+ &epoch,
+ )
+ .unwrap();
+ }
+}
+
+fn cost_voting_test_cost_definer(use_mainnet: bool) -> QualifiedContractIdentifier {
+ let p1 = execute_on_network("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", use_mainnet);
+ let Value::Principal(PrincipalData::Standard(p1_principal)) = p1 else {
+ panic!("Expected a standard principal data");
+ };
+ QualifiedContractIdentifier::new(p1_principal, ContractName::from_literal("cost-definer"))
+}
+
+fn deploy_cost_voting_test_cost_definer(
+ clarity_instance: &mut ClarityInstance,
+ tip: &mut StacksBlockId,
+ block_id_byte: &mut u8,
+ use_mainnet: bool,
+) -> QualifiedContractIdentifier {
+ let cost_definer = cost_voting_test_cost_definer(use_mainnet);
+ let cost_definer_src = "
+ (define-read-only (cost-definition-le (size uint))
+ {
+ runtime: u0, write_length: u0, write_count: u0, read_count: u0, read_length: u0
+ })
+ ";
+ let burn_state_db = generate_test_burn_state_db(StacksEpochId::Epoch20);
+ let next_block = next_test_block_id(block_id_byte);
+ let mut block_conn =
+ clarity_instance.begin_block(tip, &next_block, &TEST_HEADER_DB, &burn_state_db);
+ block_conn.as_transaction(|tx| {
+ let (ast, analysis) = tx
+ .analyze_smart_contract(&cost_definer, ClarityVersion::Clarity1, cost_definer_src)
+ .unwrap();
+ tx.initialize_smart_contract(
+ &cost_definer,
+ ClarityVersion::Clarity1,
+ &ast,
+ cost_definer_src,
+ None,
+ |_, _| None,
+ None,
+ )
+ .unwrap();
+ tx.save_analysis(&cost_definer, &analysis).unwrap();
+ });
+ block_conn.commit_block();
+ *tip = next_block;
+
+ cost_definer
+}
+
+fn write_voted_le_cost_state(
+ clarity_instance: &mut ClarityInstance,
+ tip: &mut StacksBlockId,
+ block_id_byte: &mut u8,
+ vote_state_epoch: StacksEpochId,
+ use_mainnet: bool,
+ cost_definer: &QualifiedContractIdentifier,
+) {
+ let burn_state_db = generate_test_burn_state_db(vote_state_epoch);
+ let next_block = next_test_block_id(block_id_byte);
+
+ let mut clarity_conn =
+ clarity_instance.begin_block(tip, &next_block, &TEST_HEADER_DB, &burn_state_db);
+ clarity_conn.as_transaction(|tx| {
+ tx.with_clarity_db(|db| {
+ db.set_clarity_epoch_version(vote_state_epoch)?;
+ write_confirmed_cost_proposals(
+ db,
+ use_mainnet,
+ 0,
+ 1,
+ [cost_proposal(
+ boot_code_id("costs", use_mainnet),
+ "cost_le",
+ cost_definer.clone(),
+ "cost-definition-le",
+ )],
+ );
+ Ok(())
+ })
+ .unwrap();
+ });
+ clarity_conn.commit_block();
+ *tip = next_block;
+}
+
+fn tracker_with_voted_le_cost_state(
+ load_epoch: StacksEpochId,
+ vote_state_epoch: StacksEpochId,
+ use_mainnet: bool,
+) -> (LimitedCostTracker, QualifiedContractIdentifier) {
+ let (mut clarity_instance, mut tip, mut block_id_byte) =
+ new_cost_test_clarity_instance(use_mainnet);
+ let cost_definer = deploy_cost_voting_test_cost_definer(
+ &mut clarity_instance,
+ &mut tip,
+ &mut block_id_byte,
+ use_mainnet,
+ );
+
+ let max_setup_epoch = std::cmp::max(load_epoch, vote_state_epoch);
+ let mut vote_state_written = false;
+
+ for setup_epoch in COST_TEST_SETUP_EPOCHS {
+ if !vote_state_written && vote_state_epoch < setup_epoch {
+ write_voted_le_cost_state(
+ &mut clarity_instance,
+ &mut tip,
+ &mut block_id_byte,
+ vote_state_epoch,
+ use_mainnet,
+ &cost_definer,
+ );
+ vote_state_written = true;
+ }
+
+ if max_setup_epoch < setup_epoch {
+ break;
+ }
+
+ setup_cost_test_epoch(
+ &mut clarity_instance,
+ &mut tip,
+ &mut block_id_byte,
+ setup_epoch,
+ );
+ }
+
+ if !vote_state_written {
+ write_voted_le_cost_state(
+ &mut clarity_instance,
+ &mut tip,
+ &mut block_id_byte,
+ vote_state_epoch,
+ use_mainnet,
+ &cost_definer,
+ );
+ }
+
+ let mut marf_kv = clarity_instance.destroy();
+
+ let burn_state_db = generate_test_burn_state_db(load_epoch);
+ let final_block = next_test_block_id(&mut block_id_byte);
+ let mut store = marf_kv.begin(&tip, &final_block);
+ let owned_env = OwnedEnvironment::new_max_limit(
+ store.as_clarity_db(&TEST_HEADER_DB, &burn_state_db),
+ load_epoch,
+ use_mainnet,
+ );
+ let (_db, tracker) = owned_env.destruct().unwrap();
+
+ (tracker, cost_definer)
+}
+
fn exec_cost(contract: &str, use_mainnet: bool, epoch: StacksEpochId) -> ExecutionCost {
let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR");
let Value::Principal(PrincipalData::Standard(p1_principal)) = p1.clone() else {
@@ -1621,121 +1877,85 @@ fn test_cost_voting_integration(use_mainnet: bool, clarity_version: ClarityVersi
let bad_cases = vec![
// non existent "replacement target"
- (
- PrincipalData::from(QualifiedContractIdentifier::local("non-existent").unwrap()),
+ cost_proposal(
+ QualifiedContractIdentifier::local("non-existent").unwrap(),
"non-existent-func",
- PrincipalData::from(cost_definer.clone()),
+ cost_definer.clone(),
"cost-definition",
),
// replacement target isn't a contract principal
- (
- p1_principal.clone().into(),
+ cost_proposal(
+ p1_principal.clone(),
"non-existent-func",
- cost_definer.clone().into(),
+ cost_definer.clone(),
"cost-definition",
),
// cost defining contract isn't a contract principal
- (
- intercepted.clone().into(),
+ cost_proposal(
+ intercepted.clone(),
"intercepted-function",
- p1_principal.into(),
+ p1_principal,
"cost-definition",
),
// replacement function doesn't exist
- (
- intercepted.clone().into(),
+ cost_proposal(
+ intercepted.clone(),
"non-existent-func",
- cost_definer.clone().into(),
+ cost_definer.clone(),
"cost-definition",
),
// replacement function isn't read-only
- (
- intercepted.clone().into(),
+ cost_proposal(
+ intercepted.clone(),
"non-read-only",
- cost_definer.clone().into(),
+ cost_definer.clone(),
"cost-definition",
),
// "boot cost" function doesn't exist
- (
- boot_code_id("costs", false).into(),
+ cost_proposal(
+ boot_code_id("costs", false),
"non-existent-func",
- cost_definer.clone().into(),
+ cost_definer.clone(),
"cost-definition",
),
// cost defining contract doesn't exist
- (
- intercepted.clone().into(),
+ cost_proposal(
+ intercepted.clone(),
"intercepted-function",
- QualifiedContractIdentifier::local("non-existent")
- .unwrap()
- .into(),
+ QualifiedContractIdentifier::local("non-existent").unwrap(),
"cost-definition",
),
// cost defining function doesn't exist
- (
- intercepted.clone().into(),
+ cost_proposal(
+ intercepted.clone(),
"intercepted-function",
- cost_definer.clone().into(),
+ cost_definer.clone(),
"cost-definition-2",
),
// cost defining contract isn't arithmetic-only
- (
- intercepted.clone().into(),
+ cost_proposal(
+ intercepted.clone(),
"intercepted-function",
- bad_cost_definer.into(),
+ bad_cost_definer,
"cost-definition",
),
// cost defining contract has incorrect number of arguments
- (
- intercepted.clone().into(),
+ cost_proposal(
+ intercepted.clone(),
"intercepted-function",
- bad_cost_args_definer.into(),
+ bad_cost_args_definer,
"cost-definition",
),
];
let bad_proposals = bad_cases.len();
- let voting_contract_to_use: &QualifiedContractIdentifier = if use_mainnet {
- &COST_VOTING_MAINNET_CONTRACT
- } else {
- &COST_VOTING_TESTNET_CONTRACT
- };
-
{
let mut store = marf_kv.begin(&StacksBlockId([1; 32]), &StacksBlockId([2; 32]));
let mut db = store.as_clarity_db(&TEST_HEADER_DB, burn_db);
db.begin();
-
- db.set_variable_unknown_descriptor(
- voting_contract_to_use,
- "confirmed-proposal-count",
- Value::UInt(bad_proposals as u128),
- )
- .unwrap();
-
- for (ix, (intercepted_ct, intercepted_f, cost_ct, cost_f)) in
- bad_cases.into_iter().enumerate()
- {
- let value = format!(
- "{{ function-contract: '{},
- function-name: \"{}\",
- cost-function-contract: '{},
- cost-function-name: \"{}\",
- confirmed-height: u1 }}",
- intercepted_ct, intercepted_f, cost_ct, cost_f
- );
- let epoch = db.get_clarity_epoch_version().unwrap();
- db.set_entry_unknown_descriptor(
- voting_contract_to_use,
- "confirmed-proposals",
- execute(&format!("{{ confirmed-id: u{} }}", ix)),
- execute(&value),
- &epoch,
- )
- .unwrap();
- }
+ write_confirmed_cost_proposals(&mut db, use_mainnet, 0, bad_proposals, bad_cases);
db.commit().unwrap();
store.test_commit();
}
@@ -1778,19 +1998,19 @@ fn test_cost_voting_integration(use_mainnet: bool, clarity_version: ClarityVersi
};
let good_cases = vec![
- (
+ cost_proposal(
intercepted.clone(),
"intercepted-function",
cost_definer.clone(),
"cost-definition",
),
- (
+ cost_proposal(
boot_code_id("costs", use_mainnet),
"cost_le",
cost_definer.clone(),
"cost-definition-le",
),
- (
+ cost_proposal(
intercepted.clone(),
"intercepted-function2",
cost_definer.clone(),
@@ -1804,35 +2024,14 @@ fn test_cost_voting_integration(use_mainnet: bool, clarity_version: ClarityVersi
let mut db = store.as_clarity_db(&TEST_HEADER_DB, burn_db);
db.begin();
- let good_proposals = good_cases.len() as u128;
- db.set_variable_unknown_descriptor(
- voting_contract_to_use,
- "confirmed-proposal-count",
- Value::UInt(bad_proposals as u128 + good_proposals),
- )
- .unwrap();
-
- for (ix, (intercepted_ct, intercepted_f, cost_ct, cost_f)) in
- good_cases.into_iter().enumerate()
- {
- let value = format!(
- "{{ function-contract: '{},
- function-name: \"{}\",
- cost-function-contract: '{},
- cost-function-name: \"{}\",
- confirmed-height: u1 }}",
- intercepted_ct, intercepted_f, cost_ct, cost_f
- );
- let epoch = db.get_clarity_epoch_version().unwrap();
- db.set_entry_unknown_descriptor(
- voting_contract_to_use,
- "confirmed-proposals",
- execute(&format!("{{ confirmed-id: u{} }}", ix + bad_proposals)),
- execute(&value),
- &epoch,
- )
- .unwrap();
- }
+ let good_proposals = good_cases.len();
+ write_confirmed_cost_proposals(
+ &mut db,
+ use_mainnet,
+ bad_proposals,
+ bad_proposals + good_proposals,
+ good_cases,
+ );
db.commit().unwrap();
store.test_commit();
@@ -1916,6 +2115,74 @@ fn test_cost_voting_integration_testnet() {
test_cost_voting_integration(false, ClarityVersion::Clarity2);
}
+/// Before epoch 4.0, confirmed `.cost-voting` state can replace boot cost functions.
+#[rstest::rstest]
+#[case::mainnet(true)]
+#[case::testnet(false)]
+fn cost_voting_state_applies_before_epoch_40(#[case] use_mainnet: bool) {
+ let (tracker, cost_definer) = tracker_with_voted_le_cost_state(
+ StacksEpochId::Epoch34,
+ StacksEpochId::Epoch34,
+ use_mainnet,
+ );
+
+ let cost_function_references = tracker.cost_function_references();
+ assert_eq!(
+ cost_function_references.len(),
+ ClarityCostFunction::ALL.len(),
+ "all cost functions must have a reference before cost-voting retires (mainnet={use_mainnet})"
+ );
+
+ let le_reference = cost_function_references
+ .get(&ClarityCostFunction::Le)
+ .expect("cost_le must have a reference");
+ let ClarityCostFunctionEvaluator::Clarity(le_reference) = le_reference else {
+ panic!("voted cost_le should be evaluated in Clarity before Epoch 4.0");
+ };
+ assert_eq!(&le_reference.contract_id, &cost_definer);
+ assert_eq!(&le_reference.function_name, "cost-definition-le");
+}
+
+/// With `.cost-voting` disabled >= epoch 4.0, both existing and newly written voting state is
+/// ignored and every cost function resolves to the boot cost contract.
+#[rstest::rstest]
+#[case::mainnet_pre_40_voting_state(true, StacksEpochId::Epoch34)]
+#[case::testnet_pre_40_voting_state(false, StacksEpochId::Epoch34)]
+#[case::mainnet_post_40_voting_state(true, StacksEpochId::Epoch40)]
+#[case::testnet_post_40_voting_state(false, StacksEpochId::Epoch40)]
+fn cost_voting_state_is_ignored_at_epoch_40(
+ #[case] use_mainnet: bool,
+ #[case] vote_state_epoch: StacksEpochId,
+) {
+ let (tracker, _cost_definer) =
+ tracker_with_voted_le_cost_state(StacksEpochId::Epoch40, vote_state_epoch, use_mainnet);
+
+ let cost_function_references = tracker.cost_function_references();
+ assert_eq!(
+ cost_function_references.len(),
+ ClarityCostFunction::ALL.len(),
+ "all cost functions must have a reference after cost-voting retires (mainnet={use_mainnet}, vote_state_epoch={vote_state_epoch})"
+ );
+ assert!(
+ cost_function_references.values().all(|evaluator| matches!(
+ evaluator,
+ ClarityCostFunctionEvaluator::Default(_, _, DefaultVersion::Costs5)
+ )),
+ "all cost functions must use the epoch 4.0 boot cost contract after cost-voting retires (mainnet={use_mainnet}, vote_state_epoch={vote_state_epoch})"
+ );
+
+ let le_reference = cost_function_references
+ .get(&ClarityCostFunction::Le)
+ .expect("cost_le must have a reference");
+ assert!(
+ matches!(
+ le_reference,
+ ClarityCostFunctionEvaluator::Default(_, _, DefaultVersion::Costs5)
+ ),
+ "voted cost_le replacement must be ignored after cost-voting retires (mainnet={use_mainnet}, vote_state_epoch={vote_state_epoch})"
+ );
+}
+
#[test]
fn test_cost_change() {
let use_mainnet = false;
diff --git a/stackslib/src/net/api/tests/postblock_v3.rs b/stackslib/src/net/api/tests/postblock_v3.rs
index 34cce000517..a50883bdbc0 100644
--- a/stackslib/src/net/api/tests/postblock_v3.rs
+++ b/stackslib/src/net/api/tests/postblock_v3.rs
@@ -15,7 +15,6 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
-use clarity::util::hash::{MerkleTree, Sha512Trunc256Sum};
use stacks_common::types::chainstate::StacksPrivateKey;
use stacks_common::types::StacksEpochId;