Skip to content

Cartridge Provenance-Chain Format

Wire layout and semantics of the append-only .provenance.bin file that lives on every cartridge’s SD card filesystem. The runtime owns writes; cart code can read but never mutate.

  • orchestration.md — §“Cartridge Provenance” / §“Cartridge SD Card Layout” — narrative spec.
  • cartridge-lifecycle.md — the FSM whose state transitions trigger provenance appends.
  • adr/ADR-0019 — moved provenance from a flash region to a filesystem path.
  • adr/ADR-0006 — note that .kn86 does NOT carry a chain_offset field; the filesystem path is the locator.

The provenance chain is the cartridge’s service record:

  1. Append-only — the chain only grows. The runtime never modifies or deletes existing blocks.
  2. Hash-linked — each block carries a digest of the previous block’s encoded bytes, so corruption of any past block is detectable by walking the chain.
  3. Byte-stable across compilers — explicit little-endian byte writes, no struct-packing tricks, no compiler-dependent alignment.
  4. Bounded RAM — the .provenance.bin file may grow to thousands of records; the runtime keeps a sliding tail window of PROVENANCE_MAX_RESIDENT (256) records in memory and treats the file as the source of truth. provenance_recent(N) returns the most-recent N records from the resident window.

/<cart_id>/.provenance.bin on the cartridge’s SD filesystem. Per ADR-0019, the filesystem path is the locator — there is no offset or pointer in the .kn86 container header.

Block layout (32 bytes, little-endian for multi-byte fields)

Section titled “Block layout (32 bytes, little-endian for multi-byte fields)”
Offset Size Field Notes
------ ---- ------------- ----------------------------------------
0x00 8 prev_hash Truncated chain digest of the previous
block's encoded bytes; zeros for the
genesis block.
0x08 1 event_type 0x01 = REGISTRATION
0x02 = MISSION
0x03 = RELAY_UPDATE
0x04 = LINK_SESSION
(decoder rejects all others)
0x09 1 reserved Must be zero.
0x0A 2 event_id le16 sequential counter, 1-based.
0x0C 4 timestamp le32 seconds since first registration
(relative clock; runtime supplies).
0x10 8 deck_handle First 8 bytes of the producing deck's
operator handle. NOT NUL-padded — a
handle shorter than 8 bytes is followed
by zeros.
0x18 8 payload Event-specific data; see "Payload by
event type" below.

Total: 32 bytes per block. The file is N * 32 bytes for N records. There is no file-level header — the file IS the chain. A non-multiple-of- 32 file size is treated as corruption and the runtime resets the chain to empty (with a stderr warning).

Recorded the first time a cartridge is inserted into a deck during a runtime session.

0..7 first 8 bytes of the deck's operator-handle hash (v1: mirrors
deck_handle bytes; future revisions may substitute SHA-256)

Recorded on mission completion (success) or abandon (failure).

0..1 threat_level (le16)
2..3 payout_low (le16; payout clamped to int16 range)
4..5 contract_id_lo (le16; low 16 bits of the contract id)
6 outcome_code 1 = completed, 0 = abandoned
7 reserved 0

Recorded when the Relay module applies an update affecting this cart’s template pool.

0..7 update manifest hash (truncated)

Recorded when this cart participates in a linked-play session.

0..7 remote-deck handle hash + outcome code (cart-author defined)

The orchestration spec calls out “truncated SHA-256” for the chain link. The v1 implementation uses 64-bit FNV-1a of the previous block’s full 32-byte encoded form. Rationale:

  • The threat model is bit-rot and accidental truncation, not cryptographic forgery — the chain lives inside the deck and is read by the deck.
  • FNV-1a is ~30 lines of code, has zero external dependencies, and compiles trivially on the Pi Zero 2 W cross toolchain.
  • The verifier is hash-function-agnostic in its checks (it recomputes whatever digest the appender produced and compares), so a future revision can substitute SHA-256-truncated-to-8 without changing the byte layout. Bumping the hash function is a per-cart event — old chains stay valid under the old function via tag.

If a future revision needs collision-resistance for cross-deck verification, this section must be updated and the change tagged with a event_type extension or a higher block-format version (added in a separate file alongside .provenance.bin to preserve the byte layout of the existing file).

provenance_append(..., cart_dir != NULL) writes the new block to <cart_dir>/.provenance.bin synchronously (fwrite + fflush + fclose) before returning. This is the durability-first path — the cart-yank threat model means an “eject” event may be the user’s hand removing the cart, so we cannot defer the write.

provenance_persist exists for full-rewrite cases (corruption recovery, test setup); production code should prefer the append path.

provenance_verify walks the resident chain and rejects:

  • reserved != 0
  • unknown event_type
  • event_id == 0
  • non-monotonic event_id (delta != 1)
  • timestamp going backwards
  • broken prev_hash link

provenance_verify_file performs the same checks plus a file-size- divisibility-by-32 check before decode.

On any rejection, the verify functions return the first detected fault.

SymbolValue
PROVENANCE_BLOCK_SIZE32
PROVENANCE_PREV_HASH_BYTES8
PROVENANCE_DECK_HANDLE_BYTES8
PROVENANCE_PAYLOAD_BYTES8
PROVENANCE_MAX_RESIDENT256
PROV_EVENT_REGISTRATION0x01
PROV_EVENT_MISSION0x02
PROV_EVENT_RELAY_UPDATE0x03
PROV_EVENT_LINK_SESSION0x04

Authoritative C header: kn86-emulator/src/provenance.h.

The test suite includes a 100-iteration stability check (round_trip_stable_100x in tests/test_provenance.c) that asserts byte-identical re-encoding after each decode. Any future change to the layout must keep this test green or define a sibling block format (provenance does not version a single-file format — corrupted layouts are treated as corruption).

  1. Cryptographic hash upgrade. Substituting SHA-256-truncated-to-8 for FNV-1a-truncated-to-8 is a one-day change inside provenance_hash_block. The trigger is when (if) link-session handshakes need cross-deck integrity. v1 ships with FNV-1a.
  2. RELAY_UPDATE / LINK_SESSION payload format. Both event types reserve the 8-byte payload but no cartridge has shipped yet that stamps these blocks. The Relay module spec and the eventual linked- play handshake spec will pin the payload layouts; until then the formats above are placeholders.
  3. Chain export. No tooling yet exists to read a cart’s chain on the desktop without booting the runtime. A kn86-prov-dump CLI utility is logical follow-up work for the homebrew community (spawnable as a Notion task once a cart starts accumulating real-world chain data).