Normative · Transport binding
SOVM-IROH — the iroh binding
The requirement keywords and the encoding rules are the specification’s conventions; shared vocabulary is terminology.
1. What a binding is and what this one binds
Section titled “1. What a binding is and what this one binds”A transport binding maps message semantics onto a wire. This one binds SSP/1’s message semantics — the capability handshake, the sync exchange, realtime mode — and SOP/1’s object transfer onto iroh (informative: iroh.computer), whose connections are QUIC (RFC 9000) between endpoints addressed by public key.
A binding MUST satisfy the five-row contract of transport §24, and MUST document how it does. This table is that statement:
| Requirement | How this binding meets it |
|---|---|
| Confidentiality | QUIC’s TLS 1.3 handshake; every stream is encrypted between the two endpoints, and a relay forwards ciphertext it cannot read (section 7) |
| Integrity | TLS 1.3 record protection; a tampered packet fails authenticated decryption |
| Peer authentication | Raw-public-key channel keys (section 3), after which trust resumption applies verbatim |
| Framing | One request-response exchange per bidirectional stream; realtime bodies on unidirectional streams (section 4) |
| Version negotiation | In-band, per messages §30; the ALPN versions the binding format only (section 2) |
2. ALPN
Section titled “2. ALPN”The ALPN string is sovm/0.
The /0 versions the binding’s wire format — the framing and stream
mapping of section 4 — and nothing else. The
protocol’s version story is ssp_version, carried in-band in every message
(messages §30), and it is deliberately
not this string: transport §23
records why — a protocol whose entire version story lives in a transport
identifier has made a change of transport a change of protocol version.
The distinction, stated once so it cannot be unread: a binding-format
revision bumps the ALPN; a protocol revision never does.
An endpoint MAY offer multiple ALPNs while a binding-format migration is in
progress — sovm/0 alongside a successor — and QUIC’s ALPN selection resolves
the overlap. That is the ordinary iroh idiom for evolving a protocol’s wire
format, and it migrates the framing, never the protocol.
3. Connection and identity
Section titled “3. Connection and identity”The iroh endpoint identity — the NodeId — is the 32-byte Ed25519 channel key of the default binding profile. Same bytes, same role: iroh authenticates its endpoints with exactly the raw-public-key shape the profile specifies, so this binding introduces no second transport identity and no mapping between two. This is the asymmetry the signature profile names deliberately: the channel key stays Ed25519 under an ES256 specification precisely because that is the format the transport ecosystem, iroh included, actually carries.
The channel key is not the node identity, and a connection is not trusted
until it resolves to one. On connection establishment, each side MUST present
a valid transport binding record (sovm/ssp-binding-v1,
transport §28) resolving its
channel key to a site_id, as the first frame on the first stream it opens
in each direction. Trust resumption
then applies verbatim: an unknown channel key, an absent or invalid binding
record, or a site_id that does not resolve as a currently trusted, not retired
peer (identity §20.4) is a hard fail — close the connection with TRUST_FAILED
(section 9), and never fall back to a pairing prompt.
4. Streams and framing
Section titled “4. Streams and framing”One request-response exchange per bidirectional stream. The requester opens the stream and sends one frame; the responder sends one frame and closes the stream. That is transport §24’s framing row met literally: the stream is the exchange, and stream closure is the response boundary.
Realtime mode’s unsolicited one-way bodies are unidirectional streams, opened by either peer on the held connection. A peer that did not negotiate realtime MUST NOT open one, and a receiver on a session where the capability was not mutually established MUST reset the stream and surface the event — reject, not drop, exactly as messages §33 requires.
A frame is a 4-byte big-endian unsigned length prefix followed by the
message’s deterministic-CBOR bytes. A receiver MUST bound the frame length it
accepts — the bound is implementation-chosen and declared per
limits §37 — and MUST fail
closed on a frame exceeding it, closing with FRAME_TOO_LARGE. The
liberal-parsing bound applies to
the CBOR inside the frame, not to the length discipline: a hostile length is
a transport error, not a malformed body.
Where padding is applied it is Padmé (the substrate’s one function), at the frame level: the CBOR body is padded to a Padmé bucket and the length prefix states the padded length. The CBOR encoding is self-delimiting, so the trailing pad bytes are unambiguous, and a receiver MUST ignore bytes after the CBOR item within a frame. Padding hides sizes, not timing (transport §27).
5. Object transfer
Section titled “5. Object transfer”SOP/1 objects move over iroh-blobs. storage_id is the iroh-blobs BLAKE3
hash of the exact encrypted object bytes —
SOP/1 §11.7 defines it and
§20.2 states the consequence; this page cites
both and restates neither. Verified streaming, download resumption and
verified range transfer are inherited from iroh-blobs, for exactly the reason
the identity is shared: the transfer verifies against the same hash the
protocol names.
Those properties belong to iroh-blobs’ bao-verified interfaces and not to every
read it offers. The crate also exposes an unvalidated range read, and the
seekable AsyncRead + AsyncSeek reader — the interface a Parquet reader is most
naturally handed — is built on that one. Bytes obtained through it carry no
binding to storage_id, so corruption of the local store after a verified
download is invisible.
SOP/1 §11.10 forbids reading object bytes
through such an interface, and for this binding that resolves to a concrete
split: the verified size probe, the bao-verified ranged get on the network leg,
and the bao-verified local range export satisfy it; the unvalidated range export
and anything built over it do not. An implementation that needs a seekable
reader builds one over the verified export. This page names which interfaces
meet the requirement; the requirement is SOP/1’s and is not restated or amended
here.
Tickets — a serialized node address plus hash — are a convenience an
implementation MAY exchange to save a discovery round trip. The protocol
identifier is storage_id alone, and a ticket MUST NOT be treated as an
authorization: holding one proves nothing about entitlement, which is
the AccessRecord’s job,
and a receiver enforces the access rules regardless of how it learned the
address.
6. Control plane
Section titled “6. Control plane”The object plane’s MLS control messages travel as ordinary request frames on this binding, inside the thin routing wrapper of SOP/1 §10.3: a destination routing token, a message kind of opaque-mls, and the opaque MLS bytes. The binding routes the wrapper and learns nothing from its contents — no group, no epoch, no message type.
7. Relay and mailbox are different roles
Section titled “7. Relay and mailbox are different roles”An iroh relay forwards live encrypted traffic between peers that are online at the same time and cannot reach each other directly. It holds nothing at rest. It MUST NOT be presented as satisfying the mailbox contract: the durability and retention-bound rows are obligations a live forwarder cannot meet, and a deployment that labels its relay a mailbox has promised store-and-forward it does not perform. Both roles remain (SOP/1 §20.4); a mesh that needs asynchronous delivery runs a mailbox as well as a relay, not instead of one.
A relay operator sees routing metadata: which channel keys talk to which, when, and how much. That is the same position transport §26 states for the mailbox operator, and it is stated here just as plainly rather than left to be inferred. Padding bounds what sizes reveal; it does not touch timing or correlation.
8. Discovery
Section titled “8. Discovery”An implementation MAY use iroh’s discovery mechanisms — DNS, pkarr, local mDNS — to resolve a NodeId to network addresses. None of them is normative, and a conforming implementation may use static addressing and nothing else.
What discovery publishes is worth naming: the mapping from a long-lived channel key to current network locations and a home relay is presence metadata, visible to whichever infrastructure serves the lookup. A deployment that must not leak presence uses static addressing or a private discovery service. Choosing between them is a deployment policy, not a protocol member.
9. Error codes
Section titled “9. Error codes”Connection closes use QUIC application close codes from the table below. The names are registered in the registry as wire-visible values.
| Code | Name | Meaning |
|---|---|---|
0x01 |
PROTOCOL_VIOLATION |
A stream or frame violated section 4: wrong stream type, a second request on a stream, a body that is not a CBOR item |
0x02 |
TRUST_FAILED |
The trust resumption hard fail: unknown channel key, absent or invalid binding record, or a site_id that is unresolvable or resolves only to a retired row |
0x03 |
FRAME_TOO_LARGE |
A length prefix exceeded the receiver’s declared bound |
0x04 |
UNSUPPORTED_BINDING_VERSION |
No mutually supported binding wire format |
A receiver of an unknown close code MUST surface it and treat the connection as closed. It MUST NOT retry-loop on it: an unknown code is a condition the peer named and this implementation cannot interpret, and reconnecting into it converts one surfaced error into a flood.
10. Decision gates
Section titled “10. Decision gates”Three gates, all closed. They are the same kind of gate as the realtime module’s — stated here, versioned with this page, and not part of the status page’s gate index. A corpus case cites those gates, never these.
GI1 interoperability two independent nodes complete a sync exchange over a direct path and over a relay, with byte-identical outcomes
GI2 object identity the storage_id returned by iroh-blobs equals the spec-derived BLAKE3 over the exact encrypted bytes, on import and on fetch
GI3 revocation a revoked node's connection hard-fails at trust resumption on the next establishment attempt, with TRUST_FAILED observable| Gate | State | Verdict |
|---|---|---|
| GI1 interoperability | closed | verdict |
| GI2 object identity | closed | verdict |
| GI3 revocation | closed | verdict |
Each verdict names the commit and the run its gate closed on, the tests that carry each clause, and what an outsider can and cannot reproduce today.
What closed GI2 and GI3, and how strong that evidence is. Both were demonstrated by two nodes, each with its own state directory, keyring and identity, running as separate processes on one host. Neither gate’s text asks for more: neither carries an independence clause, and both name properties two such nodes exhibit completely. A gate closes when its own text is satisfied, not when a sibling’s evidence catches up.
GI2 closed on an equality whose two halves have different provenance — one derived by iroh-blobs inside the transfer plane, the other by hashing the encrypted bytes directly, with no iroh-blobs in the call — asserted on the fetching node as well as on the importing one, and paired with a fetch of an object no one holds. An equality that asks iroh-blobs for both of its halves is a store agreeing with itself and closes nothing.
GI3 closed on both sides of the same establishment: the dialling node observed
TRUST_FAILED and the refusing node reported the trust failure, after a
successful establishment between that same pair had first been demonstrated
and then revoked. A peer that was never trusted proves nothing here, and a
connection that dropped for some other reason satisfies neither side.
What closed GI1, and what it cost to say so. The byte-identity clause is exercised by a run in which topology rather than a flag chose the carriage: one dataset, a direct leg to a peer with no relay configured at all, a relayed leg to a peer on a network with no route to the author, and the two legs’ materialized outcomes compared against each other and required to match rather than each merely being required to converge. Two discriminators are held beside it, one per plane, each failing on the shape it exists for, and the isolation the relayed leg rests on carries a falsifier of its own: a witness holding an interface on both networks reads the route as present, so a leg that passes because the isolation silently lapsed is separated from a leg that passes because the relay carried it.
Independent is read here as the clause’s own falsification target, not as a count of kernels. The word is in the gate to exclude what a single-process exchange fakes — one address space, one store serving both roles, one keypair standing in for two identities, and a loopback path no relay ever sees. Two containers on separate networks remove all four and force the relay by routing. The residual is one shared kernel, and therefore one clock and one network stack: real clock skew on the hybrid-logical-clock path and real path characteristics — MTU, address translation, loss, reordering — are not demonstrated by this evidence. Neither is a clause of GI1, whose byte-identity comparison is between the direct leg and the relayed leg rather than between two hosts. A two-host run would strengthen the record and is not required to close the gate; reading independent as reaching the clock would instead narrow a transport gate onto a property it does not name.
The gate closes on a named executed run, never on a pipeline’s colour. The container roster’s job step can decline to run — a wedged container daemon is a fact about the host, not about the change — and a declined run must not be counted as a passing one. The closing evidence is a single job in which the whole roster executed and every test in it passed, recorded by run identity rather than inferred from a green build. The roster remains subject to host-class faults that withdraw its evidence without contradicting it; such a run is not adverse and is not a closure either.
With all three gates closed, a stack carrying this binding is no longer a
prototype of it. SOVM-IROH remains a claim like every other on
the conformance page: an implementation states it
by satisfying the mapping and the published vectors, and the gates having
closed removes the reservation on the binding, not the burden on the
implementation.
11. What this document does not do
Section titled “11. What this document does not do”- It does not amend SSP/1 or SOP/1. Every sentence of transport, messages and SOP/1 §20 stays exactly where it is. This page maps them onto a wire and points back.
- It does not make iroh mandatory. Transport §23 requires a mesh to run at least one transport role; this binding is one way of meeting that, and another binding satisfying transport §24 may exist beside it or instead of it.
- It does not define a mailbox. Section 7 is explicit that a relay is not one, and nothing on this page satisfies the mailbox contract.
- It does not choose discovery. Section 8 permits mechanisms and names their cost; it makes none of them normative.