Skip to content

Commit a7ffe58

Browse files
committed
add clarity-serialization
1 parent 32461ef commit a7ffe58

File tree

12 files changed

+6050
-1
lines changed

12 files changed

+6050
-1
lines changed

.cargo/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[alias]
22
stacks-node = "run --package stacks-node --"
33
fmt-stacks = "fmt -- --config group_imports=StdExternalCrate,imports_granularity=Module"
4-
clippy-stacks = "clippy -p stx-genesis -p libstackerdb -p stacks-signer -p pox-locking -p clarity -p libsigner -p stacks-common --no-deps --tests --all-features -- -D warnings"
4+
clippy-stacks = "clippy -p stx-genesis -p libstackerdb -p stacks-signer -p pox-locking -p clarity-serialization -p clarity -p libsigner -p stacks-common --no-deps --tests --all-features -- -D warnings"
55
clippy-stackslib = "clippy -p stackslib --no-deps -- -Aclippy::all -Wclippy::indexing_slicing"
66

77
# Uncomment to improve performance slightly, at the cost of portability

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"stackslib",
55
"stacks-common",
66
"pox-locking",
7+
"clarity-serialization",
78
"clarity",
89
"stx-genesis",
910
"libstackerdb",

clarity-serialization/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "clarity-serialization"
3+
version = "0.0.1"
4+
edition = "2024"
5+
description = "Serialization and deserialization for Stacks Clarity smart contract language types."
6+
license = "GPLv3"
7+
homepage = "https://github.com/stacks-network/stacks-core"
8+
repository = "https://github.com/stacks-network/stacks-core"
9+
keywords = [ "stacks", "stx", "bitcoin", "crypto", "blockstack", "decentralized", "dapps", "blockchain" ]
10+
readme = "README.md"
11+
12+
[dependencies]
13+
hashbrown = { workspace = true }
14+
lazy_static = { workspace = true }
15+
regex = { version = "1", default-features = false }
16+
serde = { workspace = true }
17+
serde_derive = { workspace = true }
18+
slog = { workspace = true }
19+
stacks_common = { package = "stacks-common", path = "../stacks-common", default-features = false }
20+
thiserror = { workspace = true }
21+
22+
[dev-dependencies]
23+
mutants = "0.0.3"
24+
25+
[features]
26+
default = []
27+
testing = []
28+
slog_json = ["stacks_common/slog_json"]
29+
30+
# Wasm-specific features for easier configuration
31+
wasm-web = ["stacks_common/wasm-web"]
32+
wasm-deterministic = ["stacks_common/wasm-deterministic"]

clarity-serialization/README.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Clarity Serialization (`clarity-serialization`)
2+
3+
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
4+
5+
A Rust crate for representing, serializing, and deserializing data types from the Stacks Clarity smart contract language.
6+
7+
## Overview
8+
9+
This crate provides the core components for working with Clarity data structures in Rust. It defines canonical Rust types for every Clarity value (e.g., `Value`, `TypeSignature`, `PrincipalData`) and implements the consensus-critical binary serialization and deserialization format used by the Stacks blockchain.
10+
11+
## Key Features
12+
13+
* **Canonical Data Structures**: Rust representations for all Clarity types, including `int`, `uint`, `bool`, `principal`, `optional`, `response`, `tuple`, `list`, `buffer`, and strings.
14+
* **Consensus-Compatible Binary Codec**: Implements the binary serialization and deserialization format required by the Stacks blockchain.
15+
* **Type Safety**: Includes type-checking logic (`admits`, `least_supertype`) for validating values against type signatures.
16+
17+
## Quick Start: Usage Examples
18+
19+
### Example 1: Serializing a Clarity Value to Hex
20+
21+
This example demonstrates how to construct a complex Clarity `(tuple)` and serialize it to its hexadecimal string representation, which is suitable for use as a transaction argument.
22+
23+
```rust
24+
use clarity_serialization::types::{Value, TupleData, PrincipalData};
25+
26+
fn main() -> Result<(), Box<dyn std::error::Error>> {
27+
// 1. Construct the individual values that will go into our tuple.
28+
let id = Value::UInt(101);
29+
let owner = Value::Principal(
30+
PrincipalData::parse("SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G")?
31+
);
32+
let metadata = Value::some(
33+
Value::buff_from(vec![0xde, 0xad, 0xbe, 0xef])?
34+
)?;
35+
36+
// 2. Create a vec of name-value pairs for the tuple.
37+
let tuple_fields = vec![
38+
("id".into(), id),
39+
("owner".into(), owner),
40+
("metadata".into(), metadata),
41+
];
42+
43+
// 3. Construct the tuple value.
44+
let my_tuple = Value::from(TupleData::from_data(tuple_fields)?);
45+
46+
// 4. Serialize the tuple to its consensus-cricital hex string.
47+
let hex_string = my_tuple.serialize_to_hex()?;
48+
49+
println!("Clarity Tuple: {}", my_tuple);
50+
println!("Serialized Hex: {}", hex_string);
51+
52+
// The output `hex_string` can now be used in a contract-call transaction.
53+
assert_eq!(
54+
hex_string,
55+
"0c000000030269640100000000000000000000000000000065086d657461646174610a0200000004deadbeef056f776e65720514a46ff88886c2ef9762d970b4d2c63678835bd39d"
56+
);
57+
58+
Ok(())
59+
}
60+
```
61+
62+
### Example 2: Deserializing a Clarity Value from Hex
63+
64+
This example shows the reverse process: taking a hex string and deserializing it into a structured `Value` object, while validating it against an expected type.
65+
66+
```rust
67+
use clarity_serialization::types::{Value, TypeSignature};
68+
69+
fn main() -> Result<(), Box<dyn std::error::Error>> {
70+
let hex_string = "0c000000030269640100000000000000000000000000000065086d657461646174610a0200000004deadbeef056f776e65720514a46ff88886c2ef9762d970b4d2c63678835bd39d";
71+
72+
// 1. First, let's deserialize without a type for inspection.
73+
// NOTE: This is not recommended for production use with data from untrusted sources.
74+
let untyped_value = Value::try_deserialize_hex_untyped(hex_string)?;
75+
println!("Deserialized (untyped): {:?}", untyped_value);
76+
77+
// 2. For robust deserialization, we should define the expected type.
78+
// This can be derived from the untyped value or known from a contract's interface.
79+
let expected_type = TypeSignature::type_of(&untyped_value)?;
80+
println!("Inferred Type Signature: {}", expected_type);
81+
82+
// 3. Deserialize again, this time enforcing the type signature.
83+
// The `sanitize` flag should be `true` when reading values from the DB
84+
// that were stored before Stacks 2.4. For new values, it can be `false`.
85+
let typed_value = Value::try_deserialize_hex(hex_string, &expected_type, false)?;
86+
87+
// 4. Now we can safely access the tuple's fields.
88+
let tuple_data = typed_value.expect_tuple()?;
89+
let id = tuple_data.get("id")?.clone().expect_u128()?;
90+
let owner = tuple_data.get("owner")?.clone().expect_principal()?;
91+
92+
println!("Successfully deserialized and validated!");
93+
println!("ID: {}", id);
94+
println!("Owner: {}", owner);
95+
96+
Ok(())
97+
}
98+
```
99+
100+
## Clarity Value Binary Format
101+
102+
The crate implements the standard binary format for Clarity values. At a high level, every value is encoded as: `[Type Prefix Byte] + [Payload]`.
103+
104+
| Type Prefix (Hex) | Clarity Type | Payload Description |
105+
| ----------------- | ----------------- | -------------------------------------------------------------------------------- |
106+
| `0x00` | `int` | 16-byte big-endian signed integer. |
107+
| `0x01` | `uint` | 16-byte big-endian unsigned integer. |
108+
| `0x02` | `(buff L)` | 4-byte big-endian length `L`, followed by `L` raw bytes. |
109+
| `0x03` | `true` | No payload. |
110+
| `0x04` | `false` | No payload. |
111+
| `0x05` | `principal` (Std) | 1-byte version, followed by 20-byte HASH160. |
112+
| `0x06` | `principal` (Cont)| Serialized Contract Principal (issuer) + 1-byte length-prefixed contract name. |
113+
| `0x07` | `(ok V)` | The serialized inner value `V`. |
114+
| `0x08` | `(err V)` | The serialized inner value `V`. |
115+
| `0x09` | `none` | No payload. |
116+
| `0x0a` | `(some V)` | The serialized inner value `V`. |
117+
| `0x0b` | `(list ...)` | 4-byte big-endian element count, followed by each serialized element. |
118+
| `0x0c` | `(tuple ...)` | 4-byte big-endian entry count, followed by each serialized `(name, value)` pair. |
119+
| `0x0d` | `(string-ascii L)`| 4-byte big-endian length `L`, followed by `L` ASCII bytes. |
120+
| `0x0e` | `(string-utf8 L)` | 4-byte big-endian byte-length `L`, followed by `L` UTF8 bytes. |
121+
122+
## Crate Features
123+
124+
This crate is designed to be minimal by default. Optional functionality is available via feature flags:
125+
126+
* `developer-mode`: Enables additional debugging and logging information useful during development.
127+
* `testing`: Enables helper functions and data structures used exclusively for unit and integration testing.
128+
* `slog_json`: Integrates with `slog` for structured JSON logging.
129+
* `wasm-web` / `wasm-deterministic`: Enables builds for WebAssembly environments with different determinism guarantees.
130+
131+
## Contributing
132+
133+
Contributions are welcome! This crate is part of the `stacks-core` monorepo. Please see the [main repository's contributing guidelines](https://github.com/stacks-network/stacks-core/blob/master/CONTRIBUTING.md) for more details on the development process.
134+
135+
## License
136+
137+
This project is licensed under the **GNU General Public License v3.0** ([GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)). See the `LICENSE` file for details.

clarity-serialization/src/errors.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::io;
2+
3+
use thiserror::Error;
4+
5+
use crate::types::{TupleTypeSignature, TypeSignature, Value};
6+
7+
/// The primary error type for the `clarity-codec` crate.
8+
///
9+
/// It represents all possible failures that can occur when encoding, decoding,
10+
/// or validating the structure and types of a Clarity value.
11+
#[derive(Error, Debug)]
12+
pub enum CodecError {
13+
#[error("I/O error during (de)serialization: {0}")]
14+
Io(#[from] io::Error),
15+
16+
#[error("Serialization error caused by IO: {0}")]
17+
Serialization(String),
18+
19+
#[error("Deserialization failed: {0}")]
20+
Deserialization(String),
21+
22+
#[error("Deserialization expected the type of the input to be: {0}")]
23+
DeserializeExpected(Box<TypeSignature>),
24+
25+
#[error("The serializer handled an input in an unexpected way")]
26+
UnexpectedSerialization,
27+
28+
#[error("Deserialization finished but there were leftover bytes in the buffer")]
29+
LeftoverBytesInDeserialization,
30+
31+
#[error("Parse error: {0}")]
32+
ParseError(String),
33+
34+
#[error("Bad type construction.")]
35+
BadTypeConstruction,
36+
37+
// --- Structural and Size Errors ---
38+
#[error("A value being constructed is larger than the 1MB Clarity limit")]
39+
ValueTooLarge,
40+
41+
#[error("A value is out of its prescribed bounds")]
42+
ValueOutOfBounds,
43+
44+
#[error("A type signature is deeper than the 32-level Clarity limit")]
45+
TypeSignatureTooDeep,
46+
47+
#[error("The supertype of two types is too large to be represented")]
48+
SupertypeTooLarge,
49+
50+
#[error("Empty tuples are not allowed")]
51+
EmptyTuplesNotAllowed,
52+
53+
#[error("Failed to construct a tuple with the given type")]
54+
FailureConstructingTupleWithType,
55+
56+
#[error("Failed to construct a list with the given type")]
57+
FailureConstructingListWithType,
58+
59+
#[error("All elements in a list must have a compatible supertype")]
60+
ListTypesMustMatch,
61+
62+
// --- Type Mismatch and Semantic Errors ---
63+
#[error("Expected a value of type '{expected}', but found a value of type '{found}'")]
64+
TypeError {
65+
expected: Box<TypeSignature>,
66+
found: Box<TypeSignature>,
67+
},
68+
69+
#[error("Expected a value of type '{expected}', but found the value '{found}'")]
70+
TypeValueError {
71+
expected: Box<TypeSignature>,
72+
found: Box<Value>,
73+
},
74+
75+
#[error("could not determine the input type for the serialization function")]
76+
CouldNotDetermineSerializationType,
77+
78+
#[error("type of expression cannot be determined")]
79+
CouldNotDetermineType,
80+
81+
#[error("Deserialization expected a value of type '{0}', but the data did not match")]
82+
DeserializeWrongType(Box<TypeSignature>),
83+
84+
// --- Naming and Identifier Errors ---
85+
#[error("Name '{0}' is already used in this tuple")]
86+
NameAlreadyUsedInTuple(String),
87+
88+
#[error("Could not find field '{0}' in tuple '{1}'")]
89+
NoSuchTupleField(String, TupleTypeSignature),
90+
91+
#[error("Failed to parse {0}: {1}")]
92+
InvalidClarityName(&'static str, String),
93+
94+
#[error("Failed to parse {0}: {1}")]
95+
InvalidContractName(&'static str, String),
96+
97+
// --- String/Buffer Content Errors ---
98+
#[error("Invalid characters detected in string")]
99+
InvalidStringCharacters,
100+
101+
#[error("Invalid UTF-8 encoding in string")]
102+
InvalidUtf8Encoding,
103+
104+
// --- Catch-all for internal logic errors ---
105+
#[error("An unexpected internal error occurred: {0}")]
106+
Expect(String),
107+
}

clarity-serialization/src/lib.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2+
// Copyright (C) 2020 Stacks Open Internet Foundation
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
#[macro_use]
18+
extern crate serde_derive;
19+
20+
#[macro_use]
21+
extern crate stacks_common;
22+
23+
pub use stacks_common::{
24+
codec, consts, impl_array_hexstring_fmt, impl_array_newtype, impl_byte_array_message_codec,
25+
impl_byte_array_serde, types as stacks_types, util,
26+
};
27+
28+
pub mod errors;
29+
pub mod representations;
30+
pub mod types;
31+
32+
#[cfg(any(test, feature = "testing"))]
33+
pub mod tests;
34+
35+
// set via _compile-time_ envars
36+
const GIT_BRANCH: Option<&'static str> = option_env!("GIT_BRANCH");
37+
const GIT_COMMIT: Option<&'static str> = option_env!("GIT_COMMIT");
38+
const GIT_TREE_CLEAN: Option<&'static str> = option_env!("GIT_TREE_CLEAN");
39+
40+
#[cfg(debug_assertions)]
41+
const BUILD_TYPE: &str = "debug";
42+
#[cfg(not(debug_assertions))]
43+
const BUILD_TYPE: &str = "release";
44+
45+
pub fn version_string(pkg_name: &str, pkg_version: &str) -> String {
46+
let git_branch = GIT_BRANCH.unwrap_or("");
47+
let git_commit = GIT_COMMIT.unwrap_or("");
48+
let git_tree_clean = GIT_TREE_CLEAN.unwrap_or("");
49+
50+
format!(
51+
"{} {} ({}:{}{}, {} build, {} [{}])",
52+
pkg_name,
53+
pkg_version,
54+
&git_branch,
55+
git_commit,
56+
git_tree_clean,
57+
BUILD_TYPE,
58+
std::env::consts::OS,
59+
std::env::consts::ARCH
60+
)
61+
}

0 commit comments

Comments
 (0)