# Contubernium wire specification v1 This document specifies the canonical encoding, arithmetic contract, object schemas, and ruleset semantics precisely enough that an implementer who has never read the reference implementation can reproduce its output byte for byte. That is the whole purpose. The trust model in architecture.md rests on independent parties deriving identical results from identical evidence; if this document is ambiguous anywhere, the ambiguity becomes a place where two honest implementations can disagree and verification silently degrades into "trust the publisher." Where the reference implementation and this document disagree, **this document is wrong** and should be corrected — but the [conformance vectors](#13-conformance-vectors) are the tiebreaker in practice, because they are executable and prose is not. ## 1. Scope and conformance An implementation is **conformant at level 1** if it reproduces every digest and encoding in `tests/vectors/` exactly. Level 1 covers everything in this document. An implementation is **not** required to fetch market data, run the Phase 0 analyses, or implement any network behaviour. Those are outside the wire specification. Two independent implementations are provided in this repository: the Python reference in `src/contubernium/`, and a Rust verifier in `verifier/` written against this document. Both consume the same vectors. ## 2. Notation `0x` prefixes hexadecimal. Byte strings are written as ASCII in double quotes with `\xNN` escapes. "Integer" means an arbitrary-precision signed integer: an implementation using fixed width types must reject or promote on overflow rather than wrapping, because a wrapped value would produce a valid-looking digest for a wrong number. ## 3. Fixed-point arithmetic ### 3.1 Representation A measured quantity is an integer **significand** together with a non-negative integer **scale**. The represented value is `significand × 10⁻ˢᶜᵃˡᵉ`. Floating point must not be used anywhere in the derivation of a value that reaches a digest. This is a normative requirement, not a performance note: binary floating point cannot represent most decimal prices exactly, so two implementations that round differently at any intermediate step produce different digests from identical inputs. ### 3.2 Parsing decimal text `parse_decimal(text, scale) → significand` 1. Strip leading and trailing ASCII whitespace. 2. Reject the empty string. 3. If the first character is `+` or `-`, record the sign and remove it. 4. Split on the first `.` into an integer part and a fractional part. A missing `.` gives an empty fractional part. 5. Reject if both parts are empty, or if their concatenation is not entirely ASCII digits. This rejects `1e5`, `1.2.3`, `--1`, `abc`, and the empty string. 6. If the fractional part is longer than `scale`: - if every excess digit is `0`, truncate to `scale` digits; - otherwise **reject**. Source data carrying more precision than the declared scale means the scale is wrong, and silently discarding it would hide that. 7. Right-pad the fractional part with `0` to exactly `scale` digits. 8. Result is `int(integer_part or "0") × 10ˢᶜᵃˡᵉ + int(fractional_part or "0")`, negated if the sign was `-`. Note that `-0.00` parses to `0`, not to a negative zero; integers have no signed zero. ### 3.3 Formatting `format_fixed(significand, scale) → text` With `scale = 0`, the decimal representation of the significand. Otherwise: take the sign, take the decimal digits of the absolute value, left-pad with `0` to at least `scale + 1` digits, and insert a `.` before the last `scale` digits. `format_fixed(5, 5)` is `"0.00005"`; `format_fixed(-5, 5)` is `"-0.00005"`. Formatting is for human-facing output only. No digest input is ever produced by formatting. ### 3.4 Rounding modes Four modes are defined. Every operation that can be inexact names one explicitly; there is no default. | Mode | Identifier | Behaviour | | --- | --- | --- | | Half to even | `half_even` | Ties go to the neighbour whose last digit is even | | Half away from zero | `half_up` | Ties go away from zero, symmetrically | | Toward zero | `down` | Truncate the magnitude | | Toward negative infinity | `floor` | Truncate downward | `down` and `floor` differ on negative values, and this is the most common source of accidental divergence: many languages' integer division floors, which is not symmetric about zero and is therefore a poor default for signed prices. `divide(-9, 2, down)` is `-4`; `divide(-9, 2, floor)` is `-5`. ### 3.5 Division `divide(numerator, denominator, rounding) → integer` Reject a zero denominator. Reject an unrecognised rounding mode; do not fall back to a default. For `floor`, the result is the mathematical floor of the quotient. For the other three modes: 1. `sign` is `-1` if exactly one of numerator and denominator is negative, otherwise `+1`. 2. `n`, `d` are the absolute values. 3. `q = n ÷ d` (truncating) and `r = n mod d`. 4. If `r = 0`, or the mode is `down`, the result is `sign × q`. 5. Otherwise compare `2r` against `d`: - `2r > d` → increment `q`; - `2r = d` → increment `q` if the mode is `half_up`, or if the mode is `half_even` and `q` is odd; - `2r < d` → leave `q`. 6. The result is `sign × q`. Working in magnitudes and applying the sign at the end is what makes `half_even` and `half_up` symmetric about zero. ### 3.6 Rescaling `rescale(significand, from_scale, to_scale, rounding?) → significand` Equal scales return the input. Widening (`to_scale > from_scale`) multiplies by `10^(to_scale − from_scale)` and is always exact. Narrowing divides by `10^(from_scale − to_scale)`: if the division is exact the rounding mode is not required and must not affect the result; if it is inexact and no mode was supplied, **reject**. ### 3.7 Mean `mean(values, rounding) → significand` Reject an empty sequence. Sum exactly — arbitrary-precision integers do not overflow, so unlike a floating-point mean the result does not depend on summation order — then divide by the count using the named mode. Only the final division rounds. ## 4. Canonical encoding ### 4.1 Value model The encodable values are: null, boolean, integer, string, byte string, list, and map with string keys. Nothing else has an encoding. ### 4.2 Encoding rules | Value | Encoding | | --- | --- | | null | `n` | | false | `F` | | true | `T` | | integer | `i` ‹decimal ASCII› `;` | | string | `s` ‹byte length, decimal ASCII› `:` ‹UTF-8 bytes› | | byte string | `b` ‹byte length, decimal ASCII› `:` ‹raw bytes› | | list | `[` ‹encoded items in order› `]` | | map | `{` ‹encoded key, encoded value, …› `}` | The integer form is the canonical decimal representation: no leading zeros, no leading `+`, a leading `-` for negatives, and exactly `0` for zero. Length prefixes count **bytes, not characters**. A character count would let two different strings encode identically once concatenated with their neighbours. ### 4.3 Map key ordering Map keys are strings and are emitted in ascending lexicographic order of their **UTF-8 bytes**, never in insertion order. Duplicate keys cannot occur. UTF-8 byte order and Unicode code point order are the same thing — that is a property of the encoding — so an implementation may sort by either. What differs is **UTF-16 code unit** order, which is the default string comparison in JavaScript, Java, and C#: surrogate pairs compare below `U+E000`–`U+FFFF`, so those languages sort `U+10000` *before* `U+FFFD` while this specification requires the reverse. An implementation in such a language must sort encoded bytes explicitly rather than relying on its native string comparison. A vector covers exactly this pair. ### 4.4 Prohibitions Encoding a floating-point value is an **error**, not a coercion. A float reaching this layer means a measured quantity escaped the fixed-point discipline upstream, which is exactly the failure this design exists to prevent, so it must fail loudly. A boolean must not encode as an integer. In languages where booleans are a numeric subtype, the boolean check must precede the integer check, or `true` and `1` collide. Non-string map keys are an error. Unknown types are an error. ## 5. Domain-separated digests `digest(value, domain) = SHA-256( encode(domain) ‖ encode(value) )` `domain` is a string and is encoded by the same string rule as any other, so the digest input begins with `s` ‹length› `:` ‹domain bytes›. Prefixing the domain — rather than appending it or mixing it in — means the hash state commits to the object's kind before any of its content. Domain separation prevents a digest computed for one kind of object being accepted as the digest of another that happens to share a structure. | Object | Domain | | --- | --- | | Observation | `contubernium.observation.v1` | | Condition fragment | `contubernium.fragment.v1` | | Commitment | `contubernium.commitment.v1` | | Anchor receipt | `contubernium.anchor.receipt.v1` | | Log leaf | `contubernium.log.leaf.v1` | | Log node | `contubernium.log.node.v1` | | Log checkpoint | `contubernium.log.checkpoint.v1` | | Evidence manifest leaf | `contubernium.availability.leaf.v1` | | Evidence manifest node | `contubernium.availability.node.v1` | | Evidence manifest | `contubernium.availability.manifest.v1` | | Stake bond | `contubernium.stake.bond.v1` | | Slashing determination | `contubernium.stake.determination.v1` | | Condition response | `contubernium.read.response.v1` | | Pooled condition response | `contubernium.read.response.v2` | | Accumulator leaf | `contubernium.accumulator.leaf.v1` | | Accumulator node | `contubernium.accumulator.node.v1` | | Batch record | `contubernium.accumulator.batch.v1` | | Pool leaf | `contubernium.pool.leaf.v1` | | Pool node | `contubernium.pool.node.v1` | | Pool record | `contubernium.pool.v1` | Digests are compared as raw bytes; where rendered as text they are lowercase hexadecimal. ## 6. Object schemas Each object canonicalises to a map. Field names are given below; the encoder sorts them, so the listing order here is documentation rather than wire order. ### 6.1 Interval A half-open interval `[start, end)`. Both fields are RFC 3339 timestamps in UTC with a literal `Z`, formatted `YYYY-MM-DDTHH:MM:SSZ`. | Field | Type | | --- | --- | | `start` | string | | `end` | string | Half-open so adjacent intervals neither overlap nor gap, which is what lets fragments tile a timeline without ambiguity about which one covers an instant. ### 6.2 Scope | Field | Type | | --- | --- | | `iso` | string | | `node` | string | ### 6.3 Observation | Field | Type | Notes | | --- | --- | --- | | `source_id` | string | Who produced the value | | `scope` | map | Encoded Scope | | `interval` | map | Encoded Interval | | `quantity` | string | See §7.1 | | `unit` | string | | | `scale` | integer | Decimal scale of `value` | | `value` | integer | Significand | | `quality` | string | Publisher-reported validity | | `attestation` | map | String→string provenance; see below | `attestation` carries how the value was obtained. In Phase 0 it holds `source_url` and `source_sha256` for public data, which is genuine provenance and weaker than signed telemetry. The signing model is specified separately and is not part of wire specification v1. ### 6.4 Condition fragment | Field | Type | Notes | | --- | --- | --- | | `scope` | map | | | `interval` | map | | | `ruleset_version` | string | Pins the rules that produced `constraints` | | `constraints` | map | Rule-specific; see §7 | | `inputs` | list of string | Observation digests, **ascending**, hex | | `derivation` | map | Rule-specific intermediate detail | `inputs` is sorted because the evidence set is a set: a fragment must not depend on the order in which an implementation happened to collect its inputs. `derivation` exists so a challenger can localise *where* a recomputation diverges rather than only learning *that* it did. ## 7. Ruleset `contubernium.ruleset.v1` ### 7.0 What a rule may depend on A rule must be a function of the input **set**, never of the sequence. A fragment records its inputs as a sorted list of digests, so the order in which a publisher happened to collect observations is not recoverable from the fragment; a rule that depended on it could not be reproduced by a challenger and would therefore be unverifiable, however deterministic it looked in isolation. Where a rule must choose between several observations of the same quantity, it must break the tie on a value carried in the observations themselves — the source identifier, for instance — and never on position. A ruleset is immutable once published. The identifiers, thresholds, and units below are part of the meaning of the version string; changing any of them means minting a new version, never editing this one. ### 7.1 Constants | Name | Value | | --- | --- | | Ruleset version | `contubernium.ruleset.v1` | | Price scale | `5` (values are 10⁻⁵ USD/MWh) | | Price unit | `USD_per_MWh` | | Congestion epsilon | `50000` (that is, $0.50/MWh) | | Quantity: price | `energy_price` | | Quantity: congestion | `congestion_component` | | Quantity: loss | `loss_component` | The epsilon exists because loss modelling and publisher rounding leave a small non-zero congestion component on almost every interval; without a floor, "congested" would be true essentially always and would carry no information. ### 7.2 Rule `nodal_condition` Inputs: a scope, an interval, and a set of observations. Preconditions, each of which is an error if violated: every observation matches the fragment's scope and interval; every observation has price scale and price unit; no quantity appears twice; the `energy_price` quantity is present. `constraints`: | Key | Value | | --- | --- | | `price` | significand of `energy_price` | | `congestion` | significand of `congestion_component`, or null if absent | | `loss` | significand of `loss_component`, or null if absent | | `congested` | null if `congestion` is null, else `|congestion| ≥ 50000` | | `scale` | `5` | | `unit` | `USD_per_MWh` | `derivation`: | Key | Value | | --- | --- | | `rule` | `nodal_condition` | | `quantities_present` | sorted list of the quantity names supplied | | `congestion_epsilon` | `50000` | | `complete` | true when both congestion and loss are present | `inputs` is the ascending list of the supplied observations' digests. **Limitation.** Rejecting a duplicate quantity means v1 cannot represent corroboration: two instruments measuring the same thing at the same place cannot both appear, so the ruleset has no way to record which was used or that they disagreed. The architecture calls for exactly that capability, so a later version will need a rule that admits redundant observations with an order-independent selection and records the conflict. Until then, disagreeing instruments surface only at challenge time. ### 7.3 Rule `corridor_condition` Inputs: a corridor scope, an interval, and two fragments produced by this same ruleset version, both covering that interval. A mismatch on either is an error. `spread` is the receiving end's price minus the sending end's. A positive spread means energy is worth more at the receiving end — the direction transfer would flow if the network permitted it. `constraints`: | Key | Value | | --- | --- | | `spread` | `to.price − from.price` | | `congestion_spread` | `to.congestion − from.congestion`, or null if either is null | | `abs_spread` | `|spread|` | | `binding` | null if `congestion_spread` is null, else `|congestion_spread| ≥ 50000` | | `direction` | `to` if spread > 0, `from` if spread < 0, else `none` | | `scale` | `5` | | `unit` | `USD_per_MWh` | `derivation`: | Key | Value | | --- | --- | | `rule` | `corridor_condition` | | `from_scope` | sending scope key, `iso:node` | | `to_scope` | receiving scope key | | `from_fragment` | sending fragment digest, hex | | `to_fragment` | receiving fragment digest, hex | | `congestion_epsilon` | `50000` | `inputs` is the ascending union of both endpoint fragments' `inputs`, deduplicated: a challenger recomputing a corridor must be able to reach the original observations. ### 7.4 Sub-hourly aggregation `hourly_mean(values)` collapses sub-hourly significands to an hourly value using §3.7 with `half_even`. It is used where a market publishes real-time prices at finer granularity than the day-ahead market being compared against. An empty input is an error. ## 8. Versioning The ruleset version string appears inside every fragment and therefore inside its digest. A change to any rule, constant, or emitted field is a new version. Adding a field to `constraints` or `derivation` changes the digest of every fragment, so it is a breaking change even though it looks additive. There is no forward compatibility rule and no "ignore unknown fields" provision: a reader that does not recognise a ruleset version must decline to verify rather than guess. Superseding a published record is done by ordering, never by editing. A correction is a new observation. ## 9. Identity, signing, and challenge This section is **not** part of wire specification v1's conformance level 1. Only the signing preimage is pinned by vectors, because only the preimage must be byte-identical across implementations; the rest is behavioural and is specified here so that the Phase 2 accountability rules have something definite to attach to. ### 9.1 Device keys A device key binds a public key to a device identifier for a half-open validity interval, and names the party that enrolled it. | Field | Type | | --- | --- | | `device_id` | string | | `algorithm` | string | | `public_key` | byte string | | `validity` | Interval | | `enrolled_by` | string | Its digest uses the domain `contubernium.devicekey.v1`. An observation is attributable only if its interval falls **entirely** inside the key's validity. A measurement half-covered by an expired key is not half-trustworthy. Revocation is expressed by a validity that ends plus a successor key, never by deleting a record. A signature made while a key was valid stays verifiable after revocation, which is what keeps a historical record auditable rather than making it unverifiable the moment a device is retired. Two keys for the same device must not have overlapping validity. Who is entitled to enroll a device, and how that entitlement is itself attested, remains open. `enrolled_by` records the claim; nothing here verifies it. ### 9.2 Signing preimage signing_input(observation) = encode("contubernium.signing.observation.v1") ‖ encode(observation.canonical()) The domain differs from the observation digest domain so that a value hashed as a digest cannot be replayed as a signature preimage. The signature covers the encoding rather than the digest, so a verifier never has to trust a hash it did not compute. `ed25519` is the specified production algorithm. `hmac-sha256-test` exists for tests and vectors only: it is symmetric, so anyone able to verify can also forge, and it is named to make substituting it for the real thing uncomfortable. ### 9.3 Attribution outcomes Three failures are reported separately rather than collapsed, because they call for different responses: no covering key is an administrative problem, an algorithm mismatch is a deployment problem, and a bad signature is a security problem. ### 9.4 Time-source attestation An observation's attestation map may carry three time claims: | Key | Meaning | | --- | --- | | `time_source` | Disciplining source: `gnss`, `ptp`, `ntp`, or `free_running` | | `time_max_offset_ms` | The device's asserted bound on its clock error, integer milliseconds | | `time_synced_at` | When the clock was last disciplined, RFC 3339 UTC | These are claims, not verified facts; assessment makes them explicit and comparable. An attestation is judged against the **interval being reported**, which is the only comparison that determines whether misfiling is possible: a clock wrong by more than its interval can file an observation under a neighbouring hour, where it aggregates into a fragment that is internally consistent and wrong. Signing does not catch that, because the device honestly signed a timestamp it honestly believed. The default rule trusts a clock whose claimed error is at most one tenth of the reported interval and whose synchronisation is no older than 24 hours before the interval begins. Both thresholds are parameters. A source outside the recognised set is no evidence, not weak evidence. Assessment takes no wall-clock reading — staleness is measured against the interval, so the verdict on a historical observation is the same whenever it is computed. Time quality never changes a challenge verdict: a mistimed observation was honestly signed and correctly aggregated. It travels with the outcome as a warning, bounding how much the fragment's interval placement should be relied on. ### 9.5 Challenge verdicts A challenge recomputes a fragment from the evidence the fragment itself declared, and returns one of: | Verdict | Meaning | Exposes stake | | --- | --- | ---: | | `verified` | Recomputation reproduces the commitment exactly | no | | `superseded` | Correct as published; later evidence exists for the same scope and interval | no | | `evidence_unavailable` | Declared inputs could not be obtained | no | | `evidence_unattributed` | Evidence does not verify against an enrolled key | no | | `evidence_disputed` | Derivation correct; attributed devices disagree | no | | `evidence_fabricated` | A declared input appears in no manifest the publisher published | **yes** | | `derivation_diverged` | Recomputation from the fragment's own declared evidence differs | **yes** | Two verdicts implicate the publisher, and the criterion is **what can be decided from the record alone** rather than how serious the failure looks. Divergence is decidable: recompute and compare. Fabrication is decidable: the digest appears in no manifest the publisher published (§10.9). A failure to serve is not decidable — unreachable and refusing are indistinguishable from outside — so `evidence_unavailable` never exposes stake however adverse it is. `evidence_fabricated` requires manifests to be available to the challenger. Without them the case collapses back into `evidence_unavailable`, which is the pre-Phase-2 behaviour and is what a challenger holding no manifests must report. If a publisher aggregated its declared evidence correctly, the fragment is correctly derived even when the underlying measurements are wrong — that is an instrument fault, and slashing must not attach to it. Conflating the two would either punish honest publishers for broken sensors or hand dishonest ones a defence. Three rules govern the hard cases: **Late-arriving evidence never invalidates.** A fragment is judged against the evidence set it declared and nothing else. Evidence appearing afterwards produces a superseding fragment ordered after the original, per the append-only discipline. **Withheld evidence is a policy choice, not a fact.** Treating an unrecomputable commitment as invalid is what makes verification meaningful, and it hands anyone who can suppress evidence a way to invalidate honest commitments. The rule is therefore selectable with a stated default rather than assumed. **Fabricated evidence is presently indistinguishable from withheld evidence.** A publisher that invents input digests fails the availability check, exactly as one that declines to serve real ones does. The verdict cannot separate them from the fragment alone; doing so requires the evidence-availability obligation of Phase 2. ## 10. Commitment and anchoring A commitment is what a publisher publishes durably. It establishes *that* a claim was made, *when* in the sequence, and *who* is economically accountable. It does not establish that the claim is true: truth comes from re-deriving the fragment from its signed evidence (§9.5). A commitment carries a digest and never a measured value. Publishing condition values would make the anchor an authority on their content, and would price anchoring by how much was measured rather than by how many claims were made. *Where* it is published is a separate question, answered in §10.5 and in [decision 0001](decisions/0001-anchor-substrate.md). Nothing in §10.1–§10.4 depends on the answer. ### 10.1 Commitment Domain `contubernium.commitment.v1`. | Field | Type | Notes | | --- | --- | --- | | `fragment_digest` | string | Hex digest of the committed fragment | | `scope` | map | Copied from the fragment | | `interval` | map | Copied from the fragment | | `ruleset_version` | string | Copied from the fragment | | `publisher_id` | string | Who is accountable | | `stake_ref` | string | Opaque here; resolved by the anchoring chain | | `sequence` | integer | The publisher's own monotonic position, from zero | Scope, interval, and ruleset version are copied from the fragment rather than supplied independently. They are already fixed by the fragment's digest, so a commitment that disagreed with them would be a commitment to nothing. `sequence` is the publisher's counter rather than a chain position, so a gap is visible to someone reading the records alone and ordering survives a reorg. ### 10.2 Accumulator Anchoring one commitment per fragment does not scale — see `docs/phase2-findings.md` for the measurement. Commitments are therefore batched: a publisher accumulates an epoch's commitments into a binary hash tree and anchors the root. Leaves are the batch's commitment digests **sorted ascending**. A batch is a set. ``` leaf_hash(d) = SHA-256( encode("contubernium.accumulator.leaf.v1") ‖ raw32(d) ) node_hash(l, r) = SHA-256( encode("contubernium.accumulator.node.v1") ‖ l ‖ r ) ``` `raw32` is the 32 raw bytes the hex digest denotes, not its hex text. The tree is built level by level. Pairs are combined left to right with `node_hash`. **If a level has an odd number of nodes, the last node is promoted to the next level unchanged.** It is never paired with itself: duplication is the classic Merkle malleability defect, under which two different leaf multisets can produce the same root. A batch of zero commitments has the root ``` empty_root = SHA-256( encode("contubernium.accumulator.batch.v1") ‖ encode(0) ) ``` An epoch in which a publisher committed to nothing is a reportable state — it is how a reader distinguishes a publisher that went quiet from one that was never there — so it has a representation rather than an error. ### 10.3 Batch record Domain `contubernium.accumulator.batch.v1`. This digest, not the bare tree root, is what gets anchored. | Field | Type | Notes | | --- | --- | --- | | `publisher_id` | string | | | `epoch` | map | Encoded Interval | | `leaf_count` | integer | Number of commitments under the root | | `tree_root` | string | Hex, per §10.2 | Binding the publisher and epoch stops the same root being replayed for a different publisher or a different hour. Binding `leaf_count` closes the remaining ambiguity that promotion alone leaves about a level's width. ### 10.4 Inclusion proof A proof carries the commitment digest, its `index` among the sorted leaves, the batch's `leaf_count`, and a `path` of sibling hashes from the leaf upward, each marked with whether the sibling is on the left. Verification recomputes the root: 1. Start at `leaf_hash(commitment_digest)`, with `position = index` and `width = leaf_count`. 2. While `width > 1`: if `position == width - 1` and `width` is odd, this node was promoted — consume nothing from the path. Otherwise consume the next path entry and combine. Then `position ← position / 2` and `width ← (width + 1) / 2`, both integer division. 3. The proof is valid if the path is exactly exhausted and the computed root matches. A path with entries left over describes a taller tree than `leaf_count` admits, which is the shape a forged path takes. It must be rejected rather than ignored. An inclusion proof establishes exactly what a per-fragment anchor established: that this publisher committed to this fragment before the root was written. Batching moves the record out of the anchor; it does not weaken it. ### 10.5 Anchor substrates Publishing a batch digest is an interface. An implementation must not assume the substrate is a chain. One is specified fully below; a chain substrate may be added without changing §10.1–§10.4. A **receipt** is what publishing returns. It names its `substrate`, the `payload_digest` that was published, the `position` the substrate assigned, and substrate-specific `evidence`. Domain `contubernium.anchor.receipt.v1`. The default substrate is a **witnessed append-only log**. A chain substrate writes the 32-byte digest as calldata; its receipt evidence is chain-specific and is not pinned by this document. ### 10.6 Log tree The log's tree has the same shape as §10.2's — pair left to right, promote the unpaired node at an odd level — under different domains, and its leaves are **never sorted**. The order of entries is the record. ``` log_leaf_hash(d) = SHA-256( encode("contubernium.log.leaf.v1") ‖ raw32(d) ) log_node_hash(l, r) = SHA-256( encode("contubernium.log.node.v1") ‖ l ‖ r ) empty_log_root = SHA-256( encode("contubernium.log.checkpoint.v1") ‖ encode(0) ) ``` Separate domains from §10.2 are required, not stylistic: without them a batch leaf could be presented as a log leaf. This construction is equivalent to RFC 6962's, which defines the tree by splitting at the largest power of two below the leaf count. The two produce identical roots at every size. §10.8 depends on that equivalence. ### 10.7 Checkpoint Domain `contubernium.log.checkpoint.v1`. A signed statement of what the log contained at one moment. | Field | Type | Notes | | --- | --- | --- | | `log_id` | string | | | `tree_size` | integer | Entries covered | | `root_hash` | string | Hex, per §10.6 | | `issued_at` | string | RFC 3339 UTC, **supplied as data** | `issued_at` is never read from a clock: a verifier's result must not depend on when it runs. The signing preimage is domain-separated from the digest, as in §9.2: ``` checkpoint_signing_input(c) = encode("contubernium.signing.checkpoint.v1") ‖ encode(c) ``` A **cosignature** is `{witness_id, signature}` over that preimage. Witness identifiers resolve through the same key registry as devices (§9.1), and a key covers an instant when `validity.start ≤ instant < validity.end` — half-open, so a key expiring exactly at the checkpoint instant does not cover it. Counting a quorum: distinct witnesses only. A repeated `witness_id` is one witness, and admitting it twice would let a single cooperating party manufacture a quorum. **The witness policy is the reader's, not the log's.** A log does not declare its witnesses; a reader declares whose cosignature it requires — a set of named parties plus a quorum floor. A log announcing its own witness set is a log vouching for itself, and a quorum alone is satisfiable by any *n* parties an operator running its own witnesses can produce. See [decision 0003](decisions/0003-witness-policy-is-the-readers.md). A reader auditing a log over time must obtain a **consistency proof** (§10.8) against a checkpoint it already holds before treating a new one as established. A checkpoint accepted without one may be retained — discarding it helps nobody — but must be distinguished from a proven one. A reader that accumulates checkpoints without linking them holds a collection, not a history. **Equivocation.** Two checkpoints with the same `log_id` and the same `tree_size` but different `root_hash` cannot both be honest, and the pair is itself the proof. Checkpoints at *different* sizes are undecidable from the checkpoints alone; use §10.8. ### 10.8 Consistency proof That the tree of size *m* is a prefix of the tree of size *n*. Without it, "append-only" is a promise rather than a checkable claim. The proof is a list of node hashes, generated by RFC 6962's `SUBPROOF` over the §10.6 tree. Verification rebuilds **both** roots from the same nodes and requires both to match; a proof that reconstructs only the new root establishes nothing about what preceded it. ``` verify(m, root_m, n, root_n, proof): if m > n: reject if m == n: accept iff proof is empty and root_m == root_n if m == 0: accept iff proof is empty # every tree extends the empty one fn, sn = m - 1, n - 1 while fn is odd: fn >>= 1; sn >>= 1 if proof is empty: reject if fn != 0: first = second = proof[0]; i = 1 else: first = second = root_m; i = 0 while sn != 0: if i >= len(proof): reject if fn is odd or fn == sn: first = log_node_hash(proof[i], first) second = log_node_hash(proof[i], second) while fn != 0 and fn is even: fn >>= 1; sn >>= 1 else: second = log_node_hash(second, proof[i]) i += 1; fn >>= 1; sn >>= 1 accept iff i == len(proof) and first == root_m and second == root_n ``` A proof with nodes left over is rejected rather than truncated. ### 10.9 Evidence manifest An **evidence manifest** is a publisher's statement of which observations it holds and how long it undertakes to serve them. Its purpose is to separate two failures that otherwise look identical: an input that was never published, and one that was published and could not be obtained. Leaves are observation digests **sorted ascending** — the evidence set is a set. Same tree shape as §10.2 and §10.6, under a third pair of domains. ``` manifest_leaf_hash(d) = SHA-256( encode("contubernium.availability.leaf.v1") ‖ raw32(d) ) manifest_node_hash(l, r) = SHA-256( encode("contubernium.availability.node.v1") ‖ l ‖ r ) empty_manifest_root = SHA-256( encode("contubernium.availability.manifest.v1") ‖ encode(0) ) ``` The manifest record, domain `contubernium.availability.manifest.v1`: | Field | Type | Notes | | --- | --- | --- | | `publisher_id` | string | Whose undertaking this is | | `epoch` | map | Encoded Interval — what period the evidence covers | | `retention` | map | Encoded Interval — how long the publisher undertakes to serve it | | `leaf_count` | integer | Observations listed | | `tree_root` | string | Hex, per above | `retention` must be non-empty: a window that has already closed is not an undertaking. It is half-open like every interval here, so an obligation ending exactly at an instant has ended at that instant. A manifest carries digests, never observations. Publishing measured values here would put them in the anchored record, which §10 exists to avoid. **Classifying an unobtainable input.** Given the publisher, the missing digest, the manifests a challenger holds, and an instant: | Condition | Finding | | --- | --- | | Appears in no manifest by that publisher | **fabricated** — never held | | Appears, and `retention` covers the instant | **withheld** — obligation live | | Appears, and `retention` has closed | **expired** | Where more than one manifest by the same publisher lists the digest, the one whose retention runs latest applies: a publisher that renewed its undertaking is held to the renewal. **What may be penalised.** Only findings decidable from the record. Fabrication is: the digest appears in no manifest. A failure to serve is not — unreachable and refusing are indistinguishable from outside, and penalising unreachability would let anyone who can drop packets burn an honest publisher's stake. See §9.5. ### 10.10 Pool record Anchor writes scale linearly with publishers: a batch root commits one publisher's fragments, so batching buys a constant factor per publisher and the aggregate cost returns at scale. A **pool** amortises the write across publishers — one more level of exactly the tree of §10.2, whose leaves are **batch digests sorted ascending**, under a fourth pair of domains: ``` pool_leaf_hash(d) = SHA-256( encode("contubernium.pool.leaf.v1") ‖ raw32(d) ) pool_node_hash(l, r) = SHA-256( encode("contubernium.pool.node.v1") ‖ l ‖ r ) ``` There is **no empty pool root**. A publisher's empty batch is a reportable state — silence is information about a standing identity — but a pool exists only to amortise an anchor write, and sealing zero members would spend the write to prove nothing about nobody. A pool over no batches is refused at construction. The pool record, domain `contubernium.pool.v1` — this digest, not the bare tree root, is what gets anchored: | Field | Type | Notes | | --- | --- | --- | | `pool_id` | string | Whose sealing this is | | `epoch` | map | Encoded Interval — the sealing round this record closes | | `leaf_count` | integer | Member batches under the root | | `tree_root` | string | Hex, per above | `epoch` labels the sealing round. It is **not** a claim about the member batches' own epochs, each of which is already bound inside its batch digest (§10.3); a verifier makes no cross-check between the two, because there is nothing such a check would protect. **Attribution does not move.** A leaf is a batch digest, and a batch digest binds `publisher_id`, epoch, leaf count, and tree root. A pool operator has no field in which to attribute one publisher's batch to another — the lie would change the digest, hence the leaf, hence the root. Likewise disputes: a challenge is against a *commitment* (§13.6), which rides under its publisher's own batch root, so no arrangement of pool membership can shift a dispute from one bond to another. **Pool inclusion proof.** Same fields and same verification algorithm as §10.4, with `batch_digest` in place of `commitment_digest` and the pool domains in place of the accumulator's. A verifier walking a pooled response checks the batch's inclusion under the pool root exactly as it checks a commitment's inclusion under the batch root, one level down. ## 11. Stake and slashing Like §9, this section is **not** part of conformance level 1's executable surface beyond the two record digests below. It is specified here so that the accountability rules have something definite to attach to, and because a determination that two parties compute differently is worse than no determination at all. The governing constraint: a slashing determination is a **pure function** of a challenge outcome, a bond record, and a versioned schedule. No clock, no registry, no judgment. That is what lets a contract, an escrow agent, and a counterparty's lawyer each arrive at the same answer without trusting whoever ran it. Nothing here assumes a token. A bond declares its own `unit` and `scale`, and `stake_ref` is opaque — it resolves against whatever the custodian is, and no part of this specification dereferences it. See [decision 0002](decisions/0002-token-is-conditional.md). ### 11.1 Bond Domain `contubernium.stake.bond.v1`. | Field | Type | Notes | | --- | --- | --- | | `party_id` | string | | | `role` | string | `publisher` or `challenger` | | `stake_ref` | string | Opaque; resolved by the custodian | | `unit` | string | Declared, never assumed | | `scale` | integer | Decimal scale of `amount` | | `amount` | integer | Significand | | `coverage` | map | Encoded Interval — when the bond stands behind claims | | `custodian` | string | | A bond covers an interval only when `coverage.start ≤ interval.start` and `interval.end ≤ coverage.end` — the same whole-interval rule as §9.1, for the same reason. A claim half-covered by a lapsed bond is not half-accountable. ### 11.2 Schedule Version `contubernium.slashing.v1`. Immutable once published: changing a fraction mints a new schedule rather than reinterpreting past determinations. | Verdict | Publisher forfeit | Challenger forfeit | | --- | ---: | ---: | | `derivation_diverged` | 10000 bp | — | | `evidence_fabricated` | 10000 bp | — | | `verified` | — | 2500 bp | | all others | — | — | The publisher's priced verdicts must be **exactly** the set in §9.5 that exposes stake. If they diverge, either a decidable finding is priced at nothing or an undecidable one carries a penalty, and both failures are silent. Only `verified` penalises a challenger: a challenge that surfaces a real problem costs nothing even when the problem turns out to be someone else's fault. The challenger's fraction is far below the publisher's because challenging is the enforcement mechanism — it should deter noise without deterring use. **Verifying is not challenging.** Recomputing a fragment from its evidence is free and unbonded, and must stay that way. A challenge is a bonded assertion that a specific commitment is wrong. **These fractions are not measured.** The mechanism is specified; the numbers are placeholders with a stated rationale, and setting them is a governance question with no answer yet. ### 11.3 Determination Domain `contubernium.stake.determination.v1`. | Field | Type | Notes | | --- | --- | --- | | `schedule_version` | string | Which schedule was applied | | `fragment_digest` | string | What was challenged | | `verdict` | string | Per §9.5 | | `role` | string | Whose bond | | `bond_digest` | string | Which bond, per §11.1 | | `stake_ref` | string | Copied from the bond | | `forfeit_bp` | integer | 0–10000 | | `forfeit_amount` | integer | Significand at `scale` | | `unit` | string | | | `scale` | integer | | `forfeit_amount = divide(amount × forfeit_bp, 10000, down)`. Rounding **toward zero** is normative and is a policy choice stated rather than inherited: a rounding error must never take more than the schedule says. A determination is produced only where a bond covers the interval. Where none does, the outcome is that no determination was made — a commitment nobody bonded is a real state of the record and must be legible as one, which means bonding is checked when a commitment is accepted rather than after it is disputed. ## 12. Public read path The coordination layer sits outside the trust boundary: read-only with respect to the record, and not required to trust whoever serves it. So a read returns **proofs, not assurances**. A consumer that must trust the responder has gained nothing over calling the source directly. ### 12.1 Condition response Domain `contubernium.read.response.v1`. | Field | Type | Notes | | --- | --- | --- | | `fragment` | map | The condition | | `commitment` | map | Per §10.1 | | `inclusion` | map | Per §10.4 — the commitment under the batch root | | `batch` | map | Per §10.3 | | `receipt` | map | Per §10.5 | | `manifest_digest` | string or null | Where the evidence was undertaken to be served (§10.9) | The observations are **not** included. A read path that inlined them would move the whole evidence set on every query, and a consumer wanting only the condition does not need it. What it needs is to know the evidence exists and who owes it, which the manifest digest gives. **Version 2: the pooled response.** Domain `contubernium.read.response.v2`. A batch anchored through a pool (§10.10) has two more links between the batch and the receipt, and such a response is a new version rather than a reinterpretation — a v1 record's bytes never move, and an implementation reading a version it does not know MUST refuse rather than best-guess. v2 carries every v1 field plus: | Field | Type | Notes | | --- | --- | --- | | `pool` | map | Per §10.10 | | `pool_inclusion` | map | Per §10.10 — the batch under the pool root | Both are present or the record is malformed; there is no flag to disagree with the evidence. ### 12.2 Verification A consumer checks the chain locally: 1. `commitment.fragment_digest` equals the fragment's digest; 2. the commitment's scope, interval, and ruleset version equal the fragment's; 3. `inclusion.commitment_digest` equals the commitment's digest; 4. the inclusion proof reaches `batch.tree_root` (§10.4); 5. `receipt.payload_digest` equals `batch.digest`; 6. the receipt verifies under its substrate (§10.5). For a v2 response, step 5 becomes three links: `pool_inclusion.batch_digest` equals `batch.digest`; the pool inclusion proof reaches `pool.tree_root` (§10.10); and `receipt.payload_digest` equals `pool.digest` — the *pool* record is what was anchored. All are checked and **every** failure is reported, not just the first: a consumer diagnosing a responder needs to know whether one link broke or all of them. A receipt naming a substrate the reader cannot check is reported as such rather than accepted or rejected. It may be perfectly good under a substrate this reader was not built to understand. Success establishes **publication, not truth**: that this publisher committed to this fragment at this position before the root was written. Whether the fragment is correct comes from re-deriving it against the evidence (§9.5). ### 12.3 Staleness Every assessment reports the seconds elapsed between the fragment's `interval.end` and an instant the consumer supplies. A negative age means the interval has not closed — a real state, since a condition describes a bounded past interval. **The tolerance belongs to the consumer**, not the protocol. A scheduler placing load next hour and a settlement process reconciling last month have nothing useful in common, and a protocol-level threshold would be wrong for both. Age degrades a condition; it never invalidates one. ### 12.4 Degradation A response is never withheld for being thinly evidenced. The assessment reports how many inputs the fragment consumed and whether its ruleset marked the evidence complete — with **not reported** distinguished from **reported incomplete** — and the consumer widens its own margin. Refusing to answer would leave the consumer with exactly the defensive margin the substrate exists to remove. ## 13. Service wire format Sections 1–12 define what a record *is*. This section defines how one moves between processes, and it is a separate question with a separate answer: the canonical encoding of section 4 is one-way by design, because a verifier must never accept a digest it did not compute from a record it parsed itself. A transport therefore needs its own encoding, and exactly one property is required of it: > `decode(encode(record))` MUST hash to what `record` hashes to, for every record in section 6. Conformance to sections 1–12 does not require implementing this section. An implementation that only verifies records somebody hands it needs nothing here. An implementation that wants to be a *node* in a deployment does. ### 13.1 Transport JSON over HTTP/1.1. Request and response bodies are JSON objects encoded in UTF-8. A node MUST bound the request body it will read and MUST reject a `Content-Length` above that bound without reading it. ### 13.2 Tagged scalars **A JSON number MUST NOT be used for any value that reaches a digest.** JSON numbers are IEEE 754 doubles in most parsers, and a fixed-point significand at scale 9 exceeds 2⁵³ at roughly nine million units — so the loss is silent, value-dependent, and would surface only as a digest mismatch nobody could reproduce. Every scalar carries a one-character type tag naming its canonical type: | tag | canonical type | example | | --- | --- | --- | | `i:` | integer | `"i:-3315976"` | | `s:` | text | `"s:CAISO"` | | `b:` | byte string | `"b:9f2c…"`, lowercase hex | Booleans and null map to JSON `true`/`false`/`null`, which JSON represents unambiguously. Map keys are untagged, since the canonical encoding admits only text keys. The tag payloads have exact grammars, and they are stricter than most languages' built-in parsers: - integer payload: `0|-?[1-9][0-9]*` — ASCII digits only, no whitespace, no `+`, no leading zeros, and no `-0`, so that every integer has exactly one wire form and encoding is a bijection; - bytes payload: `([0-9a-f]{2})*` — lowercase, even length, no separators. A decoder MUST reject an untagged JSON string, MUST reject a bare JSON number, and MUST reject a tagged payload outside its grammar. All are the sender's error, and coercing any of them would either produce a record hashing to something its author never signed or leave two conformant nodes disagreeing on whether a payload is valid — which is a split view of the record even when they agree on every payload both accept. Tags apply to every scalar in a body, including ones a handler adds that are not part of any record. Envelope *keys* are ordinary names. ### 13.3 Records on the wire A record's wire form carries the fields of the record, not the fields of its `canonical()` structure. Three records carry more than they hash: - a batch carries its commitment digests, though its digest covers only the leaf count; - an evidence manifest carries its observation digests, on the same terms; - a pool carries its member batch digests, on the same terms. A peer needs the leaves to build its own proof. None affects a digest, which is taken over the canonical structure of section 6 and never over the wire form. A **v2 response** additionally carries a `version` key — `s:contubernium.read.response.v2` — that exists only on the wire: the canonical structure has no version field, because the domain is what carries the version. The key exists for the other implementation. A decoder that has never heard of pooling refuses a marked v2 record outright instead of quietly reading the fields it recognises and reporting a verdict about a chain it did not walk; a decoder MUST refuse a versionless response that carries pool fields, and any version it does not implement. ### 13.4 Node interfaces Six roles. Every path below is relative to a node's base URL, and every node answers `GET /health`. **Log.** | method | path | purpose | | --- | --- | --- | | `POST` | `/entries` | submit a payload digest; returns its index and whether it was already present | | `GET` | `/checkpoint` | the current checkpoint, or one at a stated `tree_size` | | `GET` | `/proof/inclusion` | by `index` or by `payload_digest`, at a stated `tree_size` | | `GET` | `/proof/consistency` | from `old` to `new` | A log MUST stamp `issued_at` itself. A caller-supplied instant would be an attack rather than a convenience: witness keys resolve *at* `issued_at` under half-open validity, so a caller able to choose it could obtain a checkpoint stamped inside the validity of a key that has since been revoked. Resubmitting a digest already present MUST return the existing index rather than appending a second copy, so that a submitter retrying after a dropped response does not grow the tree on every retry. **Witness.** | method | path | purpose | | --- | --- | --- | | `POST` | `/cosign` | cosign the named log at a tree size | | `GET` | `/latest` | the last checkpoint this witness cosigned for a log, and its signature | A cosign request names a log and a size; it MUST NOT carry the checkpoint. The witness fetches the checkpoint and any consistency proof from the log itself, because a witness that signs what a submitter hands it is a notary rather than a witness. A witness MUST refuse to cosign when: the tree is smaller than the one it last cosigned (rollback); a consistency proof from its last cosigned size does not verify (a rewritten log); or it has already cosigned a different root at that size (equivocation). A refusal SHOULD carry the conflicting checkpoint, so the caller can check the claim rather than trust the witness's account of it. State — the last cosigned checkpoint per log — MUST be durable before the signature is returned. A witness that forgot what it signed across a restart would cosign a rewritten history without noticing. **Publisher.** | method | path | purpose | | --- | --- | --- | | `GET` | `/conditions` | interval starts this publisher holds | | `GET` | `/condition` | the full response of section 12 for an interval, plus the witnessed checkpoint | | `GET` | `/manifest` | the evidence manifest for an interval | | `GET` | `/evidence` | the signed observation for a digest | A publisher MUST NOT serve a condition before its receipt and cosignatures are in hand. A `GET /evidence` for a digest the publisher does not hold is a 404 and is not, on its own, evidence of anything — section 10.9 decides what it means. **Pool.** | method | path | purpose | | --- | --- | --- | | `POST` | `/batches` | submit a member's batch record for its epoch | | `POST` | `/seal` | seal an epoch with whoever has submitted | | `GET` | `/status` | open (who has submitted, who is awaited) or sealed (which batches) | | `GET` | `/proof` | for a sealed epoch: the pool record, the member's inclusion proof, and the anchor receipt | A pool recomputes every batch digest from the submitted record, never taking it from the submitter. An epoch seals when every configured member has submitted, or on `POST /seal` with whoever has — there is no timer, because a sealing deadline is operator policy and this node otherwise never reads a clock. Identical resubmission is idempotent; a *different* batch from a member that already submitted for the epoch MUST be refused, because batches are deterministic and a differing resubmission is a rewrite. A member sealed out of an epoch anchors its batch directly and publishes a v1 response — the refusals distinguish "not sealed yet" (retry) from "sealed without this batch" (fall back), and the distinction is part of the interface. A member MUST verify the pool's bundle — the inclusion proof against the pool root, the pool digest against the receipt, the receipt against the log — before serving the epoch, and MUST surface a bundle that does not verify rather than falling back: routing around a malfunctioning pool would hide the evidence its members need to replace its operator. **Notary.** | method | path | purpose | | --- | --- | --- | | `POST` | `/attest` | attest a submitted digest; returns an attestation receipt | | `GET` | `/attestation` | re-issue the receipt for a digest this notary attested for this submitter | A notary accepts a **digest only** — 32 bytes as lowercase hex, exactly — and MUST reject anything else. It MUST stamp `submitted_at` itself; there is no field for a submitter-supplied instant, because the attestation *is* the notary's statement of when it saw the digest, and a submitter able to choose the time would be buying a blank certificate. The record that enters the log is the digest of the attestation — `submitted_digest`, `submitter_id`, `submitted_at`, `reference`, domain `contubernium.attestation.v1` — not the submitted digest itself, so the anchored history binds who and when, and two submitters attesting one document occupy two positions rather than colliding on one. Re-submission of a digest already attested for the same submitter MUST return the original attestation — original instant, original log position — under a current checkpoint, so that a retrying client can never accidentally acquire a later timestamp than the one it holds. This requires the attested set to be durable across restarts. A notary MUST NOT enumerate attestations across submitters. **Monitor.** | method | path | purpose | | --- | --- | --- | | `POST` | `/poll` | collect a witnessed checkpoint and prove it extends what is held | | `POST` | `/gossip` | compare checkpoints with a named peer | | `GET` | `/checkpoints` | what this monitor holds, for a peer to compare against | | `GET` | `/report` | coverage: observed, accepted, unproven, adverse, and the sizes held | A monitor's report MUST NOT contain a summary field asserting the log is sound. A monitor has only ever seen what the log chose to show it. ### 13.5 What a reader must do beyond section 12 Section 12's `verify` establishes that a response is internally consistent and that its receipt is internally sound. It does **not** establish that the checkpoint in that receipt is one the log ever showed to anybody else — a dishonest log can mint a checkpoint for one reader alone, and every local check passes. A reader that wants that property MUST, in addition: 1. obtain the last cosigned checkpoint from each witness *it* named, from the witness rather than from the log; 2. reject when two of those checkpoints have the same tree size and different roots, and check differing sizes against each other with a consistency proof; 3. apply its own witness policy (section 10.7) to the checkpoint those witnesses agreed on; 4. verify a consistency proof from the receipt's checkpoint to that agreed checkpoint. Step 4 is what establishes the entry is in the same history everyone else is being shown. A receipt whose tree size exceeds anything the reader's witnesses have signed is not established and is not evidence of dishonesty; witnesses lag. ### 13.6 Challenging A challenger MUST NOT dispute a fragment whose digest the commitment does not name. Anyone can serve bytes; only the publisher signed the commitment. Re-deriving an edited fragment against a publisher's genuine evidence diverges, so a challenger that disputed whatever it was served would produce a divergence finding — and a forfeiture under section 11 — against a publisher who did nothing wrong. The check is a digest comparison and MUST happen before any evidence is fetched. ## 14. Conformance vectors `tests/vectors/` holds the executable form of this document. `wire.json` pins section 13's transport encoding, in both directions. `scalars` pins that each wire form decodes to its canonical value *and* that the value packs back to that exact wire form; `rejections` pins the payloads a decoder must refuse, one per grammar rule, because two nodes disagreeing on validity is a split; `records` pins that decoding a record's wire form — including the batch, manifest, and pool carried-list transforms of section 13.3, and the v2 response's wire-only version key — reaches the same digest the canonical encoding produces. `fragments.json` pins the reference fragments: their canonical structures, full hex encodings, and digests. `primitives.json` pins the encoding, arithmetic, signing-preimage, accumulator, log, evidence-manifest, and pool layers case by case, including the edge cases most likely to diverge — negative halfway values under each rounding mode, excess precision, non-ASCII string lengths, boolean/integer separation, and map key ordering across a byte-versus-codepoint boundary. The accumulator cases cover leaf and node hashing separately from whole roots, and include the odd leaf counts where promotion applies, so a failure names which of the three rules diverged. The log cases add consistency proofs, including two that must be *rejected*: a proof against a wrong prior root, and one carrying a surplus node. A conformance runner that only ever accepts is not checking anything. Each file declares a `schema` field naming its layout. Implementations should treat a vector file whose schema they do not recognise as a failure, not as something to skip. Regenerating vectors is deliberate: `python3 tests/test_determinism.py --write-vectors`, `python3 tests/make_primitive_vectors.py`, and `python3 tests/make_wire_vectors.py`. A diff in the regenerated output means the wire format moved, which requires a new version rather than an edited old one. CI regenerates on every supported interpreter and fails on any difference.