Skip to content

feat(wire): split the frame discriminant into trait and method bytes - #357

Draft
decrypto21 wants to merge 1 commit into
mainfrom
feat/wire-trait-method-split
Draft

feat(wire): split the frame discriminant into trait and method bytes#357
decrypto21 wants to merge 1 commit into
mainfrom
feat/wire-trait-method-split

Conversation

@decrypto21

@decrypto21 decrypto21 commented Aug 7, 2026

Copy link
Copy Markdown

Draft: not for merge yet. Tracking: #351

What

  • The frame discriminant becomes two u8s instead of one: the API trait (namespace) and the method within that trait.
    • before: [SCALE str requestId][u8 id][payload]
    • after: [SCALE str requestId][u8 trait][u8 method][payload]
  • Trait ids are explicit literals via a new trait-level #[wire_trait(id = N)] attribute, carried through the macro, rustdoc extraction, and both emitters. Missing or duplicate trait ids are hard codegen errors; there is no fallback that derives an id from declaration or module order.
  • Method ids restart from 0 within each trait, preserving each trait's existing relative order. The existing derivation rules are unchanged, now scoped per trait: a request takes n/n+1, a subscription takes n..n+3. Append-only becomes a per-trait rule.
  • Codec version goes 1 to 2 (--codec-version default, scripts/codegen.sh, the generated client constant, and the handshake's acceptance check).
  • Unknown discriminant pairs and undecodable frames now fail visibly instead of being dropped silently.

Why

  • The flat u8 is the whole address today, so new methods can only append at the global tail. Five traits already have non-contiguous id blocks as a result (account spans 18-28, 110-112, and 164), and ids 70-75 are permanently burned by the retired JsonRpc trait. 163 of 255 values are already assigned, and a single subscription costs four slots.
  • Splitting gives every trait its own 256-slot method space, keeps each trait's block contiguous permanently, and puts the namespace on the wire where dispatch and debug tooling can see it.
  • Since this is a coordinated cutover, existing ids are renumbered once and append-only resumes per trait. Preserving the old global numbers would carry today's fragmentation into the new scheme for no compatibility benefit: the envelope grows a byte, so no existing peer can parse a new frame regardless of which numbers it carries.

Compatibility

  • A peer on codec 1 talking to a new host gets an explicit UnsupportedProtocolVersion rather than a hang.
  • The reverse direction cannot be rescued by the version bump: the handshake frame itself rides the changed envelope, so an old peer cannot parse a new host's reply well enough to learn it is incompatible. It fails on its own side.
  • The codec bump plus the loud-failure changes exist so mismatches surface as errors instead of the silent-drop-then-hang failure this protocol produced the last time a byte layout moved under an unchanged version tag.
  • Phone pairing and the inter-host SSO messages are untouched: that layer carries a string message_id with its own codec indices and has no reference to this envelope's discriminant.

Verification

  • cargo test --workspace --all-features: 648 passed, 0 failed. cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, and the wasm32-unknown-unknown build of truapi-server are all clean.
  • js/packages/truapi: tsc build clean, bun test 206 passed, 0 failed. js/packages/truapi-host: 40 passed.
  • Playground: yarn build and yarn lint clean against a refreshed package snapshot.
  • Byte-level goldens were recomputed rather than relaxed, and a new test pins the handshake frame end to end so trait 0 stays the system trait. Both per-byte range guards and both truncation errors (missing trait byte, missing method byte) have distinct-message tests in Rust and TypeScript.
  • The Rust and TypeScript wire tables are compared against each other by an existing parity test, now including trait ids.
  • Reviewed across multiple adversarial lenses: the old-to-new mapping was independently re-derived for all 160 wire actions and matches the documented appendix row for row, and every generated artifact was confirmed byte-reproducible from the branch sources.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant