blueprint/core.md "Carried unknown fields" now specifies a produce-side truncation: an encode over the block ceiling drops carried unknown envelope fields until the record fits, rather than refusing to publish. The refusal it replaces was a wedge anyone who could publish at a name could drive — including against the rotation that revokes them — so the truncation is right. What it costs is the other half of the D10 promise: an old client no longer always preserves a newer client's field.
crates/core/src/seal/envelope.rs protects the two fields that carry protocol meaning today by name:
const UNCUTTABLE: &[&str] = &[GRANT_SECTION_KEY, WRITE_SEALED_KEY];
That list is frozen at the moment each binary ships. A v2.0 client has no way to learn that a v2.1 revocationPin, a second sealed body, or any other additive top-level or epochTag field is load-bearing — protocol-bearing-ness is expressed by membership in a constant that predates the field. So every already-shipped reader will silently drop it under pressure, and a v2.1 reader that treats absence as "no pin" is downgraded with nothing to detect.
The cut ranks largest first, which bounds the number of fields dropped rather than the bytes. That is a deliberate choice — it relieves the pressure in one pass instead of leaving the next edit to cut again — but it means a party padding a record with fields smaller than an honest one can aim the first cut at that honest field. Combined with the frozen list, the field most likely to be protocol-bearing is often the largest, which is the one largest-first takes.
Why this has to be decided before the v2.0 format freezes
The rule has to ride the bytes rather than the reader's build date, and that is a wire-format reservation. Retrofitting is impossible once v2.0 binaries are in the field: an old binary cannot be taught a new key name.
The cheapest shape consistent with the existing strict det-CBOR profile is a reserved key-name prefix, the same idea as COSE's crit header (RFC 8152 §3.1):
/// A carried field whose key begins with this marker is critical: a rewrite
/// keeps it or refuses, never silently drops it.
const CRITICAL_KEY_PREFIX: &str = "!";
canonical_key_cmp is length-first, so a one-byte prefix does not perturb ordering semantics. UNCUTTABLE then becomes the two grandfathered names plus the marker test.
Wanted
Found by the crypto-privacy gate on the PR implementing the truncation.
Part of #655
blueprint/core.md"Carried unknown fields" now specifies a produce-side truncation: an encode over the block ceiling drops carried unknown envelope fields until the record fits, rather than refusing to publish. The refusal it replaces was a wedge anyone who could publish at a name could drive — including against the rotation that revokes them — so the truncation is right. What it costs is the other half of the D10 promise: an old client no longer always preserves a newer client's field.crates/core/src/seal/envelope.rsprotects the two fields that carry protocol meaning today by name:That list is frozen at the moment each binary ships. A v2.0 client has no way to learn that a v2.1
revocationPin, a second sealed body, or any other additive top-level orepochTagfield is load-bearing — protocol-bearing-ness is expressed by membership in a constant that predates the field. So every already-shipped reader will silently drop it under pressure, and a v2.1 reader that treats absence as "no pin" is downgraded with nothing to detect.The cut ranks largest first, which bounds the number of fields dropped rather than the bytes. That is a deliberate choice — it relieves the pressure in one pass instead of leaving the next edit to cut again — but it means a party padding a record with fields smaller than an honest one can aim the first cut at that honest field. Combined with the frozen list, the field most likely to be protocol-bearing is often the largest, which is the one largest-first takes.
Why this has to be decided before the v2.0 format freezes
The rule has to ride the bytes rather than the reader's build date, and that is a wire-format reservation. Retrofitting is impossible once v2.0 binaries are in the field: an old binary cannot be taught a new key name.
The cheapest shape consistent with the existing strict det-CBOR profile is a reserved key-name prefix, the same idea as COSE's
critheader (RFC 8152 §3.1):canonical_key_cmpis length-first, so a one-byte prefix does not perturb ordering semantics.UNCUTTABLEthen becomes the two grandfathered names plus the marker test.Wanted
blueprint/core.mdbeside the truncation paragraph and covered by acrates/coretest asserting a marked field survives a cut that takes everything else.Found by the crypto-privacy gate on the PR implementing the truncation.
Part of #655