Cord is a compact deterministic serialization format for Rust with first-class serde integration.
- Rich type system — structs, enums, sets, maps, byte arrays, date-times, decimals, UUIDs, options, and more
- Forward evolution — wrap fields in
Evolving<T>to round-trip unknown data (e.g., new enum variants) without data loss - Fine-grained wire control — tune integer encoding, length prefix widths, and variant index sizes per field
- Deterministic output — every unique value produces exactly one byte sequence, making it safe to sign, hash, cache, and deduplicate serialized data
Installation
Quick Start
Any type that derives Cord just works:
use ;
let user = User ;
let bytes = serialize.unwrap;
let deserialized: User = deserialize.unwrap;
assert_eq!;
#[derive(Cord)] generates both Serialize and Deserialize implementations. Types that already derive serde::Serialize and serde::Deserialize also work — #[derive(Cord)] is only needed when using Cord-specific field attributes.
Cord supports booleans, integers (i8–i128, u8–u128), floats (f32, f64), strings, byte arrays, options, sequences, structs, tuple structs, and enums out of the box.
Beyond the Basics
Beyond primitive types and structs, Cord provides DateTime, Map, Set, Decimal, and Uuid — use them directly as field types, no annotations needed. Enums, options, and Vec<u8> all work out of the box.
use ;
use ;
let mut tags = new;
tags.insert;
tags.insert;
let mut attributes = new;
attributes.insert;
attributes.insert;
let doc = Document ;
let bytes = serialize.unwrap;
let decoded: Document = deserialize.unwrap;
assert_eq!;
Forward Evolution
When different parts of a system run different versions of the same schema, you need a way to handle unknown data without losing it. Evolving<T> length-prefixes the serialized payload so that if deserialization of the inner type fails (e.g., an unknown enum variant), the raw bytes are preserved and can be round-tripped without data loss:
use ;
let msg = Message ;
let bytes = serialize.unwrap;
let decoded: Message = deserialize.unwrap;
// Known values are accessible
assert!;
assert_eq!;
If a newer version adds Status::Pending and serializes it, older code will deserialize it as Evolving::Unknown(bytes) — and re-serializing produces identical bytes.
The #[cord(evolving = N)] attribute controls the width of the length prefix used for the envelope:
| Attribute | Payload Length Prefix | Max Payload Size |
|---|---|---|
#[cord(evolving = 8)] |
u8 | 255 bytes |
#[cord(evolving = 16)] |
u16 | 65,535 bytes |
#[cord(evolving = 32)] |
u32 (default) | ~4 GiB |
Without the attribute, Evolving<T> defaults to a 32-bit length prefix.
Hashing
Since Cord guarantees deterministic serialization, you can compute canonical hashes of any serializable value with the built-in SHA3-256 hashing:
use ;
let user = User ;
// Compute a canonical SHA3-256 hash
let h: = hash.unwrap;
// Same value always produces the same hash, regardless of when or where
let h2: = hash.unwrap;
assert_eq!;
Or bring your own hash — Cord's deterministic encoding means serialize(value) always produces the same bytes for the same value:
use ;
let user = User ;
let bytes = serialize.unwrap;
// Hash bytes with any algorithm you prefer
Tuning the Wire Format
By default, Cord uses fixed-width big-endian encoding for integers, 32-bit (u32) length prefixes for sequences/strings/bytes, and 32-bit (u32) variant indices for enums. This makes the format predictable and easy to implement across languages.
For size-sensitive protocols, Cord provides field attributes to control encoding width. These require #[derive(Cord)] on the containing type.
Variable-Length Integers
Use #[cord(varint)] for compact variable-length encoding (LEB128 for unsigned, zigzag + LEB128 for signed). Works with all integer types from u8 to u128:
use Cord;
Width
Control the width of length prefixes (strings, byte arrays, sequences) and variant indices (enums) with #[cord(width = N)]. The attribute applies to whichever is relevant for the field type:
| Attribute | Wire Width | Applies To |
|---|---|---|
#[cord(width = 8)] |
u8 (1B) | Length prefix or variant index |
#[cord(width = 16)] |
u16 (2B) | Length prefix or variant index |
#[cord(width = 64)] |
u64 (8B) | Length prefix or variant index |
Custom Variant Indices
Use #[cord(index = N)] on enum variants to assign explicit wire indices:
use Cord;
If any variant has #[cord(index)], all variants must have it.
Combining Attributes
use Cord;
Deterministic Serialization
Cord guarantees that every unique value has exactly one binary representation. This is a property of the format itself — sorted collections, NFC-normalized strings, fixed-width or minimal-length encodings — not something you opt into.
This matters most when serialized bytes are inputs to cryptographic operations. If you sign or hash a data structure and later need to re-serialize it to verify the signature, you need identical bytes. Most formats can't promise that — key order in maps, variable-length integer encodings, and Unicode normalization differences can all silently produce different output for the same logical value.
With Cord, any implementation that follows the spec will produce the same bytes for the same data. You can serialize, deserialize, re-serialize, and the output is always identical. This makes it straightforward to use with signing, hashing, content-addressing, caching, and deduplication.
Threat Model
Cord is designed to defend against scenarios where attackers exploit ambiguities in data representation to bypass security controls, particularly in cryptographic contexts:
- Canonicalization bypass: Cryptographic systems often verify signatures against a normalized form while operating on raw input. Attackers exploit this gap by crafting inputs with trailing data, comment fields, or flexible encodings that bypass verification but execute differently. Classic examples include XML signature wrapping attacks and JWT header manipulation.
- Protocol confusion: When data is parsed differently across system boundaries, attackers can craft inputs that pass one subsystem's verifications and authorize malicious actions in downstream systems, effectively amounting to a payload substitution attack.
- Inconsistency: When third parties cannot independently reproduce the exact byte sequence of cryptographically authenticated data, verification becomes dependent on trusting the original signer's environment. In distributed verification systems like blockchains or certificate transparency logs, this can lead to consensus failures or validation errors.
Cord does not protect against:
- Side-channel attacks during serialization/deserialization
- Memory safety issues outside of Cord's implementation
- Malicious inputs exceeding reasonable size limits
- Implementation flaws in cryptographic primitives used with Cord outputs
Unicode Normalization
Cord enforces NFC (Canonical Decomposition followed by Canonical Composition) normalization for all strings. Strings are automatically normalized to NFC during serialization, and the deserializer rejects non-NFC strings. This prevents equivalent Unicode sequences (e.g., e as a single code point vs. e + combining acute accent) from producing different binary representations.
Depth Limiting
The deserializer enforces a maximum nesting depth of 128 to protect against stack overflows from deeply nested or malicious input, returning CordError::DepthLimitExceeded if the limit is exceeded.
use deserialize;
// Deeply nested options: Some(Some(Some(... None ...)))
// A 200-level nesting will be rejected at depth 128
let mut bytes = vec!; // 200 layers of Some(...)
bytes.push; // innermost None
let result: = ;
// Fails with CordError::DepthLimitExceeded
Feature Flags
| Feature | Default | Description |
|---|---|---|
hash |
off | Adds cord::hash() (SHA3-256 hashing) |
Supported Types Reference
| Type | Support | Notes |
|---|---|---|
| Boolean | yes | |
| Integers (i8–i128, u8–u128) | yes | Fixed-width big-endian encoding (default) |
| Integers (varints) | yes | Opt-in variable-length encoding (LEB128/zigzag) |
| Floats (f32, f64) | yes | Big-endian IEEE 754; NaN rejected, −0 canonicalized to +0 |
| Char | yes | UTF-8, NFC-normalized, with length prefix |
| Strings | yes | UTF-8, NFC-normalized, with length prefix (u32 default) |
| Byte arrays | yes | With length prefix (u32 default) |
| Sequences | yes | With length prefix (u32 default) |
| Options | yes | |
| Struct/Tuple struct | yes | |
| Enums | yes | Variant index u32 default |
| Evolving | yes | Forward-compatible enum wrapper with length-prefixed payload |
| Set | yes | Sorted during serialization |
| Map | yes | Sorted by key during serialization |
| DateTime | yes | Nanosecond-precision UTC timestamp (seconds + nanos) |
| Decimal | yes | Arbitrary-precision decimal (u8 scale + two's complement unscaled) |
| Uuid | yes | 16-byte canonical UUID |
Limitations and Trade-offs
- Not human-readable: Binary output requires tooling to inspect
- Additive schema evolution: Fields cannot be removed once added without breaking compatibility
- Wire format versioning: The format may change between major versions (v1 and v2 are not wire-compatible)
Performance
Cord v2 uses fixed-width big-endian encoding by default (16 bytes for 128-bit integers), which is fast to encode and decode. For size-sensitive applications, #[cord(varint)] and #[cord(width = N)] trade some speed for smaller output. Sets and Maps incur a sort during serialization.
Migrating from v1
Cord v2 is a breaking change — the wire format is not compatible with v1. Data serialized with v1 cannot be deserialized with v2, and vice versa. If you have persisted v1 data, you will need to migrate it (deserialize with v1, re-serialize with v2).
Current Status
Cord is a mature project that has seen production use in Backbone. Nevertheless, we urge users to:
- Thoroughly test before using in critical systems
- Be prepared for breaking changes in major versions
- Consider serialization format lock-in for long-term data storage
Roadmap
Our current priorities are:
- Comprehensive fuzzing
- Language bindings (Python, JavaScript, ...)
- Configurable limits for nested structures
- Formal verification of components
Anything else you'd like to see? Suggest a feature!
Built by Backbone