Lace / docs

Lace-060 · Inter-Lace Transfer Protocol (ILTP)

Tags: transport, iltp

Purpose

ILTP binds the 050 interlace state machine to bidirectional byte carriers. One carrier may carry successive logical sessions. Each logical session begins with a nonce preface pair and owns fresh proof, role-assigned operand Blobs, one immutable exchange plan, concurrent symmetric rounds, and fixed point.

ILTP carries canonical operand Blobs, control fact blocks, and full H3 response records over stdio, Unix sockets, TCP, QUIB, or WebSocket byte streams. A carrier starts after the host establishes the byte stream; listening, accepting, and service admission are outside ILTP. The host owns carrier IO and proof inputs; each logical session derives immutable session authority. The codec owns framing and canonical bytes; the carrier owner handles session cuts, and 050 owns protocol meaning and lifecycle.

Fixed ILTP 1.0 profile

ILTP 1.0 is the only supported version. It fixes:

Record formats, policy profiles, advertisement capabilities, parser limits, and operation budgets are not negotiated. An incompatible set requires another protocol version.

QUIB profile authentication

QUIB means QUIC Interlace Binding. Its listener emits the exact six ASCII bytes below as authenticated QUIC handshake data:

QUIB/1

The value has no terminator. The client MUST require exact equality before it reports a connected QUIB profile. Missing or unequal authenticated handshake data is a typed transport-profile failure. These bytes are not an ILTP stream item, marker, preface, or negotiable value.

Address strings

Most addresses use scheme:address; stdio is the one colon-free literal.

Scheme Address form Default port
stdio (empty)
unix /absolute/path
tcp host[:port] 4790
quib host[:port] 4790
ws host[:port][/interlace] 80
wss host[:port][/interlace] 443

Host is a hostname, IPv4 address, or bracketed IPv6 address. Port is decimal. An absent WebSocket path means /interlace. Query strings and URL-style ws:// or wss:// forms are not standard Lace addresses.

stdio
unix:/tmp/lace.sock
tcp:127.0.0.1:4790
tcp:[::1]:4790
quib:example.com
ws:127.0.0.1:4790/interlace
wss:example.com/interlace

Stream roles and proof-assurance resolution

Before decoding establishment content, the binding fixes listener/server as side0 and connector/client as side1. The transport-owning runtime resolves one optional public override exactly once:

automatic: QUIB exporter -> ChannelBound(exporter32)
           canonical wss -> ConfidentialChannel
           otherwise     -> Disabled
override Disabled -> Disabled
override TrustConfidentialChannel -> keep ChannelBound when available,
                                     otherwise ConfidentialChannel

The host also supplies canonical Transport(address), an optional local By-secret value, and one unpredictable nonzero 32-byte carrier nonce seed. Rust nonce derivation and proof validation, not a connected host setter, derive each logical session’s nonces and final Here and PeerConfidential facts. A trusted WSS reverse-proxy path ending in backend WS uses the explicit trust override; the proxy and backend leg are then part of the trusted computing base.

Codec boundary

The codec owns:

The decoder emits one framing input at a time:

SessionPreface(nonce)
FactBlock(rows)
Record(record)

SessionPreface is consumed by the carrier owner; it is not a Datalog fact, endpoint message, record, store input, or public event. rows may be empty. A fact block carries no semantic family, phase, side, or round tag. A record carries no setup or response tag. The decoder does not track logical sessions, establishment, listings, obligations, fences, or fixed point. The carrier and 050 machine interpret each input from current owning state.

The encoder has one explicit begin_session(nonce) operation that emits the preface before ordinary items in that direction’s fresh session. Ordinary item encoding never invents or repeats a preface. The semantic machine computes outgoing canonical sizes with the same renderer and enforces the same hard caps before routing, so direct typed routing cannot bypass a connected-wire limit.

Per-item accounting

The 64 MiB codec cap applies independently to the current item, not to a caller’s input chunk or the lifetime of a decoder. push_bytes MAY contain any number of complete items. As soon as one item completes, the decoder resets current-item accounting before parsing following bytes from the same call.

The decoder rejects an item as soon as it cannot fit within its hard cap. A connected peer’s malformed, noncanonical, truncated, or oversized item is a 050 PeerProtocol failure. Local inability to canonically construct an output is an Integration failure.

Stream items

Each direction is a stream of self-delimiting items. At an item boundary, the decoder dispatches by the complete leading sequence:

Leading bytes Item
🪢: session preface
[A-Za-z] nonempty fact block
LF zero-row fact block
🖧: complete H3 Blob, Plex, or Seal record

The decoder MUST match the full UTF-8 knot and record-glyph prefixes; both begin with 0xF0. Any other leading sequence is invalid.

A stored record is delimited by 010 structure and Data-Length, not transport frames or a following blank line. Consecutive items may therefore be adjacent. WebSocket frame and message boundaries are transport fragmentation only.

Session preface and logical-session cut

Each logical session begins in each direction with exactly:

🪢: iltp/1.0 <nonce>\n

<nonce> is the 006 canonical B64A encoding of exactly 32 bytes: exactly 43 ASCII characters, no padding, and canonical zero tail bits. The decoded bytes MUST be nonzero and fresh and unpredictable for that sender’s logical session. The complete preface is exactly 59 bytes: UTF-8 🪢 (F0 9F AA A2), ASCII : iltp/1.0, the nonce, and LF. CR, CRLF, missing or extra text, padding, malformed B64A, noncanonical tail bits, wrong width, and all-zero bytes are invalid. There is no version negotiation or parser for another version.

Both roles emit their local preface without waiting for the peer. Before the initial session machine exists, only the peer preface is legal; an ordinary fact block or record is premature input. The carrier’s fixed role maps the texts to Challenge-side0 and Challenge-side1; they MUST differ within the pair. A preface is simultaneously the version marker, the sender role’s peer-proof challenge, and that direction’s hard cut into a fresh logical session. There is no challenge fact block, reset fact, or reset acknowledgment.

The complete bytes F0 9F AA A2 3A 20 are reserved for a preface at an item boundary. Once matched, malformed or unterminated remainder is a preface error and cannot fall through to another parser. Knot-shaped bytes inside Blob data remain data because record parsing consumes exactly Data-Length first. On a preface the decoder stops before following bytes, lets the carrier transition, resets per-session state, and resumes from the bounded suffix only after the fresh machine exists.

The carrier derives local nonce bytes without another host callback:

BLAKE3_KEYED(
  key = carrier_nonce_seed,
  input = "lace-iltp/1.0/session-nonce/v1" || NUL || u64be(sequence)
)

Sequence starts at zero. Derivation consumes each value; u64::MAX is usable once. An all-zero candidate, or one equal to a peer nonce already known while forming the session, retries with the next sequence; exhaustion is Integration. An equal independently crossed pair is PeerProtocol. The receiver keeps no nonce history. The seed and sequence are private carrier state, not protocol authority, policy identity, or diagnostics.

Fact blocks

A nonempty fact line is:

Predicate('constant',...)

Zero-arity facts use Predicate(). Predicate names and constants follow 030. Each line is UTF-8 NFC, begins with an ASCII letter, contains no CR, and ends with LF. One additional bare LF terminates the block:

Row(...)\n
Row(...)\n
\n

An empty fact block is one bare LF:

\n

Fact-block framing, not a row inside the block or a transport flush, completes a listing. The codec accepts zero rows generically; 050 permits an empty block only where the current semantic state permits an empty listing.

The decoder validates canonical tuple text and fact-line and item caps. The 050 machine validates family, arity, grouping, duplication, ordering, phase, request obligations, and other semantics. Peer control tuples never become Datalog or runtime facts.

Connected establishment is role ordered

Every logical session uses this fixed grammar after both prefaces:

1. side0 proof Seal or zero-row fact block
2. side1 proof Seal or zero-row fact block
3. side0 operand Blob
4. side1 operand Blob
5. both endpoints independently begin Advertising

Proofs and operands remain ordinary generic codec items. There is no proof delimiter, empty-item kind, authority codec, negotiated proof profile, operand reuse signal, or ready item. A proof is one valid Seal over an empty Blob and a Plex containing exactly:

Group: lace
App: peer-proof
Name: session/side0|side1
TAI: 0000000000:000000000
Challenge-side0: <side0 preface nonce>
Challenge-side1: <side1 preface nonce>
[Channel-Binding-Hash: <b64a digest>]

Role comes from the proof phase and MUST match Name. No other extra field is legal. ConfidentialChannel omits the binding field. ChannelBound requires B64A text for:

BLAKE3(
  "lace-session-peer-proof-channel-binding/v1"
  || NUL || "lace-🖧/quib/peer-proof-channel-binding-v1"
  || NUL || exporter32
)

The QUIB exporter output is exactly 32 bytes with label lace-🖧/quib/peer-proof-channel-binding-v1 and context iltp, from the same encrypted connection carrying this stream. The sender decodes its local preface nonce and uses those 32 bytes as the Seal Mark auxiliary input. There is no TAI replay mechanism: the fixed zero TAI is domain data, while the Mark over the fresh nonce pair supplies replay resistance.

A zero-row proof block means no presented proof. Disabled sends that block and derives no authority, but accepts either exact unbound or structurally valid bound proof after common transcript validation. Enabled assurance derives PeerConfidential(peer_key) only from an exact proof of the locally expected binding shape. Proof bytes never enter stores, facts, advertisements, transfers, or application events.

Each operand is a complete valid 010 Blob no larger than 1 MiB containing one canonical 030/040 module. Operand bytes are setup-only and never admitted. After the side1 operand, both endpoints freshly derive authority and the same immutable plan. Proof and operand bytes count toward the logical session’s establishment control-byte budget.

Round stream order

Each endpoint emits this order in its own stream for every round:

advertisement fact block
request fact block
zero or more H3 responses to the peer request set
RoundComplete fact block

The two streams progress concurrently. There is no side0-then-side1 ownership inside a symmetric round phase. Per-direction order and the 050 state machine permit temporary phase skew while preserving bounded input.

An endpoint may send its advertisement as soon as its local snapshot is ready. It sends its request only after its own advertisement has been accepted and the peer advertisement received. It sends responses only after its own request has been accepted and the peer request received. It sends its fence after all local incoming and outgoing response obligations settle.

An advertisement block contains zero or more complete advertisement records. Each begins with one Advertised/1 row immediately followed by its AdvertisedField/4 rows in field-name and numeric-index order:

Advertised('B.A.H3')
AdvertisedField('B.A.H3','Type','0','B')

The plan-derived schema governs exact projection. Duplicate advertisement records, duplicate rows, omitted schema rows, and out-of-schema rows are invalid. A side with no advertisements sends the zero-row fact block.

Each listing is exactly one fact block. Partial writes do not create additional listings.

Request blocks

A request block contains zero or more unique MayRequest/1 rows:

MayRequest('B.A.H3')

A side with no requests sends the zero-row fact block. The block is the complete frozen request set and cannot be divided across items.

Response records

Each response is one complete H3 stored-record item. Its glyph hash associates it with one outstanding peer request. The frozen request set determines the number of expected responses, so no batch delimiter is needed. A record is legal only when it matches an outstanding request in the endpoint’s Answering state.

Both directions may carry responses concurrently. Receiving, validating, and admitting responses proceeds concurrently with servicing peer requests, subject to the state-local slots and backpressure defined by 050.

Fences

Each endpoint emits one standalone block after its local Answering obligations settle:

RoundComplete()

Either fence may arrive first. A peer fence may arrive while local Answering is still finishing and occupies the round’s dedicated peer-fence slot. The two fences, not EOF or transport flush, establish the settled boundary.

If either frozen request set was nonempty, the next item in each direction is that endpoint’s next-round advertisement block. If both were empty, both endpoints enter fixed point.

Re-prefacing and hard replacement

A fresh preface at an item boundary retires that direction’s old bytes and contributes one half of a fresh logical-session pair. A preface received while a session is establishing, running a round, or at fixed point therefore resets the complete logical interlace; carrier role, address, proof assurance, channel binding, By-secret value, limits, and nonce generator remain carrier-owned. Proof result, authority facts, operands, plan, snapshots, obligations, and unsettled machine-owned observations do not survive.

Local replacement enters prefacing on a later carrier poll or step, emits one fresh local preface, and waits for the peer preface. During this wait, complete old peer items are canonically decoded and discarded. They remain subject to framing and item hard caps and one phase-local received-control budget. A malformed item is terminal because a preface cannot interrupt it. Crossed fresh prefaces form one pair without acknowledgment; a second peer preface before fresh-machine construction is a duplicate protocol failure.

After the local preface has been accepted by the ordered writer and the peer preface consumed, one fresh machine starts at side0 proof. A preface followed by fresh proof bytes in one read stops decoding at the preface until that transition is complete. There is no rollback after a preface is emitted or consumed.

Open rounds

At fixed point, a local store advance starts local Advertising. The resulting advertisement block is the peer’s round-start notification. Receiving an advertisement block at fixed point starts the same new round, records the peer listing, and starts local advertisement work.

Simultaneous local advances produce crossed advertisement blocks. Stream order coalesces them into one round without side priority or another control item. A zero-row advertisement block still starts the round.

State-local ingress and transport backpressure

The connected host hands decoded inputs to the carrier or 050 machine immediately. It reads or decodes only while the current state has a legal free peer slot and the endpoint has no active StoreCall. A preface is handled by the carrier rather than an endpoint slot. Without active StoreCall, one peer listing may arrive while matching local evaluation is pending and one peer fence while local Answering finishes.

When no legal slot is free or StoreCall is active, the host stops reading and relies on stream backpressure. At most one complete decoded input may wait for immediate handoff. Wrong-family, duplicate, skipped-phase, unrequested, or otherwise out-of-order input fails immediately rather than entering a semantic backlog.

A transport adapter may retain bounded transport bytes while semantic input is paused. Overflow of that transport buffer is a transport failure.

Retained writes

One retained writer owns one accepted connected output batch and offset until every byte is written. A batch may contain one or more complete ILTP items from one machine output. Partial writes preserve batch order and item framing. Awaiting a generic write call does not transfer semantic ownership.

On replacement, unaccepted old machine output is dropped, an accepted old batch drains completely, the fresh preface is the next accepted output, and proof and operand output waits behind it. No second writer or reset queue exists. The machine may advance after the writer accepts ownership, but bounded completion and clean close wait until the final fence batch is fully written. For QUIB, fully written at this layer means the codec writer is empty and every final ILTP byte has entered the QUIC send stream; accepting semantic output into the codec does not satisfy that boundary. A terminal local failure closes transport immediately and drops any unwritten remainder.

For browser WebSocket bindings, send() is enqueue acceptance, not completion. The host retains final-output ownership until bufferedAmount is zero or the platform supplies an equivalent completion signal. Close or error while bytes remain is a transport failure.

QUIB post-fixed-point settlement

QUIB transport completion is authorized only after 050 fixed-point entry, an empty ILTP codec writer, and transfer of every final ILTP byte into the QUIC send stream. The authorization is monotonic and belongs to the runtime/interlace composition; fixed-point knowledge alone does not authorize transport settlement. A bounded route then begins graceful finish. An open route may authorize peer settlement without locally finishing its send stream.

After authorization, peer application close with code zero and authenticated stateless reset are distinct successful settlement causes, PeerClose and PeerStatelessReset. They preserve the committed semantic result. On an open route either cause also delivers one boundary EOF to 050 so the semantic machine emits Closed(PeerClosed). Before authorization, zero-code peer close is premature orderly EOF and stateless reset is a transport failure. Nonzero peer application close is always a transport failure.

The QUIB connection owner creates and retransmits stream FIN, waits for final data and FIN acknowledgement during ordinary graceful finish, emits normal close, and retains required endpoint events. The connection first emits a RouteDrainEvent. After consuming it, the endpoint emits RouteRemoval. The host takes that effect, removes the exact generation-qualified route, and calls confirm_route_removed. An AcceptedRouteLease remains armed until that confirmation. QUIB finish, close, and drain use transport deadlines and host cancellation rather than 050’s semantic phase timeout.

End of stream

Transport disconnect ends both directions. Each incremental decoder is finalized exactly once. Finalization distinguishes an item boundary from a partial preface, fact block, or record.

Ordinary boundary EOF at 050 fixed point is clean and transitions the endpoint to closed. A QUIB close is clean only under the post-fixed-point authorization above; a close before that boundary remains premature even after semantic fixed-point entry. Boundary EOF during prefacing, establishment, or an active or mandatory round is premature peer input. EOF inside a preface or ordinary item is framing truncation. An unauthorized stateless reset and every other non-EOF transport error remain transport failures.

Example session

Symbolic hashes abbreviate real H3 values. The two streams may cross after establishment; this example shows side1’s empty advertisement becoming ready first and side1 requesting side0’s record.

side0 -> side1: 🪢: iltp/1.0 <N0>\n
side1 -> side0: 🪢: iltp/1.0 <N1>\n
side0 -> side1: \n
side1 -> side0: \n
side0 -> side1: <complete B.OP0.H3 operand Blob>
side1 -> side0: <complete B.OP1.H3 operand Blob>

side1 -> side0: \n
side0 -> side1: Advertised('B.A.H3')\n\n
side0 -> side1: \n
side1 -> side0: MayRequest('B.A.H3')\n\n
side0 -> side1: <complete B.A.H3 response record>
side0 -> side1: RoundComplete()\n\n
side1 -> side0: RoundComplete()\n\n

Because one frozen request set was nonempty, both endpoints then run another complete round. If both request blocks in that round are empty, its two fences reach fixed point.

Transport proof defaults

stdio, Unix, TCP, and WS default to Disabled. They may use the explicit TrustConfidentialChannel override only when the host can assert confidentiality, integrity protection, and endpoint confinement for the complete ILTP channel. There is no transport-authenticated By-value injection.

QUIB automatically uses its 32-byte exporter and ChannelBound proof. Explicit disable suppresses proof; explicit trust retains the real binding.

WSS automatically uses ConfidentialChannel after certificate and hostname verification. Explicit disable suppresses proof. An accepted host that labels a channel wss: is responsible for preserving WSS semantics. Plain WS behind a trusted WSS reverse proxy uses explicit trust on the backend route.

After WebSocket upgrade, ILTP bytes MUST use binary messages; text messages are invalid. The canonical omitted path is /interlace.

Hard caps and accounting

Item Hard cap
fact line 1024 bytes excluding LF
complete ILTP item 64 MiB
operand Blob 1 MiB
session preface 59 bytes
operand Blobs sent per direction per logical session 1
H3 Blob data 32 MiB, from 010

The complete-item cap resets after every item, including when one decoder input chunk contains several items. Stricter operand and 010 record limits still apply. These caps are fixed protocol limits, not peer-supplied settings.

050’s configurable operation budgets are local and unnegotiated. Received control accounting belongs to one decoder direction and prefacing, establishment, or round. Transferred-record accounting belongs to its transfer direction and round. A re-preface starts fresh logical-session accounting but does not change configured values or add a reset-specific limit.