Skip to content

Commit 73df355

Browse files
authored
Merge pull request #3132 from stacks-network/feat/to-from-buff
Implement consensus de/serialization in Clarity
2 parents f38a266 + d8f7628 commit 73df355

File tree

12 files changed

+713
-14
lines changed

12 files changed

+713
-14
lines changed

clarity/src/vm/analysis/arithmetic_checker/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ impl<'a> ArithmeticOnlyChecker<'a> {
181181
| FetchEntry | SetEntry | DeleteEntry | InsertEntry | SetVar | MintAsset
182182
| MintToken | TransferAsset | TransferToken | ContractCall | StxTransfer
183183
| StxTransferMemo | StxBurn | AtBlock | GetStxBalance | GetTokenSupply | BurnToken
184-
| BurnAsset | StxGetAccount => {
184+
| FromConsensusBuff | ToConsensusBuff | BurnAsset | StxGetAccount => {
185185
return Err(Error::FunctionNotPermitted(function));
186186
}
187187
Append | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | Print

clarity/src/vm/analysis/errors.rs

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub enum CheckErrors {
6868
ExpectedOptionalOrResponseValue(Value),
6969
CouldNotDetermineResponseOkType,
7070
CouldNotDetermineResponseErrType,
71+
CouldNotDetermineSerializationType,
7172
UncheckedIntermediaryResponses,
7273

7374
CouldNotDetermineMatchTypes,
@@ -425,6 +426,7 @@ impl DiagnosableError for CheckErrors {
425426
},
426427
CheckErrors::UncheckedIntermediaryResponses => format!("intermediary responses in consecutive statements must be checked"),
427428
CheckErrors::CostComputationFailed(s) => format!("contract cost computation failed: {}", s),
429+
CheckErrors::CouldNotDetermineSerializationType => format!("could not determine the input type for the serialization function"),
428430
}
429431
}
430432

clarity/src/vm/analysis/read_only_checker/mod.rs

+10-4
Original file line numberDiff line numberDiff line change
@@ -282,13 +282,19 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> {
282282
| UnwrapErrRet | IsOkay | IsNone | Asserts | Unwrap | UnwrapErr | Match | IsErr
283283
| IsSome | TryRet | ToUInt | ToInt | BuffToIntLe | BuffToUIntLe | BuffToIntBe
284284
| BuffToUIntBe | IntToAscii | IntToUtf8 | StringToInt | StringToUInt | IsStandard
285-
| PrincipalDestruct | PrincipalConstruct | Append | Concat | AsMaxLen | ContractOf
286-
| PrincipalOf | ListCons | GetBlockInfo | GetBurnBlockInfo | TupleGet | TupleMerge
287-
| Len | Print | AsContract | Begin | FetchVar | GetStxBalance | StxGetAccount
288-
| GetTokenBalance | GetAssetOwner | GetTokenSupply | ElementAt | IndexOf | Slice => {
285+
| ToConsensusBuff | PrincipalDestruct | PrincipalConstruct | Append | Concat
286+
| AsMaxLen | ContractOf | PrincipalOf | ListCons | GetBlockInfo | GetBurnBlockInfo
287+
| TupleGet | TupleMerge | Len | Print | AsContract | Begin | FetchVar
288+
| GetStxBalance | StxGetAccount | GetTokenBalance | GetAssetOwner | GetTokenSupply
289+
| ElementAt | IndexOf | Slice => {
289290
// Check all arguments.
290291
self.check_each_expression_is_read_only(args)
291292
}
293+
FromConsensusBuff => {
294+
// Check only the second+ arguments: the first argument is a type parameter
295+
check_argument_count(2, args)?;
296+
self.check_each_expression_is_read_only(&args[1..])
297+
}
292298
AtBlock => {
293299
check_argument_count(2, args)?;
294300

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use crate::vm::analysis::read_only_checker::check_argument_count;
2+
use crate::vm::analysis::type_checker::contexts::TypingContext;
3+
use crate::vm::analysis::type_checker::{TypeChecker, TypeResult};
4+
use crate::vm::analysis::CheckError;
5+
use crate::vm::types::{BufferLength, SequenceSubtype, TypeSignature};
6+
use crate::vm::SymbolicExpression;
7+
8+
/// to-consensus-buff admits exactly one argument:
9+
/// * the Clarity value to serialize
10+
/// it returns an `(optional (buff x))` where `x` is the maximum possible
11+
/// consensus buffer length based on the inferred type of the supplied value.
12+
pub fn check_special_to_consensus_buff(
13+
checker: &mut TypeChecker,
14+
args: &[SymbolicExpression],
15+
context: &TypingContext,
16+
) -> TypeResult {
17+
check_argument_count(1, args)?;
18+
let input_type = checker.type_check(&args[0], context)?;
19+
let buffer_max_len = BufferLength::try_from(input_type.max_serialized_size()?)?;
20+
TypeSignature::new_option(TypeSignature::SequenceType(SequenceSubtype::BufferType(
21+
buffer_max_len,
22+
)))
23+
.map_err(CheckError::from)
24+
}
25+
26+
/// from-consensus-buff admits exactly two arguments:
27+
/// * a type signature indicating the expected return type `t1`
28+
/// * a buffer (of up to max length)
29+
/// it returns an `(optional t1)`
30+
pub fn check_special_from_consensus_buff(
31+
checker: &mut TypeChecker,
32+
args: &[SymbolicExpression],
33+
context: &TypingContext,
34+
) -> TypeResult {
35+
check_argument_count(2, args)?;
36+
let result_type = TypeSignature::parse_type_repr(&args[0], checker)?;
37+
checker.type_check_expects(&args[1], context, &TypeSignature::max_buffer())?;
38+
TypeSignature::new_option(result_type).map_err(CheckError::from)
39+
}

clarity/src/vm/analysis/type_checker/natives/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use crate::vm::costs::{
3939
};
4040

4141
mod assets;
42+
mod conversions;
4243
mod maps;
4344
mod options;
4445
mod sequences;
@@ -882,6 +883,12 @@ impl TypedNativeFunction {
882883
IsNone => Special(SpecialNativeFunction(&options::check_special_is_optional)),
883884
IsSome => Special(SpecialNativeFunction(&options::check_special_is_optional)),
884885
AtBlock => Special(SpecialNativeFunction(&check_special_at_block)),
886+
ToConsensusBuff => Special(SpecialNativeFunction(
887+
&conversions::check_special_to_consensus_buff,
888+
)),
889+
FromConsensusBuff => Special(SpecialNativeFunction(
890+
&conversions::check_special_from_consensus_buff,
891+
)),
885892
}
886893
}
887894
}

clarity/src/vm/analysis/type_checker/tests/mod.rs

+140
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use crate::vm::analysis::AnalysisDatabase;
2727
use crate::vm::ast::errors::ParseErrors;
2828
use crate::vm::ast::{build_ast, parse};
2929
use crate::vm::contexts::OwnedEnvironment;
30+
use crate::vm::execute_v2;
3031
use crate::vm::representations::SymbolicExpression;
3132
use crate::vm::types::{
3233
BufferLength, FixedFunction, FunctionType, PrincipalData, QualifiedContractIdentifier,
@@ -74,6 +75,145 @@ fn ascii_type(size: u32) -> TypeSignature {
7475
TypeSignature::SequenceType(StringType(ASCII(size.try_into().unwrap()))).into()
7576
}
7677

78+
#[test]
79+
fn test_from_consensus_buff() {
80+
let good = [
81+
("(from-consensus-buff int 0x00)", "(optional int)"),
82+
(
83+
"(from-consensus-buff { a: uint, b: principal } 0x00)",
84+
"(optional (tuple (a uint) (b principal)))",
85+
),
86+
];
87+
88+
let bad = [
89+
(
90+
"(from-consensus-buff)",
91+
CheckErrors::IncorrectArgumentCount(2, 0),
92+
),
93+
(
94+
"(from-consensus-buff 0x00 0x00 0x00)",
95+
CheckErrors::IncorrectArgumentCount(2, 3),
96+
),
97+
(
98+
"(from-consensus-buff 0x00 0x00)",
99+
CheckErrors::InvalidTypeDescription,
100+
),
101+
(
102+
"(from-consensus-buff int u6)",
103+
CheckErrors::TypeError(TypeSignature::max_buffer(), TypeSignature::UIntType),
104+
),
105+
(
106+
"(from-consensus-buff (buff 1048576) 0x00)",
107+
CheckErrors::ValueTooLarge,
108+
),
109+
];
110+
111+
for (good_test, expected) in good.iter() {
112+
let type_result = type_check_helper(good_test).unwrap();
113+
assert_eq!(expected, &type_result.to_string());
114+
115+
assert!(
116+
type_result.admits(&execute_v2(good_test).unwrap().unwrap()),
117+
"The analyzed type must admit the evaluated type"
118+
);
119+
}
120+
121+
for (bad_test, expected) in bad.iter() {
122+
assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err);
123+
}
124+
}
125+
126+
#[test]
127+
fn test_to_consensus_buff() {
128+
let good = [
129+
(
130+
"(to-consensus-buff (if true (some u1) (some u2)))",
131+
"(optional (buff 18))",
132+
),
133+
(
134+
"(to-consensus-buff (if true (ok u1) (ok u2)))",
135+
"(optional (buff 18))",
136+
),
137+
(
138+
"(to-consensus-buff (if true (ok 1) (err u2)))",
139+
"(optional (buff 18))",
140+
),
141+
(
142+
"(to-consensus-buff (if true (ok 1) (err true)))",
143+
"(optional (buff 18))",
144+
),
145+
(
146+
"(to-consensus-buff (if true (ok false) (err true)))",
147+
"(optional (buff 2))",
148+
),
149+
(
150+
"(to-consensus-buff (if true (err u1) (err u2)))",
151+
"(optional (buff 18))",
152+
),
153+
("(to-consensus-buff none)", "(optional (buff 1))"),
154+
("(to-consensus-buff 0x00)", "(optional (buff 6))"),
155+
("(to-consensus-buff \"a\")", "(optional (buff 6))"),
156+
("(to-consensus-buff u\"ab\")", "(optional (buff 13))"),
157+
("(to-consensus-buff 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)", "(optional (buff 151))"),
158+
("(to-consensus-buff 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde)", "(optional (buff 151))"),
159+
("(to-consensus-buff true)", "(optional (buff 1))"),
160+
("(to-consensus-buff -1)", "(optional (buff 17))"),
161+
("(to-consensus-buff u1)", "(optional (buff 17))"),
162+
("(to-consensus-buff (list 1 2 3 4))", "(optional (buff 73))"),
163+
(
164+
"(to-consensus-buff { apple: u1, orange: 2, blue: true })",
165+
"(optional (buff 58))",
166+
),
167+
(
168+
"(define-private (my-func (x (buff 1048566)))
169+
(to-consensus-buff x))
170+
(my-func 0x001122334455)
171+
",
172+
"(optional (buff 1048571))",
173+
),
174+
];
175+
176+
let bad = [
177+
(
178+
"(to-consensus-buff)",
179+
CheckErrors::IncorrectArgumentCount(1, 0),
180+
),
181+
(
182+
"(to-consensus-buff 0x00 0x00)",
183+
CheckErrors::IncorrectArgumentCount(1, 2),
184+
),
185+
(
186+
"(define-private (my-func (x (buff 1048576)))
187+
(to-consensus-buff x))",
188+
CheckErrors::ValueTooLarge,
189+
),
190+
(
191+
"(define-private (my-func (x (buff 1048570)))
192+
(to-consensus-buff x))",
193+
CheckErrors::ValueTooLarge,
194+
),
195+
(
196+
"(define-private (my-func (x (buff 1048567)))
197+
(to-consensus-buff x))",
198+
CheckErrors::ValueTooLarge,
199+
),
200+
];
201+
202+
for (good_test, expected) in good.iter() {
203+
let type_result = type_check_helper(good_test).unwrap();
204+
assert_eq!(expected, &type_result.to_string());
205+
206+
assert!(
207+
type_result.admits(&execute_v2(good_test).unwrap().unwrap()),
208+
"The analyzed type must admit the evaluated type"
209+
);
210+
}
211+
212+
for (bad_test, expected) in bad.iter() {
213+
assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err);
214+
}
215+
}
216+
77217
#[test]
78218
fn test_get_block_info() {
79219
let good = [

clarity/src/vm/docs/mod.rs

+51
Original file line numberDiff line numberDiff line change
@@ -1978,6 +1978,55 @@ one of the following error codes:
19781978
"
19791979
};
19801980

1981+
const TO_CONSENSUS_BUFF: SpecialAPI = SpecialAPI {
1982+
input_type: "any",
1983+
output_type: "(optional buff)",
1984+
signature: "(to-consensus-buff value)",
1985+
description: "`to-consensus-buff` is a special function that will serialize any
1986+
Clarity value into a buffer, using the SIP-005 serialization of the
1987+
Clarity value. Not all values can be serialized: some value's
1988+
consensus serialization is too large to fit in a Clarity buffer (this
1989+
is because of the type prefix in the consensus serialization).
1990+
1991+
If the value cannot fit as serialized into the maximum buffer size,
1992+
this returns `none`, otherwise, it will be
1993+
`(some consensus-serialized-buffer)`. During type checking, the
1994+
analyzed type of the result of this method will be the maximum possible
1995+
consensus buffer length based on the inferred type of the supplied value.
1996+
",
1997+
example: r#"
1998+
(to-consensus-buff 1) ;; Returns (some 0x0000000000000000000000000000000001)
1999+
(to-consensus-buff u1) ;; Returns (some 0x0100000000000000000000000000000001)
2000+
(to-consensus-buff true) ;; Returns (some 0x03)
2001+
(to-consensus-buff false) ;; Returns (some 0x04)
2002+
(to-consensus-buff none) ;; Returns (some 0x09)
2003+
(to-consensus-buff 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns (some 0x051fa46ff88886c2ef9762d970b4d2c63678835bd39d)
2004+
(to-consensus-buff { abc: 3, def: 4 }) ;; Returns (some 0x0c00000002036162630000000000000000000000000000000003036465660000000000000000000000000000000004)
2005+
"#,
2006+
};
2007+
2008+
const FROM_CONSENSUS_BUFF: SpecialAPI = SpecialAPI {
2009+
input_type: "type-signature(t), buff",
2010+
output_type: "(optional t)",
2011+
signature: "(from-consensus-buff type-signature buffer)",
2012+
description: "`from-consensus-buff` is a special function that will deserialize a
2013+
buffer into a Clarity value, using the SIP-005 serialization of the
2014+
Clarity value. The type that `from-consensus-buff` tries to deserialize
2015+
into is provided by the first parameter to the function. If it fails
2016+
to deserialize the type, the method returns `none`.
2017+
",
2018+
example: r#"
2019+
(from-consensus-buff int 0x0000000000000000000000000000000001) ;; Returns (some 1)
2020+
(from-consensus-buff uint 0x0000000000000000000000000000000001) ;; Returns none
2021+
(from-consensus-buff uint 0x0100000000000000000000000000000001) ;; Returns (some u1)
2022+
(from-consensus-buff bool 0x0000000000000000000000000000000001) ;; Returns none
2023+
(from-consensus-buff bool 0x03) ;; Returns (some true)
2024+
(from-consensus-buff bool 0x04) ;; Returns (some false)
2025+
(from-consensus-buff principal 0x051fa46ff88886c2ef9762d970b4d2c63678835bd39d) ;; Returns (some SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)
2026+
(from-consensus-buff { abc: int, def: int } 0x0c00000002036162630000000000000000000000000000000003036465660000000000000000000000000000000004) ;; Returns (some (tuple (abc 3) (def 4)))
2027+
"#,
2028+
};
2029+
19812030
fn make_api_reference(function: &NativeFunctions) -> FunctionAPI {
19822031
use crate::vm::functions::NativeFunctions::*;
19832032
let name = function.get_name();
@@ -2079,6 +2128,8 @@ fn make_api_reference(function: &NativeFunctions) -> FunctionAPI {
20792128
StxTransfer => make_for_special(&STX_TRANSFER, name),
20802129
StxTransferMemo => make_for_special(&STX_TRANSFER_MEMO, name),
20812130
StxBurn => make_for_simple_native(&STX_BURN, &StxBurn, name),
2131+
ToConsensusBuff => make_for_special(&TO_CONSENSUS_BUFF, name),
2132+
FromConsensusBuff => make_for_special(&FROM_CONSENSUS_BUFF, name),
20822133
}
20832134
}
20842135

clarity/src/vm/functions/conversions.rs

+57
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
use stacks_common::codec::StacksMessageCodec;
18+
1719
use crate::vm::costs::cost_functions::ClarityCostFunction;
1820
use crate::vm::costs::runtime_cost;
1921
use crate::vm::errors::{check_argument_count, CheckErrors, InterpreterResult as Result};
@@ -209,3 +211,58 @@ pub fn native_int_to_utf8(value: Value) -> Result<Value> {
209211
// Given a string representing an integer, convert this to Clarity UTF8 value.
210212
native_int_to_string_generic(value, Value::string_utf8_from_bytes)
211213
}
214+
215+
/// Returns `value` consensus serialized into a `(optional buff)` object.
216+
/// If the value cannot fit as serialized into the maximum buffer size,
217+
/// this returns `none`, otherwise, it will be `(some consensus-serialized-buffer)`
218+
pub fn to_consensus_buff(value: Value) -> Result<Value> {
219+
let clar_buff_serialized = match Value::buff_from(value.serialize_to_vec()) {
220+
Ok(x) => x,
221+
Err(_) => return Ok(Value::none()),
222+
};
223+
224+
match Value::some(clar_buff_serialized) {
225+
Ok(x) => Ok(x),
226+
Err(_) => Ok(Value::none()),
227+
}
228+
}
229+
230+
/// Deserialize a Clarity value from a consensus serialized buffer.
231+
/// If the supplied buffer either fails to deserialize or deserializes
232+
/// to an unexpected type, returns `none`. Otherwise, it will be `(some value)`
233+
pub fn from_consensus_buff(
234+
args: &[SymbolicExpression],
235+
env: &mut Environment,
236+
context: &LocalContext,
237+
) -> Result<Value> {
238+
check_argument_count(2, args)?;
239+
240+
let type_arg = TypeSignature::parse_type_repr(&args[0], env)?;
241+
let value = eval(&args[1], env, context)?;
242+
243+
// get the buffer bytes from the supplied value. if not passed a buffer,
244+
// this is a type error
245+
let input_bytes = if let Value::Sequence(SequenceData::Buffer(buff_data)) = value {
246+
Ok(buff_data.data)
247+
} else {
248+
Err(CheckErrors::TypeValueError(
249+
TypeSignature::max_buffer(),
250+
value,
251+
))
252+
}?;
253+
254+
runtime_cost(ClarityCostFunction::Unimplemented, env, input_bytes.len())?;
255+
256+
// Perform the deserialization and check that it deserialized to the expected
257+
// type. A type mismatch at this point is an error that should be surfaced in
258+
// Clarity (as a none return).
259+
let result = match Value::try_deserialize_bytes_exact(&input_bytes, &type_arg) {
260+
Ok(value) => value,
261+
Err(_) => return Ok(Value::none()),
262+
};
263+
if !type_arg.admits(&result) {
264+
return Ok(Value::none());
265+
}
266+
267+
Value::some(result)
268+
}

0 commit comments

Comments
 (0)