Skip to content

Commit 15e72e3

Browse files
Merge branch 'develop' of https://github.com/stacks-network/stacks-core into chore/aac-runtime-check-error-consensus-tests
2 parents 61db3bb + f9f144d commit 15e72e3

File tree

57 files changed

+33920
-43
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+33920
-43
lines changed

clarity-types/src/types/mod.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,10 @@ impl PrincipalData {
14471447
literal: &str,
14481448
) -> Result<StandardPrincipalData, VmExecutionError> {
14491449
let (version, data) = c32::c32_address_decode(literal).map_err(|x| {
1450+
// This `TypeParseFailure` is unreachable in normal Clarity execution.
1451+
// - All principal literals are validated by the Clarity lexer *before* reaching `parse_standard_principal`.
1452+
// - The lexer rejects any literal containing characters outside the C32 alphabet.
1453+
// Therefore, only malformed input fed directly into low-level VM entry points can cause this branch to execute.
14501454
RuntimeError::TypeParseFailure(format!("Invalid principal literal: {x}"))
14511455
})?;
14521456
if data.len() != 20 {
@@ -1640,10 +1644,7 @@ impl TupleData {
16401644
})
16411645
}
16421646

1643-
pub fn shallow_merge(
1644-
mut base: TupleData,
1645-
updates: TupleData,
1646-
) -> Result<TupleData, VmExecutionError> {
1647+
pub fn shallow_merge(mut base: TupleData, updates: TupleData) -> TupleData {
16471648
let TupleData {
16481649
data_map,
16491650
mut type_signature,
@@ -1652,7 +1653,7 @@ impl TupleData {
16521653
base.data_map.insert(name, value);
16531654
}
16541655
base.type_signature.shallow_merge(&mut type_signature);
1655-
Ok(base)
1656+
base
16561657
}
16571658
}
16581659

clarity/src/vm/contexts.rs

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,37 +286,52 @@ impl AssetMap {
286286
}
287287
}
288288

289-
// This will get the next amount for a (principal, stx) entry in the stx table.
289+
/// This will get the next amount for a (principal, stx) entry in the stx table.
290290
fn get_next_stx_amount(
291291
&self,
292292
principal: &PrincipalData,
293293
amount: u128,
294294
) -> Result<u128, VmExecutionError> {
295+
// `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
296+
// - Every `stx-transfer?` or `stx-burn?` is validated against the sender’s
297+
// **unlocked balance** before being queued in `AssetMap`.
298+
// - The unlocked balance is a subset of `stx-liquid-supply`.
299+
// - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
300+
// - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
295301
let current_amount = self.stx_map.get(principal).unwrap_or(&0);
296302
current_amount
297303
.checked_add(amount)
298304
.ok_or(RuntimeError::ArithmeticOverflow.into())
299305
}
300306

301-
// This will get the next amount for a (principal, stx) entry in the burn table.
307+
/// This will get the next amount for a (principal, stx) entry in the burn table.
302308
fn get_next_stx_burn_amount(
303309
&self,
304310
principal: &PrincipalData,
305311
amount: u128,
306312
) -> Result<u128, VmExecutionError> {
313+
// `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
314+
// - Every `stx-burn?` is validated against the sender’s **unlocked balance** first.
315+
// - Unlocked balance is a subset of `stx-liquid-supply`, which is <= `u128::MAX`.
316+
// - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
317+
// - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
307318
let current_amount = self.burn_map.get(principal).unwrap_or(&0);
308319
current_amount
309320
.checked_add(amount)
310321
.ok_or(RuntimeError::ArithmeticOverflow.into())
311322
}
312323

313-
// This will get the next amount for a (principal, asset) entry in the asset table.
324+
/// This will get the next amount for a (principal, asset) entry in the asset table.
314325
fn get_next_amount(
315326
&self,
316327
principal: &PrincipalData,
317328
asset: &AssetIdentifier,
318329
amount: u128,
319330
) -> Result<u128, VmExecutionError> {
331+
// `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
332+
// - The inner transaction must have **partially succeeded** to log any assets.
333+
// - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
334+
// - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
320335
let current_amount = self
321336
.token_map
322337
.get(principal)
@@ -1011,6 +1026,13 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
10111026
.expressions;
10121027

10131028
if parsed.is_empty() {
1029+
// `TypeParseFailure` is **unreachable** in standard Clarity VM execution.
1030+
// - `eval_read_only` parses a raw program string into an AST.
1031+
// - Any empty or invalid program would be rejected at publish/deploy time or earlier parsing stages.
1032+
// - Therefore, `parsed.is_empty()` cannot occur for programs originating from a valid contract
1033+
// or transaction.
1034+
// - Only malformed input fed directly to this internal method (e.g., in unit tests or
1035+
// artificial VM invocations) can trigger this error.
10141036
return Err(RuntimeError::TypeParseFailure(
10151037
"Expected a program of at least length 1".to_string(),
10161038
)
@@ -1060,6 +1082,12 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
10601082
.expressions;
10611083

10621084
if parsed.is_empty() {
1085+
// `TypeParseFailure` is **unreachable** in standard Clarity VM execution.
1086+
// - `eval_raw` parses a raw program string into an AST.
1087+
// - All programs deployed or called via the standard VM go through static parsing and validation first.
1088+
// - Any empty or invalid program would be rejected at publish/deploy time or earlier parsing stages.
1089+
// - Therefore, `parsed.is_empty()` cannot occur for a program that originates from a valid Clarity contract or transaction.
1090+
// Only malformed input directly fed to this internal method (e.g., in unit tests) can trigger this error.
10631091
return Err(RuntimeError::TypeParseFailure(
10641092
"Expected a program of at least length 1".to_string(),
10651093
)
@@ -1940,6 +1968,14 @@ impl<'a> LocalContext<'a> {
19401968

19411969
pub fn extend(&'a self) -> Result<LocalContext<'a>, VmExecutionError> {
19421970
if self.depth >= MAX_CONTEXT_DEPTH {
1971+
// `MaxContextDepthReached` in this function is **unreachable** in normal Clarity execution because:
1972+
// - Every function call in Clarity increments both the call stack depth and the local context depth.
1973+
// - The VM enforces `MAX_CALL_STACK_DEPTH` (currently 64) **before** `MAX_CONTEXT_DEPTH` (256).
1974+
// - This means no contract can create more than 64 nested function calls, preventing context depth from reaching 256.
1975+
// - Nested expressions (`let`, `begin`, `if`, etc.) increment context depth, but the Clarity parser enforces
1976+
// `ExpressionStackDepthTooDeep` long before MAX_CONTEXT_DEPTH nested contexts can be written.
1977+
// - As a result, `MaxContextDepthReached` can only occur in artificial Rust-level tests calling `LocalContext::extend()`,
1978+
// not in deployed contract execution.
19431979
Err(RuntimeError::MaxContextDepthReached.into())
19441980
} else {
19451981
Ok(LocalContext {
@@ -2297,6 +2333,110 @@ mod test {
22972333
);
22982334
}
22992335

2336+
#[test]
2337+
fn asset_map_arithmetic_overflows() {
2338+
let a_contract_id = QualifiedContractIdentifier::local("a").unwrap();
2339+
let b_contract_id = QualifiedContractIdentifier::local("b").unwrap();
2340+
let p1 = PrincipalData::Contract(a_contract_id.clone());
2341+
let p2 = PrincipalData::Contract(b_contract_id.clone());
2342+
let t1 = AssetIdentifier {
2343+
contract_identifier: a_contract_id,
2344+
asset_name: "a".into(),
2345+
};
2346+
2347+
let mut am1 = AssetMap::new();
2348+
let mut am2 = AssetMap::new();
2349+
2350+
// Token transfer: add u128::MAX followed by 1 to overflow
2351+
am1.add_token_transfer(&p1, t1.clone(), u128::MAX).unwrap();
2352+
assert!(matches!(
2353+
am1.add_token_transfer(&p1, t1.clone(), 1).unwrap_err(),
2354+
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
2355+
));
2356+
2357+
// STX burn: add u128::MAX followed by 1 to overflow
2358+
am1.add_stx_burn(&p1, u128::MAX).unwrap();
2359+
assert!(matches!(
2360+
am1.add_stx_burn(&p1, 1).unwrap_err(),
2361+
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
2362+
));
2363+
2364+
// STX transfer: add u128::MAX followed by 1 to overflow
2365+
am1.add_stx_transfer(&p1, u128::MAX).unwrap();
2366+
assert!(matches!(
2367+
am1.add_stx_transfer(&p1, 1).unwrap_err(),
2368+
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
2369+
));
2370+
2371+
// commit_other: merge two maps where sum exceeds u128::MAX
2372+
am2.add_token_transfer(&p1, t1.clone(), u128::MAX).unwrap();
2373+
assert!(matches!(
2374+
am1.commit_other(am2).unwrap_err(),
2375+
VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, _)
2376+
));
2377+
}
2378+
2379+
#[test]
2380+
fn eval_raw_empty_program() {
2381+
// Setup environment
2382+
let mut tl_env_factory = tl_env_factory();
2383+
let mut env = tl_env_factory.get_env(StacksEpochId::latest());
2384+
2385+
// Call eval_read_only with an empty program
2386+
let program = ""; // empty program triggers parsed.is_empty()
2387+
let err = env.eval_raw(program).unwrap_err();
2388+
2389+
assert!(
2390+
matches!(
2391+
err,
2392+
VmExecutionError::Runtime(RuntimeError::TypeParseFailure(msg), _) if msg.contains("Expected a program of at least length 1")),
2393+
"Expected a type parse failure"
2394+
);
2395+
}
2396+
2397+
#[test]
2398+
fn eval_read_only_empty_program() {
2399+
// Setup environment
2400+
let mut tl_env_factory = tl_env_factory();
2401+
let mut env = tl_env_factory.get_env(StacksEpochId::latest());
2402+
2403+
// Construct a dummy contract context
2404+
let contract_id = QualifiedContractIdentifier::local("dummy-contract").unwrap();
2405+
2406+
// Call eval_read_only with an empty program
2407+
let program = ""; // empty program triggers parsed.is_empty()
2408+
let err = env.eval_read_only(&contract_id, program).unwrap_err();
2409+
2410+
assert!(
2411+
matches!(
2412+
err,
2413+
VmExecutionError::Runtime(RuntimeError::TypeParseFailure(msg), _) if msg.contains("Expected a program of at least length 1")),
2414+
"Expected a type parse failure"
2415+
);
2416+
}
2417+
2418+
#[test]
2419+
fn max_context_depth_exceeded() {
2420+
let root = LocalContext {
2421+
function_context: None,
2422+
parent: None,
2423+
callable_contracts: HashMap::new(),
2424+
variables: HashMap::new(),
2425+
depth: MAX_CONTEXT_DEPTH - 1,
2426+
};
2427+
// We should be able to extend once successfully.
2428+
let result = root.extend().unwrap();
2429+
// We are now at the MAX_CONTEXT_DEPTH and should fail.
2430+
let result_2 = result.extend();
2431+
assert!(matches!(
2432+
result_2,
2433+
Err(VmExecutionError::Runtime(
2434+
RuntimeError::MaxContextDepthReached,
2435+
_
2436+
))
2437+
));
2438+
}
2439+
23002440
#[apply(test_clarity_versions)]
23012441
fn vm_initialize_contract_already_exists(
23022442
#[case] version: ClarityVersion,

clarity/src/vm/costs/cost_functions.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ pub fn linear(n: u64, a: u64, b: u64) -> u64 {
171171
}
172172
pub fn logn(n: u64, a: u64, b: u64) -> Result<u64, VmExecutionError> {
173173
if n < 1 {
174+
// This branch is **unreachable** in standard Clarity execution:
175+
// - `logn` is only called from tuple access operations.
176+
// - Tuples must have at least one field, so `n >= 1` is always true (this is enforced via static checks).
177+
// - Hitting this branch requires manual VM manipulation or internal test harnesses.
174178
return Err(VmExecutionError::Runtime(
175179
RuntimeError::Arithmetic("log2 must be passed a positive integer".to_string()),
176180
Some(vec![]),

0 commit comments

Comments
 (0)