Summary
vitaminc-aead-value::transport and Go's vcffi describe the FFI codec as "a transport encoding, not a storage format … not a compatibility commitment". In practice every non-Rust binding has to implement it, fuzz it, and agree with every other binding on it: Go has one implementation, and the native SDK plan in cipherstash-suite (docs/plans/stack-encrypt-ffi-sdks.md) needs C, Python and then JVM, .NET, Ruby and Elixir to have one each. A codec that each language re-derives from Rust source is a compatibility commitment in everything but name.
Scope change (2026-09-12)
The original part 1, a leading version byte, is dropped. The encoding is ephemeral: it exists between a host writing wasm linear memory and the guest decoding it, within one call, and never reaches a column, a file or the network. Host and guest ship together (the Go module embeds the wasm blob; a native library ships with its binding), so the two sides of the codec are version-locked by construction and a version byte would never get to disambiguate anything.
What stays: documenting the codec in one place as the contract (the tag constants, which reuse the frozen leaf tags for scalars plus ARRAY / OBJECT / PASSTHROUGH and the CT_* node kinds; the u32 little-endian length rule; MAX_DEPTH; the passthrough embedding rule), and keeping the existing codec rather than moving to CBOR: it is small, deterministic, already hardened against hostile input (depth, counts vs remaining bytes, duplicate keys), and CBOR's flexibility would reintroduce exactly the parsing concerns it was written to avoid.
Proposal
- Document the codec as the contract, as above, in
transport.rs and the vcffi README.
- Ship a language-neutral conformance corpus in the repo: known-answer vectors for every leaf tag (the sealed
[tag] ++ payload encoding), every codec node kind including the empty containers and passthrough, and a set of hostile inputs with the expected refusal. Go's gen_fixture example is the seed. Each binding's test suite reads the corpus directly; a binding passes conformance by decoding and re-encoding every vector byte-for-byte. Scope extended (CIP-4037, decision 15): the corpus also carries the stack-encrypt guest-level objects every binding must build or read (the init config, the record plan, the per-call options object with its tagged keyset selector, and the record and term result shapes), so a Python or JS request builder proves itself against the same vectors Go does, not just against the codec framing.
- Make the tag table's governance rule a test. The README says a new tag "requires a defined decode mapping for every supported language before it ships". With a corpus, adding a tag without a vector, or a language without a mapping, fails CI instead of relying on review.
Why now
The Go binding's vcffi README already says "one codec, one hostile-input test suite, one fuzz". That is the right instinct; the corpus is what lets a second and third language inherit those tests rather than rewrite them.
Context
- cipherstash/cipherstash-suite
docs/plans/stack-encrypt-ffi-sdks.md, decisions 3 and Phase 5.
packages/aead-value/src/transport.rs, bindings/go/vcffi.
Summary
vitaminc-aead-value::transportand Go'svcffidescribe the FFI codec as "a transport encoding, not a storage format … not a compatibility commitment". In practice every non-Rust binding has to implement it, fuzz it, and agree with every other binding on it: Go has one implementation, and the native SDK plan in cipherstash-suite (docs/plans/stack-encrypt-ffi-sdks.md) needs C, Python and then JVM, .NET, Ruby and Elixir to have one each. A codec that each language re-derives from Rust source is a compatibility commitment in everything but name.Scope change (2026-09-12)
The original part 1, a leading version byte, is dropped. The encoding is ephemeral: it exists between a host writing wasm linear memory and the guest decoding it, within one call, and never reaches a column, a file or the network. Host and guest ship together (the Go module embeds the wasm blob; a native library ships with its binding), so the two sides of the codec are version-locked by construction and a version byte would never get to disambiguate anything.
What stays: documenting the codec in one place as the contract (the tag constants, which reuse the frozen leaf tags for scalars plus
ARRAY/OBJECT/PASSTHROUGHand theCT_*node kinds; theu32little-endian length rule;MAX_DEPTH; the passthrough embedding rule), and keeping the existing codec rather than moving to CBOR: it is small, deterministic, already hardened against hostile input (depth, counts vs remaining bytes, duplicate keys), and CBOR's flexibility would reintroduce exactly the parsing concerns it was written to avoid.Proposal
transport.rsand thevcffiREADME.[tag] ++ payloadencoding), every codec node kind including the empty containers and passthrough, and a set of hostile inputs with the expected refusal. Go'sgen_fixtureexample is the seed. Each binding's test suite reads the corpus directly; a binding passes conformance by decoding and re-encoding every vector byte-for-byte. Scope extended (CIP-4037, decision 15): the corpus also carries the stack-encrypt guest-level objects every binding must build or read (the init config, the record plan, the per-call options object with its tagged keyset selector, and the record and term result shapes), so a Python or JS request builder proves itself against the same vectors Go does, not just against the codec framing.Why now
The Go binding's
vcffiREADME already says "one codec, one hostile-input test suite, one fuzz". That is the right instinct; the corpus is what lets a second and third language inherit those tests rather than rewrite them.Context
docs/plans/stack-encrypt-ffi-sdks.md, decisions 3 and Phase 5.packages/aead-value/src/transport.rs,bindings/go/vcffi.