Skip to content

Commit 542346f

Browse files
Merge branch 'develop' of https://github.com/stacks-network/stacks-core into chore/add-custom-error-to-clarity-types
2 parents e211cee + fdf1041 commit 542346f

File tree

40 files changed

+13904
-64
lines changed

40 files changed

+13904
-64
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"args": [
2828
"build",
2929
"--bin=clarity-cli",
30-
"--package=stackslib"
30+
"--package=clarity-cli"
3131
],
3232
"filter": {
3333
"name": "clarity-cli",

Cargo.lock

Lines changed: 61 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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
@@ -194,16 +194,14 @@ pub fn special_secp256k1_recover(
194194
}
195195
};
196196

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

209207
pub fn special_secp256k1_verify(

0 commit comments

Comments
 (0)