open-standards/joulecontract/provenance/joulecontract-diff-v0.6-impl.md
Transaction Science 2557c4c58e docs + no-vanity-metrics: reword two counts, fix scanner false positive
- 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]>
2026-06-13 22:14:13 -04:00

46 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.*