.CHAOS
The .chaos file
512 bytes. The seed, the ticket, the cert, a few numbers the registry derived. So like, copying the file just gives you a copy of a receipt. The registry's row is what says who owns the seed and which copy is the current one. check reads any .chaos and tells you what the file says and what the registry says about that exact copy.
Why it's built like this
/* CHAOS .chaos file, version 3.
*
* Single source of truth for the client (C++) and the registry (TypeScript port, keep in sync).
* 512 bytes, little-endian, fixed layout. Nothing in here is secret.
*
* The roll is commit-reveal, so neither side can grind rarity:
* 1. client draws client_random (32), sends commit = blake3("CHAOS-COMMIT-v3" || client_random) with the ticket request
* 2. registry answers with a signed ticket: ticket_id, user, issued, that commit, and server_random (32)
* 3. seed_full = blake3("CHAOS-SEED-v3" || ticket_id || server_random || client_random)
* The client cannot pick client_random after seeing server_random (the commit is in the signed ticket), the
* registry cannot pick server_random knowing the outcome (it only knows the commit). instance_id is metadata,
* it is never hashed: anything the client could vary freely must stay out of the seed.
*
* Signed ranges:
* ticket_sig = ed25519( "CHAOS-TICKET-v3" || bytes[88..184) ) ticket_id, user, issued, commit, server_random
* cert_sig = ed25519( "CHAOS-CERT-v3" || bytes[8..288) ) seed, inputs, whole ticket, props, owner, nonce
* Preview rolls (no ticket) use ticket_id = 0, state = CHAOS_STATE_PREVIEW; they are never claimable.
*/The layout
Fixed, little endian, packed. One header for both the c++ client and the TypeScript registry.
#pragma pack(push, 1)
typedef struct chaos_file {
/* header ---------------------------------------------------------- 0 */
uint32_t magic; /* 0 CHAOS_MAGIC */
uint8_t version; /* 4 CHAOS_VERSION */
uint8_t state; /* 5 enum chaos_state */
uint8_t reserved0[2]; /* 6 */
/* seed and the client's half of it --------------------------------- 8 */
uint8_t seed_full[32]; /* 8 blake3, see above */
uint8_t instance_id[16]; /* 40 random, generated once per install. metadata, not hashed */
uint8_t client_random[32]; /* 56 the reveal. preview rolls: the raw seed */
/* generation ticket, exactly as the registry issued it -------------- 88 */
uint8_t ticket_id[16]; /* 88 zero for preview rolls */
uint64_t ticket_user_id; /* 104 */
uint64_t ticket_issued_ms; /* 112 unix ms */
uint8_t ticket_commit[32]; /* 120 blake3(tag || client_random), what the client sent before the roll */
uint8_t ticket_server_random[32]; /* 152 the registry's half */
uint8_t ticket_sig[64]; /* 184 ed25519 */
/* derived properties (registry recomputes, never trusts) ----------- 248 */
uint8_t family; /* 248 0..15 */
uint8_t tier; /* 249 enum chaos_tier */
uint8_t mode; /* 250 enum chaos_mode */
uint8_t reserved1; /* 251 */
uint32_t mutation_seed; /* 252 */
/* ownership (filled by the registry on claim) ---------------------- 256 */
uint64_t claimed_at_ms; /* 256 */
uint64_t owner_id; /* 264 */
uint8_t nonce[16]; /* 272 current valid copy, rotated on transfer */
uint8_t cert_sig[64]; /* 288 ed25519 */
/* presentation ---------------------------------------------------- 352 */
uint8_t preview_hash[32]; /* 352 blake3 of the 1920x1080 preview png */
uint8_t seed_display[44]; /* 384 base58 of the whole seed_full, not NUL terminated */
uint8_t display_len; /* 428 */
uint8_t reserved2[83]; /* 429 zero, room for later fields */
} chaos_file_t; /* 512 */
#pragma pack(pop)States
| Preview | Ticket all zeros, no signatures. Renders, you can keep it, can never be claimed. Worth nothing. |
| Rolled | Made with a ticket, the signed ticket is inside. Claim it or it lapses. |
| Claimed | The registry signed the cert over bytes 8..288. Yours. |
| Revoked | You gave it up, or a transfer made this copy old (its nonce doesn't match the row anymore). |
The 3 hashes
The commit is what the app sends before the roll. The seed is the hash of ticket id + the registry's random + the app's random. Instance id is just metadata, never hashed: anything the client could change freely stays out of the seed.
std::array<uint8_t, 32> commitOf(const uint8_t cr[32]) {
blake3_hasher h; tagged(h, CHAOS_TAG_COMMIT);
blake3_hasher_update(&h, cr, 32);
std::array<uint8_t, 32> out; blake3_hasher_finalize(&h, out.data(), 32);
return out;
}
Seed32 deriveSeed(const Ticket& t, const uint8_t cr[32]) {
blake3_hasher h; tagged(h, CHAOS_TAG_SEED);
blake3_hasher_update(&h, t.id, 16);
blake3_hasher_update(&h, t.server_random, 32);
blake3_hasher_update(&h, cr, 32);
Seed32 s; blake3_hasher_finalize(&h, s.data(), 32);
return s;
}What verify checks
Same function in the app, in the registry and behind the check page. Layout first, then the ticket signature, then that the reveal matches the commit, then that the seed derives from it, then the cert.
Check verify(const chaos_file_t& f, const uint8_t pk[32]) {
Check c;
auto fail = [&](const char* w) { if (c.why.empty()) c.why = w; };
c.layout = f.magic == CHAOS_MAGIC && f.version == CHAOS_VERSION && f.state <= CHAOS_STATE_REVOKED
&& f.family < CHAOS_FAMILY_COUNT && f.tier < CHAOS_TIER_COUNT && f.mode < CHAOS_MODE_COUNT && f.display_len <= 44;
if (!c.layout) { fail("layout: bad magic, version or out-of-range field"); return c; }
if (f.state == CHAOS_STATE_PREVIEW) {
c.commit = c.seed = c.cert = true; c.ticket = ticketIsZero(f);
if (!c.ticket) fail("preview carries a ticket");
return c;
}
if (ticketIsZero(f)) { fail("no ticket"); return c; }
Ticket t = getTicket(f);
c.ticket = verifyTicketSig(t, pk);
if (!c.ticket) fail("ticket signature");
auto want = commitOf(f.client_random);
c.commit = std::memcmp(want.data(), t.commit, 32) == 0;
if (!c.commit) fail("client_random does not match the commit in the ticket");
Seed32 s = deriveSeed(t, f.client_random);
c.seed = std::memcmp(s.data(), f.seed_full, 32) == 0;
if (!c.seed) fail("seed_full does not derive from ticket + client_random");
if (f.state == CHAOS_STATE_ROLLED) { c.cert = true; return c; }
c.cert = verifyCertSig(f, pk);
if (!c.cert) fail("cert signature");
if (f.owner_id == 0) fail("claimed with no owner");
return c;
}On disk
The name is the first 16 hex of the seed + .chaos, the PNG next to it is the preview the app took when you kept it. Back it up if you want, it's still just the receipt.