MESH ONLINECODENAME: Paranoid
v0.36
reference

Subprotocol Registry

Every Net packet carries a 16-bit subprotocol_id in its header. The id tells the receiver how to interpret the payload — plain event, causal event, snapshot, daemon migration message, NAT-traversal probe, and so on. This page is the canonical list of assigned ids, the ranges reserved for future use, and the rules that govern how the space evolves.

ID space

The space is 16 bits — 65,536 IDs. The substrate has carved out the first 0x2200 for its own use; vendors get most of the rest; the top 0x1000 is for experiments that don't need a permanent assignment.

IDPurpose
0x0000Plain events — no subprotocol, raw payload
0x0001..=0x21FFReserved for substrate / core protocols (see assignments below)
0x0400Causal events
0x0401State snapshots
0x0500Daemon migration
0x0600Subprotocol negotiation
0x0700Continuity proofs
0x0701Fork announcements
0x0702Continuity proof transfer
0x0800Partition detection
0x0801Log reconciliation
0x0900Replica group coordination (reserved, not active)
0x0A00Channel membership
0x0B00Stream-window flow control (24-byte payload, carries ack_seq for ack-driven retransmit pruning)
0x0B01Stream NACK — reliable-stream retransmit signaling
0x0B02Stream RESET — reliable-stream hard-failure signal
0x0B03Stream ACK — SACK-range acknowledgements for retransmit pruning
0x0C00Capability announcement
0x0C01Route withdrawal — poison-reverse; a node floods "destination unreachable via me" on peer failure (0.32)
0x0C02Sensing interest — capability-sensing interest registration; never emitted unless enable_sensing_coalescing is on (0.33)
0x0C03Readiness attestation — the provider-signed reply to a sensing interest; same flag gate as 0x0C02 (0.33)
0x0C04Scoped capability announcement — the encrypted owner-scoped / granted-audience form of 0x0C00 (0.34)
0x0D00NAT-traversal reflex
0x0D01NAT-traversal rendezvous
0x0E00RedEX distributed replication
0x0F00MeshDB federated query
0x1000Fold framework dispatch (typed fold envelopes, kind selects the fold)
0x1100Blob transfer (content-addressed fetch over scheduled streams)
0x1200..=0x21FFReserved for future substrate subprotocols
0x2200..=0xEFFFVendor / third-party
0xF000..=0xFFFFExperimental / ephemeral

Three of the 0x0C family are conditional, and a mesh that never enables them is byte-for-byte identical on the wire to one that has never heard of them. 0x0C02 and 0x0C03 are inert unless enable_sensing_coalescing is set (it defaults to off). 0x0C04 is emitted only by a node serving an org-protected capability: a confidential service is advertised on that id and never inside a plaintext 0x0C00 payload, so a peer without the audience key cannot open it, and a peer that has never heard of the id drops the packet at the dispatch loop's unknown-subprotocol guard. In all three cases mixed-version degradation is the same as 0x0C01: an un-upgraded peer falls back to previous behavior rather than to anything wrong.

The opaque-forwarding guarantee

Packets carrying an unregistered subprotocol_id are forwarded by intermediate nodes without modification or decryption. This is the rule that makes incremental subprotocol deployment possible: a new subprotocol can roll out to producers and consumers before every node in the mesh understands it, and the in-between nodes will pass the packets through.

The guarantee applies to forwarding only — the destination must understand the subprotocol to do anything with the packet beyond delivering it. The capability layer is the typical way to know in advance whether a destination supports a subprotocol; nodes advertise subprotocol:0x<id> tags for every protocol they handle.

Version negotiation

Each subprotocol has a major/minor version. When two peers establish a session, they exchange SubprotocolManifests listing what they support; the result is a NegotiatedSet of subprotocols both peers handle at compatible versions:

rust
pub struct SubprotocolDescriptor {
    pub id: u16,
    pub name: String,
    pub version: SubprotocolVersion,
    pub min_compatible: SubprotocolVersion,
    pub handler_present: bool,
}
 
pub struct SubprotocolVersion {
    pub major: u8,
    pub minor: u8,
}

Compatibility check: both peers' version must satisfy the other's min_compatible. Subprotocols not in the negotiated set fall back to opaque forwarding — they can still travel through the mesh, they just can't be interpreted by the peer that didn't sign up for them.

The negotiation itself rides on subprotocol 0x0600.

SUBPROTOCOL_REDEX dispatch codes

The RedEX replication subprotocol (0x0E00) partitions its payload by a single-byte dispatch code immediately after the subprotocol id:

CodeDirectionPurpose
0x20replica → leaderSyncRequest — replica asks for events [since_seq, since_seq+chunk)
0x21leader → replicaSyncResponse — bounded chunk of in-order events
0x22bidirectionalSyncHeartbeat — liveness + tail-seq exchange
0x23leader → replicaSyncNack — typed rejection (see error codes below)
0x24..=0x2FreservedFuture variants (range-bounded sync, parallel-stream sync, etc.)

SyncNack carries one of four typed error codes:

CodeNameReplica retry policy
1NotLeaderRe-resolve leader via Mesh::find_chain_holders and retry
2BadRangeTrim local tail; retry from leader's first available seq
3BackpressureExponential backoff; retry the same SyncRequest
4ChannelClosedWithdraw replica role; emit metric

There's deliberately no LeaderElection code in the replication subprotocol. Leader election is a deterministic function over each node's locally-known state (proximity-graph RTT, replica-set membership, NodeId ordering) — no election message rides on the wire. The capability-tag layer is what peers observe; the result of elect() drives a transition_to call that announces or withdraws the channel's capability tag.

Capability advertisement

Nodes advertise the subprotocols they handle through the capability graph. SubprotocolRegistry::enrich_capabilities() adds a tag subprotocol:0x<id> for each handled subprotocol to the node's CapabilitySet. Other nodes use these tags to find peers for capability-routed operations — a daemon migration request looks for nodes advertising subprotocol:0x0500, a partition-recovery handshake looks for subprotocol:0x0801.

rust
let caps = subprotocol_registry.enrich_capabilities(CapabilitySet::new());
// caps now carries: "subprotocol:0x0400", "subprotocol:0x0500", ...
 
let filter = SubprotocolRegistry::capability_filter_for(0x0500);
let migration_targets = capability_index.query(&filter);

Adding a new subprotocol

The process is the same whether you're adding to core or to a vendor extension:

  1. Pick an id. Core subprotocols go into 0x0001..=0x21FF — by convention each new subsystem claims a 0xXY00 base and uses the low byte for variants (e.g. 0x0B00 / 0x0B01 / 0x0B02 for the stream-window family). Vendor extensions go into 0x2200..=0xEFFF. Experimental work goes into 0xF000..=0xFFFF.
  2. Define a descriptor. Name, version, min_compatible. The version pair tracks breaking changes (major) and backward-compatible changes (minor).
  3. Register a handler. SubprotocolRegistry::register(descriptor); the dispatch surface picks up the registration.
  4. Enrich capabilities. Once the handler is registered, enrich_capabilities() will add the subprotocol:0x<id> tag automatically.
  5. Advertise the descriptor. Other peers see the new subprotocol on session establishment via the manifest exchange.

Forward compatibility is on by default — peers that don't understand the new subprotocol fall back to opaque forwarding, so deployment is rolling, not coordinated.