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.kn86does NOT carry achain_offsetfield; the filesystem path is the locator.
Design intent
Section titled “Design intent”The provenance chain is the cartridge’s service record:
- Append-only — the chain only grows. The runtime never modifies or deletes existing blocks.
- 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.
- Byte-stable across compilers — explicit little-endian byte writes, no struct-packing tricks, no compiler-dependent alignment.
- Bounded RAM — the
.provenance.binfile may grow to thousands of records; the runtime keeps a sliding tail window ofPROVENANCE_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.
File path
Section titled “File path”/<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).
Payload by event type
Section titled “Payload by event type”REGISTRATION (0x01)
Section titled “REGISTRATION (0x01)”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)MISSION (0x02)
Section titled “MISSION (0x02)”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 = abandoned7 reserved 0RELAY_UPDATE (0x03)
Section titled “RELAY_UPDATE (0x03)”Recorded when the Relay module applies an update affecting this cart’s template pool.
0..7 update manifest hash (truncated)LINK_SESSION (0x04)
Section titled “LINK_SESSION (0x04)”Recorded when this cart participates in a linked-play session.
0..7 remote-deck handle hash + outcome code (cart-author defined)Hash function
Section titled “Hash function”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).
Persistence semantics
Section titled “Persistence semantics”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.
Verification
Section titled “Verification”provenance_verify walks the resident chain and rejects:
reserved != 0- unknown
event_type event_id == 0- non-monotonic
event_id(delta != 1) timestampgoing backwards- broken
prev_hashlink
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.
Constants reference
Section titled “Constants reference”| Symbol | Value |
|---|---|
PROVENANCE_BLOCK_SIZE | 32 |
PROVENANCE_PREV_HASH_BYTES | 8 |
PROVENANCE_DECK_HANDLE_BYTES | 8 |
PROVENANCE_PAYLOAD_BYTES | 8 |
PROVENANCE_MAX_RESIDENT | 256 |
PROV_EVENT_REGISTRATION | 0x01 |
PROV_EVENT_MISSION | 0x02 |
PROV_EVENT_RELAY_UPDATE | 0x03 |
PROV_EVENT_LINK_SESSION | 0x04 |
Authoritative C header:
kn86-emulator/src/provenance.h.
Round-trip stability
Section titled “Round-trip stability”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).
Open questions
Section titled “Open questions”- 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. - 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.
- Chain export. No tooling yet exists to read a cart’s chain on
the desktop without booting the runtime. A
kn86-prov-dumpCLI utility is logical follow-up work for the homebrew community (spawnable as a Notion task once a cart starts accumulating real-world chain data).