// Copyright (C) 2013-2020 Blockstack 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 .
use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::mem::replace;
use clarity_types::representations::ClarityName;
use serde::Serialize;
use serde_json::json;
use stacks_common::types::StacksEpochId;
use stacks_common::types::chainstate::StacksBlockId;
use super::EvalHook;
use crate::vm::ast::ContractAST;
use crate::vm::ast::errors::{ParseError, ParseErrorKind};
use crate::vm::callables::{DefinedFunction, FunctionIdentifier};
use crate::vm::contracts::Contract;
use crate::vm::costs::cost_functions::ClarityCostFunction;
use crate::vm::costs::execution_cost::ExecutionCost;
use crate::vm::costs::{CostErrors, CostTracker, LimitedCostTracker, runtime_cost};
use crate::vm::database::{
ClarityDatabase, DataMapMetadata, DataVariableMetadata, FungibleTokenMetadata,
NonFungibleTokenMetadata,
};
use crate::vm::errors::{
ClarityEvalError, RuntimeCheckErrorKind, RuntimeError, StackTrace, VmExecutionError,
VmInternalError,
};
use crate::vm::events::*;
use crate::vm::representations::SymbolicExpression;
use crate::vm::resource_limiter::ResourceLimiter;
use crate::vm::types::signatures::FunctionSignature;
use crate::vm::types::{
AssetIdentifier, BuffData, CallableData, PrincipalData, QualifiedContractIdentifier,
TraitIdentifier, TypeSignature, Value,
};
use crate::vm::version::ClarityVersion;
use crate::vm::{ValueRef, ast, eval, is_reserved, stx_transfer_consolidated};
pub const MAX_CONTEXT_DEPTH: u64 = 256;
pub const MAX_EVENTS_BATCH: u64 = 50 * 1024 * 1024;
/// Immutable metadata describing a single contract invocation.
///
/// `InvocationContext` captures *who* is executing *which contract* under what authority.
/// It contains the principals that define call semantics (`sender`, `caller`, `sponsor`)
/// together with the active `ContractContext`.
///
/// A new `InvocationContext` is derived whenever a contract call changes authority
/// (e.g., `contract-call?`, `as-contract`, or sponsor propagation). It is intentionally
/// immutable so that nested calls cannot mutate the caller's view of authority.
///
/// This type does **not** contain mutable VM state (database, cost tracker, events, stack).
/// Those live in [`ExecutionState`]. Lexical variables and scope live in [`LocalContext`].
///
/// Together:
/// - `InvocationContext` → authority + contract binding
/// - `ExecutionState` → mutable runtime state
/// - `LocalContext` → lexical variables/scope
pub struct InvocationContext<'a> {
/// The contract currently being executed.
pub contract_context: &'a ContractContext,
/// The transaction sender for this invocation (tx origin or `as-contract` principal).
pub sender: Option,
/// The immediate caller of the current contract (may differ from `sender` in nested calls).
pub caller: Option,
/// The sponsor responsible for paying execution costs, if any.
pub sponsor: Option,
}
impl InvocationContext<'_> {
/// Returns a derived invocation context executing *as* the given principal.
///
/// Both `sender` and `caller` are set to `sender`
/// The sponsor and contract context are preserved.
pub fn with_principal(&self, sender: PrincipalData) -> Self {
InvocationContext {
contract_context: self.contract_context,
sender: Some(sender.clone()),
caller: Some(sender),
sponsor: self.sponsor.clone(),
}
}
/// Returns a derived invocation context with a different immediate caller.
///
/// This models a nested contract call where authority flows from the same
/// transaction sender but the caller changes to the calling contract.
/// The sender, sponsor, and contract context are preserved.
pub fn with_caller(&self, caller: PrincipalData) -> Self {
InvocationContext {
contract_context: self.contract_context,
sender: self.sender.clone(),
caller: Some(caller),
sponsor: self.sponsor.clone(),
}
}
}
/// `ExecutionState` contains the parts of the VM environment that may change during
/// evaluation: the global chainstate (`GlobalContext`) and the Clarity call stack.
/// All database writes, event emission, cost tracking, and stack mutations occur
/// through this structure.
///
/// Unlike [`InvocationContext`], this state is shared and mutated throughout the
/// lifetime of a single invocation. Nested contract or function calls reborrow the
/// same `ExecutionState` while deriving new `InvocationContext` and/or `LocalContext`
/// values.
///
/// Separation of concerns:
/// - `ExecutionState` → mutable VM/runtime state
/// - `InvocationContext` → authority + contract binding
/// - `LocalContext` → lexical variables/scope
pub struct ExecutionState<'a, 'b, 'hooks> {
/// Global chainstate and database access for this execution.
pub global_context: &'a mut GlobalContext<'b, 'hooks>,
/// The Clarity call stack tracking nested function/contract calls.
pub call_stack: &'a mut CallStack,
}
pub struct OwnedEnvironment<'a, 'hooks> {
pub(crate) context: GlobalContext<'a, 'hooks>,
call_stack: CallStack,
}
#[derive(Debug, PartialEq, Eq)]
pub enum AssetMapEntry {
STX(u128),
Burn(u128),
Token(u128),
Asset(Vec),
Stacking(u128),
}
/**
The AssetMap is used to track which assets have been transferred from whom
during the execution of a transaction.
*/
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssetMap {
/// Sum of all STX transfers by principal
stx_map: HashMap,
/// Sum of all STX burns by principal
burn_map: HashMap,
/// Sum of FT transfers by principal, by asset identifier
token_map: HashMap>,
/// NFT transfers by principal, by asset identifier
asset_map: HashMap>>,
/// Amount of STX stacked or delegated for stacking by principal
stacking_map: HashMap,
/// Principals that attempted a position-altering PoX action (`unstake`,
/// `unstake-sbtc`, `update-bond-registration`, `announce-l1-early-exit`)
/// during the transaction -- recorded whether or not the call succeeded, so
/// a `Pox` post-condition / `with-pox` allowance can gate even a failed
/// attempt.
pox_action_set: HashSet,
}
impl AssetMap {
pub fn to_json(&self) -> serde_json::Value {
let stx: serde_json::map::Map<_, _> = self
.stx_map
.iter()
.map(|(principal, amount)| {
(
format!("{principal}"),
serde_json::value::Value::String(format!("{amount}")),
)
})
.collect();
let burns: serde_json::map::Map<_, _> = self
.burn_map
.iter()
.map(|(principal, amount)| {
(
format!("{principal}"),
serde_json::value::Value::String(format!("{amount}")),
)
})
.collect();
let tokens: serde_json::map::Map<_, _> = self
.token_map
.iter()
.map(|(principal, token_map)| {
let token_json: serde_json::map::Map<_, _> = token_map
.iter()
.map(|(asset_id, amount)| {
(
format!("{asset_id}"),
serde_json::value::Value::String(format!("{amount}")),
)
})
.collect();
(
format!("{principal}"),
serde_json::value::Value::Object(token_json),
)
})
.collect();
let assets: serde_json::map::Map<_, _> = self
.asset_map
.iter()
.map(|(principal, nft_map)| {
let nft_json: serde_json::map::Map<_, _> = nft_map
.iter()
.map(|(asset_id, nft_values)| {
let nft_array = nft_values
.iter()
.map(|nft_value| {
serde_json::value::Value::String(format!("{nft_value}"))
})
.collect();
(
format!("{asset_id}"),
serde_json::value::Value::Array(nft_array),
)
})
.collect();
(
format!("{principal}"),
serde_json::value::Value::Object(nft_json),
)
})
.collect();
let stacking: serde_json::map::Map<_, _> = self
.stacking_map
.iter()
.map(|(principal, amount)| {
(
format!("{principal}"),
serde_json::value::Value::String(format!("{amount}")),
)
})
.collect();
let pox: Vec = self
.pox_action_set
.iter()
.map(|principal| serde_json::value::Value::String(format!("{principal}")))
.collect();
json!({
"stx": stx,
"burns": burns,
"tokens": tokens,
"assets": assets,
"stacking": stacking,
"pox": pox,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct EventBatch {
pub events: Vec,
}
/** GlobalContext represents the outermost context for a single transaction's
execution. It tracks any asset changes that occurred during the
processing of the transaction, whether or not the current context is read_only,
and is responsible for committing/rolling-back transactions as they error or
abort.
*/
pub struct GlobalContext<'a, 'hooks> {
asset_maps: Vec,
pub event_batches: Vec<(EventBatch, u64)>,
pub database: ClarityDatabase<'a>,
read_only: Vec,
pub cost_track: LimitedCostTracker,
pub mainnet: bool,
/// This is the epoch of the block that this transaction is executing within.
pub epoch_id: StacksEpochId,
/// This is the chain ID of the transaction
pub chain_id: u32,
pub eval_hooks: Option>,
/// A resource limiter that will be polled on every `eval` to check that execution
/// time and heap allocation don't exceed configured maximums
pub execution_resource_limiter: ResourceLimiter,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ContractContext {
/// The identifier of this contract
pub contract_identifier: QualifiedContractIdentifier,
/// Despite being called `variables`, these are actually the constants defined in the contract
pub variables: HashMap,
/// The functions defined in this contract, mapped by their name
pub functions: HashMap,
/// The traits defined in this contract, mapped by their name, to a map of the trait's function
/// signatures
pub defined_traits: HashMap>,
/// The traits implemented by this contract
pub implemented_traits: HashSet,
/// The names of NFTs, FTs, Maps, and Data Vars, used to ensure that they never are defined twice
pub persisted_names: HashSet,
/// Key/value types for contract defined maps
pub meta_data_map: HashMap,
/// Types for contract defined data variables
pub meta_data_var: HashMap,
/// Key types for contract defined non-fungible tokens
pub meta_nft: HashMap,
/// Total supply for contract defined fungible tokens
pub meta_ft: HashMap,
/// The total size of constants stored by this contract
pub data_size: u64,
/// The clarity version of this contract
clarity_version: ClarityVersion,
/// True while the contract is being deployed (inside `initialize_from_ast`).
/// Constants may only be used as `contract-call?` dispatch targets
/// after deployment, when their values are frozen.
#[serde(skip)]
pub is_deploying: bool,
}
pub struct LocalContext<'a> {
pub function_context: Option<&'a LocalContext<'a>>,
pub parent: Option<&'a LocalContext<'a>>,
pub variables: HashMap,
pub callable_contracts: HashMap,
depth: u64,
}
pub struct CallStack {
stack: Vec,
set: HashSet,
apply_depth: u64,
}
pub const TRANSIENT_CONTRACT_NAME: &str = "__transient";
impl Default for AssetMap {
fn default() -> Self {
Self::new()
}
}
impl AssetMap {
pub fn new() -> AssetMap {
AssetMap {
stx_map: HashMap::new(),
burn_map: HashMap::new(),
token_map: HashMap::new(),
asset_map: HashMap::new(),
stacking_map: HashMap::new(),
pox_action_set: HashSet::new(),
}
}
/// This will get the next amount for a (principal, stx) entry in the stx table.
fn get_next_stx_amount(
&self,
principal: &PrincipalData,
amount: u128,
) -> Result {
// `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
// - Every `stx-transfer?` or `stx-burn?` is validated against the sender’s
// **unlocked balance** before being queued in `AssetMap`.
// - The unlocked balance is a subset of `stx-liquid-supply`.
// - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
// - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
let current_amount = self.stx_map.get(principal).unwrap_or(&0);
current_amount
.checked_add(amount)
.ok_or(RuntimeError::ArithmeticOverflow.into())
}
/// This will get the next amount for a (principal, stx) entry in the burn table.
fn get_next_stx_burn_amount(
&self,
principal: &PrincipalData,
amount: u128,
) -> Result {
// `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
// - Every `stx-burn?` is validated against the sender’s **unlocked balance** first.
// - Unlocked balance is a subset of `stx-liquid-supply`, which is <= `u128::MAX`.
// - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
// - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
let current_amount = self.burn_map.get(principal).unwrap_or(&0);
current_amount
.checked_add(amount)
.ok_or(RuntimeError::ArithmeticOverflow.into())
}
/// This will get the next amount for a (principal, stx) entry in the
/// stacking table. Used in Epoch 4.0+ (PoX-5), where multiple stacking
/// entries for the same principal in one transaction are summed rather
/// than rejected. Overflow returns `ArithmeticOverflow`; it is unreachable
/// in normal execution because every stacked amount is bounded by the
/// principal's balance, which is a subset of `stx-liquid-supply`.
fn get_next_stacking_amount(
&self,
principal: &PrincipalData,
amount: u128,
) -> Result {
let current_amount = self.stacking_map.get(principal).unwrap_or(&0);
current_amount
.checked_add(amount)
.ok_or(RuntimeError::ArithmeticOverflow.into())
}
/// This will get the next amount for a (principal, asset) entry in the asset table.
fn get_next_amount(
&self,
principal: &PrincipalData,
asset: &AssetIdentifier,
amount: u128,
) -> Result {
// `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
// - The inner transaction must have **partially succeeded** to log any assets.
// - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
// - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
let current_amount = self
.token_map
.get(principal)
.and_then(|x| x.get(asset))
.unwrap_or(&0);
current_amount
.checked_add(amount)
.ok_or(RuntimeError::ArithmeticOverflow.into())
}
pub fn add_stx_transfer(
&mut self,
principal: &PrincipalData,
amount: u128,
) -> Result<(), VmExecutionError> {
let next_amount = self.get_next_stx_amount(principal, amount)?;
self.stx_map.insert(principal.clone(), next_amount);
Ok(())
}
pub fn add_stx_burn(
&mut self,
principal: &PrincipalData,
amount: u128,
) -> Result<(), VmExecutionError> {
let next_amount = self.get_next_stx_burn_amount(principal, amount)?;
self.burn_map.insert(principal.clone(), next_amount);
Ok(())
}
pub fn add_asset_transfer(
&mut self,
principal: &PrincipalData,
asset: AssetIdentifier,
transferred: Value,
) {
let principal_map = self.asset_map.entry(principal.clone()).or_default();
if let Some(map_entry) = principal_map.get_mut(&asset) {
map_entry.push(transferred);
} else {
principal_map.insert(asset, vec![transferred]);
}
}
pub fn add_token_transfer(
&mut self,
principal: &PrincipalData,
asset: AssetIdentifier,
amount: u128,
) -> Result<(), VmExecutionError> {
let next_amount = self.get_next_amount(principal, &asset, amount)?;
let principal_map = self.token_map.entry(principal.clone()).or_default();
principal_map.insert(asset, next_amount);
Ok(())
}
/// Add stacking entry for `principal` of `amount`. The EpochId
/// controls whether or not an existing entry is replaced or
/// accumulated.
pub fn add_stacking(
&mut self,
principal: &PrincipalData,
amount: u128,
epoch_id: StacksEpochId,
) -> Result<(), VmExecutionError> {
match self.stacking_map.entry(principal.clone()) {
Entry::Occupied(mut occupied_entry) => {
let next_amt = if epoch_id.sums_stacking_assetmap() {
occupied_entry
.get()
.checked_add(amount)
.ok_or(RuntimeError::ArithmeticOverflow)?
} else {
amount
};
occupied_entry.insert(next_amt);
}
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(amount);
}
};
Ok(())
}
/// Record that a principal attempted a position-altering PoX action
/// (`unstake`, `unstake-sbtc`, `update-bond-registration`,
/// `announce-l1-early-exit`) during the transaction. Recorded whether or
/// not the call succeeded.
pub fn add_pox_action(&mut self, principal: &PrincipalData) {
self.pox_action_set.insert(principal.clone());
}
// This will add any asset transfer data from other to self,
// aborting _all_ changes in the event of an error, leaving self unchanged
//
// `epoch_id` selects how concurrent stacking entries for the same principal
// are handled (see the stacking-merge block below): pre-Epoch-4.0, a second
// entry is rejected (`PoxStxAssetMapOverwrite`, a soft-fork safety net);
// Epoch-4.0+ (PoX-5) sums the amounts.
pub fn commit_other(
&mut self,
mut other: AssetMap,
epoch_id: StacksEpochId,
) -> Result<(), VmExecutionError> {
let mut to_add = Vec::new();
let mut stx_to_add = Vec::with_capacity(other.stx_map.len());
let mut stx_burn_to_add = Vec::with_capacity(other.burn_map.len());
let mut stacking_to_add = Vec::with_capacity(other.stacking_map.len());
for (principal, mut principal_map) in other.token_map.drain() {
for (asset, amount) in principal_map.drain() {
let next_amount = self.get_next_amount(&principal, &asset, amount)?;
to_add.push((principal.clone(), asset, next_amount));
}
}
for (principal, stx_amount) in other.stx_map.drain() {
let next_amount = self.get_next_stx_amount(&principal, stx_amount)?;
stx_to_add.push((principal.clone(), next_amount));
}
for (principal, stx_burn_amount) in other.burn_map.drain() {
let next_amount = self.get_next_stx_burn_amount(&principal, stx_burn_amount)?;
stx_burn_to_add.push((principal.clone(), next_amount));
}
if epoch_id.sums_stacking_assetmap() {
// Epoch 4.0+ (PoX-5): sum a principal's stacking entries, mirroring
// how STX transfers/burns accumulate. Computed before any mutation
// so an overflow aborts the whole merge with `self` unchanged.
for (principal, stacking_amount) in other.stacking_map.drain() {
let next_amount = self.get_next_stacking_amount(&principal, stacking_amount)?;
stacking_to_add.push((principal, next_amount));
}
} else {
// Pre-Epoch-4.0 soft-fork behavior: reject any transaction that
// would overwrite an existing asset-map stacking entry.
for principal in other.stacking_map.keys() {
if self.stacking_map.contains_key(principal) {
return Err(VmExecutionError::from(
RuntimeCheckErrorKind::PoxStxAssetMapOverwrite,
));
}
}
// No collision is possible, so each entry carries its own amount.
for (principal, stacking_amount) in other.stacking_map.drain() {
stacking_to_add.push((principal, stacking_amount));
}
}
// After this point, this function will not fail.
for (principal, mut principal_map) in other.asset_map.drain() {
for (asset, mut transfers) in principal_map.drain() {
let landing_map = self.asset_map.entry(principal.clone()).or_default();
if let Some(landing_vec) = landing_map.get_mut(&asset) {
landing_vec.append(&mut transfers);
} else {
landing_map.insert(asset, transfers);
}
}
}
for (principal, stx_amount) in stx_to_add.into_iter() {
self.stx_map.insert(principal, stx_amount);
}
for (principal, stx_burn_amount) in stx_burn_to_add.into_iter() {
self.burn_map.insert(principal, stx_burn_amount);
}
for (principal, asset, amount) in to_add.into_iter() {
let principal_map = self.token_map.entry(principal).or_default();
principal_map.insert(asset, amount);
}
for (principal, stacking_amount) in stacking_to_add.into_iter() {
self.stacking_map.insert(principal, stacking_amount);
}
for principal in other.pox_action_set.drain() {
self.pox_action_set.insert(principal);
}
Ok(())
}
pub fn to_table(mut self) -> HashMap> {
let mut map = HashMap::with_capacity(self.token_map.len());
for (principal, mut principal_map) in self.token_map.drain() {
let mut output_map = HashMap::with_capacity(principal_map.len());
for (asset, amount) in principal_map.drain() {
output_map.insert(asset, AssetMapEntry::Token(amount));
}
map.insert(principal, output_map);
}
for (principal, stx_amount) in self.stx_map.drain() {
let output_map = map.entry(principal.clone()).or_default();
output_map.insert(AssetIdentifier::STX(), AssetMapEntry::STX(stx_amount));
}
for (principal, stx_burned_amount) in self.burn_map.drain() {
let output_map = map.entry(principal.clone()).or_default();
output_map.insert(
AssetIdentifier::STX_burned(),
AssetMapEntry::Burn(stx_burned_amount),
);
}
for (principal, mut principal_map) in self.asset_map.drain() {
let output_map = map.entry(principal.clone()).or_default();
for (asset, transfers) in principal_map.drain() {
output_map.insert(asset, AssetMapEntry::Asset(transfers));
}
}
map
}
pub fn get_stx(&self, principal: &PrincipalData) -> Option {
self.stx_map.get(principal).copied()
}
pub fn get_stx_burned(&self, principal: &PrincipalData) -> Option {
self.burn_map.get(principal).copied()
}
pub fn get_stx_burned_total(&self) -> Result {
let mut total: u128 = 0;
for principal in self.burn_map.keys() {
total = total
.checked_add(*self.burn_map.get(principal).unwrap_or(&0u128))
.ok_or_else(|| VmInternalError::Expect("BURN OVERFLOW".into()))?;
}
Ok(total)
}
pub fn get_fungible_tokens(
&self,
principal: &PrincipalData,
asset_identifier: &AssetIdentifier,
) -> Option {
let assets = self.token_map.get(principal)?;
assets.get(asset_identifier).copied()
}
pub fn get_all_fungible_tokens(
&self,
principal: &PrincipalData,
) -> Option<&HashMap> {
let assets = self.token_map.get(principal)?;
Some(assets)
}
pub fn get_nonfungible_tokens(
&self,
principal: &PrincipalData,
asset_identifier: &AssetIdentifier,
) -> Option<&Vec> {
let assets = self.asset_map.get(principal)?;
assets.get(asset_identifier)
}
pub fn get_all_nonfungible_tokens(
&self,
principal: &PrincipalData,
) -> Option<&HashMap>> {
let assets = self.asset_map.get(principal)?;
Some(assets)
}
pub fn get_stacking(&self, principal: &PrincipalData) -> Option {
self.stacking_map.get(principal).copied()
}
/// Returns the full map of STX stacked (locked for PoX) by each principal
/// during the transaction. Used to enforce transaction-level `Staking`
/// post-conditions, since the stacking map is intentionally excluded from
/// `to_table`.
pub fn get_all_stacking(&self) -> &HashMap {
&self.stacking_map
}
pub fn did_pox_action(&self, principal: &PrincipalData) -> bool {
self.pox_action_set.contains(principal)
}
/// Returns the set of principals that performed a position-altering PoX
/// action during the transaction. Used to enforce transaction-level `Pox`
/// post-conditions; like the stacking map it is intentionally excluded from
/// `to_table`.
pub fn get_all_pox_actions(&self) -> &HashSet {
&self.pox_action_set
}
}
impl fmt::Display for AssetMap {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[")?;
for (principal, principal_map) in self.token_map.iter() {
for (asset, amount) in principal_map.iter() {
writeln!(f, "{principal} spent {amount} {asset}")?;
}
}
for (principal, principal_map) in self.asset_map.iter() {
for (asset, transfer) in principal_map.iter() {
write!(f, "{principal} transferred [")?;
for t in transfer {
write!(f, "{t}, ")?;
}
writeln!(f, "] {asset}")?;
}
}
for (principal, stx_amount) in self.stx_map.iter() {
writeln!(f, "{principal} spent {stx_amount} microSTX")?;
}
for (principal, stx_burn_amount) in self.burn_map.iter() {
writeln!(f, "{principal} burned {stx_burn_amount} microSTX")?;
}
write!(f, "]")
}
}
impl EventBatch {
pub fn new() -> EventBatch {
EventBatch::default()
}
}
impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> {
#[cfg(any(test, feature = "testing"))]
pub fn new(database: ClarityDatabase<'a>, epoch: StacksEpochId) -> OwnedEnvironment<'a, 'a> {
OwnedEnvironment {
context: GlobalContext::new(
false,
stacks_common::consts::CHAIN_ID_TESTNET,
database,
LimitedCostTracker::new_free(),
epoch,
),
call_stack: CallStack::new(),
}
}
#[cfg(any(test, feature = "testing"))]
pub fn new_toplevel(mut database: ClarityDatabase<'a>) -> OwnedEnvironment<'a, 'a> {
database.begin();
let epoch = database.get_clarity_epoch_version().unwrap();
let version = ClarityVersion::default_for_epoch(epoch);
database.roll_back().unwrap();
debug!("Begin OwnedEnvironment(epoch = {epoch}, version = {version})");
OwnedEnvironment {
context: GlobalContext::new(
false,
stacks_common::consts::CHAIN_ID_TESTNET,
database,
LimitedCostTracker::new_free(),
epoch,
),
call_stack: CallStack::new(),
}
}
#[cfg(any(test, feature = "testing"))]
pub fn new_max_limit(
mut database: ClarityDatabase<'a>,
epoch: StacksEpochId,
use_mainnet: bool,
) -> OwnedEnvironment<'a, 'a> {
use crate::vm::tests::test_only_mainnet_to_chain_id;
let cost_track = LimitedCostTracker::new_max_limit(&mut database, epoch, use_mainnet)
.expect("FAIL: problem instantiating cost tracking");
let chain_id = test_only_mainnet_to_chain_id(use_mainnet);
OwnedEnvironment {
context: GlobalContext::new(use_mainnet, chain_id, database, cost_track, epoch),
call_stack: CallStack::new(),
}
}
pub fn new_free(
mainnet: bool,
chain_id: u32,
database: ClarityDatabase<'a>,
epoch_id: StacksEpochId,
) -> OwnedEnvironment<'a, 'a> {
OwnedEnvironment {
context: GlobalContext::new(
mainnet,
chain_id,
database,
LimitedCostTracker::new_free(),
epoch_id,
),
call_stack: CallStack::new(),
}
}
pub fn new_cost_limited(
mainnet: bool,
chain_id: u32,
database: ClarityDatabase<'a>,
cost_tracker: LimitedCostTracker,
epoch_id: StacksEpochId,
) -> OwnedEnvironment<'a, 'a> {
OwnedEnvironment {
context: GlobalContext::new(mainnet, chain_id, database, cost_tracker, epoch_id),
call_stack: CallStack::new(),
}
}
pub fn set_execution_resource_limiter(&mut self, resource_limiter: ResourceLimiter) {
self.context
.set_execution_resource_limiter(resource_limiter);
}
pub fn get_exec_environment<'b>(
&'b mut self,
sender: Option,
sponsor: Option,
context: &'b ContractContext,
) -> (ExecutionState<'b, 'a, 'hooks>, InvocationContext<'b>) {
(
ExecutionState {
global_context: &mut self.context,
call_stack: &mut self.call_stack,
},
InvocationContext {
contract_context: context,
sender: sender.clone(),
caller: sender,
sponsor,
},
)
}
pub fn execute_in_env(
&mut self,
sender: PrincipalData,
sponsor: Option,
initial_context: Option,
f: F,
) -> std::result::Result<(A, AssetMap, Vec), E>
where
E: From,
F: FnOnce(&mut ExecutionState, &InvocationContext) -> std::result::Result,
{
assert!(self.context.is_top_level());
self.begin();
let result = {
let initial_context = initial_context.unwrap_or(ContractContext::new(
QualifiedContractIdentifier::transient(),
ClarityVersion::Clarity1,
));
let (mut exec_state, invoke_ctx) =
self.get_exec_environment(Some(sender), sponsor, &initial_context);
f(&mut exec_state, &invoke_ctx)
};
match result {
Ok(return_value) => {
let (asset_map, event_batch) = self.commit()?;
Ok((return_value, asset_map, event_batch.events))
}
Err(e) => {
self.context.roll_back()?;
Err(e)
}
}
}
/// Initialize a contract with the "default" contract context (i.e. clarity1, transient ID).
/// No longer appropriate outside of testing, now that there are multiple clarity versions.
#[cfg(any(test, feature = "testing"))]
pub fn initialize_contract(
&mut self,
contract_identifier: QualifiedContractIdentifier,
contract_content: &str,
sponsor: Option,
) -> Result<((), AssetMap, Vec), ClarityEvalError> {
self.execute_in_env(
contract_identifier.issuer.clone().into(),
sponsor,
None,
|exec_state, invoke_ctx| {
exec_state.initialize_contract(invoke_ctx, contract_identifier, contract_content)
},
)
}
pub fn initialize_versioned_contract(
&mut self,
contract_identifier: QualifiedContractIdentifier,
version: ClarityVersion,
contract_content: &str,
sponsor: Option,
) -> Result<((), AssetMap, Vec), ClarityEvalError> {
self.execute_in_env(
contract_identifier.issuer.clone().into(),
sponsor,
Some(ContractContext::new(
QualifiedContractIdentifier::transient(),
version,
)),
|exec_state, invoke_ctx| {
exec_state.initialize_contract(invoke_ctx, contract_identifier, contract_content)
},
)
}
pub fn initialize_contract_from_ast(
&mut self,
contract_identifier: QualifiedContractIdentifier,
clarity_version: ClarityVersion,
contract_content: &ContractAST,
contract_string: &str,
sponsor: Option,
) -> Result<((), AssetMap, Vec), VmExecutionError> {
self.execute_in_env(
contract_identifier.issuer.clone().into(),
sponsor,
Some(ContractContext::new(
QualifiedContractIdentifier::transient(),
clarity_version,
)),
|exec_state, invoke_ctx| {
exec_state.initialize_contract_from_ast(
invoke_ctx,
contract_identifier,
clarity_version,
contract_content,
contract_string,
)
},
)
}
pub fn execute_transaction(
&mut self,
sender: PrincipalData,
sponsor: Option,
contract_identifier: QualifiedContractIdentifier,
tx_name: &str,
args: &[SymbolicExpression],
) -> Result<(Value, AssetMap, Vec), VmExecutionError> {
self.execute_in_env(sender, sponsor, None, |exec_state, invoke_ctx| {
exec_state.execute_contract(invoke_ctx, &contract_identifier, tx_name, args, false)
})
}
pub fn stx_transfer(
&mut self,
from: &PrincipalData,
to: &PrincipalData,
amount: u128,
memo: &BuffData,
) -> Result<(Value, AssetMap, Vec), VmExecutionError> {
self.execute_in_env(from.clone(), None, None, |exec_state, invoke_ctx| {
exec_state.stx_transfer(invoke_ctx, from, to, amount, memo)
})
}
pub fn is_mainnet(&self) -> bool {
self.context.mainnet
}
#[cfg(any(test, feature = "testing"))]
pub fn stx_faucet(&mut self, recipient: &PrincipalData, amount: u128) {
self.execute_in_env::<_, _, VmExecutionError>(
recipient.clone(),
None,
None,
|exec_state, _invoke_ctx| {
let mut snapshot = exec_state
.global_context
.database
.get_stx_balance_snapshot(recipient)
.unwrap();
snapshot.credit(amount).unwrap();
snapshot.save().unwrap();
exec_state
.global_context
.database
.increment_ustx_liquid_supply(amount)
.unwrap();
let res: std::result::Result<(), VmExecutionError> = Ok(());
res
},
)
.unwrap();
}
#[cfg(any(test, feature = "testing"))]
pub fn eval_raw(
&mut self,
program: &str,
) -> Result<(Value, AssetMap, Vec), ClarityEvalError> {
self.execute_in_env(
QualifiedContractIdentifier::transient().issuer.into(),
None,
None,
|exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, program),
)
}
pub fn eval_read_only(
&mut self,
contract: &QualifiedContractIdentifier,
program: &str,
) -> Result<(Value, AssetMap, Vec), ClarityEvalError> {
self.execute_in_env(
QualifiedContractIdentifier::transient().issuer.into(),
None,
None,
|exec_state, invoke_ctx| exec_state.eval_read_only(invoke_ctx, contract, program),
)
}
pub fn begin(&mut self) {
self.context.begin();
}
pub fn commit(&mut self) -> Result<(AssetMap, EventBatch), VmExecutionError> {
let (asset_map, event_batch) = self.context.commit()?;
let asset_map = asset_map.ok_or(VmInternalError::FailedToConstructAssetTable)?;
let event_batch = event_batch.ok_or(VmInternalError::FailedToConstructEventBatch)?;
Ok((asset_map, event_batch))
}
pub fn get_cost_total(&self) -> ExecutionCost {
self.context.cost_track.get_total()
}
#[cfg(any(test, feature = "testing"))]
pub fn mut_cost_tracker(&mut self) -> &mut LimitedCostTracker {
&mut self.context.cost_track
}
/// Destroys this environment, returning ownership of its database reference.
/// If the context wasn't top-level (i.e., it had uncommitted data), return None,
/// because the database is not guaranteed to be in a sane state.
pub fn destruct(self) -> Option<(ClarityDatabase<'a>, LimitedCostTracker)> {
self.context.destruct()
}
pub fn add_eval_hook(&mut self, hook: &'hooks mut dyn EvalHook) {
if let Some(mut hooks) = self.context.eval_hooks.take() {
hooks.push(hook);
self.context.eval_hooks = Some(hooks);
} else {
self.context.eval_hooks = Some(vec![hook]);
}
}
}
impl CostTracker for ExecutionState<'_, '_, '_> {
fn compute_cost(
&mut self,
cost_function: ClarityCostFunction,
input: &[u64],
) -> std::result::Result {
self.global_context
.cost_track
.compute_cost(cost_function, input)
}
fn add_cost(&mut self, cost: ExecutionCost) -> std::result::Result<(), CostErrors> {
self.global_context.cost_track.add_cost(cost)
}
fn add_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> {
self.global_context.cost_track.add_memory(memory)
}
fn drop_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> {
self.global_context.cost_track.drop_memory(memory)
}
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<'_, '_> {
fn compute_cost(
&mut self,
cost_function: ClarityCostFunction,
input: &[u64],
) -> std::result::Result {
self.cost_track.compute_cost(cost_function, input)
}
fn add_cost(&mut self, cost: ExecutionCost) -> std::result::Result<(), CostErrors> {
self.cost_track.add_cost(cost)
}
fn add_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> {
self.cost_track.add_memory(memory)
}
fn drop_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> {
self.cost_track.drop_memory(memory)
}
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,
contract_identifier: &QualifiedContractIdentifier,
program: &str,
) -> Result {
let parsed = self.parse_nonempty_program(invoke_ctx, contract_identifier, program)?;
self.global_context.begin();
let contract = self
.global_context
.database
.get_contract(contract_identifier)
.or_else(|e| {
self.global_context.roll_back()?;
Err(e)
})?;
let result = {
let nested_view = InvocationContext {
contract_context: &contract,
sender: invoke_ctx.sender.clone(),
caller: invoke_ctx.caller.clone(),
sponsor: invoke_ctx.sponsor.clone(),
};
let local_context = LocalContext::new();
eval(&parsed[0], self, &nested_view, &local_context)
.and_then(|value| value.clone_with_cost(self))
}
.map_err(ClarityEvalError::from);
self.global_context.roll_back()?;
result
}
pub fn eval_raw(
&mut self,
invoke_ctx: &InvocationContext,
program: &str,
) -> Result {
let parsed = self.parse_nonempty_program(
invoke_ctx,
&QualifiedContractIdentifier::transient(),
program,
)?;
let local_context = LocalContext::new();
eval(&parsed[0], self, invoke_ctx, &local_context)
.and_then(|value| value.clone_with_cost(self))
.map_err(ClarityEvalError::from)
}
/// Parse `program` into a **non-empty** list of `SymbolicExpression`s.
///
/// This is a wrapper around `ast::build_ast(..)` that enforces the invariant
/// that a parsed program must contain at least one top-level expression.
///
/// # Errors
/// - Returns `Err` if the program fails to parse/build an AST.
/// - Returns `Err(UnexpectedParserFailure)` if parsing succeeds but yields *zero* expressions.
///
/// # Notes
/// The empty-expression case should be unreachable for normal VM execution because
/// published/deployed contract code and transaction programs are validated earlier.
/// It exists as a defensive check for malformed input in tests, fuzzing, or internal
/// callers that bypass normal validation paths.
fn parse_nonempty_program(
&mut self,
invoke_ctx: &InvocationContext,
contract_identifier: &QualifiedContractIdentifier,
program: &str,
) -> Result, ClarityEvalError> {
let expressions = ast::build_ast(
contract_identifier,
program,
self,
invoke_ctx.contract_context.clarity_version,
self.global_context.epoch_id,
)?
.expressions;
if expressions.is_empty() {
return Err(ParseError::from(ParseErrorKind::UnexpectedParserFailure).into());
}
Ok(expressions)
}
/// This is the epoch of the block that this transaction is executing within.
/// Note: in the current plans for 2.1, there is also a contract-specific **Clarity version**
/// which governs which native functions are available / defined. That is separate from this
/// epoch identifier, and most Clarity VM changes should consult that value instead. This
/// epoch identifier is used for determining how cost functions should be applied.
pub fn epoch(&self) -> &StacksEpochId {
&self.global_context.epoch_id
}
pub fn execute_contract(
&mut self,
invoke_ctx: &InvocationContext,
contract: &QualifiedContractIdentifier,
tx_name: &str,
args: &[SymbolicExpression],
read_only: bool,
) -> Result {
self.inner_execute_contract(invoke_ctx, contract, tx_name, args, read_only, false)
}
/// This method is exposed for callers that need to invoke a private method directly.
/// For example, this is used by the Stacks chainstate for invoking private methods
/// on the pox-2 contract. This should not be called by user transaction processing.
pub fn execute_contract_allow_private(
&mut self,
invoke_ctx: &InvocationContext,
contract: &QualifiedContractIdentifier,
tx_name: &str,
args: &[SymbolicExpression],
read_only: bool,
) -> Result {
self.inner_execute_contract(invoke_ctx, contract, tx_name, args, read_only, true)
}
/// This method handles actual execution of contract-calls on a contract.
///
/// `allow_private` should always be set to `false` for user transactions:
/// this ensures that only `define-public` and `define-read-only` methods can
/// be invoked. The `allow_private` mode should only be used by
/// `Environment::execute_contract_allow_private`.
fn inner_execute_contract(
&mut self,
invoke_ctx: &InvocationContext,
contract_identifier: &QualifiedContractIdentifier,
tx_name: &str,
args: &[SymbolicExpression],
read_only: bool,
allow_private: bool,
) -> Result {
let contract_size = self
.global_context
.database
.get_contract_size(contract_identifier)?;
runtime_cost(ClarityCostFunction::LoadContract, self, contract_size)?;
self.global_context.add_memory(contract_size)?;
// NOTE: When contract caching is used, then the memory counters here will drop the
// `contract_size` after the contract execution has completed, but the contracts will remain
// in the cache (up to the cache eviction policy's limits).
finally_drop_memory!(self.global_context, contract_size; {
let contract = self.global_context.database.get_contract(contract_identifier)?;
let func = contract.lookup_function(tx_name)
.ok_or_else(|| { RuntimeCheckErrorKind::UndefinedFunction(tx_name.to_string()) })?;
if !allow_private && !func.is_public() {
return Err(RuntimeCheckErrorKind::NoSuchPublicFunction(contract_identifier.to_string(), tx_name.to_string()).into());
} else if read_only && !func.is_read_only() {
return Err(RuntimeCheckErrorKind::Unreachable(format!("Public function not read-only: {contract_identifier} {tx_name}")).into());
}
let args: Result, VmExecutionError> = args.iter()
.map(|arg| {
let value = arg.match_atom_value()
.ok_or_else(|| VmInternalError::InvariantViolation(format!("Passed non-value expression to exec_tx on {tx_name}!")))?;
// sanitize contract-call inputs in epochs >= 2.4
// testing todo: ensure sanitize_value() preserves trait callability!
let expected_type = TypeSignature::type_of(value)?;
let (sanitized_value, _) = Value::sanitize_value(
self.epoch(),
&expected_type,
value.clone(),
).ok_or_else(|| RuntimeCheckErrorKind::TypeValueError(
Box::new(expected_type),
value.to_error_string(),
)
)?;
Ok(sanitized_value)
})
.collect();
let args = args?;
let func_identifier = func.get_identifier();
if self.call_stack.contains(&func_identifier) {
return Err(RuntimeCheckErrorKind::CircularReference(vec![func_identifier.to_string()]).into())
}
self.call_stack.insert(&func_identifier, true);
let res = self.execute_function_as_transaction(invoke_ctx, &func, &args, Some(&*contract), allow_private);
self.call_stack.remove(&func_identifier, true)?;
match res {
Ok(value) => {
if let Some(handler) = self.global_context.database.get_cc_special_cases_handler() {
handler(
self.global_context,
invoke_ctx.sender.as_ref(),
invoke_ctx.sponsor.as_ref(),
contract_identifier,
tx_name,
&args,
&value
)?;
}
Ok(value)
},
Err(e) => Err(e)
}
})
}
pub fn execute_function_as_transaction(
&mut self,
invoke_ctx: &InvocationContext,
function: &DefinedFunction,
args: &[Value],
next_contract_context: Option<&ContractContext>,
allow_private: bool,
) -> Result {
let make_read_only = function.is_read_only();
if make_read_only {
self.global_context.begin_read_only();
} else {
self.global_context.begin();
}
let next_contract_context = next_contract_context.unwrap_or(invoke_ctx.contract_context);
let result = {
let nested_view = InvocationContext {
contract_context: next_contract_context,
sender: invoke_ctx.sender.clone(),
caller: invoke_ctx.caller.clone(),
sponsor: invoke_ctx.sponsor.clone(),
};
function.execute_apply(args, self, &nested_view)
};
if make_read_only {
self.global_context.roll_back()?;
result
} else {
self.global_context.handle_tx_result(result, allow_private)
}
}
pub fn evaluate_at_block<'e>(
&mut self,
bhh: StacksBlockId,
closure: &'e SymbolicExpression,
invoke_ctx: &'e InvocationContext,
local: &'e LocalContext,
) -> Result, VmExecutionError> {
self.global_context.begin_read_only();
let result = self
.global_context
.database
.set_block_hash(bhh, false)
.and_then(|prior_bhh| {
let result = eval(closure, self, invoke_ctx, local);
self.global_context
.database
.set_block_hash(prior_bhh, true)
.map_err(|_| {
VmInternalError::Expect(
"ERROR: Failed to restore prior active block after time-shifted evaluation."
.into())
})?;
result
});
self.global_context.roll_back()?;
result
}
pub fn initialize_contract(
&mut self,
invoke_ctx: &InvocationContext,
contract_identifier: QualifiedContractIdentifier,
contract_content: &str,
) -> Result<(), ClarityEvalError> {
let clarity_version = invoke_ctx.contract_context.clarity_version;
let contract_ast = ast::build_ast(
&contract_identifier,
contract_content,
self,
clarity_version,
self.global_context.epoch_id,
)?;
self.initialize_contract_from_ast(
invoke_ctx,
contract_identifier,
clarity_version,
&contract_ast,
contract_content,
)
.map_err(ClarityEvalError::from)
}
pub fn initialize_contract_from_ast(
&mut self,
invoke_ctx: &InvocationContext,
contract_identifier: QualifiedContractIdentifier,
contract_version: ClarityVersion,
contract_content: &ContractAST,
contract_string: &str,
) -> Result<(), VmExecutionError> {
self.global_context.begin();
// wrap in a closure so that `?` can be caught and the global_context can roll_back()
// before returning.
let result = (|| {
runtime_cost(
ClarityCostFunction::ContractStorage,
self,
contract_string.len(),
)?;
if self
.global_context
.database
.has_contract(&contract_identifier)
{
return Err(RuntimeCheckErrorKind::Unreachable(format!(
"Contract already exists: {contract_identifier}"
))
.into());
}
// first, store the contract _content hash_ in the data store.
// this is necessary before creating and accessing metadata fields in the data store,
// --or-- storing any analysis metadata in the data store.
self.global_context
.database
.insert_contract_hash(&contract_identifier, contract_string)?;
let memory_use = contract_string.len() as u64;
self.add_memory(memory_use)?;
let result = Contract::initialize_from_ast(
contract_identifier.clone(),
contract_content,
invoke_ctx.sponsor.clone(),
self.global_context,
contract_version,
);
self.drop_memory(memory_use)?;
result
})();
match result {
Ok(contract) => {
let data_size = contract.data_size;
self.global_context
.database
.insert_contract(&contract_identifier, contract)?;
self.global_context
.database
.set_contract_data_size(&contract_identifier, data_size)?;
self.global_context.commit()?;
Ok(())
}
Err(e) => {
self.global_context.roll_back()?;
Err(e)
}
}
}
/// Top-level STX-transfer, invoked by TokenTransfer transactions.
/// Only commits if the inner stx_transfer_consolidated() returns an (ok true) value.
/// Rolls back if it returns an (err ..) value, or if the method itself fails for some reason
/// (miners should never build blocks that spend non-existent STX in a top-level token-transfer)
pub fn stx_transfer(
&mut self,
invoke_ctx: &InvocationContext,
from: &PrincipalData,
to: &PrincipalData,
amount: u128,
memo: &BuffData,
) -> Result {
self.global_context.begin();
let result = stx_transfer_consolidated(self, invoke_ctx, from, to, amount, memo);
match result {
Ok(value) => match value
.clone()
.expect_result()
.map_err(|_| VmInternalError::Expect("Expected result".into()))?
{
Ok(_) => {
self.global_context.commit()?;
Ok(value)
}
Err(_) => {
self.global_context.roll_back()?;
Err(VmInternalError::InsufficientBalance.into())
}
},
Err(e) => {
self.global_context.roll_back()?;
Err(e)
}
}
}
pub fn run_as_transaction(
&mut self,
invoke_ctx: &InvocationContext,
f: F,
) -> std::result::Result
where
F: FnOnce(&mut Self, &InvocationContext) -> std::result::Result,
E: From,
{
self.global_context.begin();
let result = f(self, invoke_ctx);
match result {
Ok(ret) => {
self.global_context.commit()?;
Ok(ret)
}
Err(e) => {
self.global_context.roll_back()?;
Err(e)
}
}
}
fn push_to_event_batch(
&mut self,
event: StacksTransactionEvent,
) -> Result<(), VmExecutionError> {
let size = if let StacksTransactionEvent::SmartContractEvent(ref ev) = event {
ev.value.size().map_err(|e| {
VmInternalError::Expect(format!("Could not calculate event size: {e}"))
})?
} else {
0
};
if let Some((batch, total_size)) = self.global_context.event_batches.last_mut() {
batch.events.push(event);
*total_size = total_size.saturating_add(size.into());
if *total_size >= MAX_EVENTS_BATCH {
return Err(VmInternalError::Expect(
"Event batch grew too large during execution".to_string(),
)
.into());
}
}
Ok(())
}
pub fn construct_print_transaction_event(
contract_id: QualifiedContractIdentifier,
value: Value,
) -> StacksTransactionEvent {
let print_event = SmartContractEventData {
key: (contract_id, "print".to_string()),
value,
};
StacksTransactionEvent::SmartContractEvent(print_event)
}
pub fn register_print_event(
&mut self,
invoke_ctx: &InvocationContext,
value: Value,
) -> Result<(), VmExecutionError> {
let event = Self::construct_print_transaction_event(
invoke_ctx.contract_context.contract_identifier.clone(),
value,
);
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_stx_transfer_event(
&mut self,
sender: PrincipalData,
recipient: PrincipalData,
amount: u128,
memo: BuffData,
) -> Result<(), VmExecutionError> {
let event_data = STXTransferEventData {
sender,
recipient,
amount,
memo,
};
let event = StacksTransactionEvent::STXEvent(STXEventType::STXTransferEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_stx_burn_event(
&mut self,
sender: PrincipalData,
amount: u128,
) -> Result<(), VmExecutionError> {
let event_data = STXBurnEventData { sender, amount };
let event = StacksTransactionEvent::STXEvent(STXEventType::STXBurnEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_nft_transfer_event(
&mut self,
sender: PrincipalData,
recipient: PrincipalData,
value: Value,
asset_identifier: AssetIdentifier,
) -> Result<(), VmExecutionError> {
let event_data = NFTTransferEventData {
sender,
recipient,
asset_identifier,
value,
};
let event = StacksTransactionEvent::NFTEvent(NFTEventType::NFTTransferEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_nft_mint_event(
&mut self,
recipient: PrincipalData,
value: Value,
asset_identifier: AssetIdentifier,
) -> Result<(), VmExecutionError> {
let event_data = NFTMintEventData {
recipient,
asset_identifier,
value,
};
let event = StacksTransactionEvent::NFTEvent(NFTEventType::NFTMintEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_nft_burn_event(
&mut self,
sender: PrincipalData,
value: Value,
asset_identifier: AssetIdentifier,
) -> Result<(), VmExecutionError> {
let event_data = NFTBurnEventData {
sender,
asset_identifier,
value,
};
let event = StacksTransactionEvent::NFTEvent(NFTEventType::NFTBurnEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_ft_transfer_event(
&mut self,
sender: PrincipalData,
recipient: PrincipalData,
amount: u128,
asset_identifier: AssetIdentifier,
) -> Result<(), VmExecutionError> {
let event_data = FTTransferEventData {
sender,
recipient,
asset_identifier,
amount,
};
let event = StacksTransactionEvent::FTEvent(FTEventType::FTTransferEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_ft_mint_event(
&mut self,
recipient: PrincipalData,
amount: u128,
asset_identifier: AssetIdentifier,
) -> Result<(), VmExecutionError> {
let event_data = FTMintEventData {
recipient,
asset_identifier,
amount,
};
let event = StacksTransactionEvent::FTEvent(FTEventType::FTMintEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
pub fn register_ft_burn_event(
&mut self,
sender: PrincipalData,
amount: u128,
asset_identifier: AssetIdentifier,
) -> Result<(), VmExecutionError> {
let event_data = FTBurnEventData {
sender,
asset_identifier,
amount,
};
let event = StacksTransactionEvent::FTEvent(FTEventType::FTBurnEvent(event_data));
self.push_to_event_batch(event)?;
Ok(())
}
}
impl<'a, 'hooks> GlobalContext<'a, 'hooks> {
// Instantiate a new Global Context
pub fn new(
mainnet: bool,
chain_id: u32,
database: ClarityDatabase<'a>,
cost_track: LimitedCostTracker,
epoch_id: StacksEpochId,
) -> GlobalContext<'a, 'hooks> {
GlobalContext {
database,
cost_track,
read_only: Vec::new(),
asset_maps: Vec::new(),
event_batches: Vec::new(),
mainnet,
epoch_id,
chain_id,
eval_hooks: None,
execution_resource_limiter: ResourceLimiter::unlimited(),
}
}
pub fn is_top_level(&self) -> bool {
self.asset_maps.is_empty()
}
pub fn set_execution_resource_limiter(&mut self, resource_limiter: ResourceLimiter) {
self.execution_resource_limiter = resource_limiter;
}
fn get_asset_map(&mut self) -> Result<&mut AssetMap, VmExecutionError> {
self.asset_maps
.last_mut()
.ok_or_else(|| VmInternalError::Expect("Failed to obtain asset map".into()).into())
}
pub fn get_readonly_asset_map(&mut self) -> Result<&AssetMap, VmExecutionError> {
self.asset_maps
.last()
.ok_or_else(|| VmInternalError::Expect("Failed to obtain asset map".into()).into())
}
pub fn log_asset_transfer(
&mut self,
sender: &PrincipalData,
contract_identifier: &QualifiedContractIdentifier,
asset_name: &ClarityName,
transferred: Value,
) -> Result<(), VmExecutionError> {
let asset_identifier = AssetIdentifier {
contract_identifier: contract_identifier.clone(),
asset_name: asset_name.clone(),
};
self.get_asset_map()?
.add_asset_transfer(sender, asset_identifier, transferred);
Ok(())
}
pub fn log_token_transfer(
&mut self,
sender: &PrincipalData,
contract_identifier: &QualifiedContractIdentifier,
asset_name: &ClarityName,
transferred: u128,
) -> Result<(), VmExecutionError> {
let asset_identifier = AssetIdentifier {
contract_identifier: contract_identifier.clone(),
asset_name: asset_name.clone(),
};
self.get_asset_map()?
.add_token_transfer(sender, asset_identifier, transferred)
}
pub fn log_stx_transfer(
&mut self,
sender: &PrincipalData,
transferred: u128,
) -> Result<(), VmExecutionError> {
self.get_asset_map()?.add_stx_transfer(sender, transferred)
}
pub fn log_stx_burn(
&mut self,
sender: &PrincipalData,
transferred: u128,
) -> Result<(), VmExecutionError> {
self.get_asset_map()?.add_stx_burn(sender, transferred)
}
pub fn log_stacking(
&mut self,
sender: &PrincipalData,
amount: u128,
) -> Result<(), VmExecutionError> {
let epoch = self.epoch_id;
self.get_asset_map()?.add_stacking(sender, amount, epoch)
}
pub fn log_pox_action(&mut self, sender: &PrincipalData) -> Result<(), VmExecutionError> {
self.get_asset_map()?.add_pox_action(sender);
Ok(())
}
pub fn execute(&mut self, f: F) -> Result
where
F: FnOnce(&mut Self) -> Result,
{
self.begin();
let result = f(self).or_else(|e| {
self.roll_back()?;
Err(e)
})?;
self.commit()?;
Ok(result)
}
/// Run a snippet of Clarity code in the given contract context
/// Only use within special-case contract-call handlers.
/// DO NOT CALL FROM ANYWHERE ELSE!
pub fn special_cc_handler_execute_read_only(
&mut self,
sender: PrincipalData,
sponsor: Option,
contract_context: &ContractContext,
f: F,
) -> std::result::Result
where
E: From,
F: FnOnce(&mut ExecutionState, &InvocationContext) -> std::result::Result,
{
self.begin();
let result = {
// this right here is why it's dangerous to call this anywhere else.
// the call stack gets reset to empyt each time!
let mut callstack = CallStack::new();
let mut exec_state = ExecutionState {
global_context: self,
call_stack: &mut callstack,
};
let invoke_ctx = InvocationContext {
contract_context,
sender: Some(sender.clone()),
caller: Some(sender),
sponsor,
};
f(&mut exec_state, &invoke_ctx)
};
self.roll_back().map_err(ClarityEvalError::from)?;
match result {
Ok(return_value) => Ok(return_value),
Err(e) => Err(e),
}
}
pub fn is_read_only(&self) -> bool {
// top level context defaults to writable.
self.read_only.last().cloned().unwrap_or(false)
}
pub fn begin(&mut self) {
self.asset_maps.push(AssetMap::new());
let total_size = self
.event_batches
.last()
.map(|(_, total_size)| *total_size)
.unwrap_or(0);
self.event_batches.push((EventBatch::new(), total_size));
self.database.begin();
let read_only = self.is_read_only();
self.read_only.push(read_only);
}
pub fn begin_read_only(&mut self) {
self.asset_maps.push(AssetMap::new());
let total_size = self
.event_batches
.last()
.map(|(_, total_size)| *total_size)
.unwrap_or(0);
self.event_batches.push((EventBatch::new(), total_size));
self.database.begin();
self.read_only.push(true);
}
pub fn commit(&mut self) -> Result<(Option, Option), VmExecutionError> {
trace!("Calling commit");
self.read_only.pop();
let asset_map = self.asset_maps.pop().ok_or_else(|| {
VmInternalError::Expect("ERROR: Committed non-nested context.".into())
})?;
let (mut event_batch, new_total_size) = self.event_batches.pop().ok_or_else(|| {
VmInternalError::Expect("ERROR: Committed non-nested context.".into())
})?;
let out_map = match self.asset_maps.last_mut() {
Some(tail_back) => {
if let Err(e) = tail_back.commit_other(asset_map, self.epoch_id) {
self.database.roll_back()?;
return Err(e);
}
None
}
None => Some(asset_map),
};
let out_batch = match self.event_batches.last_mut() {
Some((tail_back, total_size)) => {
tail_back.events.append(&mut event_batch.events);
*total_size = new_total_size;
None
}
None => Some(event_batch),
};
self.database.commit()?;
Ok((out_map, out_batch))
}
pub fn roll_back(&mut self) -> Result<(), VmExecutionError> {
let popped = self.asset_maps.pop();
if popped.is_none() {
return Err(VmInternalError::Expect("Expected entry to rollback".into()).into());
}
let popped = self.read_only.pop();
if popped.is_none() {
return Err(VmInternalError::Expect("Expected entry to rollback".into()).into());
}
let popped = self.event_batches.pop();
if popped.is_none() {
return Err(VmInternalError::Expect("Expected entry to rollback".into()).into());
}
self.database.roll_back()
}
// the allow_private parameter allows private functions calls to return any Clarity type
// and not just Response. It only has effect is the devtools feature is enabled. eg:
// clarity = { version = "*", features = ["devtools"] }
pub fn handle_tx_result(
&mut self,
result: Result,
allow_private: bool,
) -> Result {
if let Ok(result) = result {
if let Value::Response(data) = result {
if data.committed {
self.commit()?;
} else {
self.roll_back()?;
}
Ok(Value::Response(data))
} else if allow_private && cfg!(feature = "devtools") {
self.commit()?;
Ok(result)
} else {
Err(RuntimeCheckErrorKind::Unreachable(format!(
"Public function must return response: {}",
TypeSignature::type_of(&result)?
))
.into())
}
} else {
self.roll_back()?;
result
}
}
/// Destroys this context, returning ownership of its database reference.
/// If the context wasn't top-level (i.e., it had uncommitted data), return None,
/// because the database is not guaranteed to be in a sane state.
pub fn destruct(self) -> Option<(ClarityDatabase<'a>, LimitedCostTracker)> {
if self.is_top_level() {
Some((self.database, self.cost_track))
} else {
None
}
}
}
impl ContractContext {
pub fn new(
contract_identifier: QualifiedContractIdentifier,
clarity_version: ClarityVersion,
) -> Self {
Self {
contract_identifier,
variables: HashMap::new(),
functions: HashMap::new(),
defined_traits: HashMap::new(),
implemented_traits: HashSet::new(),
persisted_names: HashSet::new(),
data_size: 0,
meta_data_map: HashMap::new(),
meta_data_var: HashMap::new(),
meta_nft: HashMap::new(),
meta_ft: HashMap::new(),
clarity_version,
is_deploying: false,
}
}
/// Lookup a contract constant by name
pub fn lookup_variable(&self, name: &str) -> Option<&Value> {
self.variables.get(name)
}
pub fn lookup_function(&self, name: &str) -> Option {
self.functions.get(name).cloned()
}
pub fn lookup_trait_definition(
&self,
name: &str,
) -> Option> {
self.defined_traits.get(name).cloned()
}
pub fn is_explicitly_implementing_trait(&self, trait_identifier: &TraitIdentifier) -> bool {
self.implemented_traits.contains(trait_identifier)
}
pub fn is_name_used(&self, name: &str) -> bool {
is_reserved(name, self.get_clarity_version())
|| self.variables.contains_key(name)
|| self.functions.contains_key(name)
|| self.persisted_names.contains(name)
|| self.defined_traits.contains_key(name)
}
pub fn get_clarity_version(&self) -> &ClarityVersion {
&self.clarity_version
}
/// Canonicalize the types for the specified epoch. Only functions and
/// defined traits are exposed externally, so other types are not
/// canonicalized.
pub fn canonicalize_types(&mut self, epoch: &StacksEpochId) -> Result<(), VmExecutionError> {
for (_, function) in self.functions.iter_mut() {
function.canonicalize_types(epoch);
}
for trait_def in self.defined_traits.values_mut() {
for (_, function) in trait_def.iter_mut() {
*function = function.canonicalize(epoch);
}
}
// In pre-sanitized-variable epochs, sanitize all contract
// variables at load time so lookups can borrow directly.
if epoch.uses_pre_sanitized_variables() {
for (_, value) in self.variables.iter_mut() {
let owned = std::mem::replace(value, Value::none());
let (sanitized, _) =
Value::sanitize_value(epoch, &TypeSignature::type_of(&owned)?, owned)
.ok_or_else(|| RuntimeCheckErrorKind::CouldNotDetermineType)?;
*value = sanitized;
}
}
Ok(())
}
}
impl Default for LocalContext<'_> {
fn default() -> Self {
Self::new()
}
}
impl<'a> LocalContext<'a> {
pub fn new() -> LocalContext<'a> {
LocalContext {
function_context: Option::None,
parent: Option::None,
callable_contracts: HashMap::new(),
variables: HashMap::new(),
depth: 0,
}
}
pub fn depth(&self) -> u64 {
self.depth
}
pub fn function_context(&self) -> &LocalContext<'_> {
match self.function_context {
Some(context) => context,
None => self,
}
}
pub fn extend(&'a self) -> Result, VmExecutionError> {
if self.depth >= MAX_CONTEXT_DEPTH {
// `MaxContextDepthReached` in this function is **unreachable** in normal Clarity execution because:
// - Every function call in Clarity increments both the call stack depth and the local context depth.
// - The VM enforces the epoch-specific `MAX_CALL_STACK_DEPTH` **before** `MAX_CONTEXT_DEPTH` (256).
// - This means no contract can create more nested function calls than the epoch limit, preventing context depth from reaching 256.
// - Nested expressions (`let`, `begin`, `if`, etc.) increment context depth, but the Clarity parser enforces
// `ExpressionStackDepthTooDeep` long before MAX_CONTEXT_DEPTH nested contexts can be written.
// - As a result, `MaxContextDepthReached` can only occur in artificial Rust-level tests calling `LocalContext::extend()`,
// not in deployed contract execution.
Err(RuntimeError::MaxContextDepthReached.into())
} else {
Ok(LocalContext {
function_context: Some(self.function_context()),
parent: Some(self),
callable_contracts: HashMap::new(),
variables: HashMap::new(),
depth: self.depth + 1,
})
}
}
pub fn lookup_variable(&self, name: &str) -> Option<&Value> {
match self.variables.get(name) {
Some(value) => Some(value),
None => match self.parent {
Some(parent) => parent.lookup_variable(name),
None => None,
},
}
}
pub fn lookup_callable_contract(&self, name: &str) -> Option<&CallableData> {
match self.callable_contracts.get(name) {
Some(found) => Some(found),
None => match self.parent {
Some(parent) => parent.lookup_callable_contract(name),
None => None,
},
}
}
}
impl Default for CallStack {
fn default() -> Self {
Self::new()
}
}
impl CallStack {
pub fn new() -> CallStack {
CallStack {
stack: Vec::new(),
set: HashSet::new(),
apply_depth: 0,
}
}
pub fn depth(&self) -> u64 {
let stack_len = u64::try_from(self.stack.len()).unwrap_or(u64::MAX);
stack_len.saturating_add(self.apply_depth)
}
pub fn contains(&self, function: &FunctionIdentifier) -> bool {
self.set.contains(function)
}
pub fn insert(&mut self, function: &FunctionIdentifier, track: bool) {
self.stack.push(function.clone());
if track {
self.set.insert(function.clone());
}
}
pub fn incr_apply_depth(&mut self) {
self.apply_depth += 1;
}
pub fn decr_apply_depth(&mut self) {
self.apply_depth -= 1;
}
pub fn remove(
&mut self,
function: &FunctionIdentifier,
tracked: bool,
) -> Result<(), VmExecutionError> {
if let Some(removed) = self.stack.pop() {
if removed != *function {
return Err(VmInternalError::InvariantViolation(
"Tried to remove item from empty call stack.".to_string(),
)
.into());
}
if tracked && !self.set.remove(function) {
return Err(VmInternalError::InvariantViolation(
"Tried to remove tracked function from call stack, but could not find in current context.".into()
)
.into());
}
Ok(())
} else {
Err(VmInternalError::InvariantViolation(
"Tried to remove item from empty call stack.".to_string(),
)
.into())
}
}
#[cfg(feature = "developer-mode")]
pub fn make_stack_trace(&self) -> StackTrace {
self.stack.clone()
}
#[cfg(not(feature = "developer-mode"))]
pub fn make_stack_trace(&self) -> StackTrace {
Vec::new()
}
}
#[cfg(test)]
mod test {
use clarity_types::ContractName;
use stacks_common::consts::CHAIN_ID_TESTNET;
use stacks_common::types::chainstate::StacksAddress;
use stacks_common::util::hash::Hash160;
use super::*;
use crate::vm::callables::DefineType;
use crate::vm::database::MemoryBackingStore;
use crate::vm::tests::{
TopLevelMemoryEnvironmentGenerator, test_clarity_versions, test_epochs, tl_env_factory,
};
use crate::vm::types::StandardPrincipalData;
use crate::vm::types::signatures::CallableSubtype;
#[test]
fn test_asset_map_abort() {
let a_contract_id = QualifiedContractIdentifier::local("a").unwrap();
let b_contract_id = QualifiedContractIdentifier::local("b").unwrap();
let p1 = PrincipalData::Contract(a_contract_id.clone());
let p2 = PrincipalData::Contract(b_contract_id.clone());
let t1 = AssetIdentifier {
contract_identifier: a_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let _t2 = AssetIdentifier {
contract_identifier: b_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let mut am1 = AssetMap::new();
let mut am2 = AssetMap::new();
am1.add_token_transfer(&p1, t1.clone(), 1).unwrap();
am1.add_token_transfer(&p2, t1.clone(), u128::MAX).unwrap();
am2.add_token_transfer(&p1, t1.clone(), 1).unwrap();
am2.add_token_transfer(&p2, t1.clone(), 1).unwrap();
am1.commit_other(am2, StacksEpochId::Epoch30).unwrap_err();
let table = am1.to_table();
assert_eq!(table[&p2][&t1], AssetMapEntry::Token(u128::MAX));
assert_eq!(table[&p1][&t1], AssetMapEntry::Token(1));
}
#[test]
fn test_asset_map_combinations() {
let a_contract_id = QualifiedContractIdentifier::local("a").unwrap();
let b_contract_id = QualifiedContractIdentifier::local("b").unwrap();
let c_contract_id = QualifiedContractIdentifier::local("c").unwrap();
let d_contract_id = QualifiedContractIdentifier::local("d").unwrap();
let e_contract_id = QualifiedContractIdentifier::local("e").unwrap();
let f_contract_id = QualifiedContractIdentifier::local("f").unwrap();
let g_contract_id = QualifiedContractIdentifier::local("g").unwrap();
let p1 = PrincipalData::Contract(a_contract_id.clone());
let p2 = PrincipalData::Contract(b_contract_id.clone());
let p3 = PrincipalData::Contract(c_contract_id.clone());
let _p4 = PrincipalData::Contract(d_contract_id.clone());
let _p5 = PrincipalData::Contract(e_contract_id.clone());
let _p6 = PrincipalData::Contract(f_contract_id);
let _p7 = PrincipalData::Contract(g_contract_id);
let t1 = AssetIdentifier {
contract_identifier: a_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let t2 = AssetIdentifier {
contract_identifier: b_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let t3 = AssetIdentifier {
contract_identifier: c_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let t4 = AssetIdentifier {
contract_identifier: d_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let t5 = AssetIdentifier {
contract_identifier: e_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let t6 = AssetIdentifier::STX();
let t7 = AssetIdentifier::STX_burned();
let mut am1 = AssetMap::new();
let mut am2 = AssetMap::new();
am1.add_token_transfer(&p1, t1.clone(), 10).unwrap();
am2.add_token_transfer(&p1, t1.clone(), 15).unwrap();
am1.add_stx_transfer(&p1, 20).unwrap();
am2.add_stx_transfer(&p2, 25).unwrap();
am1.add_stx_burn(&p1, 30).unwrap();
am2.add_stx_burn(&p2, 35).unwrap();
// test merging in a token that _didn't_ have an entry in the parent
am2.add_token_transfer(&p1, t4.clone(), 1).unwrap();
// test merging in a principal that _didn't_ have an entry in the parent
am2.add_token_transfer(&p2, t2.clone(), 10).unwrap();
am2.add_token_transfer(&p2, t2.clone(), 1).unwrap();
// test merging in a principal that _didn't_ have an entry in the parent
am2.add_asset_transfer(&p3, t3.clone(), Value::Int(10));
// test merging in an asset that _didn't_ have an entry in the parent
am1.add_asset_transfer(&p1, t5.clone(), Value::Int(0));
am2.add_asset_transfer(&p1, t3.clone(), Value::Int(1));
am2.add_asset_transfer(&p1, t3.clone(), Value::Int(0));
// test merging in an asset that _does_ have an entry in the parent
am1.add_asset_transfer(&p2, t3.clone(), Value::Int(2));
am1.add_asset_transfer(&p2, t3.clone(), Value::Int(5));
am2.add_asset_transfer(&p2, t3.clone(), Value::Int(3));
am2.add_asset_transfer(&p2, t3.clone(), Value::Int(4));
// test merging in STX transfers
am1.add_stx_transfer(&p1, 21).unwrap();
am2.add_stx_transfer(&p2, 26).unwrap();
// test merging in STX burns
am1.add_stx_burn(&p1, 31).unwrap();
am2.add_stx_burn(&p2, 36).unwrap();
am1.commit_other(am2, StacksEpochId::Epoch30).unwrap();
let table = am1.to_table();
// 3 Principals
assert_eq!(table.len(), 3);
assert_eq!(table[&p1][&t1], AssetMapEntry::Token(25));
assert_eq!(table[&p1][&t4], AssetMapEntry::Token(1));
assert_eq!(table[&p2][&t2], AssetMapEntry::Token(11));
assert_eq!(
table[&p2][&t3],
AssetMapEntry::Asset(vec![
Value::Int(2),
Value::Int(5),
Value::Int(3),
Value::Int(4)
])
);
assert_eq!(
table[&p1][&t3],
AssetMapEntry::Asset(vec![Value::Int(1), Value::Int(0)])
);
assert_eq!(table[&p1][&t5], AssetMapEntry::Asset(vec![Value::Int(0)]));
assert_eq!(table[&p3][&t3], AssetMapEntry::Asset(vec![Value::Int(10)]));
assert_eq!(table[&p1][&t6], AssetMapEntry::STX(20 + 21));
assert_eq!(table[&p2][&t6], AssetMapEntry::STX(25 + 26));
assert_eq!(table[&p1][&t7], AssetMapEntry::Burn(30 + 31));
assert_eq!(table[&p2][&t7], AssetMapEntry::Burn(35 + 36));
}
/// Merging a child frame whose stacking entry collides with the parent's
/// is rejected (`PoxStxAssetMapOverwrite`) before Epoch 4.0, but summed in
/// Epoch 4.0+ (PoX-5). A non-colliding entry merges identically in both.
#[test]
fn test_asset_map_stacking_merge() {
let p1 = PrincipalData::from(StandardPrincipalData::transient());
let p2 = PrincipalData::Contract(QualifiedContractIdentifier::local("b").unwrap());
// Pre-Epoch-4.0: a colliding stacking entry is rejected.
let mut am1 = AssetMap::new();
let mut am2 = AssetMap::new();
am1.add_stacking(&p1, 100, StacksEpochId::Epoch40).unwrap();
am2.add_stacking(&p1, 50, StacksEpochId::Epoch40).unwrap();
assert!(matches!(
am1.commit_other(am2, StacksEpochId::Epoch30).unwrap_err(),
VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::PoxStxAssetMapOverwrite)
));
// `self` is left unchanged by the aborted merge.
assert_eq!(am1.get_stacking(&p1), Some(100));
// Epoch 4.0+: the colliding entry is summed; non-colliding entries pass
// through unchanged.
let mut am1 = AssetMap::new();
let mut am2 = AssetMap::new();
am1.add_stacking(&p1, 100, StacksEpochId::Epoch40).unwrap();
am2.add_stacking(&p1, 50, StacksEpochId::Epoch40).unwrap();
am2.add_stacking(&p2, 25, StacksEpochId::Epoch40).unwrap();
am1.commit_other(am2, StacksEpochId::Epoch40).unwrap();
assert_eq!(am1.get_stacking(&p1), Some(150));
assert_eq!(am1.get_stacking(&p2), Some(25));
// Epoch 4.0+: a summed stacking amount that overflows `u128` aborts the
// merge with an arithmetic error rather than panicking, and leaves
// `self` unchanged.
let mut am1 = AssetMap::new();
let mut am2 = AssetMap::new();
am1.add_stacking(&p1, u128::MAX, StacksEpochId::Epoch40)
.unwrap();
am2.add_stacking(&p1, 1, StacksEpochId::Epoch40).unwrap();
assert!(matches!(
am1.commit_other(am2, StacksEpochId::Epoch40).unwrap_err(),
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
));
assert_eq!(am1.get_stacking(&p1), Some(u128::MAX));
}
/// Test the stx-transfer consolidation tx invalidation
/// bug from 2.4.0.1.0-rc1
#[apply(test_epochs)]
fn stx_transfer_consolidate_regr_24010(
epoch: StacksEpochId,
mut tl_env_factory: TopLevelMemoryEnvironmentGenerator,
) {
let mut exec_state = tl_env_factory.get_env(epoch);
let u1 = StacksAddress::new(0, Hash160([1; 20])).unwrap();
let u2 = StacksAddress::new(0, Hash160([2; 20])).unwrap();
// insufficient balance must be a non-includable transaction. it must error here,
// not simply rollback the tx and squelch the error as includable.
let e = exec_state
.stx_transfer(
&PrincipalData::from(u1),
&PrincipalData::from(u2),
1000,
&BuffData::empty(),
)
.unwrap_err();
assert_eq!(e.to_string(), "Internal(InsufficientBalance)");
}
#[test]
fn test_canonicalize_contract_context() {
let trait_id = TraitIdentifier::new(
StandardPrincipalData::transient(),
ContractName::from_literal("my-contract"),
ClarityName::from_literal("my-trait"),
);
let mut contract_context = ContractContext::new(
QualifiedContractIdentifier::local("foo").unwrap(),
ClarityVersion::Clarity1,
);
contract_context.functions.insert(
ClarityName::from_literal("foo"),
DefinedFunction::new(
vec![(
ClarityName::from_literal("a"),
TypeSignature::TraitReferenceType(trait_id.clone()),
)],
SymbolicExpression::atom_value(Value::Int(3)),
DefineType::Public,
&ClarityName::from_literal("foo"),
"testing",
),
);
let mut trait_functions = BTreeMap::new();
trait_functions.insert(
ClarityName::from_literal("alpha"),
FunctionSignature {
args: vec![TypeSignature::TraitReferenceType(trait_id.clone())],
returns: TypeSignature::ResponseType(Box::new((
TypeSignature::UIntType,
TypeSignature::UIntType,
))),
},
);
contract_context
.defined_traits
.insert(ClarityName::from_literal("bar"), trait_functions);
contract_context
.canonicalize_types(&StacksEpochId::Epoch21)
.unwrap();
assert_eq!(
contract_context.functions["foo"].get_arg_types()[0],
TypeSignature::CallableType(CallableSubtype::Trait(trait_id.clone()))
);
assert_eq!(
contract_context
.defined_traits
.get("bar")
.unwrap()
.get("alpha")
.unwrap()
.args[0],
TypeSignature::CallableType(CallableSubtype::Trait(trait_id))
);
}
#[test]
fn asset_map_arithmetic_overflows() {
let a_contract_id = QualifiedContractIdentifier::local("a").unwrap();
let b_contract_id = QualifiedContractIdentifier::local("b").unwrap();
let p1 = PrincipalData::Contract(a_contract_id.clone());
let p2 = PrincipalData::Contract(b_contract_id.clone());
let t1 = AssetIdentifier {
contract_identifier: a_contract_id,
asset_name: ClarityName::from_literal("a"),
};
let mut am1 = AssetMap::new();
let mut am2 = AssetMap::new();
// Token transfer: add u128::MAX followed by 1 to overflow
am1.add_token_transfer(&p1, t1.clone(), u128::MAX).unwrap();
assert!(matches!(
am1.add_token_transfer(&p1, t1.clone(), 1).unwrap_err(),
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
));
// STX burn: add u128::MAX followed by 1 to overflow
am1.add_stx_burn(&p1, u128::MAX).unwrap();
assert!(matches!(
am1.add_stx_burn(&p1, 1).unwrap_err(),
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
));
// STX transfer: add u128::MAX followed by 1 to overflow
am1.add_stx_transfer(&p1, u128::MAX).unwrap();
assert!(matches!(
am1.add_stx_transfer(&p1, 1).unwrap_err(),
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
));
// commit_other: merge two maps where sum exceeds u128::MAX
am2.add_token_transfer(&p1, t1.clone(), u128::MAX).unwrap();
assert!(matches!(
am1.commit_other(am2, StacksEpochId::Epoch30).unwrap_err(),
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
));
}
#[test]
fn eval_raw_empty_program() {
// Setup environment
let mut tl_env_factory = tl_env_factory();
let mut exec_state = tl_env_factory.get_env(StacksEpochId::latest());
// Call eval_read_only with an empty program
let program = ""; // empty program triggers parsed.is_empty()
let err = exec_state.eval_raw(program).unwrap_err();
let expected_err =
ClarityEvalError::from(ParseError::new(ParseErrorKind::UnexpectedParserFailure));
assert_eq!(err, expected_err, "Expected a type parse failure");
}
#[test]
fn eval_read_only_empty_program() {
// Setup environment
let mut tl_env_factory = tl_env_factory();
let mut exec_state = tl_env_factory.get_env(StacksEpochId::latest());
// Construct a dummy contract context
let contract_id = QualifiedContractIdentifier::local("dummy-contract").unwrap();
// Call eval_read_only with an empty program
let program = ""; // empty program triggers parsed.is_empty()
let err = exec_state
.eval_read_only(&contract_id, program)
.unwrap_err();
let expected_err =
ClarityEvalError::from(ParseError::new(ParseErrorKind::UnexpectedParserFailure));
assert_eq!(err, expected_err, "Expected a type parse failure");
}
#[test]
fn max_context_depth_exceeded() {
let root = LocalContext {
function_context: None,
parent: None,
callable_contracts: HashMap::new(),
variables: HashMap::new(),
depth: MAX_CONTEXT_DEPTH - 1,
};
// We should be able to extend once successfully.
let result = root.extend().unwrap();
// We are now at the MAX_CONTEXT_DEPTH and should fail.
let result_2 = result.extend();
assert!(matches!(
result_2,
Err(VmExecutionError::Runtime(
RuntimeError::MaxContextDepthReached,
_
))
));
}
#[apply(test_clarity_versions)]
fn vm_initialize_contract_already_exists(
#[case] version: ClarityVersion,
#[case] epoch: StacksEpochId,
) {
// --- Setup VM ---
let mut marf = MemoryBackingStore::new();
let mut global_context = GlobalContext::new(
false,
CHAIN_ID_TESTNET,
marf.as_clarity_db(),
LimitedCostTracker::new_free(),
StacksEpochId::Epoch21, // any modern epoch
);
let mut call_stack = CallStack::new();
let contract_context =
ContractContext::new(QualifiedContractIdentifier::transient(), version);
let mut exec_state = ExecutionState {
global_context: &mut global_context,
call_stack: &mut call_stack,
};
let invoke_ctx = InvocationContext {
contract_context: &contract_context,
sender: None,
caller: None,
sponsor: None,
};
let contract_id = QualifiedContractIdentifier::local("dup").unwrap();
let contract_src = "(define-public (ping) (ok u1))";
let ast =
ast::build_ast(&contract_id, contract_src, &mut exec_state, version, epoch).unwrap();
// First initialization succeeds
exec_state
.initialize_contract_from_ast(
&invoke_ctx,
contract_id.clone(),
version,
&ast,
contract_src,
)
.unwrap();
// Second initialization hits ContractAlreadyExists
let err = exec_state
.initialize_contract_from_ast(
&invoke_ctx,
contract_id.clone(),
version,
&ast,
contract_src,
)
.unwrap_err();
assert_eq!(
err,
VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::Unreachable(
"Contract already exists: S1G2081040G2081040G2081040G208105NK8PE5.dup".to_string()
))
);
}
}