Skip to content

Wire envelope: split the single u8 discriminant into (trait, method) u8 pair #350

Description

@decrypto21

Context

Agreed in the SDK team chat (PG + Sergey, 2026-08-06): change the TrUAPI wire envelope's method identifier from a single u8 to two u8s — one for the namespace (the API trait), one for the method within that trait.

This is a wire-breaking change. It will not merge until every host runs the Rust-based implementation; until then the branch is kept rebased and ready.

Current state

The envelope is [SCALE str requestId][u8 discriminant][payload] (js/packages/truapi/src/transport.ts, truapi-server/src/frame.rs on the PR-104/PR-295 lineage). The single u8 is the whole address:

  • Ids are hand-written #[wire(request_id = N)] attributes in rust/crates/truapi/src/api/*.rs; a request burns 2 slots, a subscription 4. They are append-only and never reused.
  • 163 of 255 values are already assigned; ids 70–75 are permanently burned by the retired JsonRpc trait, and coin_payment alone consumed 28 slots.
  • Five traits (notifications, account, signing, statement_store, payment) already have non-contiguous id blocks because new methods can only append at the global tail.

Splitting into (trait: u8, method: u8) gives each trait its own 256-slot method space, keeps trait id blocks contiguous forever, and makes the namespace visible on the wire for dispatch and debug tooling.

Proposed change

New envelope: [SCALE str requestId][u8 trait][u8 method][payload].

  • Trait ids are assigned explicitly (new trait-level wire attribute), pinned forever.
  • Method ids restart from 0 within each trait. Since this ships as a coordinated cutover, existing method ids are renumbered once; append-only then resumes per trait.
  • Handshake codec version bumps 1 → 2. The handshake itself rides the changed envelope, so old↔new peers cannot negotiate — the point of the bump plus loud unknown-discriminant handling is that a skewed pair fails with a visible error instead of the silent drop + infinite "Handshaking" hang we shipped in the 0.6↔0.7 window (Wire regression between @novasamatech/host-api@0.8.3 and 0.8.4: silent handshake drop, misleading 'Host doesn't support it' log triangle-js-sdks#200 is the same failure mode one protocol layer up).

Scope

In truapi:

  • truapi-macros: trait-level namespace id + per-method ids in #[wire(...)]; keep the rustdoc doc-smuggling path working
  • truapi-codegen: extraction (rustdoc.rs), sort/collision/overflow logic and TS emitters (ts.rs), Rust emitters (rust/wire_table.rs, dispatcher) — collision detection becomes per-trait
  • Envelope codecs: js/packages/truapi/src/transport.ts encode/decode, truapi-server/src/frame.rs
  • Dispatch: truapi-server/src/dispatcher.rs and truapi-host TS dispatch keyed on the pair
  • Id-keyed debug surfaces: SENSITIVE_FRAME_IDS, WIRE_DECODE_TABLE, truapi-debugger frame decoding
  • Re-pin byte-exact tests: wire-equality.test.mjs golden hex, wire-table-loop.test.mjs, frame.rs frame-length asserts, codegen golden files
  • Codec version 2 + explicit error on unknown (trait, method) on both sides

Follow-up, separate PR, only if it executes quickly (per PG): consume @parity/truapi's generated wire table inside triangle-js-sdks instead of the hand-maintained allocator in packages/host-api/src/protocol/impl.ts — that table has already diverged from the spec (max id 135 vs 163), and triangle-js-sdks is retired once hosts move to the Rust SDK.

Sequencing

Open questions

  • Trait id assignment order — proposal: explicit literal per trait, starting from the current api/mod.rs order, pinned in the spec.
  • Renumber method ids per trait from 0 (proposed) vs. preserving current global numbers in the method byte.
  • Confirm codec-version bump to 2 as the cutover gate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions