diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs
index 955ce1f7ac..42a5d1d60b 100644
--- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs
+++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs
@@ -181,7 +181,7 @@ impl<'a> ArithmeticOnlyChecker<'a> {
| FetchEntry | SetEntry | DeleteEntry | InsertEntry | SetVar | MintAsset
| MintToken | TransferAsset | TransferToken | ContractCall | StxTransfer
| StxTransferMemo | StxBurn | AtBlock | GetStxBalance | GetTokenSupply | BurnToken
- | BurnAsset | StxGetAccount => {
+ | FromConsensusBuff | ToConsensusBuff | BurnAsset | StxGetAccount => {
return Err(Error::FunctionNotPermitted(function));
}
Append | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | Print
diff --git a/clarity/src/vm/analysis/errors.rs b/clarity/src/vm/analysis/errors.rs
index bb1837c61e..a23a43abcf 100644
--- a/clarity/src/vm/analysis/errors.rs
+++ b/clarity/src/vm/analysis/errors.rs
@@ -68,6 +68,7 @@ pub enum CheckErrors {
ExpectedOptionalOrResponseValue(Value),
CouldNotDetermineResponseOkType,
CouldNotDetermineResponseErrType,
+ CouldNotDetermineSerializationType,
UncheckedIntermediaryResponses,
CouldNotDetermineMatchTypes,
@@ -425,6 +426,7 @@ impl DiagnosableError for CheckErrors {
},
CheckErrors::UncheckedIntermediaryResponses => format!("intermediary responses in consecutive statements must be checked"),
CheckErrors::CostComputationFailed(s) => format!("contract cost computation failed: {}", s),
+ CheckErrors::CouldNotDetermineSerializationType => format!("could not determine the input type for the serialization function"),
}
}
diff --git a/clarity/src/vm/analysis/read_only_checker/mod.rs b/clarity/src/vm/analysis/read_only_checker/mod.rs
index d4576847ae..947a5e2890 100644
--- a/clarity/src/vm/analysis/read_only_checker/mod.rs
+++ b/clarity/src/vm/analysis/read_only_checker/mod.rs
@@ -282,13 +282,19 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> {
| UnwrapErrRet | IsOkay | IsNone | Asserts | Unwrap | UnwrapErr | Match | IsErr
| IsSome | TryRet | ToUInt | ToInt | BuffToIntLe | BuffToUIntLe | BuffToIntBe
| BuffToUIntBe | IntToAscii | IntToUtf8 | StringToInt | StringToUInt | IsStandard
- | PrincipalDestruct | PrincipalConstruct | Append | Concat | AsMaxLen | ContractOf
- | PrincipalOf | ListCons | GetBlockInfo | GetBurnBlockInfo | TupleGet | TupleMerge
- | Len | Print | AsContract | Begin | FetchVar | GetStxBalance | StxGetAccount
- | GetTokenBalance | GetAssetOwner | GetTokenSupply | ElementAt | IndexOf | Slice => {
+ | ToConsensusBuff | PrincipalDestruct | PrincipalConstruct | Append | Concat
+ | AsMaxLen | ContractOf | PrincipalOf | ListCons | GetBlockInfo | GetBurnBlockInfo
+ | TupleGet | TupleMerge | Len | Print | AsContract | Begin | FetchVar
+ | GetStxBalance | StxGetAccount | GetTokenBalance | GetAssetOwner | GetTokenSupply
+ | ElementAt | IndexOf | Slice => {
// Check all arguments.
self.check_each_expression_is_read_only(args)
}
+ FromConsensusBuff => {
+ // Check only the second+ arguments: the first argument is a type parameter
+ check_argument_count(2, args)?;
+ self.check_each_expression_is_read_only(&args[1..])
+ }
AtBlock => {
check_argument_count(2, args)?;
diff --git a/clarity/src/vm/analysis/type_checker/natives/conversions.rs b/clarity/src/vm/analysis/type_checker/natives/conversions.rs
new file mode 100644
index 0000000000..780e4ca625
--- /dev/null
+++ b/clarity/src/vm/analysis/type_checker/natives/conversions.rs
@@ -0,0 +1,39 @@
+use crate::vm::analysis::read_only_checker::check_argument_count;
+use crate::vm::analysis::type_checker::contexts::TypingContext;
+use crate::vm::analysis::type_checker::{TypeChecker, TypeResult};
+use crate::vm::analysis::CheckError;
+use crate::vm::types::{BufferLength, SequenceSubtype, TypeSignature};
+use crate::vm::SymbolicExpression;
+
+/// to-consensus-buff admits exactly one argument:
+/// * the Clarity value to serialize
+/// it returns an `(optional (buff x))` where `x` is the maximum possible
+/// consensus buffer length based on the inferred type of the supplied value.
+pub fn check_special_to_consensus_buff(
+ checker: &mut TypeChecker,
+ args: &[SymbolicExpression],
+ context: &TypingContext,
+) -> TypeResult {
+ check_argument_count(1, args)?;
+ let input_type = checker.type_check(&args[0], context)?;
+ let buffer_max_len = BufferLength::try_from(input_type.max_serialized_size()?)?;
+ TypeSignature::new_option(TypeSignature::SequenceType(SequenceSubtype::BufferType(
+ buffer_max_len,
+ )))
+ .map_err(CheckError::from)
+}
+
+/// from-consensus-buff admits exactly two arguments:
+/// * a type signature indicating the expected return type `t1`
+/// * a buffer (of up to max length)
+/// it returns an `(optional t1)`
+pub fn check_special_from_consensus_buff(
+ checker: &mut TypeChecker,
+ args: &[SymbolicExpression],
+ context: &TypingContext,
+) -> TypeResult {
+ check_argument_count(2, args)?;
+ let result_type = TypeSignature::parse_type_repr(&args[0], checker)?;
+ checker.type_check_expects(&args[1], context, &TypeSignature::max_buffer())?;
+ TypeSignature::new_option(result_type).map_err(CheckError::from)
+}
diff --git a/clarity/src/vm/analysis/type_checker/natives/mod.rs b/clarity/src/vm/analysis/type_checker/natives/mod.rs
index 9b7e8dc07d..01a7c02e8d 100644
--- a/clarity/src/vm/analysis/type_checker/natives/mod.rs
+++ b/clarity/src/vm/analysis/type_checker/natives/mod.rs
@@ -39,6 +39,7 @@ use crate::vm::costs::{
};
mod assets;
+mod conversions;
mod maps;
mod options;
mod sequences;
@@ -882,6 +883,12 @@ impl TypedNativeFunction {
IsNone => Special(SpecialNativeFunction(&options::check_special_is_optional)),
IsSome => Special(SpecialNativeFunction(&options::check_special_is_optional)),
AtBlock => Special(SpecialNativeFunction(&check_special_at_block)),
+ ToConsensusBuff => Special(SpecialNativeFunction(
+ &conversions::check_special_to_consensus_buff,
+ )),
+ FromConsensusBuff => Special(SpecialNativeFunction(
+ &conversions::check_special_from_consensus_buff,
+ )),
}
}
}
diff --git a/clarity/src/vm/analysis/type_checker/tests/mod.rs b/clarity/src/vm/analysis/type_checker/tests/mod.rs
index 1c4a32db3b..367e42d0f8 100644
--- a/clarity/src/vm/analysis/type_checker/tests/mod.rs
+++ b/clarity/src/vm/analysis/type_checker/tests/mod.rs
@@ -27,6 +27,7 @@ use crate::vm::analysis::AnalysisDatabase;
use crate::vm::ast::errors::ParseErrors;
use crate::vm::ast::{build_ast, parse};
use crate::vm::contexts::OwnedEnvironment;
+use crate::vm::execute_v2;
use crate::vm::representations::SymbolicExpression;
use crate::vm::types::{
BufferLength, FixedFunction, FunctionType, PrincipalData, QualifiedContractIdentifier,
@@ -74,6 +75,145 @@ fn ascii_type(size: u32) -> TypeSignature {
TypeSignature::SequenceType(StringType(ASCII(size.try_into().unwrap()))).into()
}
+#[test]
+fn test_from_consensus_buff() {
+ let good = [
+ ("(from-consensus-buff int 0x00)", "(optional int)"),
+ (
+ "(from-consensus-buff { a: uint, b: principal } 0x00)",
+ "(optional (tuple (a uint) (b principal)))",
+ ),
+ ];
+
+ let bad = [
+ (
+ "(from-consensus-buff)",
+ CheckErrors::IncorrectArgumentCount(2, 0),
+ ),
+ (
+ "(from-consensus-buff 0x00 0x00 0x00)",
+ CheckErrors::IncorrectArgumentCount(2, 3),
+ ),
+ (
+ "(from-consensus-buff 0x00 0x00)",
+ CheckErrors::InvalidTypeDescription,
+ ),
+ (
+ "(from-consensus-buff int u6)",
+ CheckErrors::TypeError(TypeSignature::max_buffer(), TypeSignature::UIntType),
+ ),
+ (
+ "(from-consensus-buff (buff 1048576) 0x00)",
+ CheckErrors::ValueTooLarge,
+ ),
+ ];
+
+ for (good_test, expected) in good.iter() {
+ let type_result = type_check_helper(good_test).unwrap();
+ assert_eq!(expected, &type_result.to_string());
+
+ assert!(
+ type_result.admits(&execute_v2(good_test).unwrap().unwrap()),
+ "The analyzed type must admit the evaluated type"
+ );
+ }
+
+ for (bad_test, expected) in bad.iter() {
+ assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err);
+ }
+}
+
+#[test]
+fn test_to_consensus_buff() {
+ let good = [
+ (
+ "(to-consensus-buff (if true (some u1) (some u2)))",
+ "(optional (buff 18))",
+ ),
+ (
+ "(to-consensus-buff (if true (ok u1) (ok u2)))",
+ "(optional (buff 18))",
+ ),
+ (
+ "(to-consensus-buff (if true (ok 1) (err u2)))",
+ "(optional (buff 18))",
+ ),
+ (
+ "(to-consensus-buff (if true (ok 1) (err true)))",
+ "(optional (buff 18))",
+ ),
+ (
+ "(to-consensus-buff (if true (ok false) (err true)))",
+ "(optional (buff 2))",
+ ),
+ (
+ "(to-consensus-buff (if true (err u1) (err u2)))",
+ "(optional (buff 18))",
+ ),
+ ("(to-consensus-buff none)", "(optional (buff 1))"),
+ ("(to-consensus-buff 0x00)", "(optional (buff 6))"),
+ ("(to-consensus-buff \"a\")", "(optional (buff 6))"),
+ ("(to-consensus-buff u\"ab\")", "(optional (buff 13))"),
+ ("(to-consensus-buff 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)", "(optional (buff 151))"),
+ ("(to-consensus-buff 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde)", "(optional (buff 151))"),
+ ("(to-consensus-buff true)", "(optional (buff 1))"),
+ ("(to-consensus-buff -1)", "(optional (buff 17))"),
+ ("(to-consensus-buff u1)", "(optional (buff 17))"),
+ ("(to-consensus-buff (list 1 2 3 4))", "(optional (buff 73))"),
+ (
+ "(to-consensus-buff { apple: u1, orange: 2, blue: true })",
+ "(optional (buff 58))",
+ ),
+ (
+ "(define-private (my-func (x (buff 1048566)))
+ (to-consensus-buff x))
+ (my-func 0x001122334455)
+ ",
+ "(optional (buff 1048571))",
+ ),
+ ];
+
+ let bad = [
+ (
+ "(to-consensus-buff)",
+ CheckErrors::IncorrectArgumentCount(1, 0),
+ ),
+ (
+ "(to-consensus-buff 0x00 0x00)",
+ CheckErrors::IncorrectArgumentCount(1, 2),
+ ),
+ (
+ "(define-private (my-func (x (buff 1048576)))
+ (to-consensus-buff x))",
+ CheckErrors::ValueTooLarge,
+ ),
+ (
+ "(define-private (my-func (x (buff 1048570)))
+ (to-consensus-buff x))",
+ CheckErrors::ValueTooLarge,
+ ),
+ (
+ "(define-private (my-func (x (buff 1048567)))
+ (to-consensus-buff x))",
+ CheckErrors::ValueTooLarge,
+ ),
+ ];
+
+ for (good_test, expected) in good.iter() {
+ let type_result = type_check_helper(good_test).unwrap();
+ assert_eq!(expected, &type_result.to_string());
+
+ assert!(
+ type_result.admits(&execute_v2(good_test).unwrap().unwrap()),
+ "The analyzed type must admit the evaluated type"
+ );
+ }
+
+ for (bad_test, expected) in bad.iter() {
+ assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err);
+ }
+}
+
#[test]
fn test_get_block_info() {
let good = [
diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs
index 8388ef5f58..33b35cf19c 100644
--- a/clarity/src/vm/docs/mod.rs
+++ b/clarity/src/vm/docs/mod.rs
@@ -1978,6 +1978,55 @@ one of the following error codes:
"
};
+const TO_CONSENSUS_BUFF: SpecialAPI = SpecialAPI {
+ input_type: "any",
+ output_type: "(optional buff)",
+ signature: "(to-consensus-buff value)",
+ description: "`to-consensus-buff` is a special function that will serialize any
+Clarity value into a buffer, using the SIP-005 serialization of the
+Clarity value. Not all values can be serialized: some value's
+consensus serialization is too large to fit in a Clarity buffer (this
+is because of the type prefix in the consensus serialization).
+
+If the value cannot fit as serialized into the maximum buffer size,
+this returns `none`, otherwise, it will be
+`(some consensus-serialized-buffer)`. During type checking, the
+analyzed type of the result of this method will be the maximum possible
+consensus buffer length based on the inferred type of the supplied value.
+",
+ example: r#"
+(to-consensus-buff 1) ;; Returns (some 0x0000000000000000000000000000000001)
+(to-consensus-buff u1) ;; Returns (some 0x0100000000000000000000000000000001)
+(to-consensus-buff true) ;; Returns (some 0x03)
+(to-consensus-buff false) ;; Returns (some 0x04)
+(to-consensus-buff none) ;; Returns (some 0x09)
+(to-consensus-buff 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns (some 0x051fa46ff88886c2ef9762d970b4d2c63678835bd39d)
+(to-consensus-buff { abc: 3, def: 4 }) ;; Returns (some 0x0c00000002036162630000000000000000000000000000000003036465660000000000000000000000000000000004)
+"#,
+};
+
+const FROM_CONSENSUS_BUFF: SpecialAPI = SpecialAPI {
+ input_type: "type-signature(t), buff",
+ output_type: "(optional t)",
+ signature: "(from-consensus-buff type-signature buffer)",
+ description: "`from-consensus-buff` is a special function that will deserialize a
+buffer into a Clarity value, using the SIP-005 serialization of the
+Clarity value. The type that `from-consensus-buff` tries to deserialize
+into is provided by the first parameter to the function. If it fails
+to deserialize the type, the method returns `none`.
+",
+ example: r#"
+(from-consensus-buff int 0x0000000000000000000000000000000001) ;; Returns (some 1)
+(from-consensus-buff uint 0x0000000000000000000000000000000001) ;; Returns none
+(from-consensus-buff uint 0x0100000000000000000000000000000001) ;; Returns (some u1)
+(from-consensus-buff bool 0x0000000000000000000000000000000001) ;; Returns none
+(from-consensus-buff bool 0x03) ;; Returns (some true)
+(from-consensus-buff bool 0x04) ;; Returns (some false)
+(from-consensus-buff principal 0x051fa46ff88886c2ef9762d970b4d2c63678835bd39d) ;; Returns (some SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)
+(from-consensus-buff { abc: int, def: int } 0x0c00000002036162630000000000000000000000000000000003036465660000000000000000000000000000000004) ;; Returns (some (tuple (abc 3) (def 4)))
+"#,
+};
+
fn make_api_reference(function: &NativeFunctions) -> FunctionAPI {
use crate::vm::functions::NativeFunctions::*;
let name = function.get_name();
@@ -2079,6 +2128,8 @@ fn make_api_reference(function: &NativeFunctions) -> FunctionAPI {
StxTransfer => make_for_special(&STX_TRANSFER, name),
StxTransferMemo => make_for_special(&STX_TRANSFER_MEMO, name),
StxBurn => make_for_simple_native(&STX_BURN, &StxBurn, name),
+ ToConsensusBuff => make_for_special(&TO_CONSENSUS_BUFF, name),
+ FromConsensusBuff => make_for_special(&FROM_CONSENSUS_BUFF, name),
}
}
diff --git a/clarity/src/vm/functions/conversions.rs b/clarity/src/vm/functions/conversions.rs
index 2f316b4294..73c8eef164 100644
--- a/clarity/src/vm/functions/conversions.rs
+++ b/clarity/src/vm/functions/conversions.rs
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+use stacks_common::codec::StacksMessageCodec;
+
use crate::vm::costs::cost_functions::ClarityCostFunction;
use crate::vm::costs::runtime_cost;
use crate::vm::errors::{check_argument_count, CheckErrors, InterpreterResult as Result};
@@ -209,3 +211,58 @@ pub fn native_int_to_utf8(value: Value) -> Result {
// Given a string representing an integer, convert this to Clarity UTF8 value.
native_int_to_string_generic(value, Value::string_utf8_from_bytes)
}
+
+/// Returns `value` consensus serialized into a `(optional buff)` object.
+/// If the value cannot fit as serialized into the maximum buffer size,
+/// this returns `none`, otherwise, it will be `(some consensus-serialized-buffer)`
+pub fn to_consensus_buff(value: Value) -> Result {
+ let clar_buff_serialized = match Value::buff_from(value.serialize_to_vec()) {
+ Ok(x) => x,
+ Err(_) => return Ok(Value::none()),
+ };
+
+ match Value::some(clar_buff_serialized) {
+ Ok(x) => Ok(x),
+ Err(_) => Ok(Value::none()),
+ }
+}
+
+/// Deserialize a Clarity value from a consensus serialized buffer.
+/// If the supplied buffer either fails to deserialize or deserializes
+/// to an unexpected type, returns `none`. Otherwise, it will be `(some value)`
+pub fn from_consensus_buff(
+ args: &[SymbolicExpression],
+ env: &mut Environment,
+ context: &LocalContext,
+) -> Result {
+ check_argument_count(2, args)?;
+
+ let type_arg = TypeSignature::parse_type_repr(&args[0], env)?;
+ let value = eval(&args[1], env, context)?;
+
+ // get the buffer bytes from the supplied value. if not passed a buffer,
+ // this is a type error
+ let input_bytes = if let Value::Sequence(SequenceData::Buffer(buff_data)) = value {
+ Ok(buff_data.data)
+ } else {
+ Err(CheckErrors::TypeValueError(
+ TypeSignature::max_buffer(),
+ value,
+ ))
+ }?;
+
+ runtime_cost(ClarityCostFunction::Unimplemented, env, input_bytes.len())?;
+
+ // Perform the deserialization and check that it deserialized to the expected
+ // type. A type mismatch at this point is an error that should be surfaced in
+ // Clarity (as a none return).
+ let result = match Value::try_deserialize_bytes_exact(&input_bytes, &type_arg) {
+ Ok(value) => value,
+ Err(_) => return Ok(Value::none()),
+ };
+ if !type_arg.admits(&result) {
+ return Ok(Value::none());
+ }
+
+ Value::some(result)
+}
diff --git a/clarity/src/vm/functions/mod.rs b/clarity/src/vm/functions/mod.rs
index 6742b88980..39c1107201 100644
--- a/clarity/src/vm/functions/mod.rs
+++ b/clarity/src/vm/functions/mod.rs
@@ -170,6 +170,8 @@ define_versioned_named_enum!(NativeFunctions(ClarityVersion) {
StxBurn("stx-burn?", ClarityVersion::Clarity1),
StxGetAccount("stx-account", ClarityVersion::Clarity2),
Slice("slice", ClarityVersion::Clarity2),
+ ToConsensusBuff("to-consensus-buff", ClarityVersion::Clarity2),
+ FromConsensusBuff("from-consensus-buff", ClarityVersion::Clarity2),
});
impl NativeFunctions {
@@ -505,6 +507,14 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option
),
StxBurn => SpecialFunction("special_stx_burn", &assets::special_stx_burn),
StxGetAccount => SpecialFunction("stx_get_account", &assets::special_stx_account),
+ ToConsensusBuff => NativeFunction(
+ "to_consensus_buff",
+ NativeHandle::SingleArg(&conversions::to_consensus_buff),
+ ClarityCostFunction::Unimplemented,
+ ),
+ FromConsensusBuff => {
+ SpecialFunction("from_consensus_buff", &conversions::from_consensus_buff)
+ }
};
Some(callable)
} else {
diff --git a/clarity/src/vm/tests/simple_apply_eval.rs b/clarity/src/vm/tests/simple_apply_eval.rs
index 3ecab6903a..690cc025fc 100644
--- a/clarity/src/vm/tests/simple_apply_eval.rs
+++ b/clarity/src/vm/tests/simple_apply_eval.rs
@@ -211,6 +211,206 @@ fn test_keccak256() {
.for_each(|(program, expectation)| assert_eq!(to_buffer(expectation), execute(program)));
}
+#[test]
+/// This test serializes two different values which do fit in
+/// the Clarity maximum value size, but whose serializations
+/// do not. These tests would _not_ pass typechecking: in fact,
+/// the code comes from `type_checker::tests::test_to_consensus_buff`
+/// failure cases.
+fn test_to_consensus_buff_too_big() {
+ let buff_setup = "
+ ;; Make a buffer with repeated concatenation.
+ (define-private (make-buff-10)
+ 0x11223344556677889900)
+ (define-private (octo-buff (x (buff 100000)))
+ (concat (concat (concat x x) (concat x x))
+ (concat (concat x x) (concat x x))))
+ (define-private (make-buff-80)
+ (unwrap-panic (as-max-len? (octo-buff (make-buff-10)) u80)))
+ (define-private (make-buff-640)
+ (unwrap-panic (as-max-len? (octo-buff (make-buff-80)) u640)))
+ (define-private (make-buff-5120)
+ (unwrap-panic (as-max-len? (octo-buff (make-buff-640)) u5120)))
+ (define-private (make-buff-40960)
+ (unwrap-panic (as-max-len? (octo-buff (make-buff-5120)) u40960)))
+ (define-private (make-buff-327680)
+ (unwrap-panic (as-max-len? (octo-buff (make-buff-40960)) u327680)))
+
+ (define-private (make-buff-24567)
+ (let ((x (make-buff-5120))
+ (y (make-buff-640))
+ (z (make-buff-80))
+ (a 0x11223344556677))
+ ;; 4x + 6y + 3z + a = 24567
+ (concat
+ (concat
+ ;; 4x
+ (concat (concat x x) (concat x x))
+ ;; 6y
+ (concat (concat (concat y y) (concat y y)) (concat y y)))
+ ;; 3z + a
+ (concat (concat z z) (concat z a)))))
+
+ ;; (3 * 327680) + 40960 + 24567 = 1048567
+ (define-private (make-buff-1048567)
+ (let ((x (make-buff-327680))
+ (y (make-buff-40960))
+ (z (make-buff-24567)))
+ (concat (concat (concat x x) (concat x y)) z)))
+
+ (define-private (make-buff-1048570)
+ (concat (make-buff-1048567) 0x112233))
+ ";
+
+ // this program prints the length of the
+ // constructed 1048570 buffer and then executes
+ // to-consensus-buff on it. if the buffer wasn't the
+ // expect length, just return (some buffer), which will
+ // cause the test assertion to fail.
+ let program_check_1048570 = format!(
+ "{}
+ (let ((a (make-buff-1048570)))
+ (if (is-eq (len a) u1048570)
+ (to-consensus-buff a)
+ (some 0x00)))
+ ",
+ buff_setup
+ );
+
+ let result = vm_execute_v2(&program_check_1048570)
+ .expect("Should execute")
+ .expect("Should have return value");
+
+ assert!(result.expect_optional().is_none());
+
+ // this program prints the length of the
+ // constructed 1048567 buffer and then executes
+ // to-consensus-buff on it. if the buffer wasn't the
+ // expect length, just return (some buffer), which will
+ // cause the test assertion to fail.
+ let program_check_1048567 = format!(
+ "{}
+ (let ((a (make-buff-1048567)))
+ (if (is-eq (len a) u1048567)
+ (to-consensus-buff a)
+ (some 0x00)))
+ ",
+ buff_setup
+ );
+
+ let result = vm_execute_v2(&program_check_1048567)
+ .expect("Should execute")
+ .expect("Should have return value");
+
+ assert!(result.expect_optional().is_none());
+}
+
+#[test]
+fn test_from_consensus_buff_type_checks() {
+ let vectors = [
+ (
+ "(from-consensus-buff uint 0x10 0x00)",
+ "Unchecked(IncorrectArgumentCount(2, 3))",
+ ),
+ (
+ "(from-consensus-buff uint 1)",
+ "Unchecked(TypeValueError(SequenceType(BufferType(BufferLength(1048576))), Int(1)))",
+ ),
+ (
+ "(from-consensus-buff 2 0x10)",
+ "Unchecked(InvalidTypeDescription)",
+ ),
+ ];
+
+ for (input, expected) in vectors.iter() {
+ let result = vm_execute_v2(input).expect_err("Should raise an error");
+ assert_eq!(&result.to_string(), expected);
+ }
+}
+
+#[test]
+/// This test tries a bunch of buffers which either
+/// do not parse, or parse to the incorrect type
+fn test_from_consensus_buff_missed_expectations() {
+ let vectors = [
+ ("0x0000000000000000000000000000000001", "uint"),
+ ("0x00ffffffffffffffffffffffffffffffff", "uint"),
+ ("0x0100000000000000000000000000000001", "int"),
+ ("0x010000000000000000000000000000000101", "uint"),
+ ("0x0200000004deadbeef", "(buff 2)"),
+ ("0x0200000004deadbeef", "(buff 3)"),
+ ("0x0200000004deadbeef", "(string-ascii 8)"),
+ ("0x03", "uint"),
+ ("0x04", "(optional int)"),
+ ("0x0700ffffffffffffffffffffffffffffffff", "(response uint int)"),
+ ("0x0800ffffffffffffffffffffffffffffffff", "(response int uint)"),
+ ("0x09", "(response int int)"),
+ ("0x0b0000000400000000000000000000000000000000010000000000000000000000000000000002000000000000000000000000000000000300fffffffffffffffffffffffffffffffc",
+ "(list 3 int)"),
+ ("0x0c000000020362617a0906666f6f62617203", "{ bat: (optional int), foobar: bool }"),
+ ("0xff", "int"),
+ ];
+
+ for (buff_repr, type_repr) in vectors.iter() {
+ let program = format!("(from-consensus-buff {} {})", type_repr, buff_repr);
+ eprintln!("{}", program);
+ let result_val = vm_execute_v2(&program)
+ .expect("from-consensus-buff should succeed")
+ .expect("from-consensus-buff should return")
+ .expect_optional();
+ assert!(
+ result_val.is_none(),
+ "from-consensus-buff should return none"
+ );
+ }
+}
+
+#[test]
+fn test_to_from_consensus_buff_vectors() {
+ let vectors = [
+ ("0x0000000000000000000000000000000001", "1", "int"),
+ ("0x00ffffffffffffffffffffffffffffffff", "-1", "int"),
+ ("0x0100000000000000000000000000000001", "u1", "uint"),
+ ("0x0200000004deadbeef", "0xdeadbeef", "(buff 8)"),
+ ("0x03", "true", "bool"),
+ ("0x04", "false", "bool"),
+ ("0x050011deadbeef11ababffff11deadbeef11ababffff", "'S08XXBDYXW8TQAZZZW8XXBDYXW8TQAZZZZ88551S", "principal"),
+ ("0x060011deadbeef11ababffff11deadbeef11ababffff0461626364", "'S08XXBDYXW8TQAZZZW8XXBDYXW8TQAZZZZ88551S.abcd", "principal"),
+ ("0x0700ffffffffffffffffffffffffffffffff", "(ok -1)", "(response int int)"),
+ ("0x0800ffffffffffffffffffffffffffffffff", "(err -1)", "(response int int)"),
+ ("0x09", "none", "(optional int)"),
+ ("0x0a00ffffffffffffffffffffffffffffffff", "(some -1)", "(optional int)"),
+ ("0x0b0000000400000000000000000000000000000000010000000000000000000000000000000002000000000000000000000000000000000300fffffffffffffffffffffffffffffffc",
+ "(list 1 2 3 -4)", "(list 4 int)"),
+ ("0x0c000000020362617a0906666f6f62617203", "{ baz: none, foobar: true }", "{ baz: (optional int), foobar: bool }"),
+ ];
+
+ // do `from-consensus-buff` tests
+ for (buff_repr, value_repr, type_repr) in vectors.iter() {
+ let program = format!("(from-consensus-buff {} {})", type_repr, buff_repr);
+ eprintln!("{}", program);
+ let result_val = vm_execute_v2(&program)
+ .expect("from-consensus-buff should succeed")
+ .expect("from-consensus-buff should return")
+ .expect_optional()
+ .expect("from-consensus-buff should return (some value)");
+ let expected_val = execute(&value_repr);
+ assert_eq!(result_val, expected_val);
+ }
+
+ // do `to-consensus-buff` tests
+ for (buff_repr, value_repr, _) in vectors.iter() {
+ let program = format!("(to-consensus-buff {})", value_repr);
+ let result_buffer = vm_execute_v2(&program)
+ .expect("to-consensus-buff should succeed")
+ .expect("to-consensus-buff should return")
+ .expect_optional()
+ .expect("to-consensus-buff should return (some buff)");
+ let expected_buff = execute(&buff_repr);
+ assert_eq!(result_buffer, expected_buff);
+ }
+}
+
#[test]
fn test_secp256k1() {
let secp256k1_evals = [
diff --git a/clarity/src/vm/types/serialization.rs b/clarity/src/vm/types/serialization.rs
index 283822a1d0..e4259023b1 100644
--- a/clarity/src/vm/types/serialization.rs
+++ b/clarity/src/vm/types/serialization.rs
@@ -18,7 +18,7 @@ use std::borrow::Borrow;
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::io::{Read, Write};
-use std::{error, fmt, str};
+use std::{cmp, error, fmt, str};
use serde_json::Value as JSONValue;
@@ -51,6 +51,7 @@ pub enum SerializationError {
BadTypeError(CheckErrors),
DeserializationError(String),
DeserializeExpected(TypeSignature),
+ LeftoverBytesInDeserialization,
}
lazy_static! {
@@ -74,6 +75,9 @@ impl std::fmt::Display for SerializationError {
"Deserialization expected the type of the input to be: {}",
e
),
+ SerializationError::LeftoverBytesInDeserialization => {
+ write!(f, "Deserialization error: bytes left over in buffer")
+ }
}
}
}
@@ -287,13 +291,171 @@ macro_rules! check_match {
};
}
+impl TypeSignature {
+ /// Return the maximum length of the consensus serialization of a
+ /// Clarity value of this type. The returned length *may* not fit
+ /// in a Clarity buffer! For example, the maximum serialized
+ /// size of a `(buff 1024*1024)` is `1+1024*1024` because of the
+ /// type prefix byte. However, that is 1 byte larger than the maximum
+ /// buffer size in Clarity.
+ pub fn max_serialized_size(&self) -> Result {
+ let type_prefix_size = 1;
+
+ let max_output_size = match self {
+ TypeSignature::NoType => {
+ // A `NoType` should *never* actually be evaluated
+ // (`NoType` corresponds to the Some branch of a
+ // `none` that is never matched with a corresponding
+ // `some` or similar with `result` types). So, when
+ // serializing an object with a `NoType`, the other
+ // branch should always be used.
+ return Err(CheckErrors::CouldNotDetermineSerializationType);
+ }
+ TypeSignature::IntType => 16,
+ TypeSignature::UIntType => 16,
+ TypeSignature::BoolType => 0,
+ TypeSignature::SequenceType(SequenceSubtype::ListType(list_type)) => {
+ // u32 length as big-endian bytes
+ let list_length_encode = 4;
+ list_type
+ .get_max_len()
+ .checked_mul(list_type.get_list_item_type().max_serialized_size()?)
+ .and_then(|x| x.checked_add(list_length_encode))
+ .ok_or_else(|| CheckErrors::ValueTooLarge)?
+ }
+ TypeSignature::SequenceType(SequenceSubtype::BufferType(buff_length)) => {
+ // u32 length as big-endian bytes
+ let buff_length_encode = 4;
+ u32::from(buff_length)
+ .checked_add(buff_length_encode)
+ .ok_or_else(|| CheckErrors::ValueTooLarge)?
+ }
+ TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII(
+ length,
+ ))) => {
+ // u32 length as big-endian bytes
+ let str_length_encode = 4;
+ // ascii is 1-byte per character
+ u32::from(length)
+ .checked_add(str_length_encode)
+ .ok_or_else(|| CheckErrors::ValueTooLarge)?
+ }
+ TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::UTF8(
+ length,
+ ))) => {
+ // u32 length as big-endian bytes
+ let str_length_encode = 4;
+ // utf-8 is maximum 4 bytes per codepoint (which is the length)
+ u32::from(length)
+ .checked_mul(4)
+ .and_then(|x| x.checked_add(str_length_encode))
+ .ok_or_else(|| CheckErrors::ValueTooLarge)?
+ }
+ TypeSignature::PrincipalType => {
+ // version byte + 20 byte hash160
+ let maximum_issuer_size = 21;
+ let contract_name_length_encode = 1;
+ // contract name maximum length is `MAX_STRING_LEN` (128), and ASCII
+ let maximum_contract_name = MAX_STRING_LEN as u32;
+ maximum_contract_name + maximum_issuer_size + contract_name_length_encode
+ }
+ TypeSignature::TupleType(tuple_type) => {
+ let type_map = tuple_type.get_type_map();
+ // u32 length as big-endian bytes
+ let tuple_length_encode: u32 = 4;
+ let mut total_size = tuple_length_encode;
+ for (key, value) in type_map.iter() {
+ let value_size = value.max_serialized_size()?;
+ total_size = total_size
+ .checked_add(1) // length of key-name
+ .and_then(|x| x.checked_add(key.len() as u32)) // ClarityName is ascii-only, so 1 byte per length
+ .and_then(|x| x.checked_add(value_size))
+ .ok_or_else(|| CheckErrors::ValueTooLarge)?;
+ }
+ total_size
+ }
+ TypeSignature::OptionalType(ref some_type) => {
+ match some_type.max_serialized_size() {
+ Ok(size) => size,
+ // if NoType, then this is just serializing a none
+ // value, which is only the type prefix
+ Err(CheckErrors::CouldNotDetermineSerializationType) => 0,
+ Err(e) => return Err(e),
+ }
+ }
+ TypeSignature::ResponseType(ref response_types) => {
+ let (ok_type, err_type) = response_types.as_ref();
+ let (ok_type_max_size, no_ok_type) = match ok_type.max_serialized_size() {
+ Ok(size) => (size, false),
+ Err(CheckErrors::CouldNotDetermineSerializationType) => (0, true),
+ Err(e) => return Err(e),
+ };
+ let err_type_max_size = match err_type.max_serialized_size() {
+ Ok(size) => size,
+ Err(CheckErrors::CouldNotDetermineSerializationType) => {
+ if no_ok_type {
+ // if both the ok type and the error type are NoType,
+ // throw a CheckError. This should not be possible, but the check
+ // is done out of caution.
+ return Err(CheckErrors::CouldNotDetermineSerializationType);
+ } else {
+ 0
+ }
+ }
+ Err(e) => return Err(e),
+ };
+ cmp::max(ok_type_max_size, err_type_max_size)
+ }
+ TypeSignature::TraitReferenceType(_) => {
+ return Err(CheckErrors::CouldNotDetermineSerializationType)
+ }
+ };
+
+ max_output_size
+ .checked_add(type_prefix_size)
+ .ok_or_else(|| CheckErrors::ValueTooLarge)
+ }
+}
+
impl Value {
pub fn deserialize_read(
r: &mut R,
expected_type: Option<&TypeSignature>,
) -> Result {
+ Self::deserialize_read_count(r, expected_type).map(|(value, _)| value)
+ }
+
+ /// Deserialize just like `deserialize_read` but also
+ /// return the bytes read
+ pub fn deserialize_read_count(
+ r: &mut R,
+ expected_type: Option<&TypeSignature>,
+ ) -> Result<(Value, u64), SerializationError> {
let mut bound_reader = BoundReader::from_reader(r, BOUND_VALUE_SERIALIZATION_BYTES as u64);
- Value::inner_deserialize_read(&mut bound_reader, expected_type, 0)
+ let value = Value::inner_deserialize_read(&mut bound_reader, expected_type, 0)?;
+ let bytes_read = bound_reader.num_read();
+ if let Some(expected_type) = expected_type {
+ let expect_size = match expected_type.max_serialized_size() {
+ Ok(x) => x,
+ Err(e) => {
+ warn!(
+ "Failed to determine max serialized size when checking expected_type argument";
+ "err" => ?e
+ );
+ return Ok((value, bytes_read));
+ }
+ };
+
+ assert!(
+ expect_size as u64 >= bytes_read,
+ "Deserialized more bytes than expected size during deserialization. Expected size = {}, bytes read = {}, type = {}",
+ expect_size,
+ bytes_read,
+ expected_type,
+ );
+ }
+
+ Ok((value, bytes_read))
}
fn inner_deserialize_read(
@@ -611,13 +773,10 @@ impl Value {
Ok(())
}
- /// This function attempts to deserialize a hex string into a Clarity Value.
- /// The `expected_type` parameter determines whether or not the deserializer should expect (and enforce)
- /// a particular type. `ClarityDB` uses this to ensure that lists, tuples, etc. loaded from the database
- /// have their max-length and other type information set by the type declarations in the contract.
- /// If passed `None`, the deserializer will construct the values as if they were literals in the contract, e.g.,
- /// list max length = the length of the list.
-
+ /// This function attempts to deserialize a byte buffer into a Clarity Value.
+ /// The `expected_type` parameter tells the deserializer to expect (and enforce)
+ /// a particular type. `ClarityDB` uses this to ensure that lists, tuples, etc. loaded from the database
+ /// have their max-length and other type information set by the type declarations in the contract.
pub fn try_deserialize_bytes(
bytes: &Vec,
expected: &TypeSignature,
@@ -625,6 +784,10 @@ impl Value {
Value::deserialize_read(&mut bytes.as_slice(), Some(expected))
}
+ /// This function attempts to deserialize a hex string into a Clarity Value.
+ /// The `expected_type` parameter tells the deserializer to expect (and enforce)
+ /// a particular type. `ClarityDB` uses this to ensure that lists, tuples, etc. loaded from the database
+ /// have their max-length and other type information set by the type declarations in the contract.
pub fn try_deserialize_hex(
hex: &str,
expected: &TypeSignature,
@@ -633,6 +796,28 @@ impl Value {
Value::try_deserialize_bytes(&mut data, expected)
}
+ /// This function attempts to deserialize a byte buffer into a
+ /// Clarity Value, while ensuring that the whole byte buffer is
+ /// consumed by the deserialization, erroring if it is not. The
+ /// `expected_type` parameter tells the deserializer to expect
+ /// (and enforce) a particular type. `ClarityDB` uses this to
+ /// ensure that lists, tuples, etc. loaded from the database have
+ /// their max-length and other type information set by the type
+ /// declarations in the contract.
+ pub fn try_deserialize_bytes_exact(
+ bytes: &Vec,
+ expected: &TypeSignature,
+ ) -> Result {
+ let input_length = bytes.len();
+ let (value, read_count) =
+ Value::deserialize_read_count(&mut bytes.as_slice(), Some(expected))?;
+ if read_count != (input_length as u64) {
+ Err(SerializationError::LeftoverBytesInDeserialization)
+ } else {
+ Ok(value)
+ }
+ }
+
pub fn try_deserialize_bytes_untyped(bytes: &Vec) -> Result {
Value::deserialize_read(&mut bytes.as_slice(), None)
}
diff --git a/src/clarity_vm/tests/costs.rs b/src/clarity_vm/tests/costs.rs
index a0acefe6ff..e826b6f4b6 100644
--- a/src/clarity_vm/tests/costs.rs
+++ b/src/clarity_vm/tests/costs.rs
@@ -156,6 +156,8 @@ pub fn get_simple_test(function: &NativeFunctions) -> &'static str {
StxBurn => "(stx-burn? u1 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)",
StxGetAccount => "(stx-account 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)",
Slice => "(slice str-foo u1 u1)",
+ ToConsensusBuff => "(to-consensus-buff u1)",
+ FromConsensusBuff => "(from-consensus-buff bool 0x03)",
}
}