Skip to content

feat: enable clarity-serialization in cargo workspace #6326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[alias]
stacks-node = "run --package stacks-node --"
fmt-stacks = "fmt -- --config group_imports=StdExternalCrate,imports_granularity=Module"
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"
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"
clippy-stackslib = "clippy -p stackslib --no-deps -- -Aclippy::all -Wclippy::indexing_slicing"

# Uncomment to improve performance slightly, at the cost of portability
Expand Down
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"stackslib",
"stacks-common",
"pox-locking",
"clarity-serialization",
"clarity",
"stx-genesis",
"libstackerdb",
Expand All @@ -12,8 +13,6 @@ members = [
"stacks-node",
"contrib/tools/config-docs-generator"]

exclude = ["contrib/clarity-serialization"]

# Dependencies we want to keep the same between workspace members
[workspace.dependencies]
ed25519-dalek = { version = "2.1.1", default-features = false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ keywords = [ "stacks", "stx", "bitcoin", "crypto", "blockstack", "decentralized"
readme = "README.md"

[dependencies]
lazy_static = "1.4.0"
lazy_static = { workspace = true }
regex = { version = "1", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_derive = { version = "1" }
slog = { version = "2.5.2", features = [ "max_level_trace" ] }
stacks_common = { package = "stacks-common", path = "../../stacks-common", default-features = false }
thiserror = { version = "1.0.65" }
serde = { workspace = true }
serde_derive = { workspace = true }
slog = { workspace = true }
stacks_common = { package = "stacks-common", path = "../stacks-common", default-features = false }
thiserror = { workspace = true }

[dev-dependencies]
mutants = "0.0.3"
test-case = { version = "3.3.1", default-features = false }
rstest = "0.17.0"

[features]
default = []
Expand Down
211 changes: 211 additions & 0 deletions clarity-serialization/src/tests/representations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright (C) 2025 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use rstest::rstest;

use crate::errors::CodecError;
use crate::representations::{
CONTRACT_MAX_NAME_LENGTH, CONTRACT_MIN_NAME_LENGTH, ClarityName, ContractName, MAX_STRING_LEN,
};
use crate::stacks_common::codec::StacksMessageCodec;

#[rstest]
#[case::valid_name("hello")]
#[case::dash("hello-dash")]
#[case::underscore("hello_underscore")]
#[case::numbers("test123")]
#[case::single_letter("a")]
#[case::exclamation_mark("set-token-uri!")]
#[case::question_mark("is-owner?")]
#[case::plus("math+")]
#[case::less_than("greater-than<")]
#[case::greater_than("less-than>")]
#[case::less_than_or_equal_to("<=")]
#[case::greater_than_or_equal_to(">=")]
#[case::asterisk("*")]
#[case::slash("/")]
#[case::dash_only("-")]
#[case::equals("=")]
fn test_clarity_name_valid(#[case] name: &str) {
let clarity_name = ClarityName::try_from(name.to_string())
.unwrap_or_else(|_| panic!("Should parse valid clarity name: {name}"));
assert_eq!(clarity_name.as_str(), name);
}

#[rstest]
#[case::empty("")]
#[case::starts_with_number("123abc")]
#[case::contains_space("hello world")]
#[case::contains_at("hello@world")]
#[case::contains_hash("hello#world")]
#[case::contains_dollar("hello$world")]
#[case::contains_percent("hello%world")]
#[case::contains_ampersand("hello&world")]
#[case::contains_dot("hello.world")]
#[case::contains_comma("hello,world")]
#[case::contains_semicolon("hello;world")]
#[case::contains_colon("hello:world")]
#[case::contains_pipe("hello|world")]
#[case::contains_backslash("hello\\world")]
#[case::contains_quote("hello\"world")]
#[case::contains_apostrophe("hello'world")]
#[case::contains_bracket_open("hello[world")]
#[case::contains_bracket_close("hello]world")]
#[case::contains_curly_open("hello{world")]
#[case::contains_curly_close("hello}world")]
#[case::contains_parenthesis_open("hello(world")]
#[case::contains_parenthesis_close("hello)world")]
#[case::too_long(&"a".repeat(MAX_STRING_LEN as usize + 1))]
fn test_clarity_name_invalid(#[case] name: &str) {
let result = ClarityName::try_from(name.to_string());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CodecError::InvalidClarityName(_, _)
));
}

#[rstest]
#[case("test-name")]
#[case::max_length(&"a".repeat(MAX_STRING_LEN as usize))]
fn test_clarity_name_serialization(#[case] name: &str) {
let name = ClarityName::try_from(name.to_string()).unwrap();

let mut buffer = Vec::new();
name.consensus_serialize(&mut buffer)
.unwrap_or_else(|_| panic!("Serialization should succeed for name: {name}"));

// Should have length byte followed by the string bytes
assert_eq!(buffer[0], name.len());
assert_eq!(&buffer[1..], name.as_bytes());

// Test deserialization
let deserialized = ClarityName::consensus_deserialize(&mut buffer.as_slice()).unwrap();
assert_eq!(deserialized, name);
}

// the first byte is the length of the buffer.
#[rstest]
#[case::invalid_utf8(vec![4, 0xFF, 0xFE, 0xFD, 0xFC], "Failed to parse Clarity name: could not contruct from utf8")]
#[case::invalid_name(vec![2, b'2', b'i'], "Failed to parse Clarity name: InvalidClarityName(\"ClarityName\", \"2i\")")] // starts with number
#[case::too_long(vec![MAX_STRING_LEN + 1], "Failed to deserialize clarity name: too long")]
#[case::wrong_length(vec![3, b'a'], "failed to fill whole buffer")]
fn test_clarity_name_deserialization_errors(#[case] buffer: Vec<u8>, #[case] error_message: &str) {
let result = ClarityName::consensus_deserialize(&mut buffer.as_slice());
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), error_message);
}

#[rstest]
#[case::valid_name("hello")]
#[case::dash("contract-name")]
#[case::underscore("hello_world")]
#[case::numbers("test123")]
#[case::transient("__transient")]
#[case::min_length("a")]
#[case::max_length(&"a".repeat(CONTRACT_MAX_NAME_LENGTH))]
#[case::max_string_len(&"a".repeat(MAX_STRING_LEN as usize))]
fn test_contract_name_valid(#[case] name: &str) {
let contract_name = ContractName::try_from(name.to_string())
.unwrap_or_else(|_| panic!("Should parse valid contract name: {name}"));
assert_eq!(contract_name.as_str(), name);
}

#[rstest]
#[case::empty("")]
#[case::starts_with_number("123contract")]
#[case::contains_space("hello world")]
#[case::contains_at("hello@world")]
#[case::contains_dot("hello.world")]
#[case::contains_exclamation("hello!world")]
#[case::contains_question("hello?world")]
#[case::contains_plus("hello+world")]
#[case::contains_asterisk("hello*world")]
#[case::contains_equals("hello=world")]
#[case::contains_slash("hello/world")]
#[case::contains_less_than("hello<world")]
#[case::contains_greater_than("hello>world")]
#[case::contains_comma("hello,world")]
#[case::contains_semicolon("hello;world")]
#[case::contains_colon("hello:world")]
#[case::contains_pipe("hello|world")]
#[case::contains_backslash("hello\\world")]
#[case::contains_quote("hello\"world")]
#[case::contains_apostrophe("hello'world")]
#[case::contains_bracket_open("hello[world")]
#[case::contains_bracket_close("hello]world")]
#[case::contains_curly_open("hello{world")]
#[case::contains_curly_close("hello}world")]
#[case::contains_parenthesis_open("hello(world")]
#[case::contains_parenthesis_close("hello)world")]
#[case::too_short(&"a".repeat(CONTRACT_MIN_NAME_LENGTH - 1))]
#[case::too_long(&"a".repeat(MAX_STRING_LEN as usize + 1))]
fn test_contract_name_invalid(#[case] name: &str) {
let result = ContractName::try_from(name.to_string());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CodecError::InvalidContractName(_, _)
));
}

#[rstest]
#[case::valid_name("test-contract")]
#[case::dash("contract-name")]
#[case::underscore("hello_world")]
#[case::numbers("test123")]
#[case::transient("__transient")]
#[case::min_length("a")]
#[case::max_length(&"a".repeat(CONTRACT_MAX_NAME_LENGTH))]
fn test_contract_name_serialization(#[case] name: &str) {
let name = ContractName::try_from(name.to_string()).unwrap();
let mut buffer = Vec::with_capacity((name.len() + 1) as usize);
name.consensus_serialize(&mut buffer)
.unwrap_or_else(|_| panic!("Serialization should succeed for name: {name}"));
assert_eq!(buffer[0], name.len());
assert_eq!(&buffer[1..], name.as_bytes());

// Test deserialization
let deserialized = ContractName::consensus_deserialize(&mut buffer.as_slice()).unwrap();
assert_eq!(deserialized, name);
}

#[test]
fn test_contract_name_serialization_too_long() {
let name =
ContractName::try_from("a".repeat(CONTRACT_MAX_NAME_LENGTH + 1)).expect("should parse");
let mut buffer = Vec::with_capacity((name.len() + 1) as usize);
let result = name.consensus_serialize(&mut buffer);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!(
"Failed to serialize contract name: too short or too long: {}",
name.len()
)
);
}

// the first byte is the length of the buffer.
#[rstest]
#[case::invalid_utf8(vec![4, 0xFF, 0xFE, 0xFD, 0xFC], "Failed to parse Contract name: could not construct from utf8")]
#[case::invalid_name(vec![2, b'2', b'i'], "Failed to parse Contract name: InvalidContractName(\"ContractName\", \"2i\")")] // starts with number
#[case::too_long(vec![MAX_STRING_LEN + 1], &format!("Failed to deserialize contract name: too short or too long: {}", MAX_STRING_LEN + 1))]
#[case::wrong_length(vec![3, b'a'], "failed to fill whole buffer")]
fn test_contract_name_deserialization_errors(#[case] buffer: Vec<u8>, #[case] error_message: &str) {
let result = ContractName::consensus_deserialize(&mut buffer.as_slice());
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), error_message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ mod signatures;

use stacks_common::types::StacksEpochId;

use crate::CodecError;
use crate::types::{
BuffData, ListTypeData, PrincipalData, SequenceData, TupleData, TypeSignature, Value,
MAX_VALUE_SIZE,
BuffData, ListTypeData, MAX_VALUE_SIZE, PrincipalData, SequenceData, TupleData, TypeSignature,
Value,
};
use crate::CodecError;

#[test]
fn test_constructors() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use std::io::Write;

use crate::errors::CodecError;
use crate::types::{
PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, TupleData, TypeSignature,
Value, MAX_VALUE_SIZE,
MAX_VALUE_SIZE, PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, TupleData,
TypeSignature, Value,
};

fn test_deser_ser(v: Value) {
Expand Down Expand Up @@ -51,12 +51,9 @@ fn test_bad_expectation(v: Value, e: TypeSignature) {

#[test]
fn test_lists() {
let list_list_int = Value::list_from(vec![Value::list_from(vec![
Value::Int(1),
Value::Int(2),
Value::Int(3),
let list_list_int = Value::list_from(vec![
Value::list_from(vec![Value::Int(1), Value::Int(2), Value::Int(3)]).unwrap(),
])
.unwrap()])
.unwrap();
test_deser_ser(list_list_int.clone());
test_deser_ser(Value::list_from(vec![]).unwrap());
Expand Down Expand Up @@ -290,8 +287,8 @@ fn test_vectors() {
Ok(StandardPrincipalData::new(
0x00,
[
0x11, 0xde, 0xad, 0xbe, 0xef, 0x11, 0xab, 0xab, 0xff, 0xff, 0x11, 0xde,
0xad, 0xbe, 0xef, 0x11, 0xab, 0xab, 0xff, 0xff,
0x11, 0xde, 0xad, 0xbe, 0xef, 0x11, 0xab, 0xab, 0xff, 0xff, 0x11, 0xde, 0xad,
0xbe, 0xef, 0x11, 0xab, 0xab, 0xff, 0xff,
],
)
.unwrap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
use std::collections::HashSet;

use crate::errors::CodecError;
use crate::types::signatures::{CallableSubtype, TypeSignature};
use crate::types::TypeSignature::{BoolType, IntType, ListUnionType, UIntType};
use crate::types::signatures::{CallableSubtype, TypeSignature};
use crate::types::{
QualifiedContractIdentifier, SequenceSubtype, TraitIdentifier, TupleTypeSignature,
};
Expand Down
Loading
Loading