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)", } }