- BENCHMARKS.md / joulecontract diff: replace "16 tests" / "64 tests"
count phrasings with shape descriptions, per the no-vanity-metrics rule.
- check-no-vanity-metrics.sh: require the count to be a standalone number
(start-of-line or a non-identifier char before it), so an identifier like
"JR-2 tests" — where "tests" is a verb — is no longer flagged. Genuine
counts (" 16 tests", "64 tests") still match.
Scanner passes clean (exit 0).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
46 lines
7.3 KiB
Markdown
46 lines
7.3 KiB
Markdown
# JouleContract round four — the implementation diff
|
||
|
||
**Target:** not the spec text (rounds 1–3) but the v0.6 reference implementation, `joulecontract-rs`.
|
||
**Lenses (mutually blind):** (1) spec-vs-code fidelity — where the code passes what the spec forbids or silently skips a mandated gate (findings F1–F19); (2) Rust soundness, API misuse, and adversarial inputs — where a malicious caller breaks the library's guarantees (findings R1–R9 + minors).
|
||
**Verdict:** 28 findings; four genuine guarantee bypasses; all blockers and serious findings repaired in the same change that records this document. The standard's law of evolution held one more time: the implementation was forced to apply the spec's own rules to itself.
|
||
|
||
---
|
||
|
||
## The four bypasses (found, then killed)
|
||
|
||
1. **R1 — case-variant key forgery.** `ContentAddress` equality was case-sensitive on the hex string while `of_bytes` emitted lowercase: writing one key as `ABCD…` in the generator set and `abcd…` on the discharge made the *same key* compute as disjoint — fake independence, self-signed meters passing as third-party, self-attestation passing the generator gate. *Fix:* identity is canonical — parse normalizes to lowercase and rejects non-digest-width hex; a case variant is the same address everywhere (schema).
|
||
2. **R2/F2 — the entity-omission Party hole.** `compute_independence` granted `Party` when the *generator* carried no entity binding at all — omitting `generator.entity` (which triggered no demotion) bought the strongest independence level with one self-asserted verifier string. *Fix:* `Party` requires **both** sides bound, normalized (trim + case-fold; round-four R8), and unequal; undecidable floors at `KeyOnly`, never ceilings at `Party`.
|
||
3. **F1 — the caller-supplied generator key set.** §5.1.1 defines the key set; the code took it as a producer-declared field and computed disjointness against it — declaring it empty and signing discharges with the receipt-signer key computed `independent: true`. The §6.2 "computed, never copied" rule was defeated because the *operand* was producer-supplied. *Fix:* validation computes the **effective** set — declared ∪ receipt signer ∪ attestation signer — so under-declaring buys nothing.
|
||
4. **R5 — reseal-under-any-key.** `seal::verify_offline` authenticated the envelope against its *own embedded key*: any forger could reseal any payload under their own key and pass. Integrity without authenticity. *Fix:* the caller supplies the expected verifier-of-record key (from the office key log, as of discharge); a mismatched signer is `WrongSigner` before any other check.
|
||
|
||
## The honest restructuring: local vs composed validation
|
||
|
||
The deepest fidelity finding (F3/F4/F5) was that `validate` returned **JR-4** from receipt-local evidence while §10 assigns JR-3+ gates the receipt alone cannot witness — quorum, loop governance, independent anchors, sealed-evidence resolution, key-log time coordinates. Worse, the shipped conformance vector canonized the over-grade as a published expected verdict. The repair is architectural, not cosmetic:
|
||
|
||
- **`validate` (receipt-local) now caps at JR-2 / `managed`** and says so — it does not pretend to see evidence it cannot.
|
||
- **`validate_sealed` (composed)** takes the evidence bundle, the office key log, the transition interval, and the profile parameters, and evaluates the full ladder: every trust-bearing reference must resolve *within the sealed bundle* (F6); verifier keys are checked against revocation windows as of the start anchor (F8); anchors must be independent and prompt (`timestamp_unanchored` / `timestamp_stale` now producible from validation, F13); quorum is k ≥ 2 distinct party-bound verifier entities, per-obligation at JR-4 (F3); loop governance must be present and generator-independent for JR-3 and for `certified` (F3/F9); an open unamended breach caps Governance (F9).
|
||
- The conformance vector now publishes **both** verdicts — the local JR-2 and the composed JR-3 — so a third-party implementation is tested on the distinction itself (F5).
|
||
|
||
## Also repaired
|
||
|
||
- **R3** — non-finite/negative joules are malformed (`IllFormedEnergy`), closing the NaN→`null` content-address collision; the canonicalizer is depth-bounded against adversarial nesting.
|
||
- **R7** — the receipt's `joules_allowed` and `enb_ref` now bind to the contract's ceiling (mismatch is malformed, not demoted): residual minting by inflated allowance is dead, and ships as a malformed vector.
|
||
- **R4/F16** — `Verdict::claim()` emits the exact §15 token vocabulary (`metered_unattested`, `qualified:false`, `evidence:external`…), deduplicated, with free-text reasons excluded from the token list.
|
||
- **R6** — `KeyLog::key_set_as_of` evaluates events in chronological order of effective time (revocation outranks addition at the same instant): an out-of-order or backdated append can never resurrect a revoked key.
|
||
- **R9/F7** — `classify` no longer accepts "any amendment whatsoever" or a caller-asserted bool: the caller names the *specific* covering amendment (temporal half enforced mechanically; content coverage stays the verifier of record's judgment, stated in the doc) and the fix-receipt is a content address to be resolved against evidence.
|
||
- **F10** — a stochastic discharge's `re_discharged_by` is verified, not trusted: it must reference a real deterministic, generator-independent, closed entry for the same obligation in this receipt.
|
||
- **F11** — qualification signers are checked against the **tier operator's** keys (new field), not merely the discharging verifier's.
|
||
- **F12** — the meter binding is validity-windowed; `ModelBased` without a sealed anchor demotes to `Unaccounted`.
|
||
- **F4** — the receipt now carries `execution`, `register_pin`, `evidence` (bundle address; absent serializes `evidence:external`), `energy_anchor`, and optional `grid_mix`; the interval is supplied to composed validation by the amend seam.
|
||
- **F17** — JR-2 tests the *frame entry's* independence, not the global weakest.
|
||
|
||
## Named, not closed (the honest ledger)
|
||
|
||
- **Composition (§3.3) and the ratchet (§5.3)** are not implemented; `component_unattributed` is not yet producible. No code claims otherwise.
|
||
- **`recourse_undeclared` and `target_self_assigned`** have no carrying fields yet (no recourse-regime binding on verifier credentials; no sector-equivalence claim on the contract).
|
||
- **`evidence_lost` vs `unresolvable`** — the distinction exists in `seal::evidence_state`, but composed validation currently treats a missing object as `unresolvable`; modeling the issuance-time validation record is follow-on work.
|
||
- **Cryptographic agility (§3.1)** is a one-variant enum with no cross-digest re-anchoring amendment record yet.
|
||
- **`residual` carried vs computed** — the spec's §5.1 lists it as a receipt field; the implementation derives it (`Receipt::residual()`) because a stored copy could lie. Candidate v0.7 spec amendment: declare the residual derived.
|
||
- **F18-adjacent** — the in-toto digest map uses the ecosystem's bare `sha256` name at the edge while the core stays algorithm-prefixed; stated in `joulecontract-wire`.
|
||
|
||
*Workspace after repairs: test suite green, clippy zero warnings, regenerated vectors whose expected verdicts come from the repaired validators. Prior rounds: `joulecontract-diff-v0.3-global.md`, `joulecontract-diff-v0.4-self.md`, `joulecontract-diff-v0.5-self.md`.*
|