diff --git a/changelog.d/7341-cost-voting-epoch-4-0-gate b/changelog.d/7341-cost-voting-epoch-4-0-gate new file mode 100644 index 00000000000..abb315646b5 --- /dev/null +++ b/changelog.d/7341-cost-voting-epoch-4-0-gate @@ -0,0 +1 @@ +Added `.cost-voting` disablement gate beginning from epoch 4.0 diff --git a/changelog.d/7342-cost-voting-functional-removal.removed b/changelog.d/7342-cost-voting-functional-removal.removed new file mode 100644 index 00000000000..6b57e04fb42 --- /dev/null +++ b/changelog.d/7342-cost-voting-functional-removal.removed @@ -0,0 +1 @@ +Removes the functional elements of the `.cost-voting` contract per `SIP-044` \ No newline at end of file diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs deleted file mode 100644 index 4f4a55c0960..00000000000 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright (C) 2013-2020 Blocstack PBC, a public benefit corporation -// Copyright (C) 2020 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! Static-analysis pass that decides whether a contract is eligible to be -//! installed as a *cost-function* contract under the SIP-006 cost-voting -//! system. -//! -//! A cost-function contract supplies a Clarity function that the VM invokes -//! on every call to a native operation in order to compute that operation's -//! runtime/read/write cost. Because such functions run inside the cost -//! accounting path itself, they must be: -//! -//! - **Deterministic**: no chain-state reads (`block-height`, `tx-sender`, -//! `contract-caller`, `chain-id`, ...), no `contract-call?`. -//! - **Side-effect free**: no map/var/FT/NFT defines or mutations, no STX -//! transfers, no asset mints or burns, no `print`. -//! - **Bounded**: no `map`/`fold`/`filter`/list-cons or other iterating -//! constructs whose work scales with input size — the cost function must -//! itself be cheap and predictable. -//! - **Trait-free**: traits introduce dynamic dispatch that the cost -//! accountant cannot reason about statically. -//! - **Cheap**: only a restricted, predictable, constant-time subset of -//! native forms is allowed, including arithmetic/logic and certain simple -//! expression-building constructs. -//! -//! The pass walks every top-level form and expression and rejects anything -//! outside the set of allowed constructs. The result is stored in -//! [`ContractAnalysis::is_cost_contract_eligible`]; the cost-voting -//! machinery in `vm::costs` later refuses to adopt a proposal whose target -//! contract is not eligible. - -use clarity_types::representations::ClarityName; - -pub use super::errors::{ - RuntimeCheckErrorKind, StaticCheckError, check_argument_count, check_arguments_at_least, -}; -use crate::vm::ClarityVersion; -use crate::vm::analysis::types::ContractAnalysis; -use crate::vm::functions::NativeFunctions; -use crate::vm::functions::define::{DefineFunctions, DefineFunctionsParsed}; -use crate::vm::representations::SymbolicExpression; -use crate::vm::representations::SymbolicExpressionType::{ - Atom, AtomValue, Field, List, LiteralValue, TraitReference, -}; -use crate::vm::variables::NativeVariables; - -#[cfg(test)] -mod tests; - -/// -/// A static-analysis pass that checks whether or not -/// a proposed cost-function defining contract is allowable. -/// Cost-function defining contracts may not use contract-call, -/// any database operations, traits, or iterating operations (e.g., list -/// operations) -/// -pub struct ArithmeticOnlyChecker<'a> { - clarity_version: &'a ClarityVersion, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum Error { - DefineTypeForbidden(DefineFunctions), - VariableForbidden(NativeVariables), - FunctionNotPermitted(NativeFunctions), - TraitReferencesForbidden, - UnexpectedContractStructure, -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") - } -} - -impl ArithmeticOnlyChecker<'_> { - pub fn check_contract_cost_eligible(contract_analysis: &mut ContractAnalysis) { - let is_eligible = ArithmeticOnlyChecker::run(contract_analysis).is_ok(); - contract_analysis.is_cost_contract_eligible = is_eligible; - } - - pub fn run(contract_analysis: &ContractAnalysis) -> Result<(), Error> { - let checker = ArithmeticOnlyChecker { - clarity_version: &contract_analysis.clarity_version, - }; - for exp in contract_analysis.expressions.iter() { - checker.check_top_levels(exp)?; - } - - Ok(()) - } - - fn check_define_function( - &self, - _signature: &[SymbolicExpression], - body: &SymbolicExpression, - ) -> Result<(), Error> { - self.check_expression(body) - } - - fn check_top_levels(&self, expr: &SymbolicExpression) -> Result<(), Error> { - use crate::vm::functions::define::DefineFunctionsParsed::*; - if let Some(define_type) = DefineFunctionsParsed::try_parse(expr) - .map_err(|_| Error::UnexpectedContractStructure)? - { - match define_type { - // The _arguments_ to constant defines must be checked to ensure that - // any _evaluated arguments_ supplied to them are valid. - Constant { value, .. } => self.check_expression(value), - PrivateFunction { signature, body } => self.check_define_function(signature, body), - ReadOnlyFunction { signature, body } => self.check_define_function(signature, body), - PersistedVariable { .. } => Err(Error::DefineTypeForbidden( - DefineFunctions::PersistedVariable, - )), - BoundedFungibleToken { .. } => { - Err(Error::DefineTypeForbidden(DefineFunctions::FungibleToken)) - } - PublicFunction { .. } => { - Err(Error::DefineTypeForbidden(DefineFunctions::PublicFunction)) - } - Map { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::Map)), - NonFungibleToken { .. } => Err(Error::DefineTypeForbidden( - DefineFunctions::NonFungibleToken, - )), - UnboundedFungibleToken { .. } => { - Err(Error::DefineTypeForbidden(DefineFunctions::FungibleToken)) - } - Trait { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::Trait)), - UseTrait { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::UseTrait)), - ImplTrait { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::ImplTrait)), - } - } else { - self.check_expression(expr) - } - } - - fn check_expression(&self, expr: &SymbolicExpression) -> Result<(), Error> { - match expr.expr { - AtomValue(_) | LiteralValue(_) => { - // values and literals are always allowed - Ok(()) - } - Atom(ref variable) => self.check_variables_allowed(variable), - Field(_) | TraitReference(_, _) => Err(Error::TraitReferencesForbidden), - List(ref expression) => self.check_function_application(expression), - } - } - - fn check_variables_allowed(&self, var_name: &ClarityName) -> Result<(), Error> { - use crate::vm::variables::NativeVariables::*; - if let Some(native_var) = - NativeVariables::lookup_by_name_at_version(var_name, self.clarity_version) - { - match native_var { - ContractCaller | TxSender | TotalLiquidMicroSTX | BlockHeight | BurnBlockHeight - | Regtest | TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight - | StacksBlockTime | CurrentContract => Err(Error::VariableForbidden(native_var)), - NativeNone | NativeTrue | NativeFalse => Ok(()), - } - } else { - Ok(()) - } - } - - fn try_native_function_check( - &self, - function: &str, - args: &[SymbolicExpression], - ) -> Option> { - NativeFunctions::lookup_by_name_at_version(function, self.clarity_version) - .map(|function| self.check_native_function(function, args)) - } - - fn check_native_function( - &self, - function: NativeFunctions, - args: &[SymbolicExpression], - ) -> Result<(), Error> { - use crate::vm::functions::NativeFunctions::*; - match function { - FetchVar | GetBlockInfo | GetBurnBlockInfo | GetStacksBlockInfo | GetTenureInfo - | GetTokenBalance | GetAssetOwner | FetchEntry | SetEntry | DeleteEntry - | InsertEntry | SetVar | MintAsset | MintToken | TransferAsset | TransferToken - | ContractCall | StxTransfer | StxTransferMemo | StxBurn | AtBlock | GetStxBalance - | GetTokenSupply | BurnToken | FromConsensusBuff | ToConsensusBuff | BurnAsset - | StxGetAccount => Err(Error::FunctionNotPermitted(function)), - Append - | Concat - | AsMaxLen - | ContractOf - | PrincipalOf - | ListCons - | Print - | AsContract - | ElementAt - | ElementAtAlias - | IndexOf - | IndexOfAlias - | Map - | Filter - | Fold - | Slice - | ReplaceAt - | ContractHash - | RestrictAssets - | AsContractSafe - | AllowanceWithStx - | AllowanceWithFt - | AllowanceWithNft - | AllowanceWithStacking - | AllowanceWithStaking - | AllowanceWithPox - | AllowanceAll => Err(Error::FunctionNotPermitted(function)), - BuffToIntLe | BuffToUIntLe | BuffToIntBe | BuffToUIntBe => { - Err(Error::FunctionNotPermitted(function)) - } - IsStandard | PrincipalDestruct | PrincipalConstruct => { - Err(Error::FunctionNotPermitted(function)) - } - IntToAscii | IntToUtf8 | StringToInt | StringToUInt | ToAscii => { - Err(Error::FunctionNotPermitted(function)) - } - Sha512 | Sha512Trunc256 | Secp256k1Recover | Secp256k1Verify | Secp256r1Verify - | Ed25519Verify | Secp256k1Decompress | Hash160 | Sha256 | Keccak256 - | VerifyMerkleProof | GetBitcoinTxOutput => Err(Error::FunctionNotPermitted(function)), - Add | Subtract | Divide | Multiply | CmpGeq | CmpLeq | CmpLess | CmpGreater - | Modulo | Power | Sqrti | Log2 | BitwiseXor | And | Or | Not | Equals | If - | ConsSome | ConsOkay | ConsError | DefaultTo | UnwrapRet | UnwrapErrRet | IsOkay - | IsNone | Asserts | Unwrap | UnwrapErr | IsErr | IsSome | TryRet | ToUInt | ToInt - | Len | Begin | TupleMerge | BitwiseOr | BitwiseAnd | BitwiseXor2 | BitwiseNot - | BitwiseLShift | BitwiseRShift => { - // Check all arguments. - self.check_all(args) - } - // we need to treat all the remaining functions specially, because these - // do not eval all of their arguments (rather, one or more of their arguments - // is a name) - TupleGet => { - // these functions use a name in the first argument - check_argument_count(2, args).map_err(|_| Error::UnexpectedContractStructure)?; - self.check_all(&args[1..]) - } - Match => { - if !(args.len() == 4 || args.len() == 5) { - return Err(Error::UnexpectedContractStructure); - } - // check the match input - self.check_expression(&args[0])?; - // check the 'ok' branch - self.check_expression(&args[2])?; - // check the 'err' branch - if args.len() == 4 { - self.check_expression(&args[3]) - } else { - self.check_expression(&args[4]) - } - } - Let => { - check_arguments_at_least(2, args) - .map_err(|_| Error::UnexpectedContractStructure)?; - - let binding_list = args[0] - .match_list() - .ok_or(Error::UnexpectedContractStructure)?; - - for pair in binding_list.iter() { - let pair_expression = pair - .match_list() - .ok_or(Error::UnexpectedContractStructure)?; - if pair_expression.len() != 2 { - return Err(Error::UnexpectedContractStructure); - } - - self.check_expression(&pair_expression[1])?; - } - - self.check_all(&args[1..args.len()]) - } - TupleCons => { - for pair in args.iter() { - let pair_expression = pair - .match_list() - .ok_or(Error::UnexpectedContractStructure)?; - if pair_expression.len() != 2 { - return Err(Error::UnexpectedContractStructure); - } - - self.check_expression(&pair_expression[1])?; - } - Ok(()) - } - } - } - - fn check_all(&self, expressions: &[SymbolicExpression]) -> Result<(), Error> { - for expr in expressions.iter() { - self.check_expression(expr)?; - } - Ok(()) - } - - fn check_function_application(&self, expression: &[SymbolicExpression]) -> Result<(), Error> { - let (function_name, args) = expression - .split_first() - .ok_or(Error::UnexpectedContractStructure)?; - - let function_name = function_name - .match_atom() - .ok_or(Error::UnexpectedContractStructure)?; - - if let Some(result) = self.try_native_function_check(function_name, args) { - result - } else { - // non-native function invocations are always okay, just check the args! - self.check_all(args) - } - } -} diff --git a/clarity/src/vm/analysis/arithmetic_checker/tests.rs b/clarity/src/vm/analysis/arithmetic_checker/tests.rs deleted file mode 100644 index 0b0c33fe372..00000000000 --- a/clarity/src/vm/analysis/arithmetic_checker/tests.rs +++ /dev/null @@ -1,521 +0,0 @@ -// Copyright (C) 2013-2020 Blocstack PBC, a public benefit corporation -// Copyright (C) 2020-2026 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#[cfg(test)] -use rstest::rstest; -#[cfg(test)] -use rstest_reuse::{self, *}; -use stacks_common::types::StacksEpochId; - -use crate::vm::ClarityVersion; -use crate::vm::analysis::ContractAnalysis; -use crate::vm::analysis::arithmetic_checker::Error::*; -use crate::vm::analysis::arithmetic_checker::{ArithmeticOnlyChecker, Error}; -use crate::vm::ast::parse; -use crate::vm::costs::LimitedCostTracker; -use crate::vm::functions::NativeFunctions; -use crate::vm::functions::define::DefineFunctions; -use crate::vm::tests::test_clarity_versions; -use crate::vm::tooling::mem_type_check; -use crate::vm::types::QualifiedContractIdentifier; -use crate::vm::variables::NativeVariables; - -/// Checks whether or not a contract only contains arithmetic expressions (for example, defining a -/// map would not pass this check). -/// This check is useful in determining the validity of new potential cost functions. -fn arithmetic_check( - contract: &str, - version: ClarityVersion, - epoch: StacksEpochId, -) -> Result<(), Error> { - let contract_identifier = QualifiedContractIdentifier::transient(); - let expressions = parse(&contract_identifier, contract, version, epoch).unwrap(); - - let analysis = ContractAnalysis::new( - contract_identifier, - expressions, - LimitedCostTracker::new_free(), - epoch, - version, - ); - - ArithmeticOnlyChecker::run(&analysis) -} - -fn check_good(contract: &str, version: ClarityVersion, epoch: StacksEpochId) { - let analysis = mem_type_check(contract, version, epoch).unwrap().1; - ArithmeticOnlyChecker::run(&analysis).expect("Should pass arithmetic checks"); -} - -#[apply(test_clarity_versions)] -fn test_bad_defines(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) { - let tests = [ - ( - "(define-public (foo) (ok 1))", - DefineTypeForbidden(DefineFunctions::PublicFunction), - ), - ( - "(define-map foo-map ((a uint)) ((b uint))) (define-private (foo) (map-get? foo-map {a: u1}))", - DefineTypeForbidden(DefineFunctions::Map), - ), - ( - "(define-data-var foo-var uint u1) (define-private (foo) (var-get foo-var))", - DefineTypeForbidden(DefineFunctions::PersistedVariable), - ), - ( - "(define-fungible-token tokaroos u500)", - DefineTypeForbidden(DefineFunctions::FungibleToken), - ), - ( - "(define-fungible-token tokaroos)", - DefineTypeForbidden(DefineFunctions::FungibleToken), - ), - ( - "(define-non-fungible-token tokaroos uint)", - DefineTypeForbidden(DefineFunctions::NonFungibleToken), - ), - ( - "(define-trait foo-trait ((foo (uint)) (response uint uint)))", - DefineTypeForbidden(DefineFunctions::Trait), - ), - ]; - - // Check bad defines for each clarity version - for (contract, error) in tests.iter() { - assert_eq!( - arithmetic_check(contract, version, epoch), - Err(error.clone()), - "Check contract:\n {contract}" - ); - } -} - -#[test] -fn test_variables_fail_arithmetic_check_clarity1() { - // Tests the behavior using Clarity1. - let tests = [ - ( - "(define-private (foo) burn-block-height)", - Err(VariableForbidden(NativeVariables::BurnBlockHeight)), - ), - ( - "(define-private (foo) block-height)", - Err(VariableForbidden(NativeVariables::BlockHeight)), - ), - ( - "(define-private (foo) tx-sender)", - Err(VariableForbidden(NativeVariables::TxSender)), - ), - ( - "(define-private (foo) contract-caller)", - Err(VariableForbidden(NativeVariables::ContractCaller)), - ), - ( - "(define-private (foo) is-in-regtest)", - Err(VariableForbidden(NativeVariables::Regtest)), - ), - ( - "(define-private (foo) stx-liquid-supply)", - Err(VariableForbidden(NativeVariables::TotalLiquidMicroSTX)), - ), - ("(define-private (foo) tx-sponsor?)", Ok(())), - ("(define-private (foo) is-in-mainnet)", Ok(())), - ("(define-private (foo) chain-id)", Ok(())), - ]; - - for (contract, result) in tests.iter() { - assert_eq!( - arithmetic_check(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch2_05), - result.clone(), - "Check contract:\n {contract}" - ); - assert_eq!( - arithmetic_check(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch21), - result.clone(), - "Check contract:\n {contract}" - ); - } - - let tests = [ - "(define-private (foo) (begin true false none))", - "(define-private (foo) 1)", - ]; - - for contract in tests.iter() { - check_good(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch2_05); - check_good(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch21); - } -} - -#[test] -fn test_variables_fail_arithmetic_check_clarity2() { - // Tests the behavior using Clarity2. - let tests = [ - ( - "(define-private (foo) burn-block-height)", - Err(VariableForbidden(NativeVariables::BurnBlockHeight)), - ), - ( - "(define-private (foo) block-height)", - Err(VariableForbidden(NativeVariables::BlockHeight)), - ), - ( - "(define-private (foo) tx-sender)", - Err(VariableForbidden(NativeVariables::TxSender)), - ), - ( - "(define-private (foo) contract-caller)", - Err(VariableForbidden(NativeVariables::ContractCaller)), - ), - ( - "(define-private (foo) is-in-regtest)", - Err(VariableForbidden(NativeVariables::Regtest)), - ), - ( - "(define-private (foo) stx-liquid-supply)", - Err(VariableForbidden(NativeVariables::TotalLiquidMicroSTX)), - ), - ( - "(define-private (foo) tx-sponsor?)", - Err(VariableForbidden(NativeVariables::TxSponsor)), - ), - ( - "(define-private (foo) is-in-mainnet)", - Err(VariableForbidden(NativeVariables::Mainnet)), - ), - ( - "(define-private (foo) chain-id)", - Err(VariableForbidden(NativeVariables::ChainId)), - ), - ]; - - for (contract, result) in tests.iter() { - assert_eq!( - arithmetic_check(contract, ClarityVersion::Clarity2, StacksEpochId::Epoch21), - result.clone(), - "Check contract:\n {contract}" - ); - } -} - -#[test] -fn test_functions_clarity1() { - // Tests all functions against Clarity1 VM. Results should be different for Clarity1 vs Clarity2 functions. - let tests = [ - // Clarity1 functions. - ("(define-private (foo) (at-block 0x0202020202020202020202020202020202020202020202020202020202020202 (+ 1 2)))", - Err(FunctionNotPermitted(NativeFunctions::AtBlock))), - ("(define-private (foo) (map-get? foo-map {a: u1}))", - Err(FunctionNotPermitted(NativeFunctions::FetchEntry))), - ("(define-private (foo) (map-delete foo-map {a: u1}))", - Err(FunctionNotPermitted(NativeFunctions::DeleteEntry))), - ("(define-private (foo) (map-set foo-map {a: u1} {b: u2}))", - Err(FunctionNotPermitted(NativeFunctions::SetEntry))), - ("(define-private (foo) (map-insert foo-map {a: u1} {b: u2}))", - Err(FunctionNotPermitted(NativeFunctions::InsertEntry))), - ("(define-private (foo) (var-get foo-var))", - Err(FunctionNotPermitted(NativeFunctions::FetchVar))), - ("(define-private (foo) (var-set foo-var u2))", - Err(FunctionNotPermitted(NativeFunctions::SetVar))), - ("(define-private (foo (a principal)) (ft-get-balance tokaroos a))", - Err(FunctionNotPermitted(NativeFunctions::GetTokenBalance))), - ("(define-private (foo (a principal)) - (ft-transfer? stackaroo u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF))", - Err(FunctionNotPermitted(NativeFunctions::TransferToken))), - ("(define-private (foo (a principal)) - (ft-mint? stackaroo u100 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))", - Err(FunctionNotPermitted(NativeFunctions::MintToken))), - ("(define-private (foo (a principal)) - (nft-mint? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))", - Err(FunctionNotPermitted(NativeFunctions::MintAsset))), - ("(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", - Err(FunctionNotPermitted(NativeFunctions::TransferAsset))), - ("(nft-get-owner? stackaroo \"Roo\")", - Err(FunctionNotPermitted(NativeFunctions::GetAssetOwner))), - ("(get-block-info? id-header-hash 0)", - Err(FunctionNotPermitted(NativeFunctions::GetBlockInfo))), - ("(define-private (foo) (contract-call? .bar outer-call))", - Err(FunctionNotPermitted(NativeFunctions::ContractCall))), - ("(stx-get-balance 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", - Err(FunctionNotPermitted(NativeFunctions::GetStxBalance))), - ("(stx-burn? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", - Err(FunctionNotPermitted(NativeFunctions::StxBurn))), - (r#"(stx-transfer? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)"#, - Err(FunctionNotPermitted(NativeFunctions::StxTransfer))), - ("(define-private (foo (a (list 3 uint))) - (map log2 a))", - Err(FunctionNotPermitted(NativeFunctions::Map))), - ("(define-private (foo (a (list 3 (optional uint)))) - (filter is-none a))", - Err(FunctionNotPermitted(NativeFunctions::Filter))), - ("(define-private (foo (a (list 3 uint))) - (append a u4))", - Err(FunctionNotPermitted(NativeFunctions::Append))), - ("(define-private (foo (a (list 3 uint))) - (concat a a))", - Err(FunctionNotPermitted(NativeFunctions::Concat))), - ("(define-private (foo (a (list 3 uint))) - (as-max-len? a u4))", - Err(FunctionNotPermitted(NativeFunctions::AsMaxLen))), - ("(define-private (foo) (print 10))", - Err(FunctionNotPermitted(NativeFunctions::Print))), - ("(define-private (foo) (list 3 4 10))", - Err(FunctionNotPermitted(NativeFunctions::ListCons))), - ("(define-private (foo) (keccak256 0))", - Err(FunctionNotPermitted(NativeFunctions::Keccak256))), - ("(define-private (foo) (hash160 0))", - Err(FunctionNotPermitted(NativeFunctions::Hash160))), - ("(define-private (foo) (secp256k1-recover? 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a1301))", - Err(FunctionNotPermitted(NativeFunctions::Secp256k1Recover))), - ("(define-private (foo) (secp256k1-verify 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 - 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a13 - 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110))", - Err(FunctionNotPermitted(NativeFunctions::Secp256k1Verify))), - ("(define-private (foo) (sha256 0))", - Err(FunctionNotPermitted(NativeFunctions::Sha256))), - ("(define-private (foo) (sha512 0))", - Err(FunctionNotPermitted(NativeFunctions::Sha512))), - ("(define-private (foo) (sha512/256 0))", - Err(FunctionNotPermitted(NativeFunctions::Sha512Trunc256))), - - // Clarity2 functions. - (r#"(stx-transfer-memo? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF 0x010203)"#, - Ok(())), - ("(define-private (foo (a (list 3 uint))) - (slice? a u2 u3))", - Ok(())), - ("(define-private (foo (a (list 3 uint)) (b uint)) - (replace-at? a u1 b))", - Ok(())), - ("(buff-to-int-le 0x0001)", - Ok(())), - ("(buff-to-uint-le 0x0001)", - Ok(())), - ("(buff-to-int-be 0x0001)", - Ok(())), - ("(buff-to-uint-be 0x0001)", - Ok(())), - ("(buff-to-uint-be 0x0001)", - Ok(())), - ("(is-standard 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)", - Ok(())), - ("(principal-destruct? 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)", - Ok(())), - ("(principal-construct? 0x22 0xfa6bf38ed557fe417333710d6033e9419391a320)", - Ok(())), - ("(string-to-int? \"-1\")", - Ok(())), - ("(string-to-uint? \"1\")", - Ok(())), - ("(int-to-ascii 5)", - Ok(())), - ("(int-to-utf8 10)", - Ok(())), - ("(get-burn-block-info? header-hash u0)", - Ok(())), - ("(stx-account 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", - Ok(())), - ("(to-consensus-buff? 3)", - Ok(())), - ("(from-consensus-buff? true 0x03)", - Ok(())), - ]; - - for (contract, result) in tests.iter() { - assert_eq!( - arithmetic_check(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch2_05), - result.clone(), - "Check contract:\n {contract}" - ); - assert_eq!( - arithmetic_check(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch21), - result.clone(), - "Check contract:\n {contract}" - ); - } -} - -#[test] -fn test_functions_clarity2() { - // Tests functions against Clarity2 VM. The Clarity1 functions should still cause an error. - let tests = [ - // Clarity2 functions. - (r#"(stx-transfer-memo? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF 0x010203)"#, - Err(FunctionNotPermitted(NativeFunctions::StxTransferMemo))), - ("(define-private (foo (a (list 3 uint))) - (slice? a u2 u3))", - Err(FunctionNotPermitted(NativeFunctions::Slice))), - ("(define-private (foo (a (list 3 uint))) - (replace-at? a u2 (list u3)))", - Err(FunctionNotPermitted(NativeFunctions::ReplaceAt))), - ("(buff-to-int-le 0x0001)", - Err(FunctionNotPermitted(NativeFunctions::BuffToIntLe))), - ("(buff-to-uint-le 0x0001)", - Err(FunctionNotPermitted(NativeFunctions::BuffToUIntLe))), - ("(buff-to-int-be 0x0001)", - Err(FunctionNotPermitted(NativeFunctions::BuffToIntBe))), - ("(buff-to-uint-be 0x0001)", - Err(FunctionNotPermitted(NativeFunctions::BuffToUIntBe))), - ("(is-standard 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)", - Err(FunctionNotPermitted(NativeFunctions::IsStandard))), - ("(principal-destruct? 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)", - Err(FunctionNotPermitted(NativeFunctions::PrincipalDestruct))), - ("(principal-construct? 0x22 0xfa6bf38ed557fe417333710d6033e9419391a320)", - Err(FunctionNotPermitted(NativeFunctions::PrincipalConstruct))), - ("(string-to-int? \"-1\")", - Err(FunctionNotPermitted(NativeFunctions::StringToInt))), - ("(string-to-uint? \"1\")", - Err(FunctionNotPermitted(NativeFunctions::StringToUInt))), - ("(int-to-ascii 5)", - Err(FunctionNotPermitted(NativeFunctions::IntToAscii))), - ("(int-to-utf8 10)", - Err(FunctionNotPermitted(NativeFunctions::IntToUtf8))), - ("(get-burn-block-info? header-hash u0)", - Err(FunctionNotPermitted(NativeFunctions::GetBurnBlockInfo))), - ("(stx-account 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", - Err(FunctionNotPermitted(NativeFunctions::StxGetAccount))), - ("(to-consensus-buff? 3)", - Err(FunctionNotPermitted(NativeFunctions::ToConsensusBuff))), - ("(from-consensus-buff? true 0x03)", - Err(FunctionNotPermitted(NativeFunctions::FromConsensusBuff))), - - // Clarity1 functions. - ("(define-private (foo) (at-block 0x0202020202020202020202020202020202020202020202020202020202020202 (+ 1 2)))", - Err(FunctionNotPermitted(NativeFunctions::AtBlock))), - ("(define-private (foo) (map-get? foo-map {a: u1}))", - Err(FunctionNotPermitted(NativeFunctions::FetchEntry))), - ("(define-private (foo) (map-delete foo-map {a: u1}))", - Err(FunctionNotPermitted(NativeFunctions::DeleteEntry))), - ("(define-private (foo) (map-set foo-map {a: u1} {b: u2}))", - Err(FunctionNotPermitted(NativeFunctions::SetEntry))), - ("(define-private (foo) (map-insert foo-map {a: u1} {b: u2}))", - Err(FunctionNotPermitted(NativeFunctions::InsertEntry))), - ("(define-private (foo) (var-get foo-var))", - Err(FunctionNotPermitted(NativeFunctions::FetchVar))), - ("(define-private (foo) (var-set foo-var u2))", - Err(FunctionNotPermitted(NativeFunctions::SetVar))), - ("(define-private (foo (a principal)) (ft-get-balance tokaroos a))", - Err(FunctionNotPermitted(NativeFunctions::GetTokenBalance))), - ("(define-private (foo (a principal)) - (ft-transfer? stackaroo u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF))", - Err(FunctionNotPermitted(NativeFunctions::TransferToken))), - ("(define-private (foo (a principal)) - (ft-mint? stackaroo u100 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))", - Err(FunctionNotPermitted(NativeFunctions::MintToken))), - ("(define-private (foo (a principal)) - (nft-mint? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))", - Err(FunctionNotPermitted(NativeFunctions::MintAsset))), - ("(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", - Err(FunctionNotPermitted(NativeFunctions::TransferAsset))), - ("(nft-get-owner? stackaroo \"Roo\")", - Err(FunctionNotPermitted(NativeFunctions::GetAssetOwner))), - ("(get-block-info? id-header-hash 0)", - Err(FunctionNotPermitted(NativeFunctions::GetBlockInfo))), - ("(define-private (foo) (contract-call? .bar outer-call))", - Err(FunctionNotPermitted(NativeFunctions::ContractCall))), - ("(stx-get-balance 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", - Err(FunctionNotPermitted(NativeFunctions::GetStxBalance))), - ("(stx-burn? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", - Err(FunctionNotPermitted(NativeFunctions::StxBurn))), - (r#"(stx-transfer? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)"#, - Err(FunctionNotPermitted(NativeFunctions::StxTransfer))), - ("(define-private (foo (a (list 3 uint))) - (map log2 a))", - Err(FunctionNotPermitted(NativeFunctions::Map))), - ("(define-private (foo (a (list 3 (optional uint)))) - (filter is-none a))", - Err(FunctionNotPermitted(NativeFunctions::Filter))), - ("(define-private (foo (a (list 3 uint))) - (append a u4))", - Err(FunctionNotPermitted(NativeFunctions::Append))), - ("(define-private (foo (a (list 3 uint))) - (concat a a))", - Err(FunctionNotPermitted(NativeFunctions::Concat))), - ("(define-private (foo (a (list 3 uint))) - (as-max-len? a u4))", - Err(FunctionNotPermitted(NativeFunctions::AsMaxLen))), - ("(define-private (foo) (print 10))", - Err(FunctionNotPermitted(NativeFunctions::Print))), - ("(define-private (foo) (list 3 4 10))", - Err(FunctionNotPermitted(NativeFunctions::ListCons))), - ("(define-private (foo) (keccak256 0))", - Err(FunctionNotPermitted(NativeFunctions::Keccak256))), - ("(define-private (foo) (hash160 0))", - Err(FunctionNotPermitted(NativeFunctions::Hash160))), - ("(define-private (foo) (secp256k1-recover? 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a1301))", - Err(FunctionNotPermitted(NativeFunctions::Secp256k1Recover))), - ("(define-private (foo) (secp256k1-verify 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 - 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a13 - 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110))", - Err(FunctionNotPermitted(NativeFunctions::Secp256k1Verify))), - ("(define-private (foo) (sha256 0))", - Err(FunctionNotPermitted(NativeFunctions::Sha256))), - ("(define-private (foo) (sha512 0))", - Err(FunctionNotPermitted(NativeFunctions::Sha512))), - ("(define-private (foo) (sha512/256 0))", - Err(FunctionNotPermitted(NativeFunctions::Sha512Trunc256))), - ]; - - for (contract, result) in tests.iter() { - assert_eq!( - arithmetic_check(contract, ClarityVersion::Clarity2, StacksEpochId::Epoch21), - result.clone(), - "Check contract:\n {contract}" - ); - } -} - -#[test] -fn test_functions_contract() { - let good_tests = [ - "(match (if (is-eq 0 1) (ok 1) (err 2)) - ok-val (+ 1 ok-val) - err-val (+ 2 err-val))", - "(match (if (is-eq 0 1) (some 1) none) - ok-val (+ 1 ok-val) - 2)", - "(get a { a: (+ 9 1), b: (if (> 2 3) (* 4 4) (/ 4 2)) })", - "(define-private (foo) - (let ((a (+ 3 2 3))) (log2 a)))", - "(define-private (foo) - (let ((a (+ 32 3 4)) (b (- 32 1)) - (c (if (and (< 3 2) (<= 3 2) (>= 4 5) (or (is-eq (mod 5 4) 0) (not (> 3 2)))) - (pow u2 (log2 (sqrti u100000))) - (xor u120 u280))) - (d (default-to u0 (some u2)))) - (begin (unwrap! (some u3) u1) - (unwrap-err! (err u5) u4) - (asserts! true u4) - (unwrap-panic (some u3)) - (unwrap-err-panic (err u5)))))", - "(define-private (foo) (to-int (to-uint 34))) - (define-private (bar) (foo))", - "(define-private (foo) (begin - (is-some (some 4)) - (is-none (some 4)) - (is-ok (ok 4)) - (is-err (ok 5)) - (try! (some 4)) - (some 5))) - (define-read-only (bar) (foo))", - ]; - - for contract in good_tests.iter() { - check_good(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch2_05); - check_good(contract, ClarityVersion::Clarity1, StacksEpochId::Epoch21); - check_good(contract, ClarityVersion::Clarity2, StacksEpochId::Epoch21); - } -} diff --git a/clarity/src/vm/analysis/contract_interface_builder/mod.rs b/clarity/src/vm/analysis/contract_interface_builder/mod.rs index 89450c48864..3b1774571b0 100644 --- a/clarity/src/vm/analysis/contract_interface_builder/mod.rs +++ b/clarity/src/vm/analysis/contract_interface_builder/mod.rs @@ -50,7 +50,6 @@ pub fn build_contract_interface( type_map: _, cost_track: _, contract_interface: _, - is_cost_contract_eligible: _, } = contract_analysis; contract_interface diff --git a/clarity/src/vm/analysis/mod.rs b/clarity/src/vm/analysis/mod.rs index 1b92af2b0b1..350fc8f02a2 100644 --- a/clarity/src/vm/analysis/mod.rs +++ b/clarity/src/vm/analysis/mod.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . pub mod analysis_db; -pub mod arithmetic_checker; pub mod contract_interface_builder; pub mod errors; pub mod read_only_checker; @@ -26,7 +25,6 @@ pub mod types; use stacks_common::types::StacksEpochId; pub use self::analysis_db::AnalysisDatabase; -use self::arithmetic_checker::ArithmeticOnlyChecker; use self::contract_interface_builder::build_contract_interface; pub use self::errors::{ CommonCheckErrorKind, RuntimeCheckErrorKind, StaticCheckError, StaticCheckErrorKind, @@ -142,7 +140,6 @@ pub fn run_analysis( TypeChecker2_05::run_pass(&epoch, &mut contract_analysis, db, build_type_map)?; } TraitChecker::run_pass(&epoch, &mut contract_analysis, db)?; - ArithmeticOnlyChecker::check_contract_cost_eligible(&mut contract_analysis); if STORE_CONTRACT_SRC_INTERFACE { let interface = build_contract_interface(&contract_analysis)?; diff --git a/clarity/src/vm/analysis/type_checker/v2_05/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/mod.rs index e32c12e7d4d..365c6a4dc43 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/mod.rs @@ -45,8 +45,8 @@ use crate::vm::representations::SymbolicExpressionType::{ use crate::vm::representations::{ClarityName, SymbolicExpression, depth_traverse}; use crate::vm::types::signatures::{FunctionSignature, TypeSignatureExt as _}; use crate::vm::types::{ - FixedFunction, FunctionArg, FunctionType, PrincipalData, QualifiedContractIdentifier, - TypeSignature, Value, parse_name_type_pairs, + FixedFunction, FunctionArg, FunctionType, PrincipalData, TypeSignature, Value, + parse_name_type_pairs, }; use crate::vm::variables::NativeVariables; @@ -98,15 +98,6 @@ impl CostTracker for TypeChecker<'_, '_> { fn reset_memory(&mut self) { self.cost_track.reset_memory() } - fn short_circuit_contract_call( - &mut self, - contract: &QualifiedContractIdentifier, - function: &ClarityName, - input: &[u64], - ) -> std::result::Result { - self.cost_track - .short_circuit_contract_call(contract, function, input) - } } impl TypeChecker<'_, '_> { diff --git a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs index 517c30b03af..9a07afd21dd 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs @@ -111,15 +111,6 @@ impl CostTracker for TypeChecker<'_, '_> { fn reset_memory(&mut self) { self.cost_track.reset_memory() } - fn short_circuit_contract_call( - &mut self, - contract: &QualifiedContractIdentifier, - function: &ClarityName, - input: &[u64], - ) -> std::result::Result { - self.cost_track - .short_circuit_contract_call(contract, function, input) - } } impl TypeChecker<'_, '_> { diff --git a/clarity/src/vm/analysis/types.rs b/clarity/src/vm/analysis/types.rs index 1821c3eb81a..e8b71a298de 100644 --- a/clarity/src/vm/analysis/types.rs +++ b/clarity/src/vm/analysis/types.rs @@ -56,7 +56,6 @@ pub struct ContractAnalysis { pub defined_traits: BTreeMap>, pub implemented_traits: BTreeSet, pub contract_interface: Option, - pub is_cost_contract_eligible: bool, pub epoch: StacksEpochId, pub clarity_version: ClarityVersion, #[serde(skip)] @@ -91,7 +90,6 @@ impl ContractAnalysis { fungible_tokens: BTreeSet::new(), non_fungible_tokens: BTreeMap::new(), cost_track: Some(cost_track), - is_cost_contract_eligible: false, epoch, clarity_version, } diff --git a/clarity/src/vm/ast/mod.rs b/clarity/src/vm/ast/mod.rs index 561df22e8ad..a43ae4aea49 100644 --- a/clarity/src/vm/ast/mod.rs +++ b/clarity/src/vm/ast/mod.rs @@ -269,9 +269,7 @@ mod test { use crate::vm::costs::{LimitedCostTracker, *}; use crate::vm::representations::depth_traverse; use crate::vm::types::QualifiedContractIdentifier; - use crate::vm::{ - ClarityCostFunction, ClarityName, ClarityVersion, max_call_stack_depth_for_epoch, - }; + use crate::vm::{ClarityCostFunction, ClarityVersion, max_call_stack_depth_for_epoch}; #[derive(PartialEq, Debug)] struct UnitTestTracker { @@ -309,14 +307,6 @@ mod test { Ok(()) } fn reset_memory(&mut self) {} - fn short_circuit_contract_call( - &mut self, - _contract: &QualifiedContractIdentifier, - _function: &ClarityName, - _input: &[u64], - ) -> Result { - Ok(false) - } } #[test] diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index b34eb7a3aef..1419b1e031f 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -16,7 +16,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt; -use std::mem::replace; use std::time::{Duration, Instant}; use clarity_types::representations::ClarityName; @@ -1114,16 +1113,6 @@ impl CostTracker for ExecutionState<'_, '_, '_> { fn reset_memory(&mut self) { self.global_context.cost_track.reset_memory() } - fn short_circuit_contract_call( - &mut self, - contract: &QualifiedContractIdentifier, - function: &ClarityName, - input: &[u64], - ) -> std::result::Result { - self.global_context - .cost_track - .short_circuit_contract_call(contract, function, input) - } } impl CostTracker for GlobalContext<'_, '_> { @@ -1147,35 +1136,9 @@ impl CostTracker for GlobalContext<'_, '_> { fn reset_memory(&mut self) { self.cost_track.reset_memory() } - fn short_circuit_contract_call( - &mut self, - contract: &QualifiedContractIdentifier, - function: &ClarityName, - input: &[u64], - ) -> std::result::Result { - self.cost_track - .short_circuit_contract_call(contract, function, input) - } } impl<'a, 'b, 'hooks> ExecutionState<'a, 'b, 'hooks> { - /// Used only for contract-call! cost short-circuiting. Once the short-circuited cost - /// has been evaluated and assessed, the contract-call! itself is executed "for free". - pub fn run_free(&mut self, invoke_ctx: &InvocationContext, to_run: F) -> A - where - F: FnOnce(&mut ExecutionState, &InvocationContext) -> A, - { - let original_tracker = replace( - &mut self.global_context.cost_track, - LimitedCostTracker::new_free(), - ); - // note: it is important that this method not return until original_tracker has been - // restored. DO NOT use the try syntax (?). - let result = to_run(self, invoke_ctx); - self.global_context.cost_track = original_tracker; - result - } - pub fn eval_read_only( &mut self, invoke_ctx: &InvocationContext, diff --git a/clarity/src/vm/costs/mod.rs b/clarity/src/vm/costs/mod.rs index 9efcab817cd..a5dc7a2426e 100644 --- a/clarity/src/vm/costs/mod.rs +++ b/clarity/src/vm/costs/mod.rs @@ -23,7 +23,6 @@ use costs_2_testnet::Costs2Testnet; use costs_3::Costs3; use costs_4::Costs4; use costs_5::Costs5; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use stacks_common::types::StacksEpochId; @@ -38,12 +37,8 @@ use crate::vm::database::ClarityDatabase; use crate::vm::database::clarity_store::NullBackingStore; use crate::vm::errors::VmExecutionError; use crate::vm::types::Value::UInt; -use crate::vm::types::signatures::FunctionType::Fixed; -use crate::vm::types::signatures::TupleTypeSignature; -use crate::vm::types::{ - FunctionType, PrincipalData, QualifiedContractIdentifier, TupleData, TypeSignature, -}; -use crate::vm::{CallStack, ClarityName, LocalContext, SymbolicExpression, Value}; +use crate::vm::types::{PrincipalData, QualifiedContractIdentifier, TypeSignature}; +use crate::vm::{CallStack, LocalContext, SymbolicExpression, Value}; pub mod constants; pub mod cost_functions; #[allow(unused_variables)] @@ -69,37 +64,6 @@ pub const COSTS_3_NAME: &str = "costs-3"; pub const COSTS_4_NAME: &str = "costs-4"; pub const COSTS_5_NAME: &str = "costs-5"; -lazy_static! { - static ref COST_TUPLE_TYPE_SIGNATURE: TypeSignature = { - #[allow(clippy::expect_used)] - TypeSignature::TupleType( - TupleTypeSignature::try_from(vec![ - ( - ClarityName::from_literal("runtime"), - TypeSignature::UIntType, - ), - ( - ClarityName::from_literal("write_length"), - TypeSignature::UIntType, - ), - ( - ClarityName::from_literal("write_count"), - TypeSignature::UIntType, - ), - ( - ClarityName::from_literal("read_count"), - TypeSignature::UIntType, - ), - ( - ClarityName::from_literal("read_length"), - TypeSignature::UIntType, - ), - ]) - .expect("BUG: failed to construct type signature for cost tuple"), - ) - }; -} - pub fn runtime_cost, C: CostTracker>( cost_function: ClarityCostFunction, tracker: &mut C, @@ -156,15 +120,6 @@ pub trait CostTracker { fn add_memory(&mut self, memory: u64) -> Result<(), CostErrors>; fn drop_memory(&mut self, memory: u64) -> Result<(), CostErrors>; fn reset_memory(&mut self); - /// Check if the given contract-call should be short-circuited. - /// If so: this charges the cost to the CostTracker, and return true - /// If not: return false - fn short_circuit_contract_call( - &mut self, - contract: &QualifiedContractIdentifier, - function: &ClarityName, - input: &[u64], - ) -> Result; } // Don't track! @@ -186,14 +141,6 @@ impl CostTracker for () { Ok(()) } fn reset_memory(&mut self) {} - fn short_circuit_contract_call( - &mut self, - _contract: &QualifiedContractIdentifier, - _function: &ClarityName, - _input: &[u64], - ) -> Result { - Ok(false) - } } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -298,64 +245,11 @@ impl ClarityCostFunctionReference { } } -#[derive(Debug, Clone)] -pub struct CostStateSummary { - pub contract_call_circuits: - HashMap<(QualifiedContractIdentifier, ClarityName), ClarityCostFunctionReference>, - pub cost_function_references: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SerializedCostStateSummary { - contract_call_circuits: Vec<( - (QualifiedContractIdentifier, ClarityName), - ClarityCostFunctionReference, - )>, - cost_function_references: Vec<(ClarityCostFunction, ClarityCostFunctionReference)>, -} - -impl From for SerializedCostStateSummary { - fn from(other: CostStateSummary) -> SerializedCostStateSummary { - let CostStateSummary { - contract_call_circuits, - cost_function_references, - } = other; - SerializedCostStateSummary { - contract_call_circuits: contract_call_circuits.into_iter().collect(), - cost_function_references: cost_function_references.into_iter().collect(), - } - } -} - -impl From for CostStateSummary { - fn from(other: SerializedCostStateSummary) -> CostStateSummary { - let SerializedCostStateSummary { - contract_call_circuits, - cost_function_references, - } = other; - CostStateSummary { - contract_call_circuits: contract_call_circuits.into_iter().collect(), - cost_function_references: cost_function_references.into_iter().collect(), - } - } -} - -impl CostStateSummary { - pub fn empty() -> CostStateSummary { - CostStateSummary { - contract_call_circuits: HashMap::new(), - cost_function_references: HashMap::new(), - } - } -} - #[derive(Clone)] /// This struct holds all of the data required for non-free LimitedCostTracker instances pub struct TrackerData { cost_function_references: HashMap<&'static ClarityCostFunction, ClarityCostFunctionEvaluator>, cost_contracts: HashMap, - contract_call_circuits: - HashMap<(QualifiedContractIdentifier, ClarityName), ClarityCostFunctionReference>, total: ExecutionCost, limit: ExecutionCost, memory: u64, @@ -377,17 +271,6 @@ pub enum LimitedCostTracker { #[cfg(any(test, feature = "testing"))] impl LimitedCostTracker { - pub fn contract_call_circuits( - &self, - ) -> HashMap<(QualifiedContractIdentifier, ClarityName), ClarityCostFunctionReference> { - match self { - Self::Free => panic!("Cannot get contract call circuits on free tracker"), - Self::Limited(TrackerData { - contract_call_circuits, - .. - }) => contract_call_circuits.clone(), - } - } pub fn cost_function_references( &self, ) -> HashMap<&'static ClarityCostFunction, ClarityCostFunctionEvaluator> { @@ -437,360 +320,6 @@ impl PartialEq for LimitedCostTracker { } } -fn load_state_summary( - mainnet: bool, - clarity_db: &mut ClarityDatabase, -) -> Result { - let cost_voting_contract = boot_code_id("cost-voting", mainnet); - - let clarity_epoch = clarity_db - .get_clarity_epoch_version() - .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))?; - let last_processed_at = match clarity_db.get_value( - "vm-costs::last-processed-at-height", - &TypeSignature::UIntType, - &clarity_epoch, - ) { - Ok(Some(v)) => u32::try_from( - v.value - .expect_u128() - .map_err(|_| CostErrors::InterpreterFailure)?, - ) - .map_err(|_| CostErrors::InterpreterFailure)?, - Ok(None) => return Ok(CostStateSummary::empty()), - Err(e) => return Err(CostErrors::CostComputationFailed(e.to_string())), - }; - - let metadata_result = clarity_db - .fetch_metadata_manual::( - last_processed_at, - &cost_voting_contract, - "::state_summary", - ) - .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))?; - let serialized: SerializedCostStateSummary = match metadata_result { - Some(serialized) => { - serde_json::from_str(&serialized).map_err(|_| CostErrors::InterpreterFailure)? - } - None => return Ok(CostStateSummary::empty()), - }; - Ok(CostStateSummary::from(serialized)) -} - -fn store_state_summary( - mainnet: bool, - clarity_db: &mut ClarityDatabase, - to_store: &CostStateSummary, -) -> Result<(), CostErrors> { - let block_height = clarity_db.get_current_block_height(); - let cost_voting_contract = boot_code_id("cost-voting", mainnet); - let epoch = clarity_db - .get_clarity_epoch_version() - .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))?; - clarity_db - .put_value( - "vm-costs::last-processed-at-height", - Value::UInt(block_height as u128), - &epoch, - ) - .map_err(|_e| CostErrors::CostContractLoadFailure)?; - let serialized_summary = - serde_json::to_string(&SerializedCostStateSummary::from(to_store.clone())) - .map_err(|_| CostErrors::InterpreterFailure)?; - clarity_db - .set_metadata( - &cost_voting_contract, - "::state_summary", - &serialized_summary, - ) - .map_err(|e| CostErrors::Expect(e.to_string()))?; - - Ok(()) -} - -/// -/// This method loads a cost state summary structure from the currently open stacks chain tip -/// In doing so, it reads from the cost-voting contract to find any newly confirmed proposals, -/// checks those proposals for validity, and then applies those changes to the cached set -/// of cost functions. -/// -/// `apply_updates` - tells this function to look for any changes in the cost voting contract -/// which would need to be applied. if `false`, just load the last computed cost state in this -/// fork. -/// -fn load_cost_functions( - mainnet: bool, - clarity_db: &mut ClarityDatabase, - apply_updates: bool, -) -> Result { - let clarity_epoch = clarity_db - .get_clarity_epoch_version() - .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))?; - let last_processed_count = clarity_db - .get_value( - "vm-costs::last_processed_count", - &TypeSignature::UIntType, - &clarity_epoch, - ) - .map_err(|_e| CostErrors::CostContractLoadFailure)? - .map(|result| result.value) - .unwrap_or(Value::UInt(0)) - .expect_u128() - .map_err(|_| CostErrors::InterpreterFailure)?; - let cost_voting_contract = boot_code_id("cost-voting", mainnet); - let confirmed_proposals_count = clarity_db - .lookup_variable_unknown_descriptor( - &cost_voting_contract, - "confirmed-proposal-count", - &clarity_epoch, - ) - .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))? - .expect_u128() - .map_err(|_| CostErrors::InterpreterFailure)?; - debug!("Check cost voting contract"; - "confirmed_proposal_count" => confirmed_proposals_count, - "last_processed_count" => last_processed_count); - - // we need to process any confirmed proposals in the range [fetch-start, fetch-end) - let (fetch_start, fetch_end) = (last_processed_count, confirmed_proposals_count); - let mut state_summary = load_state_summary(mainnet, clarity_db)?; - if !apply_updates { - return Ok(state_summary); - } - - for confirmed_proposal in fetch_start..fetch_end { - // fetch the proposal data - let entry = clarity_db - .fetch_entry_unknown_descriptor( - &cost_voting_contract, - "confirmed-proposals", - &Value::from( - TupleData::from_data(vec![( - ClarityName::from_literal("confirmed-id"), - Value::UInt(confirmed_proposal), - )]) - .map_err(|_| { - CostErrors::Expect("BUG: failed to construct simple tuple".into()) - })?, - ), - &clarity_epoch, - ) - .map_err(|_| CostErrors::Expect("BUG: Failed querying confirmed-proposals".into()))? - .expect_optional() - .map_err(|_| CostErrors::InterpreterFailure)? - .ok_or_else(|| { - CostErrors::Expect("BUG: confirmed-proposal-count exceeds stored proposals".into()) - })? - .expect_tuple() - .map_err(|_| CostErrors::InterpreterFailure)?; - let target_contract = match entry - .get("function-contract") - .map_err(|_| CostErrors::Expect("BUG: malformed cost proposal tuple".into()))? - .clone() - .expect_principal() - .map_err(|_| CostErrors::InterpreterFailure)? - { - PrincipalData::Contract(contract_id) => contract_id, - _ => { - warn!("Confirmed cost proposal invalid: function-contract is not a contract principal"; - "confirmed_proposal_id" => confirmed_proposal); - continue; - } - }; - let target_function = match ClarityName::try_from( - entry - .get("function-name") - .map_err(|_| CostErrors::Expect("BUG: malformed cost proposal tuple".into()))? - .clone() - .expect_ascii() - .map_err(|_| CostErrors::InterpreterFailure)?, - ) { - Ok(x) => x, - Err(_) => { - warn!("Confirmed cost proposal invalid: function-name is not a valid function name"; - "confirmed_proposal_id" => confirmed_proposal); - continue; - } - }; - let cost_contract = match entry - .get("cost-function-contract") - .map_err(|_| CostErrors::Expect("BUG: malformed cost proposal tuple".into()))? - .clone() - .expect_principal() - .map_err(|_| CostErrors::InterpreterFailure)? - { - PrincipalData::Contract(contract_id) => contract_id, - _ => { - warn!("Confirmed cost proposal invalid: cost-function-contract is not a contract principal"; - "confirmed_proposal_id" => confirmed_proposal); - continue; - } - }; - - let cost_function = match ClarityName::try_from( - entry - .get_owned("cost-function-name") - .map_err(|_| CostErrors::Expect("BUG: malformed cost proposal tuple".into()))? - .expect_ascii() - .map_err(|_| CostErrors::InterpreterFailure)?, - ) { - Ok(x) => x, - Err(_) => { - warn!("Confirmed cost proposal invalid: cost-function-name is not a valid function name"; - "confirmed_proposal_id" => confirmed_proposal); - continue; - } - }; - - // Here is where we perform the required validity checks for a confirmed proposal: - // * Replaced contract-calls _must_ be `define-read-only` _or_ refer to one of the boot code - // cost functions - // * cost-function contracts must be arithmetic only - - // make sure the contract is "cost contract eligible" via the - // arithmetic-checking analysis pass - let (cost_func_ref, cost_func_type) = match clarity_db - .load_contract_analysis(&cost_contract) - .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))? - { - Some(c) => { - if !c.is_cost_contract_eligible { - warn!("Confirmed cost proposal invalid: cost-function-contract uses non-arithmetic or otherwise illegal operations"; - "confirmed_proposal_id" => confirmed_proposal, - "contract_name" => %cost_contract, - ); - continue; - } - - if let Some(FunctionType::Fixed(cost_function_type)) = c - .read_only_function_types - .get(&cost_function) - .or_else(|| c.private_function_types.get(&cost_function)) - { - if !cost_function_type.returns.eq(&COST_TUPLE_TYPE_SIGNATURE) { - warn!("Confirmed cost proposal invalid: cost-function-name does not return a cost tuple"; - "confirmed_proposal_id" => confirmed_proposal, - "contract_name" => %cost_contract, - "function_name" => %cost_function, - "return_type" => %cost_function_type.returns, - ); - continue; - } - if !cost_function_type.args.len() == 1 - || cost_function_type.args[0].signature != TypeSignature::UIntType - { - warn!("Confirmed cost proposal invalid: cost-function-name args should be length-1 and only uint"; - "confirmed_proposal_id" => confirmed_proposal, - "contract_name" => %cost_contract, - "function_name" => %cost_function, - ); - continue; - } - ( - ClarityCostFunctionReference { - contract_id: cost_contract, - function_name: cost_function.to_string(), - }, - cost_function_type.clone(), - ) - } else { - warn!("Confirmed cost proposal invalid: cost-function-name not defined"; - "confirmed_proposal_id" => confirmed_proposal, - "contract_name" => %cost_contract, - "function_name" => %cost_function, - ); - continue; - } - } - None => { - warn!("Confirmed cost proposal invalid: cost-function-contract is not a published contract"; - "confirmed_proposal_id" => confirmed_proposal, - "contract_name" => %cost_contract, - ); - continue; - } - }; - - if target_contract == boot_code_id("costs", mainnet) { - // refering to one of the boot code cost functions - let target = match ClarityCostFunction::lookup_by_name(&target_function) { - Some(ClarityCostFunction::Unimplemented) => { - warn!("Attempted vote on unimplemented cost function"; - "confirmed_proposal_id" => confirmed_proposal, - "cost_function" => %target_function); - continue; - } - Some(cost_func) => cost_func, - None => { - warn!("Confirmed cost proposal invalid: function-name does not reference a Clarity cost function"; - "confirmed_proposal_id" => confirmed_proposal, - "cost_function" => %target_function); - continue; - } - }; - state_summary - .cost_function_references - .insert(target, cost_func_ref); - } else { - // referring to a user-defined function - match clarity_db - .load_contract_analysis(&target_contract) - .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))? - { - Some(c) => { - if let Some(Fixed(tf)) = c.read_only_function_types.get(&target_function) { - if cost_func_type.args.len() != tf.args.len() { - warn!("Confirmed cost proposal invalid: cost-function contains the wrong number of arguments"; - "confirmed_proposal_id" => confirmed_proposal, - "target_contract_name" => %target_contract, - "target_function_name" => %target_function, - ); - continue; - } - for arg in &cost_func_type.args { - if arg.signature != TypeSignature::UIntType { - warn!("Confirmed cost proposal invalid: contains non uint argument"; - "confirmed_proposal_id" => confirmed_proposal, - ); - continue; - } - } - } else { - warn!("Confirmed cost proposal invalid: function-name not defined or is not read-only"; - "confirmed_proposal_id" => confirmed_proposal, - "target_contract_name" => %target_contract, - "target_function_name" => %target_function, - ); - continue; - } - } - None => { - warn!("Confirmed cost proposal invalid: contract-name not a published contract"; - "confirmed_proposal_id" => confirmed_proposal, - "target_contract_name" => %target_contract, - ); - continue; - } - } - state_summary - .contract_call_circuits - .insert((target_contract, target_function), cost_func_ref); - } - } - if confirmed_proposals_count > last_processed_count { - store_state_summary(mainnet, clarity_db, &state_summary)?; - clarity_db - .put_value( - "vm-costs::last_processed_count", - Value::UInt(confirmed_proposals_count), - &clarity_epoch, - ) - .map_err(|_e| CostErrors::CostContractLoadFailure)?; - } - - Ok(state_summary) -} - impl LimitedCostTracker { pub fn new( mainnet: bool, @@ -802,7 +331,6 @@ impl LimitedCostTracker { let mut cost_tracker = TrackerData { cost_function_references: HashMap::new(), cost_contracts: HashMap::new(), - contract_call_circuits: HashMap::new(), limit, memory_limit: CLARITY_MEMORY_LIMIT, total: ExecutionCost::ZERO, @@ -812,7 +340,7 @@ impl LimitedCostTracker { chain_id, }; assert!(clarity_db.is_stack_empty()); - cost_tracker.load_costs(clarity_db, true)?; + cost_tracker.load_costs(clarity_db)?; Ok(Self::Limited(cost_tracker)) } @@ -826,7 +354,6 @@ impl LimitedCostTracker { let mut cost_tracker = TrackerData { cost_function_references: HashMap::new(), cost_contracts: HashMap::new(), - contract_call_circuits: HashMap::new(), limit, memory_limit: CLARITY_MEMORY_LIMIT, total: ExecutionCost::ZERO, @@ -835,7 +362,7 @@ impl LimitedCostTracker { mainnet, chain_id, }; - cost_tracker.load_costs(clarity_db, false)?; + cost_tracker.load_costs(clarity_db)?; Ok(Self::Limited(cost_tracker)) } @@ -913,7 +440,6 @@ impl LimitedCostTracker { let cost_tracker = TrackerData { cost_function_references: cost_functions, cost_contracts: HashMap::new(), - contract_call_circuits: HashMap::new(), limit, memory_limit: CLARITY_MEMORY_LIMIT, total: ExecutionCost::ZERO, @@ -928,16 +454,11 @@ impl LimitedCostTracker { } impl TrackerData { - // TODO: add tests from mutation testing results #4831 - #[cfg_attr(test, mutants::skip)] - /// `apply_updates` - tells this function to look for any changes in the cost voting contract - /// which would need to be applied. if `false`, just load the last computed cost state in this - /// fork. - fn load_costs( - &mut self, - clarity_db: &mut ClarityDatabase, - apply_updates: bool, - ) -> Result<(), CostErrors> { + /// Load the default cost functions for this tracker's epoch. + /// + /// Every Clarity cost function is evaluated against the boot cost contract + /// for the current epoch (`costs`, `costs-2`, `costs-3`, `costs-4`, or `costs-5`). + fn load_costs(&mut self, clarity_db: &mut ClarityDatabase) -> Result<(), CostErrors> { clarity_db.begin(); let epoch_id = clarity_db .get_clarity_epoch_version() @@ -953,86 +474,38 @@ impl TrackerData { )) })?; - 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, - } - })?; - - self.contract_call_circuits = contract_call_circuits; - - let iter = ClarityCostFunction::ALL.iter(); - let iter_len = iter.len(); - let mut cost_contracts = HashMap::with_capacity(iter_len); - let mut m = HashMap::with_capacity(iter_len); - - for f in iter { - let cost_function_ref = cost_function_references.remove(f).unwrap_or_else(|| { - ClarityCostFunctionReference::new(boot_costs_id.clone(), f.get_name()) - }); - if !cost_contracts.contains_key(&cost_function_ref.contract_id) { - let contract = match clarity_db.get_contract(&cost_function_ref.contract_id) { - Ok(contract) => contract, - Err(e) => { - error!("Failed to load intended Clarity cost contract"; - "contract" => %cost_function_ref.contract_id, - "error" => ?e); - clarity_db - .roll_back() - .map_err(|e| CostErrors::Expect(e.to_string()))?; - return Err(CostErrors::CostContractLoadFailure); - } - }; - cost_contracts.insert(cost_function_ref.contract_id.clone(), contract); + let boot_cost_contract = match clarity_db.get_contract(&boot_costs_id) { + Ok(contract) => contract, + Err(e) => { + error!("Failed to load intended Clarity cost contract"; + "contract" => %boot_costs_id, + "error" => ?e); + clarity_db + .roll_back() + .map_err(|e| CostErrors::Expect(e.to_string()))?; + return Err(CostErrors::CostContractLoadFailure); } + }; - if cost_function_ref.contract_id == boot_costs_id { - m.insert( - f, - ClarityCostFunctionEvaluator::Default(cost_function_ref, f.clone(), v), - ); - } else { - m.insert(f, ClarityCostFunctionEvaluator::Clarity(cost_function_ref)); - } + let mut m = HashMap::with_capacity(ClarityCostFunction::ALL.len()); + for f in ClarityCostFunction::ALL.iter() { + let cost_function_ref = + ClarityCostFunctionReference::new(boot_costs_id.clone(), f.get_name()); + m.insert( + f, + ClarityCostFunctionEvaluator::Default(cost_function_ref, f.clone(), v), + ); } - for (_, circuit_target) in self.contract_call_circuits.iter() { - if !cost_contracts.contains_key(&circuit_target.contract_id) { - let contract = match clarity_db.get_contract(&circuit_target.contract_id) { - Ok(contract) => contract, - Err(e) => { - error!("Failed to load intended Clarity cost contract"; - "contract" => %boot_costs_id.to_string(), - "error" => %format!("{:?}", e)); - clarity_db - .roll_back() - .map_err(|e| CostErrors::Expect(e.to_string()))?; - return Err(CostErrors::CostContractLoadFailure); - } - }; - cost_contracts.insert(circuit_target.contract_id.clone(), contract); - } - } + let mut cost_contracts = HashMap::with_capacity(1); + cost_contracts.insert(boot_costs_id, boot_cost_contract); self.cost_function_references = m; self.cost_contracts = cost_contracts; - if apply_updates { - clarity_db - .commit() - .map_err(|e| CostErrors::Expect(e.to_string()))?; - } else { - clarity_db - .roll_back() - .map_err(|e| CostErrors::Expect(e.to_string()))?; - } + clarity_db + .commit() + .map_err(|e| CostErrors::Expect(e.to_string()))?; Ok(()) } @@ -1274,29 +747,6 @@ impl CostTracker for LimitedCostTracker { } } } - fn short_circuit_contract_call( - &mut self, - contract: &QualifiedContractIdentifier, - function: &ClarityName, - input: &[u64], - ) -> Result { - match self { - Self::Free => { - // if we're already free, no need to worry about short circuiting contract-calls - Ok(false) - } - Self::Limited(data) => { - // grr, if HashMap::get didn't require Borrow, we wouldn't need this cloning. - let lookup_key = (contract.clone(), function.clone()); - if let Some(cost_function) = data.contract_call_circuits.get(&lookup_key).cloned() { - compute_cost(data, cost_function, input, data.epoch)?; - Ok(true) - } else { - Ok(false) - } - } - } - } } impl CostTracker for &mut LimitedCostTracker { @@ -1319,14 +769,6 @@ impl CostTracker for &mut LimitedCostTracker { fn reset_memory(&mut self) { LimitedCostTracker::reset_memory(self) } - fn short_circuit_contract_call( - &mut self, - contract: &QualifiedContractIdentifier, - function: &ClarityName, - input: &[u64], - ) -> Result { - LimitedCostTracker::short_circuit_contract_call(self, contract, function, input) - } } // ONLY WORKS IF INPUT IS u64 diff --git a/clarity/src/vm/functions/database.rs b/clarity/src/vm/functions/database.rs index e3adb9b2f45..380a446f7ab 100644 --- a/clarity/src/vm/functions/database.rs +++ b/clarity/src/vm/functions/database.rs @@ -81,10 +81,8 @@ pub fn special_contract_call( let rest_args_slice = &args[2..]; let rest_args_len = rest_args_slice.len(); let mut rest_args = Vec::with_capacity(rest_args_len); - let mut rest_args_sizes = Vec::with_capacity(rest_args_len); for arg in rest_args_slice.iter() { let evaluated_arg = eval(arg, exec_state, invoke_ctx, context)?; - rest_args_sizes.push(evaluated_arg.as_ref().size()?.into()); rest_args.push(SymbolicExpression::atom_value( evaluated_arg.clone_with_cost(exec_state)?, )); @@ -238,29 +236,13 @@ pub fn special_contract_call( .into(); let nested_ctx = invoke_ctx.with_caller(contract_principal); - let result = if exec_state.short_circuit_contract_call( + let result = exec_state.execute_contract( + &nested_ctx, &contract_identifier, function_name, - &rest_args_sizes, - )? { - exec_state.run_free(&nested_ctx, |free_exec_state, nested_ctx| { - free_exec_state.execute_contract( - nested_ctx, - &contract_identifier, - function_name, - &rest_args, - false, - ) - }) - } else { - exec_state.execute_contract( - &nested_ctx, - &contract_identifier, - function_name, - &rest_args, - false, - ) - }?; + &rest_args, + false, + )?; // sanitize contract-call outputs in epochs >= 2.4 let result_type = TypeSignature::type_of(&result)?; 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..7d4fa5ebf01 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; @@ -814,34 +816,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] + } - &Self::ALL[start_idx..=end_idx] + /// 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(), + }; + + // 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..32bbaa9b4ce 100644 --- a/stacks-common/src/types/tests.rs +++ b/stacks-common/src/types/tests.rs @@ -14,12 +14,100 @@ // 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_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/stacks-node/src/tests/neon_integrations.rs b/stacks-node/src/tests/neon_integrations.rs index 916e3a5b297..08d1d3a035b 100644 --- a/stacks-node/src/tests/neon_integrations.rs +++ b/stacks-node/src/tests/neon_integrations.rs @@ -4182,352 +4182,6 @@ fn block_replay_integration_test() { channel.stop_chains_coordinator(); } -#[test] -#[ignore] -fn cost_voting_integration() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - // let's make `<` free... - 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 - }) - "; - - // the contract that we'll test the costs of - let caller_src = " - (define-public (execute-2 (a uint)) - (ok (< a a))) - "; - - let power_vote_src = " - (define-public (propose-vote-confirm) - (let - ((proposal-id (unwrap-panic (contract-call? 'ST000000000000000000002AMW42H.cost-voting submit-proposal - 'ST000000000000000000002AMW42H.costs \"cost_le\" - .cost-definer \"cost-definition-le\"))) - (vote-amount (* u9000000000 u1000000))) - (try! (contract-call? 'ST000000000000000000002AMW42H.cost-voting vote-proposal proposal-id vote-amount)) - (try! (contract-call? 'ST000000000000000000002AMW42H.cost-voting confirm-votes proposal-id)) - (ok proposal-id))) - "; - - let spender_sk = StacksPrivateKey::random(); - let spender_addr = to_addr(&spender_sk); - let spender_princ: PrincipalData = spender_addr.clone().into(); - - let (mut conf, miner_account) = neon_integration_test_conf(); - - conf.miner.microblock_attempt_time_ms = 1_000; - conf.node.wait_time_for_microblocks = 0; - conf.node.microblock_frequency = 1_000; - conf.miner.first_attempt_time_ms = 2_000; - conf.miner.subsequent_attempt_time_ms = 5_000; - conf.burnchain.max_rbf = 10_000_000; - conf.node.wait_time_for_blocks = 1_000; - - test_observer::spawn(); - test_observer::register_any(&mut conf); - - let spender_bal = 10_000_000_000 * u64::from(core::MICROSTACKS_PER_STACKS); - - conf.initial_balances.push(InitialBalance { - address: spender_princ.clone(), - amount: spender_bal, - }); - - let mut btcd_controller = BitcoinCoreController::from_stx_config(&conf); - btcd_controller - .start_bitcoind() - .expect("Failed starting bitcoind"); - - let burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - - let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( - conf.clone(), - None, - Some(burnchain_config.clone()), - None, - ); - let http_origin = format!("http://{}", &conf.node.rpc_bind); - - btc_regtest_controller.bootstrap_chain(201); - - eprintln!("Chain bootstrapped..."); - - let mut run_loop = neon::RunLoop::new(conf.clone()); - let blocks_processed = run_loop.get_blocks_processed_arc(); - let channel = run_loop.get_coordinator_channel().unwrap(); - - thread::spawn(move || run_loop.start(Some(burnchain_config), 0)); - - // give the run loop some time to start up! - wait_for_runloop(&blocks_processed); - - // first block wakes up the run loop - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // first block will hold our VRF registration - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // second block will be the first mined Stacks block - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // let's query the miner's account nonce: - let res = get_account(&http_origin, &miner_account); - assert_eq!(res.balance, 0); - assert_eq!(res.nonce, 1); - - // and our spender: - let res = get_account(&http_origin, &spender_princ); - assert_eq!(res.balance, spender_bal as u128); - assert_eq!(res.nonce, 0); - - let transactions = vec![ - make_contract_publish( - &spender_sk, - 0, - 1000, - conf.burnchain.chain_id, - "cost-definer", - cost_definer_src, - ), - make_contract_publish( - &spender_sk, - 1, - 1000, - conf.burnchain.chain_id, - "caller", - caller_src, - ), - make_contract_publish( - &spender_sk, - 2, - 1000, - conf.burnchain.chain_id, - "voter", - power_vote_src, - ), - ]; - - for tx in transactions.into_iter() { - submit_tx(&http_origin, &tx); - } - - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - let vote_tx = make_contract_call( - &spender_sk, - 3, - 1000, - conf.burnchain.chain_id, - &spender_addr, - "voter", - "propose-vote-confirm", - &[], - ); - - let call_le_tx = make_contract_call( - &spender_sk, - 4, - 1000, - conf.burnchain.chain_id, - &spender_addr, - "caller", - "execute-2", - &[Value::UInt(1)], - ); - - test_observer::clear(); - submit_tx(&http_origin, &vote_tx); - submit_tx(&http_origin, &call_le_tx); - - // Mine blocks until both txs are confirmed (nonces 3 and 4) - mine_blocks_until(&btc_regtest_controller, &blocks_processed, 3, 60, || { - let res = get_account(&http_origin, &spender_princ); - Ok(res.nonce >= 5) - }) - .expect("vote and execute txs should have been mined"); - - let blocks = test_observer::get_blocks(); - let mut tested = false; - let mut exec_cost = ExecutionCost::ZERO; - for block in blocks.iter() { - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "execute-2" { - exec_cost = - serde_json::from_value(tx.get("execution_cost").cloned().unwrap()).unwrap(); - } else if contract_call.function_name.as_str() == "propose-vote-confirm" { - let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); - let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); - assert_eq!(parsed.to_string(), "(ok u0)"); - tested = true; - } - } - } - } - assert!(tested, "Should have found a contract call tx"); - - // try to confirm the passed vote (this will fail) - let confirm_proposal = make_contract_call( - &spender_sk, - 5, - 1000, - conf.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "cost-voting", - "confirm-miners", - &[Value::UInt(0)], - ); - - test_observer::clear(); - submit_tx(&http_origin, &confirm_proposal); - - // Mine blocks until early confirm-miners is confirmed (nonce 5) - mine_blocks_until(&btc_regtest_controller, &blocks_processed, 3, 60, || { - let res = get_account(&http_origin, &spender_princ); - Ok(res.nonce >= 6) - }) - .expect("early confirm-miners tx should have been mined"); - - let blocks = test_observer::get_blocks(); - let mut tested = false; - for block in blocks.iter() { - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "confirm-miners" { - let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); - let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); - assert_eq!(parsed.to_string(), "(err 13)"); - tested = true; - } - } - } - } - assert!(tested, "Should have found a contract call tx"); - - for _i in 0..58 { - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - } - - // confirm the passed vote - let confirm_proposal = make_contract_call( - &spender_sk, - 6, - 1000, - conf.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "cost-voting", - "confirm-miners", - &[Value::UInt(0)], - ); - - test_observer::clear(); - submit_tx(&http_origin, &confirm_proposal); - - // Mine blocks until confirm-miners after maturation is confirmed (nonce 6) - mine_blocks_until(&btc_regtest_controller, &blocks_processed, 3, 60, || { - let res = get_account(&http_origin, &spender_princ); - Ok(res.nonce >= 7) - }) - .expect("confirm-miners tx should have been mined"); - - let blocks = test_observer::get_blocks(); - let mut tested = false; - for block in blocks.iter() { - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "confirm-miners" { - let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); - let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); - assert_eq!(parsed.to_string(), "(ok true)"); - tested = true; - } - } - } - } - assert!(tested, "Should have found a contract call tx"); - - let call_le_tx = make_contract_call( - &spender_sk, - 7, - 1000, - conf.burnchain.chain_id, - &spender_addr, - "caller", - "execute-2", - &[Value::UInt(1)], - ); - - test_observer::clear(); - submit_tx(&http_origin, &call_le_tx); - - // Mine blocks until execute-2 with new cost is confirmed (nonce 7) - mine_blocks_until(&btc_regtest_controller, &blocks_processed, 3, 60, || { - let res = get_account(&http_origin, &spender_princ); - Ok(res.nonce >= 8) - }) - .expect("execute-2 tx should have been mined"); - - let blocks = test_observer::get_blocks(); - let mut tested = false; - let mut new_exec_cost = ExecutionCost::max_value(); - for block in blocks.iter() { - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "execute-2" { - new_exec_cost = - serde_json::from_value(tx.get("execution_cost").cloned().unwrap()).unwrap(); - tested = true; - } - } - } - } - assert!(tested, "Should have found a contract call tx"); - - assert!(exec_cost.exceeds(&new_exec_cost)); - - test_observer::clear(); - channel.stop_chains_coordinator(); -} - #[test] #[ignore] fn mining_events_integration_test() { diff --git a/stackslib/src/chainstate/stacks/boot/contract_tests.rs b/stackslib/src/chainstate/stacks/boot/contract_tests.rs index 87f3b607018..c5c42552b95 100644 --- a/stackslib/src/chainstate/stacks/boot/contract_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/contract_tests.rs @@ -15,16 +15,13 @@ use std::ops::Deref; use clarity::util::get_epoch_time_secs; -use clarity::vm::analysis::arithmetic_checker::ArithmeticOnlyChecker; -use clarity::vm::analysis::mem_type_check; use clarity::vm::clarity::TransactionConnection; use clarity::vm::contexts::OwnedEnvironment; use clarity::vm::database::*; use clarity::vm::errors::{RuntimeCheckErrorKind, StaticCheckErrorKind, VmExecutionError}; use clarity::vm::test_util::{execute, symbols_from_values, TEST_BURN_STATE_DB, TEST_HEADER_DB}; use clarity::vm::types::{ - OptionalData, PrincipalData, QualifiedContractIdentifier, ResponseData, StandardPrincipalData, - TupleData, Value, + PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, TupleData, Value, }; use clarity::vm::version::ClarityVersion; use lazy_static::lazy_static; @@ -38,10 +35,7 @@ use super::SIGNERS_MAX_LIST_SIZE; use crate::burnchains::PoxConstants; use crate::chainstate::burn::ConsensusHash; use crate::chainstate::stacks::address::PoxAddress; -use crate::chainstate::stacks::boot::{ - BOOT_CODE_COST_VOTING_TESTNET as BOOT_CODE_COST_VOTING, BOOT_CODE_POX_TESTNET, - POX_2_TESTNET_CODE, -}; +use crate::chainstate::stacks::boot::{BOOT_CODE_POX_TESTNET, POX_2_TESTNET_CODE}; use crate::chainstate::stacks::index::ClarityMarfTrieId; use crate::chainstate::stacks::{C32_ADDRESS_VERSION_TESTNET_SINGLESIG, *}; use crate::clarity_vm::clarity::{ @@ -67,8 +61,6 @@ lazy_static! { pub static ref POX_CONTRACT_TESTNET: QualifiedContractIdentifier = boot_code_id("pox", false); pub static ref POX_2_CONTRACT_TESTNET: QualifiedContractIdentifier = boot_code_id("pox-2", false); - pub static ref COST_VOTING_CONTRACT_TESTNET: QualifiedContractIdentifier = - boot_code_id("cost-voting", false); pub static ref USER_KEYS: Vec = (0..50).map(|_| StacksPrivateKey::random()).collect(); pub static ref POX_ADDRS: Vec = (0..50u64) @@ -333,26 +325,6 @@ pub fn test_sim_hash_to_fork(in_bytes: &[u8; 32]) -> Option { } } -#[cfg(test)] -fn check_arithmetic_only(contract: &str, version: ClarityVersion) { - let analysis = mem_type_check(contract, version, StacksEpochId::latest()) - .unwrap() - .1; - ArithmeticOnlyChecker::run(&analysis).expect("Should pass arithmetic checks"); -} - -#[test] -fn cost_contract_is_arithmetic_only() { - use crate::chainstate::stacks::boot::BOOT_CODE_COSTS; - check_arithmetic_only(BOOT_CODE_COSTS, ClarityVersion::Clarity1); -} - -#[test] -fn cost_2_contract_is_arithmetic_only() { - use crate::chainstate::stacks::boot::BOOT_CODE_COSTS_2; - check_arithmetic_only(BOOT_CODE_COSTS_2, ClarityVersion::Clarity2); -} - impl BurnStateDB for TestSimBurnStateDB { fn get_tip_burn_block_height(&self) -> Option { Some(self.height) @@ -2481,664 +2453,3 @@ fn delegation_tests() { ); }); } - -#[test] -fn test_vote_withdrawal() { - let mut sim = ClarityTestSim::new(); - - sim.execute_next_block(|env| { - env.initialize_versioned_contract( - COST_VOTING_CONTRACT_TESTNET.clone(), - ClarityVersion::Clarity1, - &BOOT_CODE_COST_VOTING, - None, - ) - .unwrap(); - - // Submit a proposal - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "submit-proposal", - &symbols_from_values(vec![ - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.function-name" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("function-name".into()).unwrap(), - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.cost-function-name" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("cost-function-name".into()).unwrap(), - ]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::UInt(0).into() - }) - ); - - // Vote on the proposal - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "vote-proposal", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(10)]), - ) - .unwrap(); - - // Assert that the number of votes is correct - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "get-proposal-votes", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Optional(OptionalData { - data: Some(Box::from(Value::UInt(10))) - }) - ); - - // Vote again on the proposal - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "vote-proposal", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(5)]), - ) - .unwrap(); - - // Assert that the number of votes is correct - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "get-proposal-votes", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Optional(OptionalData { - data: Some(Box::from(Value::UInt(15))) - }) - ); - - // Assert votes are assigned to principal - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "get-principal-votes", - &symbols_from_values(vec![ - Value::Principal(StandardPrincipalData::from(&USER_KEYS[0]).into()), - Value::UInt(0), - ]) - ) - .unwrap() - .0, - Value::Optional(OptionalData { - data: Some(Box::from(Value::UInt(15))) - }) - ); - - // Assert withdrawal fails if amount is more than voted - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "withdraw-votes", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(20)]), - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: false, - data: Value::Int(5).into() - }) - ); - - // Withdraw votes - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "withdraw-votes", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(5)]), - ) - .unwrap(); - - // Assert withdrawal worked - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "get-proposal-votes", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Optional(OptionalData { - data: Some(Box::from(Value::UInt(10))) - }) - ); - }); - - // Fast forward to proposal expiration - for _ in 0..2016 { - sim.execute_next_block(|_| {}); - } - - sim.execute_next_block(|env| { - // Withdraw STX after proposal expires - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "withdraw-votes", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(10)]), - ) - .unwrap(); - }); - - sim.execute_next_block(|env| { - // Assert that stx balance is correct - assert_eq!( - env.eval_read_only( - &COST_VOTING_CONTRACT_TESTNET, - &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[0])) - ) - .unwrap() - .0, - Value::UInt(1000000) - ); - }); -} - -#[test] -fn test_vote_fail() { - let mut sim = ClarityTestSim::new(); - - // Test voting in a proposal - sim.execute_next_block(|env| { - env.initialize_contract( - COST_VOTING_CONTRACT_TESTNET.clone(), - &BOOT_CODE_COST_VOTING, - None, - ) - .unwrap(); - - // Submit a proposal - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "submit-proposal", - &symbols_from_values(vec![ - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.function-name2" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("function-name2".into()).unwrap(), - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.cost-function-name2" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("cost-function-name2".into()).unwrap(), - ]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::UInt(0).into() - }) - ); - - // Assert confirmation fails - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-votes", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: false, - data: Value::Int(11).into() - }) - ); - - // Assert voting with more STX than are in an account fails - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "vote-proposal", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(USTX_PER_HOLDER + 1)]), - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: false, - data: Value::Int(5).into() - }) - ); - - // Commit all liquid stacks to vote - for user in USER_KEYS.iter() { - env.execute_transaction( - user.into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "vote-proposal", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(USTX_PER_HOLDER)]), - ) - .unwrap(); - } - - // Assert confirmation returns true - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-votes", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::Bool(true).into() - }) - ); - }); - - sim.execute_next_block(|env| { - env.execute_transaction( - (&MINER_KEY.clone()).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "veto", - &symbols_from_values(vec![Value::UInt(0)]), - ) - .unwrap(); - - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "get-proposal-vetos", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Optional(OptionalData { - data: Some(Box::from(Value::UInt(1))) - }) - ); - }); - - let fork_start = sim.block_height; - - for i in 0..25 { - sim.execute_next_block(|env| { - env.execute_transaction( - (&MINER_KEY.clone()).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "veto", - &symbols_from_values(vec![Value::UInt(0)]), - ) - .unwrap(); - - // assert error if already vetoed in this block - assert_eq!( - env.execute_transaction( - (&MINER_KEY.clone()).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "veto", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: false, - data: Value::Int(9).into() - }) - ); - }) - } - - for _ in 0..100 { - sim.execute_next_block(|_| {}); - } - - sim.execute_next_block(|env| { - // Assert confirmation fails because of majority veto - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-miners", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: false, - data: Value::Int(14).into() - }) - ); - }); - - // let's fork, and overcome the veto - sim.execute_block_as_fork(fork_start, |_| {}); - for _ in 0..125 { - sim.execute_next_block(|_| {}); - } - - sim.execute_next_block(|env| { - // Assert confirmation passes because there are no vetos - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-miners", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::Bool(true).into(), - }) - ); - }); -} - -#[test] -fn test_vote_confirm() { - let mut sim = ClarityTestSim::new(); - - sim.execute_next_block(|env| { - env.initialize_contract( - COST_VOTING_CONTRACT_TESTNET.clone(), - &BOOT_CODE_COST_VOTING, - None, - ) - .unwrap(); - - // Submit a proposal - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "submit-proposal", - &symbols_from_values(vec![ - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.function-name2" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("function-name2".into()).unwrap(), - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.cost-function-name2" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("cost-function-name2".into()).unwrap(), - ]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::UInt(0).into() - }) - ); - - // Assert confirmation fails - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-votes", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: false, - data: Value::Int(11).into() - }) - ); - - // Commit all liquid stacks to vote - for user in USER_KEYS.iter() { - env.execute_transaction( - user.into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "vote-proposal", - &symbols_from_values(vec![Value::UInt(0), Value::UInt(USTX_PER_HOLDER)]), - ) - .unwrap(); - } - - // Assert confirmation returns true - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-votes", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::Bool(true).into() - }) - ); - }); - - // Fast forward to proposal expiration - for _ in 0..2016 { - sim.execute_next_block(|_| {}); - } - - for _ in 0..1007 { - sim.execute_next_block(|_| {}); - } - - sim.execute_next_block(|env| { - // Assert confirmation passes - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-miners", - &symbols_from_values(vec![Value::UInt(0)]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::Bool(true).into() - }) - ); - }); -} - -#[test] -fn test_vote_too_many_confirms() { - let mut sim = ClarityTestSim::new(); - - let MAX_CONFIRMATIONS_PER_BLOCK = 10; - sim.execute_next_block(|env| { - env.initialize_contract( - COST_VOTING_CONTRACT_TESTNET.clone(), - &BOOT_CODE_COST_VOTING, - None, - ) - .unwrap(); - - // Submit a proposal - for i in 0..(MAX_CONFIRMATIONS_PER_BLOCK + 1) { - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "submit-proposal", - &symbols_from_values(vec![ - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.function-name2" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("function-name2".into()).unwrap(), - Value::Principal( - PrincipalData::parse_qualified_contract_principal( - "ST000000000000000000002AMW42H.cost-function-name2" - ) - .unwrap() - ), - Value::string_ascii_from_bytes("cost-function-name2".into()).unwrap(), - ]) - ) - .unwrap() - .0, - Value::Response(ResponseData { - committed: true, - data: Value::UInt(i).into() - }) - ); - } - - for i in 0..(MAX_CONFIRMATIONS_PER_BLOCK + 1) { - // Commit all liquid stacks to vote - for user in USER_KEYS.iter() { - assert_eq!( - env.execute_transaction( - user.into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "vote-proposal", - &symbols_from_values(vec![Value::UInt(i), Value::UInt(USTX_PER_HOLDER)]), - ) - .unwrap() - .0, - Value::okay_true() - ); - } - - // Assert confirmation returns true - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-votes", - &symbols_from_values(vec![Value::UInt(i)]) - ) - .unwrap() - .0, - Value::okay_true(), - ); - - // withdraw - for user in USER_KEYS.iter() { - env.execute_transaction( - user.into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "withdraw-votes", - &symbols_from_values(vec![Value::UInt(i), Value::UInt(USTX_PER_HOLDER)]), - ) - .unwrap(); - } - } - }); - - // Fast forward to proposal expiration - for _ in 0..2016 { - sim.execute_next_block(|_| {}); - } - - for _ in 0..1007 { - sim.execute_next_block(|_| {}); - } - - sim.execute_next_block(|env| { - for i in 0..MAX_CONFIRMATIONS_PER_BLOCK { - // Assert confirmation passes - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-miners", - &symbols_from_values(vec![Value::UInt(i)]) - ) - .unwrap() - .0, - Value::okay_true(), - ); - } - - // Assert next confirmation fails - assert_eq!( - env.execute_transaction( - (&USER_KEYS[0]).into(), - None, - COST_VOTING_CONTRACT_TESTNET.clone(), - "confirm-miners", - &symbols_from_values(vec![Value::UInt(MAX_CONFIRMATIONS_PER_BLOCK)]) - ) - .unwrap() - .0, - Value::error(Value::Int(17)).unwrap() - ); - }); -} 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..cdb7361e96c 100644 --- a/stackslib/src/clarity_vm/tests/costs.rs +++ b/stackslib/src/clarity_vm/tests/costs.rs @@ -14,44 +14,33 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use clarity::vm::clarity::TransactionConnection; use clarity::vm::contexts::{AssetMap, OwnedEnvironment}; use clarity::vm::costs::cost_functions::ClarityCostFunction; use clarity::vm::costs::{ - compute_cost, ClarityCostFunctionEvaluator, ClarityCostFunctionReference, CostErrors, - DefaultVersion, ExecutionCost, LimitedCostTracker, COSTS_1_NAME, COSTS_2_NAME, COSTS_3_NAME, - COSTS_4_NAME, + compute_cost, ClarityCostFunctionReference, CostErrors, DefaultVersion, ExecutionCost, + LimitedCostTracker, COSTS_1_NAME, COSTS_2_NAME, COSTS_3_NAME, COSTS_4_NAME, }; use clarity::vm::errors::VmExecutionError; use clarity::vm::events::StacksTransactionEvent; use clarity::vm::functions::NativeFunctions; use clarity::vm::representations::SymbolicExpression; use clarity::vm::test_util::{ - execute, execute_on_network, generate_test_burn_state_db, symbols_from_values, - TEST_BURN_STATE_DB, TEST_BURN_STATE_DB_21, TEST_HEADER_DB, + execute, generate_test_burn_state_db, symbols_from_values, TEST_BURN_STATE_DB, TEST_HEADER_DB, }; use clarity::vm::tests::test_only_mainnet_to_chain_id; use clarity::vm::types::{ PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, Value, }; -use clarity::vm::{ClarityName, ClarityVersion, ContractName}; -use lazy_static::lazy_static; +use clarity::vm::{ClarityVersion, ContractName}; use stacks_common::types::chainstate::StacksBlockId; use stacks_common::types::StacksEpochId; use crate::chainstate::stacks::index::ClarityMarfTrieId; -use crate::clarity_vm::clarity::{ClarityInstance, ClarityMarfStore, ClarityMarfStoreTransaction}; +use crate::clarity_vm::clarity::{ClarityInstance, ClarityMarfStore}; use crate::clarity_vm::database::marf::MarfedKV; use crate::core::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH}; use crate::util_lib::boot::boot_code_id; -lazy_static! { - static ref COST_VOTING_MAINNET_CONTRACT: QualifiedContractIdentifier = - boot_code_id("cost-voting", true); - static ref COST_VOTING_TESTNET_CONTRACT: QualifiedContractIdentifier = - boot_code_id("cost-voting", false); -} - pub fn get_simple_test(function: &NativeFunctions) -> Option<&'static str> { use clarity::vm::functions::NativeFunctions::*; let s = match function { @@ -215,10 +204,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 +233,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), @@ -1226,696 +1248,6 @@ fn epoch_33_test_all_testnet() { epoch_33_test_all(false) } -fn test_cost_contract_short_circuits(use_mainnet: bool, clarity_version: ClarityVersion) { - 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); - let burn_db = if clarity_version == ClarityVersion::Clarity2 { - &TEST_BURN_STATE_DB_21 - } else { - &TEST_BURN_STATE_DB - }; - - clarity_instance - .begin_test_genesis_block( - &StacksBlockId::sentinel(), - &StacksBlockId::new(&FIRST_BURNCHAIN_CONSENSUS_HASH, &FIRST_STACKS_BLOCK_HASH), - &TEST_HEADER_DB, - burn_db, - ) - .commit_block(); - - let marf_kv = clarity_instance.destroy(); - - let p1 = execute_on_network("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", use_mainnet); - let p2 = execute_on_network("'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G", use_mainnet); - - let Value::Principal(PrincipalData::Standard(p1_principal)) = p1.clone() else { - panic!("Expected a standard principal data"); - }; - - let Value::Principal(p2_principal) = p2.clone() else { - panic!("Expected a principal data"); - }; - - let cost_definer = QualifiedContractIdentifier::new( - p1_principal.clone(), - ContractName::from_literal("cost-definer"), - ); - let intercepted = QualifiedContractIdentifier::new( - p1_principal.clone(), - ContractName::from_literal("intercepted"), - ); - let caller = - QualifiedContractIdentifier::new(p1_principal, ContractName::from_literal("caller")); - - let mut marf_kv = { - let mut clarity_inst = ClarityInstance::new(use_mainnet, chain_id, marf_kv); - let mut block_conn = clarity_inst.begin_block( - &StacksBlockId::new(&FIRST_BURNCHAIN_CONSENSUS_HASH, &FIRST_STACKS_BLOCK_HASH), - &StacksBlockId([1; 32]), - &TEST_HEADER_DB, - burn_db, - ); - - let cost_definer_src = " - (define-read-only (cost-definition (size uint)) - { - runtime: u1, write_length: u1, write_count: u1, read_count: u1, read_length: u1 - }) - "; - - let intercepted_src = " - (define-read-only (intercepted-function (a uint)) - (if (>= a u10) - (+ (+ a a) (+ a a) - (+ a a) (+ a a)) - u0)) - "; - - let caller_src = " - (define-public (execute (a uint)) - (ok (contract-call? .intercepted intercepted-function a))) - "; - - for (contract_name, contract_src) in [ - (&cost_definer, cost_definer_src), - (&intercepted, intercepted_src), - (&caller, caller_src), - ] - .iter() - { - block_conn.as_transaction(|tx| { - let (ast, analysis) = tx - .analyze_smart_contract(contract_name, clarity_version, contract_src) - .unwrap(); - tx.initialize_smart_contract( - contract_name, - clarity_version, - &ast, - contract_src, - None, - |_, _| None, - None, - ) - .unwrap(); - tx.save_analysis(contract_name, &analysis).unwrap(); - }); - } - - block_conn.commit_block(); - clarity_inst.destroy() - }; - - let without_interposing_5 = { - let mut store = marf_kv.begin(&StacksBlockId([1; 32]), &StacksBlockId([2; 32])); - let mut owned_env = OwnedEnvironment::new_max_limit( - store.as_clarity_db(&TEST_HEADER_DB, burn_db), - StacksEpochId::Epoch20, - use_mainnet, - ); - - execute_transaction( - &mut owned_env, - p2_principal.clone(), - &caller, - "execute", - &symbols_from_values(vec![Value::UInt(5)]), - ) - .unwrap(); - - let (_db, tracker) = owned_env.destruct().unwrap(); - - store.test_commit(); - tracker.get_total() - }; - - let without_interposing_10 = { - let mut store = marf_kv.begin(&StacksBlockId([2; 32]), &StacksBlockId([3; 32])); - let mut owned_env = OwnedEnvironment::new_max_limit( - store.as_clarity_db(&TEST_HEADER_DB, burn_db), - StacksEpochId::Epoch20, - use_mainnet, - ); - - execute_transaction( - &mut owned_env, - p2_principal.clone(), - &caller, - "execute", - &symbols_from_values(vec![Value::UInt(10)]), - ) - .unwrap(); - - let (_db, tracker) = owned_env.destruct().unwrap(); - - store.test_commit(); - tracker.get_total() - }; - - 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([3; 32]), &StacksBlockId([4; 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(1), - ) - .unwrap(); - let value = format!( - "{{ function-contract: '{}, - function-name: {}, - cost-function-contract: '{}, - cost-function-name: {}, - confirmed-height: u1 }}", - intercepted, "\"intercepted-function\"", cost_definer, "\"cost-definition\"" - ); - let epoch = db.get_clarity_epoch_version().unwrap(); - db.set_entry_unknown_descriptor( - voting_contract_to_use, - "confirmed-proposals", - execute_on_network("{ confirmed-id: u0 }", use_mainnet), - execute_on_network(&value, use_mainnet), - &epoch, - ) - .unwrap(); - db.commit().unwrap(); - store.test_commit(); - } - - let with_interposing_5 = { - let mut store = marf_kv.begin(&StacksBlockId([4; 32]), &StacksBlockId([5; 32])); - - let mut owned_env = OwnedEnvironment::new_max_limit( - store.as_clarity_db(&TEST_HEADER_DB, burn_db), - StacksEpochId::Epoch20, - use_mainnet, - ); - - execute_transaction( - &mut owned_env, - p2_principal.clone(), - &caller, - "execute", - &symbols_from_values(vec![Value::UInt(5)]), - ) - .unwrap(); - - let (_db, tracker) = owned_env.destruct().unwrap(); - - store.test_commit(); - tracker.get_total() - }; - - let with_interposing_10 = { - let mut store = marf_kv.begin(&StacksBlockId([5; 32]), &StacksBlockId([6; 32])); - let mut owned_env = OwnedEnvironment::new_max_limit( - store.as_clarity_db(&TEST_HEADER_DB, burn_db), - StacksEpochId::Epoch20, - use_mainnet, - ); - - execute_transaction( - &mut owned_env, - p2_principal, - &caller, - "execute", - &symbols_from_values(vec![Value::UInt(10)]), - ) - .unwrap(); - - let (_db, tracker) = owned_env.destruct().unwrap(); - - tracker.get_total() - }; - - assert!(without_interposing_5.exceeds(&with_interposing_5)); - assert!(without_interposing_10.exceeds(&with_interposing_10)); - - assert_eq!(with_interposing_5, with_interposing_10); - assert!(without_interposing_5 != without_interposing_10); -} - -#[test] -fn test_cost_contract_short_circuits_mainnet() { - test_cost_contract_short_circuits(true, ClarityVersion::Clarity1); - test_cost_contract_short_circuits(true, ClarityVersion::Clarity2); -} - -#[test] -fn test_cost_contract_short_circuits_testnet() { - test_cost_contract_short_circuits(false, ClarityVersion::Clarity1); - test_cost_contract_short_circuits(false, ClarityVersion::Clarity2); -} - -fn test_cost_voting_integration(use_mainnet: bool, clarity_version: ClarityVersion) { - 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); - let burn_db = if clarity_version == ClarityVersion::Clarity2 { - &TEST_BURN_STATE_DB_21 - } else { - &TEST_BURN_STATE_DB - }; - - clarity_instance - .begin_test_genesis_block( - &StacksBlockId::sentinel(), - &StacksBlockId::new(&FIRST_BURNCHAIN_CONSENSUS_HASH, &FIRST_STACKS_BLOCK_HASH), - &TEST_HEADER_DB, - burn_db, - ) - .commit_block(); - - let marf_kv = clarity_instance.destroy(); - - let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); - let p2 = execute("'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G"); - - let Value::Principal(PrincipalData::Standard(p1_principal)) = p1.clone() else { - panic!("Expected a standard principal data"); - }; - - let Value::Principal(p2_principal) = p2.clone() else { - panic!("Expected a principal data"); - }; - - let cost_definer = QualifiedContractIdentifier::new( - p1_principal.clone(), - ContractName::from_literal("cost-definer"), - ); - let bad_cost_definer = QualifiedContractIdentifier::new( - p1_principal.clone(), - ContractName::from_literal("bad-cost-definer"), - ); - let bad_cost_args_definer = QualifiedContractIdentifier::new( - p1_principal.clone(), - ContractName::from_literal("bad-cost-args-definer"), - ); - let intercepted = QualifiedContractIdentifier::new( - p1_principal.clone(), - ContractName::from_literal("intercepted"), - ); - let caller = QualifiedContractIdentifier::new( - p1_principal.clone(), - ContractName::from_literal("caller"), - ); - - let mut marf_kv = { - let mut clarity_inst = ClarityInstance::new(use_mainnet, chain_id, marf_kv); - let mut block_conn = clarity_inst.begin_block( - &StacksBlockId::new(&FIRST_BURNCHAIN_CONSENSUS_HASH, &FIRST_STACKS_BLOCK_HASH), - &StacksBlockId([1; 32]), - &TEST_HEADER_DB, - burn_db, - ); - - let cost_definer_src = " - (define-read-only (cost-definition (size uint)) - { - runtime: u1, write_length: u1, write_count: u1, read_count: u1, read_length: u1 - }) - (define-read-only (cost-definition-le (size uint)) - { - runtime: u0, write_length: u0, write_count: u0, read_count: u0, read_length: u0 - }) - (define-read-only (cost-definition-multi-arg (a uint) (b uint) (c uint)) - { - runtime: u1, write_length: u0, write_count: u0, read_count: u0, read_length: u0 - }) - - "; - - let bad_cost_definer_src = " - (define-data-var my-var uint u10) - (define-read-only (cost-definition (size uint)) - { - runtime: (var-get my-var), write_length: u1, write_count: u1, read_count: u1, read_length: u1 - }) - "; - - let bad_cost_args_definer_src = " - (define-read-only (cost-definition (a uint) (b uint)) - { - runtime: u1, write_length: u1, write_count: u1, read_count: u1, read_length: u1 - }) - "; - - let intercepted_src = " - (define-read-only (intercepted-function (a uint)) - (if (>= a u10) - (+ (+ a a) (+ a a) - (+ a a) (+ a a)) - u0)) - - (define-read-only (intercepted-function2 (a uint) (b uint) (c uint)) - (- (+ a b) c)) - - (define-public (non-read-only) (ok (+ 1 2 3))) - "; - - let caller_src = " - (define-public (execute (a uint)) - (ok (contract-call? .intercepted intercepted-function a))) - (define-public (execute-2 (a uint)) - (ok (< a a))) - "; - - for (contract_name, contract_src) in [ - (&cost_definer, cost_definer_src), - (&intercepted, intercepted_src), - (&caller, caller_src), - (&bad_cost_definer, bad_cost_definer_src), - (&bad_cost_args_definer, bad_cost_args_definer_src), - ] - .iter() - { - block_conn.as_transaction(|tx| { - let (ast, analysis) = tx - .analyze_smart_contract(contract_name, clarity_version, contract_src) - .unwrap(); - tx.initialize_smart_contract( - contract_name, - clarity_version, - &ast, - contract_src, - None, - |_, _| None, - None, - ) - .unwrap(); - tx.save_analysis(contract_name, &analysis).unwrap(); - }); - } - - block_conn.commit_block(); - clarity_inst.destroy() - }; - - let bad_cases = vec![ - // non existent "replacement target" - ( - PrincipalData::from(QualifiedContractIdentifier::local("non-existent").unwrap()), - "non-existent-func", - PrincipalData::from(cost_definer.clone()), - "cost-definition", - ), - // replacement target isn't a contract principal - ( - p1_principal.clone().into(), - "non-existent-func", - cost_definer.clone().into(), - "cost-definition", - ), - // cost defining contract isn't a contract principal - ( - intercepted.clone().into(), - "intercepted-function", - p1_principal.into(), - "cost-definition", - ), - // replacement function doesn't exist - ( - intercepted.clone().into(), - "non-existent-func", - cost_definer.clone().into(), - "cost-definition", - ), - // replacement function isn't read-only - ( - intercepted.clone().into(), - "non-read-only", - cost_definer.clone().into(), - "cost-definition", - ), - // "boot cost" function doesn't exist - ( - boot_code_id("costs", false).into(), - "non-existent-func", - cost_definer.clone().into(), - "cost-definition", - ), - // cost defining contract doesn't exist - ( - intercepted.clone().into(), - "intercepted-function", - QualifiedContractIdentifier::local("non-existent") - .unwrap() - .into(), - "cost-definition", - ), - // cost defining function doesn't exist - ( - intercepted.clone().into(), - "intercepted-function", - cost_definer.clone().into(), - "cost-definition-2", - ), - // cost defining contract isn't arithmetic-only - ( - intercepted.clone().into(), - "intercepted-function", - bad_cost_definer.into(), - "cost-definition", - ), - // cost defining contract has incorrect number of arguments - ( - intercepted.clone().into(), - "intercepted-function", - bad_cost_args_definer.into(), - "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(); - } - db.commit().unwrap(); - store.test_commit(); - } - - let le_cost_without_interception = { - let mut store = marf_kv.begin(&StacksBlockId([2; 32]), &StacksBlockId([3; 32])); - let mut owned_env = OwnedEnvironment::new_max_limit( - store.as_clarity_db(&TEST_HEADER_DB, burn_db), - StacksEpochId::Epoch20, - use_mainnet, - ); - - execute_transaction( - &mut owned_env, - p2_principal.clone(), - &caller, - "execute-2", - &symbols_from_values(vec![Value::UInt(5)]), - ) - .unwrap(); - - let (_db, tracker) = owned_env.destruct().unwrap(); - - assert!( - tracker.contract_call_circuits().is_empty(), - "No contract call circuits should have been processed" - ); - for (target, referenced_function) in tracker.cost_function_references().into_iter() { - assert!( - matches!( - referenced_function, - ClarityCostFunctionEvaluator::Default(_, _, DefaultVersion::Costs1) - ), - "All cost functions should still point to the boot costs" - ); - } - store.test_commit(); - - tracker.get_total() - }; - - let good_cases = vec![ - ( - intercepted.clone(), - "intercepted-function", - cost_definer.clone(), - "cost-definition", - ), - ( - boot_code_id("costs", use_mainnet), - "cost_le", - cost_definer.clone(), - "cost-definition-le", - ), - ( - intercepted.clone(), - "intercepted-function2", - cost_definer.clone(), - "cost-definition-multi-arg", - ), - ]; - - { - let mut store = marf_kv.begin(&StacksBlockId([3; 32]), &StacksBlockId([4; 32])); - - 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(); - } - db.commit().unwrap(); - - store.test_commit(); - } - - { - let mut store = marf_kv.begin(&StacksBlockId([4; 32]), &StacksBlockId([5; 32])); - let mut owned_env = OwnedEnvironment::new_max_limit( - store.as_clarity_db(&TEST_HEADER_DB, burn_db), - StacksEpochId::Epoch20, - use_mainnet, - ); - - execute_transaction( - &mut owned_env, - p2_principal, - &caller, - "execute-2", - &symbols_from_values(vec![Value::UInt(5)]), - ) - .unwrap(); - - let (_db, tracker) = owned_env.destruct().unwrap(); - - // cost of `le` should be less now, because the proposal made it free - assert!(le_cost_without_interception.exceeds(&tracker.get_total())); - - let circuits = tracker.contract_call_circuits(); - assert_eq!(circuits.len(), 2); - - let circuit1 = circuits.get(&( - intercepted.clone(), - ClarityName::from_literal("intercepted-function"), - )); - let circuit2 = circuits.get(&( - intercepted, - ClarityName::from_literal("intercepted-function2"), - )); - - assert!(circuit1.is_some()); - assert!(circuit2.is_some()); - - assert_eq!(circuit1.unwrap().contract_id, cost_definer); - assert_eq!(circuit1.unwrap().function_name, "cost-definition"); - - assert_eq!(circuit2.unwrap().contract_id, cost_definer); - assert_eq!(circuit2.unwrap().function_name, "cost-definition-multi-arg"); - - for (target, referenced_function) in tracker.cost_function_references().into_iter() { - if target == &ClarityCostFunction::Le { - let ClarityCostFunctionEvaluator::Clarity(referenced_function) = - referenced_function - else { - panic!("Replaced function should be evaluated in Clarity"); - }; - assert_eq!(&referenced_function.contract_id, &cost_definer); - assert_eq!(&referenced_function.function_name, "cost-definition-le"); - } else { - assert!( - matches!( - referenced_function, - ClarityCostFunctionEvaluator::Default(_, _, DefaultVersion::Costs1) - ), - "Cost function should still point to the boot costs" - ); - } - } - store.test_commit(); - }; -} - -#[test] -fn test_cost_voting_integration_mainnet() { - test_cost_voting_integration(true, ClarityVersion::Clarity1); - test_cost_voting_integration(true, ClarityVersion::Clarity2); -} - -#[test] -fn test_cost_voting_integration_testnet() { - test_cost_voting_integration(false, ClarityVersion::Clarity1); - test_cost_voting_integration(false, ClarityVersion::Clarity2); -} - #[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;