A receipt whose oer is exactly "2.1" may state a null energy.joules_total, the unmetered marker, reported as unmetered and never as a figure, and energy.class, the figure's acquisition class with its evidence, held to the grade it supports. Minor version 0 keeps its members, so no 2.0 receipt reads differently. Three reasons (unmetered_invalid, class_invalid, class_grade_mismatch), the oer.job.* namespace for job receipts, a third canonical fixture, job_vectors.json written by hand-declared generate_job_vectors.py, and the differential fuzzer over both forms. Co-Authored-By: Claude Opus 5.5 <[email protected]>
183 lines
12 KiB
Markdown
183 lines
12 KiB
Markdown
# oer-conformance
|
||
|
||
Reason-set-equivalence for [OER receipts](../extensions/oer-receipts.md): for every
|
||
vector, a conforming verifier reports **exactly** the reasons the vector declares —
|
||
the valid vectors none, each tampered vector exactly its own. A verifier that fails a
|
||
vector for the wrong reason does not conform.
|
||
|
||
Three implementations check this corpus, and they share no canonicalisation code:
|
||
the reference verifier here, the portable C kernel in [`settle/`](settle/) that
|
||
*produces* receipts, and `wai-rs` (`--features oer`).
|
||
|
||
## Files
|
||
|
||
| file | vectors | what it is |
|
||
|---|---|---|
|
||
| `vectors.json` | 9 | OER/2 (draft 0.1, as the extension profiles it): the last 150 decision receipts of one simulated day of Settlement World's gatewayless corridor, signed with the Ed25519 ledger key published in the file, an hourly bundle with its provenance root, an Iridium anchor, and seven tampered twins |
|
||
| `firmware_vectors.json` | 1 | 400 receipts written by the C kernel with its own SHA-256 and Ed25519 — pinned, and regenerated byte-for-byte in CI |
|
||
| `edge_vectors.json` | 99 | one case for each behaviour the upstream nine never exercise — see below |
|
||
| `job_vectors.json` | 38 | `job` receipts of minor version 1 (extension §3.1), each expectation written by hand — see below |
|
||
| `unreadable/` | 11 documents | one for each way the extension refuses a document whole — a number whose nearest binary64 is infinite (the midpoint above the largest, as a float and as an integer, and far past it), `NaN`, `Infinity`, `-Infinity`, an unpaired surrogate, a repeated member name (plain, and a second `joules_total` inside a sealed receipt), nesting 65 deep, and nesting 5000 deep |
|
||
| `sw_export.json` | — | the Settlement World export `generate_vectors.py` builds `vectors.json` from |
|
||
|
||
The upstream nine cover eight reason names. The edge vectors pin the rest:
|
||
|
||
- **every remaining reason** — `unknown_kind` (also with `kind` absent),
|
||
`boundary_missing`, `floor_mismatch`, `grade_unknown`, `signer_unknown`,
|
||
`missing_capability` (both routes), `bundle_sum_mismatch`, `efficiency_missing`,
|
||
`canonical_failure`, `abstention_settled`, and `malformed` (a missing member, a
|
||
receipt that is not an object);
|
||
- **the rule that a repeated receipt id cannot hide a reason**;
|
||
- **the number profile** — a float, an exponent on an integer, negative zero, a float
|
||
inside an array, and each I-JSON integer bound from both sides — and **the
|
||
decimal-string grammar**: a leading `+`, surrounding space, a non-ASCII digit, 35
|
||
significand digits (also when the 35th is a leading zero), a four-digit exponent,
|
||
and a ledger figure that is not one — and, from the accepting side, exactly 34
|
||
digits and a one- and a three-digit exponent;
|
||
- **the order of the reasons reported alone** — each adjacent pair of the six steps
|
||
(`unknown_major` before `canonical_failure`, before a forbidden number, before an
|
||
unsupported hash, before a missing member), a version written as a JSON number,
|
||
and an unsupported `sig_alg`, which stops nothing;
|
||
- **every tolerance edge** — the sum, the Landauer floor, the bundle and the ledger,
|
||
each exactly at its edge (passes) and just past it, on both sides, with a negative
|
||
figure, and with a small one or the figure that sets its base; and for each of the
|
||
four a verdict that flips if it is computed to 34 digits (the sum also to 28:
|
||
misses in the 33rd and the 60th decimal place), which such a verifier gets wrong;
|
||
- **every member judged on its value** — a `kind`, signer or receipt id that is not
|
||
a string, a boundary that is absent or not a string, an `efficiency` that is not an
|
||
object, `stages` that are not a list, records that are not an object, a record id
|
||
that is absent or not a string, a record whose answer is not a string or whose
|
||
`created` is a boolean, a published key that is not a string, a member not listed
|
||
for `chain` or `chain.anchor`, a chain link to a hash that is not a string — and,
|
||
against those, a receipt id or signer that is absent, which is `malformed`;
|
||
- **keys a signature cannot rest on** — the identity point, canonically encoded (small
|
||
order), with its sign bit set on x = 0, and encoded as y = p + 1, each under a
|
||
signature that verifies over any message for a verifier that decodes leniently.
|
||
All three are small-order points, so they do not separate the canonical-encoding
|
||
rule from the small-order one, and no vector can: a non-canonical encoding exists
|
||
only for a point with y < 19, and a valid signature under such a point that is not
|
||
of small order needs its discrete log. Both verifiers enforce both rules;
|
||
- **the extremes a document may hold** — the largest binary64 as a float and as an
|
||
integer, a float just below the midpoint above it, a surrogate pair, and nesting
|
||
exactly 64 deep, all in one vector, so a verifier that refuses the document fails
|
||
the whole file;
|
||
- **abstention** — the valid shape, and one each carrying an answer, a record id, a
|
||
record hash, and `served_from_record`.
|
||
|
||
Each carries at most one defect. Where the edit leaves the seal intact the receipt
|
||
is re-sealed, with the published test seed, so the declared reason is the only one
|
||
it can honestly produce; a vector whose reason is reported alone before any hash is
|
||
checked is left unsealed. Some literals cannot be written by `json.dump`, so they
|
||
are swapped in after serialisation. `-0` and `1e2` are sealed over the value a
|
||
lenient parser would read (`0`, `100`): for `-0`, a verifier without the rule whose
|
||
parser reads it as the integer zero finds a valid receipt, and one whose parser reads
|
||
it as a float computes a different id — neither is the declared reason. The version
|
||
written as the number `2.99999999999999977` (e25) keeps the base receipt's seal,
|
||
since `unknown_major` is reported before any hash is checked. `e79`'s float just
|
||
below the midpoint and its escaped surrogate pair sit in a note, outside any
|
||
receipt. Every expectation is written by hand from the extension, not copied from a
|
||
verifier.
|
||
|
||
Run against the verifier as it stood before this pack was ingested, the first
|
||
thirteen edge vectors find five defects the upstream nine could not: an unsupported
|
||
hash reported as `'missing_capability` (a stray quote — `str()` of a `KeyError`),
|
||
three uncaught `KeyError` crashes on a receipt, record store or ledger missing a
|
||
member, and a repeated receipt id silently replacing an earlier receipt's reasons.
|
||
The rest found a sixth class, between the verifiers rather than in either: the
|
||
reference and `wai-rs` as they stood before the number profile, the decimal grammar,
|
||
exact arithmetic and the typing rules reported **different** reason sets for many of
|
||
them — integers wider than 64 bits and `-0` (each parser read them its own way), a
|
||
version written as a number, an absent `kind`, lenient decimals, a receipt that is
|
||
not an object, sums decided past the 28th digit, members of the wrong type, and
|
||
documents at the edge of what a parser can read. All are fixed in both.
|
||
|
||
## Minor version 1: the job vectors
|
||
|
||
`job_vectors.json` holds `job` receipts of minor version 1 (`"oer": "2.1"`, extension
|
||
§3.1), one hop of a routed media job each, named `oer.job.media.encode`:
|
||
|
||
- **valid** — a hop measured by its host's counter, its class `OnChipCounter` graded
|
||
`measured`; a model's figure graded `derived`, an estimate graded `projected`, a
|
||
calibrated instrument's graded `measured`; a hop with no class; the largest relative
|
||
uncertainty a class states; and the **unmetered marker** (`joules_total: null`, every
|
||
breakdown term null, graded `projected`) alone, after a metered hop in a failover
|
||
chain, and on a failed hop (`status` −1);
|
||
- **the gate** — a class, and the marker, under minor version 0, and a class under
|
||
`"2.10"`, which this text does not define: `canonical_failure` and `malformed` as
|
||
before minor version 1;
|
||
- **`unmetered_invalid`** — the marker beside a breakdown term, graded `measured`,
|
||
beside a class, and in a bundle;
|
||
- **`class_invalid`** — the legacy `HwShunt`, a counter class without its uncertainty or
|
||
its filtering state, a filtering state outside the three, a relative uncertainty of
|
||
zero, past 2³² − 1, as a string and as `true`, a window of zero, a model's figure
|
||
with evidence, a calibrated instrument with an empty reference or with a filtering
|
||
state, a class that is a string and a label outside the vocabulary (and a member the
|
||
class does not list, `canonical_failure`);
|
||
- **`class_grade_mismatch`** — a counter reading graded `derived`, an estimate graded
|
||
`measured`, a model's figure graded `simulated`, a calibrated instrument's graded
|
||
`projected` (and a class beside a grade outside the vocabulary, `grade_unknown`
|
||
alone);
|
||
- a **ledger** over a chain that holds the marker (`ledger_mismatch`), and a sealed
|
||
unmetered receipt given a figure afterwards (the two hash mismatches).
|
||
|
||
Every vector declares `unmetered`, the receipts it states the marker in, and a
|
||
verifier passes it only when it reports exactly those as unmetered.
|
||
|
||
## Running
|
||
|
||
```sh
|
||
# wai-rs — the three fixtures, every vector file, and every document in unreadable/
|
||
cargo run --no-default-features --features oer --bin wai_oer_vectors -- verify ../oer-conformance
|
||
|
||
# the reference verifier, one file at a time (needs `cryptography`)
|
||
python3 oer2_verify.py vectors.json
|
||
python3 oer2_verify.py firmware_vectors.json
|
||
python3 oer2_verify.py edge_vectors.json
|
||
python3 oer2_verify.py job_vectors.json
|
||
python3 oer2_verify.py --unreadable unreadable # every document there must be refused
|
||
|
||
# the C kernel: rebuild and regenerate its vectors; the output is deterministic
|
||
make -C settle && ./settle/settle_host /tmp/fw.json 400 && cmp /tmp/fw.json firmware_vectors.json
|
||
```
|
||
|
||
Every run prints the fixture check first. The vectors cannot check the canonical
|
||
form themselves: `generate_vectors.py` imports this verifier's
|
||
canonicaliser, so a defect in the field order is baked into every receipt id and
|
||
then confirmed — reverse the declared order, regenerate, and all nine still pass
|
||
with different ids. The fixture pins the bytes of a small receipt whose members are
|
||
given in the wrong order at two levels, against a literal. The C kernel proves the
|
||
same property from the other side: it shares no code with either verifier, and a
|
||
verifier with a perturbed field order rejects every receipt it writes. A second
|
||
fixture pins the stage list `bundle.provenance_root` addresses: every stage list in
|
||
the corpus is ASCII, where escaped and unescaped serialisations agree, so only a
|
||
non-ASCII fixture can hold the rule that the stage list is serialised exactly as the
|
||
body is. A third pins minor version 1's members: `energy.class` and its own members
|
||
out of order, against a literal, and the same input under `"2.0"`, which is refused.
|
||
|
||
## Differential fuzzing
|
||
|
||
The vectors pin reason-set-equivalence one case at a time. `fuzz_differential.py`
|
||
checks the space between them: it mutates real corpus receipts, chains, records,
|
||
ledgers, published keys and signatures — a type swapped at any member, a member
|
||
deleted or added, a decimal respelled, a member name repeated, literals no JSON
|
||
library writes (`-0`, `1e2`, a lone surrogate) — and requires the reference verifier
|
||
and `wai-rs` (`wai_oer_diff`) to report the same reasons, or the same refusal, for
|
||
every document, and neither to crash. It exits non-zero on any divergence and prints
|
||
the document.
|
||
|
||
```sh
|
||
python3 fuzz_differential.py --seed 1 --cases 20000
|
||
```
|
||
|
||
Receipts hashed with blake3 are included only when the `blake3` module is installed;
|
||
without it the reference rightly reports `missing_capability` for them where `wai-rs`
|
||
goes on to check the hash. A known divergence planted in either verifier is caught.
|
||
|
||
## Regenerating
|
||
|
||
`python3 generate_vectors.py`, `python3 generate_edge_vectors.py` and
|
||
`python3 generate_job_vectors.py` resolve their inputs and outputs against their own
|
||
directory and reproduce `vectors.json`, `edge_vectors.json` and `job_vectors.json` byte
|
||
for byte (the job vectors' seed is `sha256("oer job vectors: the hop key")`). The signing seed is published
|
||
(`sha256("settlement world node ledger vector key")`); it attests nothing and exists
|
||
only to make the vectors reproducible.
|