Skip to content

Commit c7fa062

Browse files
Merge pull request #6717 from jacinta-stacks/chore/aac-runtime-check-error-consensus-tests
AAC Testing: Runtime `CheckErrorKind` batch number 1
2 parents d2b24d9 + 15e72e3 commit c7fa062

14 files changed

+2819
-42
lines changed

clarity-types/src/errors/analysis.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,6 @@ pub enum CheckErrorKind {
480480
/// String contains invalid UTF-8 encoding.
481481
InvalidUTF8Encoding,
482482

483-
// secp256k1 signature
484-
/// Invalid secp256k1 signature provided in an expression.
485-
InvalidSecp65k1Signature,
486-
487483
/// Attempt to write to contract state in a read-only function.
488484
WriteAttemptedInReadOnly,
489485
/// `at-block` closure must be read-only but contains write operations.
@@ -814,7 +810,6 @@ impl DiagnosableError for CheckErrorKind {
814810
CheckErrorKind::TraitTooManyMethods(found, allowed) => format!("too many trait methods specified: found {found}, the maximum is {allowed}"),
815811
CheckErrorKind::InvalidCharactersDetected => "invalid characters detected".into(),
816812
CheckErrorKind::InvalidUTF8Encoding => "invalid UTF8 encoding".into(),
817-
CheckErrorKind::InvalidSecp65k1Signature => "invalid seckp256k1 signature".into(),
818813
CheckErrorKind::TypeAlreadyAnnotatedFailure | CheckErrorKind::CheckerImplementationFailure => {
819814
"internal error - please file an issue on https://github.com/stacks-network/stacks-blockchain".into()
820815
},

clarity-types/src/tests/types/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,3 +585,46 @@ fn test_utf8_data_len_returns_vm_internal_error() {
585585
err
586586
);
587587
}
588+
589+
#[test]
590+
fn invalid_utf8_encoding_from_oob_unicode_escape() {
591+
// This is a syntactically valid escape: \u{HEX}
592+
// BUT 110000 > 10FFFF (max Unicode scalar)
593+
// So oob Unicode → char::from_u32(None) → InvalidUTF8Encoding
594+
let bad_utf8_literal = "\\u{110000}".to_string();
595+
596+
let err = Value::string_utf8_from_string_utf8_literal(bad_utf8_literal).unwrap_err();
597+
assert!(matches!(
598+
err,
599+
VmExecutionError::Unchecked(CheckErrorKind::InvalidUTF8Encoding)
600+
));
601+
}
602+
603+
#[test]
604+
fn invalid_string_ascii_from_bytes() {
605+
// 0xFF is NOT:
606+
// - ASCII alphanumeric
607+
// - ASCII punctuation
608+
// - ASCII whitespace
609+
let bad_bytes = vec![0xFF];
610+
611+
let err = Value::string_ascii_from_bytes(bad_bytes).unwrap_err();
612+
613+
assert!(matches!(
614+
err,
615+
VmExecutionError::Unchecked(CheckErrorKind::InvalidCharactersDetected)
616+
));
617+
}
618+
619+
#[test]
620+
fn invalid_utf8_string_from_bytes() {
621+
// 0x80 is an invalid standalone UTF-8 continuation byte
622+
let bad_bytes = vec![0x80];
623+
624+
let err = Value::string_utf8_from_bytes(bad_bytes).unwrap_err();
625+
626+
assert!(matches!(
627+
err,
628+
VmExecutionError::Unchecked(CheckErrorKind::InvalidCharactersDetected)
629+
));
630+
}

clarity-types/src/types/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,8 @@ impl Value {
10521052
.ok_or_else(|| VmInternalError::Expect("Expected capture".into()))?;
10531053
let scalar_value = window[matched.start()..matched.end()].to_string();
10541054
let unicode_char = {
1055+
// This first InvalidUTF8Encoding is logically unreachable: the escape regex rejects non-hex digits,
1056+
// so from_str_radix only sees valid hex and never errors here.
10551057
let u = u32::from_str_radix(&scalar_value, 16)
10561058
.map_err(|_| CheckErrorKind::InvalidUTF8Encoding)?;
10571059
let c = char::from_u32(u).ok_or_else(|| CheckErrorKind::InvalidUTF8Encoding)?;

clarity/src/vm/contexts.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2087,12 +2087,16 @@ impl CallStack {
20872087

20882088
#[cfg(test)]
20892089
mod test {
2090+
use stacks_common::consts::CHAIN_ID_TESTNET;
20902091
use stacks_common::types::chainstate::StacksAddress;
20912092
use stacks_common::util::hash::Hash160;
20922093

20932094
use super::*;
20942095
use crate::vm::callables::DefineType;
2095-
use crate::vm::tests::{test_epochs, tl_env_factory, TopLevelMemoryEnvironmentGenerator};
2096+
use crate::vm::database::MemoryBackingStore;
2097+
use crate::vm::tests::{
2098+
test_clarity_versions, test_epochs, tl_env_factory, TopLevelMemoryEnvironmentGenerator,
2099+
};
20962100
use crate::vm::types::signatures::CallableSubtype;
20972101
use crate::vm::types::StandardPrincipalData;
20982102

@@ -2432,4 +2436,54 @@ mod test {
24322436
))
24332437
));
24342438
}
2439+
2440+
#[apply(test_clarity_versions)]
2441+
fn vm_initialize_contract_already_exists(
2442+
#[case] version: ClarityVersion,
2443+
#[case] epoch: StacksEpochId,
2444+
) {
2445+
// --- Setup VM ---
2446+
let mut marf = MemoryBackingStore::new();
2447+
let mut global_context = GlobalContext::new(
2448+
false,
2449+
CHAIN_ID_TESTNET,
2450+
marf.as_clarity_db(),
2451+
LimitedCostTracker::new_free(),
2452+
StacksEpochId::Epoch21, // any modern epoch
2453+
);
2454+
2455+
let mut call_stack = CallStack::new();
2456+
2457+
let contract_context =
2458+
ContractContext::new(QualifiedContractIdentifier::transient(), version);
2459+
2460+
let mut env = Environment::new(
2461+
&mut global_context,
2462+
&contract_context,
2463+
&mut call_stack,
2464+
None,
2465+
None,
2466+
None,
2467+
);
2468+
2469+
let contract_id = QualifiedContractIdentifier::local("dup").unwrap();
2470+
2471+
let contract_src = "(define-public (ping) (ok u1))";
2472+
2473+
let ast = ast::build_ast(&contract_id, contract_src, &mut env, version, epoch).unwrap();
2474+
2475+
// First initialization succeeds
2476+
env.initialize_contract_from_ast(contract_id.clone(), version, &ast, contract_src)
2477+
.unwrap();
2478+
2479+
// Second initialization hits ContractAlreadyExists
2480+
let err = env
2481+
.initialize_contract_from_ast(contract_id.clone(), version, &ast, contract_src)
2482+
.unwrap_err();
2483+
2484+
assert!(matches!(
2485+
err,
2486+
VmExecutionError::Unchecked(CheckErrorKind::ContractAlreadyExists(_))
2487+
));
2488+
}
24352489
}

clarity/src/vm/functions/crypto.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,14 @@ pub fn special_secp256k1_recover(
193193
}
194194
};
195195

196-
match secp256k1_recover(message, signature)
197-
.map_err(|_| CheckErrorKind::InvalidSecp65k1Signature)
198-
{
199-
Ok(pubkey) => Ok(Value::okay(
200-
Value::buff_from(pubkey.to_vec())
201-
.map_err(|_| VmInternalError::Expect("Failed to construct buff".into()))?,
202-
)
203-
.map_err(|_| VmInternalError::Expect("Failed to construct ok".into()))?),
204-
_ => Ok(Value::err_uint(1)),
205-
}
196+
let Ok(pubkey) = secp256k1_recover(message, signature) else {
197+
// We do not return the runtime error. Immediately map this to an error code.
198+
return Ok(Value::err_uint(1));
199+
};
200+
let pubkey_buff = Value::buff_from(pubkey.to_vec())
201+
.map_err(|_| VmInternalError::Expect("Failed to construct buff".into()))?;
202+
Ok(Value::okay(pubkey_buff)
203+
.map_err(|_| VmInternalError::Expect("Failed to construct ok".into()))?)
206204
}
207205

208206
pub fn special_secp256k1_verify(

clarity/src/vm/functions/define.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,129 @@ pub fn evaluate_define(
460460
Ok(DefineResult::NoDefine)
461461
}
462462
}
463+
464+
#[cfg(test)]
465+
mod test {
466+
use clarity_types::errors::CheckErrorKind;
467+
use clarity_types::representations::SymbolicExpression;
468+
use clarity_types::types::QualifiedContractIdentifier;
469+
use clarity_types::{Value, VmExecutionError};
470+
use stacks_common::consts::CHAIN_ID_TESTNET;
471+
use stacks_common::types::StacksEpochId;
472+
473+
use crate::vm::analysis::type_checker::v2_1::MAX_FUNCTION_PARAMETERS;
474+
use crate::vm::callables::DefineType;
475+
use crate::vm::contexts::GlobalContext;
476+
use crate::vm::costs::LimitedCostTracker;
477+
use crate::vm::database::MemoryBackingStore;
478+
use crate::vm::functions::define::{handle_define_function, handle_define_trait};
479+
use crate::vm::tests::test_clarity_versions;
480+
use crate::vm::{CallStack, ClarityVersion, ContractContext, Environment, LocalContext};
481+
482+
#[apply(test_clarity_versions)]
483+
fn bad_syntax_binding_define_function(
484+
#[case] version: ClarityVersion,
485+
#[case] epoch: StacksEpochId,
486+
) {
487+
// ---- BAD SIGNATURE ----
488+
// Instead of ((x uint)), we pass (x)
489+
let bad_signature = vec![
490+
SymbolicExpression::atom("f".into()),
491+
SymbolicExpression::atom("x".into()), // NOT a (name type) list
492+
];
493+
494+
let body = SymbolicExpression::atom_value(Value::UInt(1));
495+
496+
let mut marf = MemoryBackingStore::new();
497+
let mut global_context = GlobalContext::new(
498+
false,
499+
CHAIN_ID_TESTNET,
500+
marf.as_clarity_db(),
501+
LimitedCostTracker::new_free(),
502+
epoch,
503+
);
504+
505+
let contract_context =
506+
ContractContext::new(QualifiedContractIdentifier::transient(), version);
507+
508+
let context = LocalContext::new();
509+
let mut call_stack = CallStack::new();
510+
511+
let mut env = Environment::new(
512+
&mut global_context,
513+
&contract_context,
514+
&mut call_stack,
515+
None,
516+
None,
517+
None,
518+
);
519+
520+
let result = handle_define_function(&bad_signature, &body, &mut env, DefineType::Public);
521+
522+
assert!(matches!(
523+
result,
524+
Err(VmExecutionError::Unchecked(
525+
CheckErrorKind::BadSyntaxBinding(_)
526+
))
527+
));
528+
}
529+
530+
#[apply(test_clarity_versions)]
531+
fn handle_define_trait_too_many_function_parameters(
532+
#[case] version: ClarityVersion,
533+
#[case] epoch: StacksEpochId,
534+
) {
535+
if epoch < StacksEpochId::Epoch33 {
536+
return;
537+
}
538+
// Build a trait method with MORE than MAX_FUNCTION_PARAMETERS arguments
539+
// (f (uint uint uint ... ) (response uint uint))
540+
let too_many_args =
541+
vec![SymbolicExpression::atom("uint".into()); MAX_FUNCTION_PARAMETERS + 1];
542+
543+
let method = SymbolicExpression::list(vec![
544+
SymbolicExpression::atom("f".into()),
545+
SymbolicExpression::list(too_many_args),
546+
SymbolicExpression::list(vec![
547+
SymbolicExpression::atom("response".into()),
548+
SymbolicExpression::atom("uint".into()),
549+
SymbolicExpression::atom("uint".into()),
550+
]),
551+
]);
552+
553+
// This is the `( (f (...) (response ...)) )` wrapper
554+
let trait_body = vec![SymbolicExpression::list(vec![method])];
555+
556+
let mut marf = MemoryBackingStore::new();
557+
let mut global_context = GlobalContext::new(
558+
false,
559+
CHAIN_ID_TESTNET,
560+
marf.as_clarity_db(),
561+
LimitedCostTracker::new_free(),
562+
epoch,
563+
);
564+
565+
let contract_context =
566+
ContractContext::new(QualifiedContractIdentifier::transient(), version);
567+
568+
let mut call_stack = CallStack::new();
569+
570+
let mut env = Environment::new(
571+
&mut global_context,
572+
&contract_context,
573+
&mut call_stack,
574+
None,
575+
None,
576+
None,
577+
);
578+
579+
let result = handle_define_trait(&"bad-trait".into(), &trait_body, &mut env);
580+
581+
assert!(matches!(
582+
result,
583+
Err(VmExecutionError::Unchecked(
584+
CheckErrorKind::TooManyFunctionParameters(found, max)
585+
))
586+
));
587+
}
588+
}

0 commit comments

Comments
 (0)