Weaver Protocol Specification (v1)
This document defines the core data structures and serialization rules for the Weaver Protocol.
1. Data Model
1.1 Intention
An Intention is the unsigned body of an atomic transaction targeting a specific Store. This is the content that gets hashed and signed. The signature lives in SignedIntention.
Field order matches the canonical Borsh serialization order.
struct Intention {
// 1. Identity
author: PubKey, // Ed25519 public key (32 bytes)
// 2. Metadata
timestamp: HLC, // Hybrid Logical Clock (wall_time_ms, counter)
// 3. Target
store_id: Uuid, // The Store this Intention applies to
// 4. Linearity (Per-Author Chain)
// Hash of the previous Intention by this author in this store.
// Hash::ZERO if this is the author's first write to the store.
store_prev: Hash,
// 5. Causal Graph
condition: Condition, // Explicit dependencies (DAG links)
// 6. Payload
ops: Vec<u8>, // Opaque operation bytes (interpretation left to the state machine)
}
1.2 SignedIntention
The wire/storage envelope. Wraps an Intention with its cryptographic proof.
// Rust in-memory representation
struct SignedIntention {
intention: Intention,
signature: Sig, // Ed25519 signature over blake3(borsh(intention))
}
On the wire and in storage, the intention is carried as opaque Borsh bytes (not a decoded struct):
message SignedIntention {
bytes intention_borsh = 1; // borsh(Intention) — canonical bytes used for hashing
bytes signature = 2; // Ed25519 signature (64 bytes)
}
Signing: signature = Ed25519.sign(signing_key, blake3(borsh(intention)))
Verification: Ed25519.verify(intention.author, blake3(borsh(intention)), signature)
1.3 Condition (The Dependency Graph)
The Condition enum defines the causal dependencies (DAG links) required for the Intention to be applied.
enum Condition {
// V1: All listed hashes must be witnessed before this Intention can be witnessed.
V1(Vec<Hash>),
// Future variants (V2+) reserved for programmable logic.
}
1.4 Operations
The ops field is opaque bytes. The store’s state machine decides how to interpret them. By convention, operations are wrapped in UniversalOp:
message UniversalOp {
oneof op {
bytes app_data = 10; // Application-specific payload (e.g. KV put/delete)
SystemOp system = 11; // System operations (hierarchy, peers, invites)
}
}
System operations include hierarchy management (ChildAdd, ChildRemove), peer management (SetPeerStatus), and invite management. Application stores (e.g. KvStore) put their own serialized operations in app_data.
2. Canonical Serialization (Borsh)
Weaver uses Borsh (Binary Object Representation Serializer for Hashing) for hashing and signing.
- Specification: borsh.io
- Why: Strict, portable, deterministic, and designed for consensus.
2.1 Hashing Rules
- Format: Little-endian, integers are fixed width.
- Structs: Fields are written in declaration order.
- Hash function:
blake3(borsh(intention))produces the 32-byte content hash.
2.2 Condition Canonicalization
The Vec<Hash> in Condition::V1 MUST be sorted lexically (byte-wise) before serialization. This is enforced by Condition::v1() which sorts on construction.
3. Debug View (S-Expression)
For debugging and inspection, intentions are rendered as structured S-Expressions via store debug commands. The server decodes ops using the store’s state machine (Introspectable) so both system and application operations are always fully expanded.
3.1 Format
(intention
(hash abcdef01...)
(author ed25519-pubkey-hex)
(store-id uuid-hex)
(store-prev hash-of-previous-intention)
(condition (v1 dep-hash-1 dep-hash-2))
(timestamp 1234567890 :counter 0)
(signature ed25519-sig-hex)
(ops
(system (child-add uuid-hex "alias"))))
Application data example (KvStore):
(ops
(data (put "key1" "val1")))
3.2 Pretty Printing
SExpr::to_pretty() renders multi-line output with indentation. Top-level list children are each placed on their own line; nested leaf lists stay inline.
4. Limits
| Constant | Value | Scope |
|---|---|---|
MAX_PAYLOAD_SIZE | 128 KiB (131072 bytes) | Maximum size of ops |
MAX_CAUSAL_DEPS | 16 | Maximum entries in Condition::V1 |
Both limits are enforced on local submit (before signing) and on insert (protecting against oversized intentions from the network). If a state machine needs more than 16 causal dependencies, it should structure them as a tree of intentions.
5. Validation Logic
A Node accepts and stores an Intention I if:
- Signature Valid:
Ed25519.verify(I.author, blake3(borsh(I)), signature)is TRUE. - Store ID Valid:
I.store_idmatches the local store. - Payload Size:
I.ops.len() <= MAX_PAYLOAD_SIZE. - Causal Dep Count: The number of hashes in
I.conditiondoes not exceedMAX_CAUSAL_DEPS.
An accepted Intention is witnessed (committed to the witness log) when:
- Linearity Resolved:
I.store_prevmatches the current tip ofI.author’s chain in this store (or isHash::ZEROfor the author’s first write). Untilstore_previs available, the intention floats. - Dependencies Met: For every
hinI.condition.V1,hmust be witnessed. If not, the intention floats until they arrive.
Note: linearity is enforced at witnessing time, not acceptance time. An intention with an unknown store_prev is accepted and stored, but floats until its predecessor arrives.
5.1 Floating Intentions
When an accepted Intention has unresolved store_prev or unmet causal dependencies, it is stored but not witnessed. These are called floating intentions. They are indexed by store_prev in TABLE_FLOATING_BY_PREV and automatically witnessed in cascade once their dependencies arrive (e.g., after sync delivers the missing intentions).
6. Witness Log (Total Apply Order)
When an Intention is applied to the state machine, a WitnessRecord is appended to the witness log. The canonical data type is the proto-generated lattice_proto::weaver::WitnessRecord:
message WitnessContent {
bytes store_id = 1; // UUID of the store
bytes intention_hash = 2; // blake3 hash of the applied intention
uint64 wall_time = 3; // Wall-clock time when witnessed (Unix ms)
bytes prev_hash = 4; // blake3 hash of previous WitnessRecord.content (32 bytes, all-zeros for first)
}
message WitnessRecord {
bytes content = 1; // protobuf-encoded WitnessContent
bytes signature = 2; // Ed25519 sign(node_key, blake3(content))
}
6.1 Hash Chain Integrity
The prev_hash field creates a tamper-evident chain across the witness log:
- The first witness record has
prev_hash = [0u8; 32](genesis sentinel). - Each subsequent record sets
prev_hash = blake3(previous_record.content). - On startup,
IntentionStore::rebuild_indexes()verifies the entire chain. Any break is a hard corruption error.
The IntentionStore caches last_witness_hash in memory for O(1) chain extension.
6.2 Signing and Verification
Witness records use a similar content + envelope pattern to SignedIntention, but with a different verification strictness level. Intentions use verify_hash_strict() (rejects small-order keys, checks canonical S), while witness records use verify_hash() (cofactored verification, less strict) since witness keys are under the node’s own control.
Signing: signature = Ed25519.sign(node_key, blake3(WitnessContent.encode()))
Verification: Ed25519.verify(node_pubkey, blake3(content), signature)
Helpers: sign_witness() and verify_witness() in lattice-kernel/src/weaver/witness.rs.