Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
- Creates epoch 3.3 and costs-4 in preparation for a hardfork to activate Clarity 4
- Adds support for new Clarity 4 builtins (not activated until epoch 3.3):
- `contract-hash?`
- `to-ascii?`
- Added `contract_cost_limit_percentage` to the miner config file — sets the percentage of a block’s execution cost at which, if a large non-boot contract call would cause a BlockTooBigError, the miner will stop adding further non-boot contract calls and only include STX transfers and boot contract calls for the remainder of the block.

### Changed
Expand Down
24 changes: 24 additions & 0 deletions clarity-types/src/types/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ use self::TypeSignature::{
ResponseType, SequenceType, TraitReferenceType, TupleType, UIntType,
};

/// Maximum string length returned from `to-ascii?`.
/// 5 bytes reserved for embedding in response.
const MAX_TO_ASCII_RESULT_LEN: u32 = MAX_VALUE_SIZE - 5;

/// Maximum buffer length returned from `to-ascii?`.
/// 2 bytes reserved for "0x" prefix and 2 characters per byte.
pub const MAX_TO_ASCII_BUFFER_LEN: u32 = (MAX_TO_ASCII_RESULT_LEN - 2) / 2;

lazy_static! {
pub static ref BUFF_64: TypeSignature = {
#[allow(clippy::expect_used)]
Expand Down Expand Up @@ -234,6 +242,22 @@ lazy_static! {
BufferLength::try_from(16u32).expect("BUG: Legal Clarity buffer length marked invalid"),
))
};
/// Maximum-sized buffer allowed for `to-ascii?` call.
pub static ref TO_ASCII_MAX_BUFF: TypeSignature = {
#[allow(clippy::expect_used)]
SequenceType(SequenceSubtype::BufferType(
BufferLength::try_from(MAX_TO_ASCII_BUFFER_LEN)
.expect("BUG: Legal Clarity buffer length marked invalid"),
))
};
/// Maximum-length string returned from `to-ascii?`
pub static ref TO_ASCII_RESPONSE_STRING: TypeSignature = {
#[allow(clippy::expect_used)]
SequenceType(SequenceSubtype::StringType(
StringSubtype::ASCII(BufferLength::try_from(MAX_TO_ASCII_RESULT_LEN)
.expect("BUG: Legal Clarity buffer length marked invalid")),
))
};
}

pub const ASCII_40: TypeSignature = SequenceType(SequenceSubtype::StringType(
Expand Down
2 changes: 1 addition & 1 deletion clarity/src/vm/analysis/arithmetic_checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ impl ArithmeticOnlyChecker<'_> {
IsStandard | PrincipalDestruct | PrincipalConstruct => {
Err(Error::FunctionNotPermitted(function))
}
IntToAscii | IntToUtf8 | StringToInt | StringToUInt => {
IntToAscii | IntToUtf8 | StringToInt | StringToUInt | ToAscii => {
Err(Error::FunctionNotPermitted(function))
}
Sha512 | Sha512Trunc256 | Secp256k1Recover | Secp256k1Verify | Hash160 | Sha256
Expand Down
2 changes: 1 addition & 1 deletion clarity/src/vm/analysis/read_only_checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> {
| AsContract | Begin | FetchVar | GetStxBalance | StxGetAccount | GetTokenBalance
| GetAssetOwner | GetTokenSupply | ElementAt | IndexOf | Slice | ReplaceAt
| BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift | BitwiseRShift | BitwiseXor2
| ElementAtAlias | IndexOfAlias | ContractHash => {
| ElementAtAlias | IndexOfAlias | ContractHash | ToAscii => {
// Check all arguments.
self.check_each_expression_is_read_only(args)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ impl TypedNativeFunction {
| StringToUInt | IntToAscii | IntToUtf8 | GetBurnBlockInfo | StxTransferMemo
| StxGetAccount | BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift
| BitwiseRShift | BitwiseXor2 | Slice | ToConsensusBuff | FromConsensusBuff
| ReplaceAt | GetStacksBlockInfo | GetTenureInfo | ContractHash => {
| ReplaceAt | GetStacksBlockInfo | GetTenureInfo | ContractHash | ToAscii => {
return Err(CheckErrors::Expects(
"Clarity 2+ keywords should not show up in 2.05".into(),
));
Expand Down
19 changes: 18 additions & 1 deletion clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::vm::diagnostic::DiagnosableError;
use crate::vm::functions::{handle_binding_list, NativeFunctions};
use crate::vm::types::signatures::{
CallableSubtype, FunctionArgSignature, FunctionReturnsSignature, SequenceSubtype, ASCII_40,
UTF8_40,
TO_ASCII_MAX_BUFF, TO_ASCII_RESPONSE_STRING, UTF8_40,
};
use crate::vm::types::{
BlockInfoProperty, BufferLength, BurnBlockInfoProperty, FixedFunction, FunctionArg,
Expand Down Expand Up @@ -1183,6 +1183,23 @@ impl TypedNativeFunction {
returns: TypeSignature::new_response(BUFF_32.clone(), TypeSignature::UIntType)
.map_err(|_| CheckErrors::Expects("Bad constructor".into()))?,
}))),
ToAscii => Simple(SimpleNativeFunction(FunctionType::UnionArgs(
vec![
TypeSignature::IntType,
TypeSignature::UIntType,
TypeSignature::BoolType,
TypeSignature::PrincipalType,
TO_ASCII_MAX_BUFF.clone(),
TypeSignature::max_string_utf8()?,
],
TypeSignature::new_response(
TO_ASCII_RESPONSE_STRING.clone(),
TypeSignature::UIntType,
)
.map_err(|_| {
CheckErrors::Expects("FATAL: Legal Clarity response type marked invalid".into())
})?,
))),
};

Ok(out)
Expand Down
131 changes: 130 additions & 1 deletion clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::vm::ast::parse;
use crate::vm::costs::LimitedCostTracker;
use crate::vm::database::MemoryBackingStore;
use crate::vm::tests::test_clarity_versions;
use crate::vm::types::signatures::CallableSubtype;
use crate::vm::types::signatures::{CallableSubtype, TO_ASCII_MAX_BUFF, TO_ASCII_RESPONSE_STRING};
use crate::vm::types::{
BufferLength, ListTypeData, QualifiedContractIdentifier, SequenceSubtype, StringSubtype,
StringUTF8Length, TypeSignature,
Expand Down Expand Up @@ -3678,3 +3678,132 @@ fn test_contract_hash(#[case] version: ClarityVersion, #[case] epoch: StacksEpoc
assert_eq!(&actual, expected, "Failed for test case: {description}");
}
}

/// Pass various types to `to-ascii?`
#[apply(test_clarity_versions)]
fn test_to_ascii(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) {
let to_ascii_response_type = Some(
TypeSignature::new_response(TO_ASCII_RESPONSE_STRING.clone(), TypeSignature::UIntType)
.unwrap(),
);
let to_ascii_expected_types = vec![
TypeSignature::IntType,
TypeSignature::UIntType,
TypeSignature::BoolType,
TypeSignature::PrincipalType,
TO_ASCII_MAX_BUFF.clone(),
TypeSignature::max_string_utf8().unwrap(),
];
let test_cases = [
(
"(to-ascii? 123)",
"int type",
Ok(to_ascii_response_type.clone()),
),
(
"(to-ascii? u123)",
"uint type",
Ok(to_ascii_response_type.clone()),
),
(
"(to-ascii? true)",
"bool type",
Ok(to_ascii_response_type.clone()),
),
(
"(to-ascii? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)",
"standard principal",
Ok(to_ascii_response_type.clone()),
),
(
"(to-ascii? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.foo)",
"contract principal",
Ok(to_ascii_response_type.clone()),
),
(
"(to-ascii? 0x1234)",
"buffer type",
Ok(to_ascii_response_type.clone()),
),
(
&format!("(to-ascii? 0x{})", "ff".repeat(524285)),
"oversized buffer type",
Err(CheckErrors::UnionTypeError(
to_ascii_expected_types.clone(),
TypeSignature::SequenceType(SequenceSubtype::BufferType(
BufferLength::try_from(524285u32).unwrap(),
)),
)),
),
(
"(to-ascii? u\"I am serious, and don't call me Shirley.\")",
"utf8 string",
Ok(to_ascii_response_type),
),
(
"(to-ascii? \"60 percent of the time, it works every time\")",
"ascii string",
Err(CheckErrors::UnionTypeError(
to_ascii_expected_types.clone(),
TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII(
BufferLength::try_from(43u32).unwrap(),
))),
)),
),
(
"(to-ascii? (list 1 2 3))",
"list type",
Err(CheckErrors::UnionTypeError(
to_ascii_expected_types.clone(),
TypeSignature::SequenceType(SequenceSubtype::ListType(
ListTypeData::new_list(TypeSignature::IntType, 3).unwrap(),
)),
)),
),
(
"(to-ascii? { a: 1, b: u2 })",
"tuple type",
Err(CheckErrors::UnionTypeError(
to_ascii_expected_types.clone(),
TypeSignature::TupleType(
vec![
(ClarityName::from("a"), TypeSignature::IntType),
(ClarityName::from("b"), TypeSignature::UIntType),
]
.try_into()
.unwrap(),
),
)),
),
(
"(to-ascii? (some u789))",
"optional type",
Err(CheckErrors::UnionTypeError(
to_ascii_expected_types.clone(),
TypeSignature::new_option(TypeSignature::UIntType).unwrap(),
)),
),
(
"(to-ascii? (ok true))",
"response type",
Err(CheckErrors::UnionTypeError(
to_ascii_expected_types.clone(),
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::NoType)
.unwrap(),
)),
),
];

for (source, description, clarity4_expected) in test_cases.iter() {
let result = mem_run_analysis(source, version, epoch);
let actual = result.map(|(type_sig, _)| type_sig).map_err(|e| e.err);

let expected = if version >= ClarityVersion::Clarity4 {
clarity4_expected
} else {
&Err(CheckErrors::UnknownFunction("to-ascii?".to_string()))
};

assert_eq!(&actual, expected, "Failed for test case: {description}");
}
}
3 changes: 3 additions & 0 deletions clarity/src/vm/costs/cost_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ define_named_enum!(ClarityCostFunction {
BitwiseLShift("cost_bitwise_left_shift"),
BitwiseRShift("cost_bitwise_right_shift"),
ContractHash("cost_contract_hash"),
ToAscii("cost_to_ascii"),
Unimplemented("cost_unimplemented"),
});

Expand Down Expand Up @@ -328,6 +329,7 @@ pub trait CostValues {
fn cost_bitwise_left_shift(n: u64) -> InterpreterResult<ExecutionCost>;
fn cost_bitwise_right_shift(n: u64) -> InterpreterResult<ExecutionCost>;
fn cost_contract_hash(n: u64) -> InterpreterResult<ExecutionCost>;
fn cost_to_ascii(n: u64) -> InterpreterResult<ExecutionCost>;
}

impl ClarityCostFunction {
Expand Down Expand Up @@ -481,6 +483,7 @@ impl ClarityCostFunction {
ClarityCostFunction::BitwiseLShift => C::cost_bitwise_left_shift(n),
ClarityCostFunction::BitwiseRShift => C::cost_bitwise_right_shift(n),
ClarityCostFunction::ContractHash => C::cost_contract_hash(n),
ClarityCostFunction::ToAscii => C::cost_to_ascii(n),
ClarityCostFunction::Unimplemented => Err(RuntimeErrorType::NotImplemented.into()),
}
}
Expand Down
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,4 +749,8 @@ impl CostValues for Costs1 {
fn cost_contract_hash(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}

fn cost_to_ascii(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}
}
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,4 +749,8 @@ impl CostValues for Costs2 {
fn cost_contract_hash(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}

fn cost_to_ascii(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}
}
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_2_testnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,4 +749,8 @@ impl CostValues for Costs2Testnet {
fn cost_contract_hash(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}

fn cost_to_ascii(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}
}
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,4 +767,8 @@ impl CostValues for Costs3 {
fn cost_contract_hash(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}

fn cost_to_ascii(n: u64) -> InterpreterResult<ExecutionCost> {
Err(RuntimeErrorType::NotImplemented.into())
}
}
6 changes: 6 additions & 0 deletions clarity/src/vm/costs/costs_4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use super::cost_functions::CostValues;
use super::costs_3::Costs3;
use super::ExecutionCost;
use crate::vm::costs::cost_functions::linear;
use crate::vm::errors::InterpreterResult;

pub struct Costs4;
Expand Down Expand Up @@ -455,4 +456,9 @@ impl CostValues for Costs4 {
read_length: 32,
})
}

fn cost_to_ascii(n: u64) -> InterpreterResult<ExecutionCost> {
// TODO: needs criterion benchmark
Ok(ExecutionCost::runtime(linear(n, 1, 100)))
}
}
20 changes: 20 additions & 0 deletions clarity/src/vm/docs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2528,6 +2528,25 @@ is not a contract or the specified contract does not exist. Returns:
"#,
};

const TO_ASCII: SpecialAPI = SpecialAPI {
input_type: "int|uint|bool|principal|(buff 524284)|(string-utf8 1048571)",
snippet: "to-ascii? ${1:value}",
output_type: "(response (string-ascii 1048571) uint)",
signature: "(to-ascii? value)",
description: "The `to-ascii?` function converts the input value to its ASCII representation.
If the input is a `string-utf8`, it will fail with `(err u1)` if the string contains non-ASCII
characters.",
example: r#"
(to-ascii? 123) ;; Returns (ok "123")
(to-ascii? u456) ;; Returns (ok "u456")
(to-ascii? false) ;; Returns (ok "false")
(to-ascii? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4) ;; Returns (ok "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4")
(to-ascii? 0x00112233) ;; Returns (ok "0x00112233")
(to-ascii? u"An ASCII smiley face: :)") ;; Returns (ok "An ASCII smiley face: :)")
(to-ascii? u"A smiley face emoji: \u{1F600}") ;; Returns (err u1)
"#,
};

pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI {
use crate::vm::functions::NativeFunctions::*;
let name = function.get_name();
Expand Down Expand Up @@ -2641,6 +2660,7 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI {
BitwiseLShift => make_for_simple_native(&BITWISE_LEFT_SHIFT_API, function, name),
BitwiseRShift => make_for_simple_native(&BITWISE_RIGHT_SHIFT_API, function, name),
ContractHash => make_for_simple_native(&CONTRACT_HASH, function, name),
ToAscii => make_for_special(&TO_ASCII, function),
}
}

Expand Down
Loading