Skip to content

UTCP-SBI Wire Protocol

SPEC-710 — Production

UTCP-SBI defines how SBI (Sovereign Binary Interface) frames are carried over UTCP datagrams for mesh transfer, peer coordination, and inter-node communication. It replaces the JSON-based NIP protocol wire format with zero-encoding binary frames.

Normative reference: SPEC-039 Sovereign Binary Interface (Libertaria Core)

Design Principles

PrincipleImplementation
Wire = MemorySBI containers are raw extern struct bytes. No serialization step on same-architecture peers.
Transport owns framingSBI defines containers, not framing. The 5-byte UTCP envelope is a transport concern.
Closed compression enumThree blessed algorithms. Adding more requires a protocol version bump.
One exchange, done or deadHandshake is a single message pair. No negotiation rounds.
Reject before allocateFrame size validated before any buffer allocation. Two comparisons close the DoS vector.

Envelope Format

Every UTCP-SBI message is a 5-byte envelope followed by a complete SBI frame:

┌─────────────────┬──────────┬─────────────────────────────────────┐
│ frame_len: u32  │ op: u8   │ SBI Frame (preamble + container)   │
│ (big-endian)    │          │                                     │
└─────────────────┴──────────┴─────────────────────────────────────┘
  • frame_len is the byte length of the SBI frame only; it does NOT include the 5-byte envelope itself
  • op identifies the message type for routing before SBI decode
  • The SBI frame inside is a vanilla SPEC-039 frame (24-byte preamble + container payload)

Frame Size Constraints

UTCP_SBI_PREAMBLE_MIN = 24       // SBI preamble only, no payload
UTCP_SBI_MAX_FRAME    = 262144   // 256 KB hard cap

Validation is the first operation after reading the envelope – before any buffer allocation:

if frame_len < UTCP_SBI_PREAMBLE_MIN or frame_len > UTCP_SBI_MAX_FRAME:
    send NACK("invalid_frame_size")
    close connection

Two comparisons. Both cheap. Both non-negotiable.

Op Code Table

Op CodeNameSBI TypeSizeDirection
0x01HANDSHAKESbiHandshake52B fixedBidirectional
0x10BLOCK_WANTSbiBlockWant33B fixedRequest
0x11BLOCK_PUTSbiBlockPutVariable (spine + DATA)Response
0x20DAG_SYNCSbiDagSyncVariable (spine + DATA)Bidirectional
0xF0ACKSbiAckFixedControl
0xF1NACKSbiNackVariable (error string)Control

Op code ranges:

  • 0x01–0x0F — Control (handshake, keepalive, future)
  • 0x10–0x2F — Data transfer (blocks, chunks, DAG nodes)
  • 0xF0–0xFF — Responses (ack, nack, status)

Message Definitions

HANDSHAKE (0x01)

Exchanged once at connection establishment. One message each direction – no negotiation round-trip.

SbiHandshake (52 bytes, extern struct, zero-pad aligned):

Offset  Field               Type      Size   Description
──────────────────────────────────────────────────────────────
0x00    peer_id             [32]u8    32     BLAKE3 peer identity (CellID)
0x20    capabilities        u32       4      Capability bitfield
0x24    required_features   u32       4      Features this peer requires
0x28    optional_features   u32       4      Features this peer supports optionally
0x2C    block_size          u32       4      Volume block size (for chunk alignment)
0x30    version             u16       2      Protocol version (1)
0x32    replica_count       u8        1      Scattered SB replica count
0x33    _pad                u8        1      Alignment padding
──────────────────────────────────────────────────────────────
        Total: 52 bytes (1 byte padding)

Capability bitfield:

BitFlagMeaning
0HAS_DEFLATESupports deflate compression
1HAS_ZSTDSupports ZSTD compression
2HAS_DEDUPContent-addressed deduplication on store
3HAS_RECOMPRESSCan recompress across algorithms
4–31ReservedMust be 0

Negotiation logic:

intersection = local.capabilities & remote.capabilities
missing = remote.required_features & ~intersection
if missing != 0:
    send NACK("missing_required_features")
    close connection

Optional features that aren't mutually supported are silently skipped. No negotiation round-trip; one exchange, one comparison, done or dead.

BLOCK_WANT (0x10)

Request a CAS chunk by content ID.

SbiBlockWant (33 bytes, extern struct):

Offset  Field         Type      Size   Description
──────────────────────────────────────────────────────────────
0x00    block_hash    [32]u8    32     BLAKE3-256 CID (of uncompressed content)
0x20    priority      u8        1      0=normal, 1=high, 2=critical
──────────────────────────────────────────────────────────────
        Total: 33 bytes

BLOCK_PUT (0x11)

Deliver a CAS chunk. Variable-length – spine + DATA region.

SbiBlockPut spine (40 bytes, extern struct):

Offset  Field         Type      Size   Description
──────────────────────────────────────────────────────────────
0x00    block_hash    [32]u8    32     BLAKE3-256 CID (of uncompressed content)
0x20    chunk_index   u32       4      Chunk position within file
0x24    comp_algo     u8        1      Compression algorithm (closed enum)
0x25    comp_level    u8        1      Compression level (0=default)
0x26    _pad          [2]u8     2      Alignment padding
──────────────────────────────────────────────────────────────
        Spine: 40 bytes

DATA region: compressed (or raw) chunk bytes immediately following spine

Receiver verification: BLAKE3(decompress(data)) == block_hash. If mismatch, send NACK.

DAG_SYNC (0x20)

Merkle tree synchronization. Peers exchange root hashes and missing node lists.

SbiDagSync spine (36 bytes, extern struct):

Offset  Field         Type      Size   Description
──────────────────────────────────────────────────────────────
0x00    root_hash     [32]u8    32     Merkle DAG root CID
0x20    depth         u16       2      Tree depth (for bounded traversal)
0x22    node_count    u16       2      Number of node hashes in DATA region
──────────────────────────────────────────────────────────────
        Spine: 36 bytes

DATA region: array of [32]u8 node hashes (node_count * 32 bytes)

ACK (0xF0)

Acknowledge successful receipt of a message.

SbiAck (8 bytes, extern struct):

Offset  Field         Type      Size   Description
──────────────────────────────────────────────────────────────
0x00    ref_seq       u32       4      Sequence number being acknowledged
0x04    status        u8        1      0=ok
0x05    _pad          [3]u8     3      Alignment padding
──────────────────────────────────────────────────────────────
        Total: 8 bytes

NACK (0xF1)

Reject a message or signal an error. Variable-length – optional error string in DATA region.

SbiNack spine (8 bytes, extern struct):

Offset  Field         Type      Size   Description
──────────────────────────────────────────────────────────────
0x00    ref_seq       u32       4      Sequence number being rejected
0x04    error_code    u16       2      Machine-readable error code
0x06    error_len     u16       2      Length of error string in DATA region (0 = none)
──────────────────────────────────────────────────────────────
        Spine: 8 bytes

DATA region: UTF-8 error string (error_len bytes, optional)

Mesh Transfer Integration

UTCP-SBI is the wire protocol for the Mesh Transfer system. The SBI codec and transport layer are wired directly to the CAS (Content-Addressable Storage) backend:

Op CodeHandlerCAS Operation
BLOCK_WANTMeshTransfer.handleBlockWant()CasManager.get(block_hash) → BLOCK_PUT reply
BLOCK_PUTMeshTransfer.handleBlockPut()Decompress → verify BLAKE3 → CasManager.put(data)
DAG_SYNCMeshTransfer.handleDagSync()Walk local DAG → compute missing set → respond
HANDSHAKESbiTransport.handleHandshake()Capability negotiation, peer registration

The mesh daemon dispatches frames by op code to the appropriate handler. Each handler is a self-contained function that reads the SBI container, performs the CAS operation, and sends a response. No shared mutable state between handlers.

Compression

Negotiation

Compression support is declared in the HANDSHAKE capability bitfield. The sender picks the best mutually-supported algorithm:

Sender preference order:
1. ZSTD (if both HAS_ZSTD) — best ratio, acceptable speed
2. Deflate (if both HAS_DEFLATE) — Graf compatibility
3. None — raw bytes

The comp_algo field in BLOCK_PUT tells the receiver which algorithm was used. The receiver decompresses before BLAKE3 verification – the content hash is always over uncompressed data.

Algorithm Enum

Closed set. Adding algorithms requires a protocol version bump.

ValueAlgorithmNotes
0x00NoneRaw, uncompressed
0x01DeflateGraf compatibility
0x02ZSTDLevels 1–22
0x03–0x0EReservedFuture blessed algorithms
0x0FExperimentalReceiver MUST reject unless out-of-band agreement exists

Zstd/Deflate Codec

The compression codec handles both algorithms through a unified interface:

  • Zstd: Default level 3 for BLOCK_PUT (good ratio, low latency). Level configurable per-connection via comp_level field.
  • Deflate: Level 6 (zlib default). Used primarily for Graf interop where Zstd is not available.
  • Passthrough: comp_algo = 0x00 skips compression entirely. Used for already-compressed content or tiny blocks where compression overhead exceeds savings.

BLAKE3 Content Addressing

All content hashes in UTCP-SBI use real BLAKE3 – 32-byte (256-bit) digests computed via C FFI to the reference BLAKE3 library. This is not a placeholder or substitute:

PropertyValue
AlgorithmBLAKE3 (via C library FFI)
Digest size32 bytes (256 bits)
Used inblock_hash (BLOCK_WANT, BLOCK_PUT), root_hash (DAG_SYNC), schema fingerprints
VerificationEvery BLOCK_PUT is verified: BLAKE3(decompress(data)) == block_hash

CLD Schema Fingerprints

SBI preambles carry a schema fingerprint – a BLAKE3 hash of the canonical CLD (Container Layout Descriptor). This binds each wire message to its exact struct layout:

fingerprint = BLAKE3(canonical_cld_bytes)

If a peer sends a frame with an unknown fingerprint, the receiver knows the schema has changed and can NACK with "unknown_schema" instead of silently misinterpreting fields. Schema evolution without silent corruption.

Mechanism over Policy does not mean infinite extension points. It means the mechanism is honest about its boundaries.

Connection Lifecycle

Peer A                                  Peer B
──────                                  ──────
  │                                       │
  │──── HANDSHAKE(caps, features) ───────→│
  │←─── HANDSHAKE(caps, features) ────────│
  │                                       │
  │  [capability intersection check]      │
  │  [if missing_required → NACK, close]  │
  │                                       │
  │──── BLOCK_WANT(cid, priority) ───────→│
  │←─── BLOCK_PUT(cid, data) ────────────│
  │──── ACK(seq) ────────────────────────→│
  │                                       │
  │──── DAG_SYNC(root, hashes) ──────────→│
  │←─── DAG_SYNC(root, missing) ─────────│
  │──── BLOCK_PUT(cid1, data) ───────────→│
  │──── BLOCK_PUT(cid2, data) ───────────→│
  │←─── ACK(seq) ─────────────────────────│
  │                                       │

Version Negotiation

The HANDSHAKE carries version: u16. If a peer sends JSON (old protocol), the receiver reads 4 bytes of frame_len – JSON starts with { (0x7B), which would indicate a 2GB+ frame. The max-frame-size check rejects it instantly. Clean cut, no ambiguity, no graceful degradation.

Relationship to Existing Specs

SpecRelationship
SPEC-039 (Libertaria)SBI container format – normative. UTCP-SBI uses vanilla SPEC-039 frames without modification.
SPEC-093 (RUMPK)UTCP transport layer – the L2 frame header (EtherType 0x88B5, CellID addressing, SYN/ACK/FIN/DATA flags). UTCP-SBI rides on top.
SPEC-101 (NexFS)Feature flags – required_features / optional_features bitmasks in HANDSHAKE match NexFS superblock fields for interoperability.
ADR N2UTCP over QUIC decision – architectural context for why UTCP exists.
ADR ST3CBOR wire format – UTCP-SBI supersedes CBOR for inter-node wire encoding. CBOR remains the on-disk format for DAG nodes inside NexFS.

Implementation Notes

Nim (Mesh Daemon)

The mesh daemon uses the SBI binary codec for all wire communication. Nim does copy-decode, not zero-copy – the daemon is a coordination layer, not a hot path. Wire compatibility with Zig and JS SBI implementations is what matters.

Read path: read(4B) → frame_len → validate → read(1B) → op → read(frame_len) → decode_sbi_frame().

The daemon wires decoded frames to MeshTransfer handlers that operate against the CAS backend. See Mesh Transfer for the full architecture.

Zig (NexFS CAS FFI)

NexFS exposes CAS chunks for export/import via C-FFI. The mesh daemon (Nim) calls nexfs_cas_get() to read chunks for BLOCK_PUT responses and nexfs_cas_put() to store received chunks.

JavaScript (Graf Workers)

Graf already has an SBI implementation (graf-forge/workers/sbi.js). Browser-based mesh peers can participate using the same wire format.

Security Considerations

  • Frame size cap (256 KB) prevents memory exhaustion attacks
  • Minimum frame size (24 bytes) prevents underflow/parsing attacks
  • BLAKE3 content verification on every BLOCK_PUT prevents data corruption and substitution
  • Schema fingerprint in SBI preamble prevents type confusion between struct versions
  • Closed compression enum prevents algorithm negotiation attacks
  • CellID authentication is handled by the UTCP layer (SPEC-093), not by UTCP-SBI