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.

2.1 Hashing Rules

  1. Format: Little-endian, integers are fixed width.
  2. Structs: Fields are written in declaration order.
  3. 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

ConstantValueScope
MAX_PAYLOAD_SIZE128 KiB (131072 bytes)Maximum size of ops
MAX_CAUSAL_DEPS16Maximum 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:

  1. Signature Valid: Ed25519.verify(I.author, blake3(borsh(I)), signature) is TRUE.
  2. Store ID Valid: I.store_id matches the local store.
  3. Payload Size: I.ops.len() <= MAX_PAYLOAD_SIZE.
  4. Causal Dep Count: The number of hashes in I.condition does not exceed MAX_CAUSAL_DEPS.

An accepted Intention is witnessed (committed to the witness log) when:

  1. Linearity Resolved: I.store_prev matches the current tip of I.author’s chain in this store (or is Hash::ZERO for the author’s first write). Until store_prev is available, the intention floats.
  2. Dependencies Met: For every h in I.condition.V1, h must 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 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.