Three byte-exact re-encode guards exist across three separate Idunn stores -- the runtime projection, the deployment brake, and the trust bindings -- each decoding a record, re-encoding it, and refusing on any byte difference. Idunn's control store does the same for its own transactions. Three independent instances is a house pattern, and it means changing how any persisted CultCache record serializes is never a flag flip. Records the measured behaviour of #[cultcache(key = N, bytes)] rather than the assumed one: reading stays compatible both ways, so the cost lands on reproduction and therefore on those guards, not on compatibility. A field with no live producer is free; a field with persisted records needs its store rewritten in the same change. Also records that Option<Vec<u8>> cannot take the attribute today, and that a shape-validated field nobody produces may be a fossil rather than an omission -- the estate has at least one other case of a live consumer outliving its producer. This was learned by nearly deploying an attribute flip that would have made Idunn refuse the projection store Odin reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
3.6 KiB
Markdown
68 lines
3.6 KiB
Markdown
# The encoding of a persisted CultCache record is part of its contract
|
|
|
|
Changing how a stored record serializes is never a flag flip in this estate.
|
|
Consumers verify canonicality by **re-encoding what they decoded and comparing
|
|
bytes**, so a record that reads back correctly but re-encodes differently is
|
|
refused — and the refusal happens on live data, at startup or on the next read.
|
|
|
|
Three guards, three separate stores, one idiom:
|
|
|
|
| store | guard | refusal |
|
|
|---|---|---|
|
|
| Idunn runtime projection | `Idunn/src/drivers.rs:3611` | `service trust-anchor projection is noncanonical` |
|
|
| Idunn deployment brake | `Idunn/src/control_plane.rs` (`read_deployment_brake`) | `deployment brake is noncanonical or keyed by another authority` |
|
|
| Idunn trust bindings | `Idunn/src/provisioning.rs:546`, `:630` | `trust store contains a noncanonical or mismatched binding` |
|
|
|
|
Idunn's control store applies the same rule to its own transactions. Assume the
|
|
guard exists and go looking for it before changing any persisted record's
|
|
serialization; three independent instances is a house pattern, not a
|
|
coincidence.
|
|
|
|
## What this means for `#[cultcache(key = N, bytes)]`
|
|
|
|
The attribute switches a `Vec<u8>` slot from a MessagePack array-of-integers
|
|
(serde's default) to a real `bin`. Measured, not assumed
|
|
(`packages/cultnet-rs/tests/byte_slot_migration.rs`):
|
|
|
|
- **Reading stays compatible in both directions.** `serde_bytes::ByteBuf`
|
|
accepts an array marker, and a plain `Vec<u8>` accepts `bin`. No dual-read
|
|
path is needed, and reverting a binary is survivable.
|
|
- **Reproduction does not.** A record written before the flip can be read but
|
|
can never re-encode to the bytes it was stored as, and its `canonical_sha256`
|
|
moves with it.
|
|
|
|
So the cost lands entirely on the guards above, not on readability. A field with
|
|
persisted records must flip the attribute **and** rewrite its store in the same
|
|
change, one target at a time, and never during a deployment — the projection
|
|
store is what the observer reads, and refusing it takes the observer down.
|
|
|
|
A field with no live producer is free. Check for one before assuming a
|
|
migration: `IdunnAuthenticatedProviderHealthProjectionRecord` is constructed
|
|
only inside a test module, so its byte field costs nothing to convert.
|
|
|
|
Size, for whether it is worth doing at all: 406 bytes as an array against 278 as
|
|
`bin` for a 256-byte payload, and 1234 against 851 for an 848-byte one. About
|
|
1.45x on every byte, which matters on a media path and is noise on a control
|
|
record.
|
|
|
|
## `Option<Vec<u8>>` cannot take the attribute yet
|
|
|
|
The derive emits `serde_bytes::Bytes::new(&self.field)`, which needs `&[u8]`,
|
|
and builds the field via `.into_vec()`, which yields `Vec<u8>`. Both are type
|
|
errors against an `Option<Vec<u8>>`, so the derive fails loudly rather than
|
|
collapsing "absent slot" into "unsigned". `IdunnDeploymentBrakeRecord.signature`
|
|
needs an `Option`-aware path in `cultcache-rs-derive` before it can move.
|
|
|
|
## Fields validated for shape and produced by nothing
|
|
|
|
`trust_binding_sha256` is declared on two records with different schemas
|
|
(`idunn.authenticated_provider_health_projection.v1` in cultnet-rs,
|
|
`idunn.authenticated_daemon_health_admission.v1` in odin-core), shape-validated
|
|
in both, and computed by nothing in CultLib, Idunn or Odin.
|
|
|
|
Do not assume that means "omission". It may be a fossil — a contract whose
|
|
producer was deleted. The estate has form: Muninn's receiver runs a Reed-Solomon
|
|
audio FEC path against parity shards the Rust sender has never emitted, while an
|
|
acceptance ledger describes that FEC as having worked. Settling which requires
|
|
reading history, not source, and the answer changes whether the right move is to
|
|
implement the field or delete it.
|