open-standards/joulecontract/provenance/joulecontract-diff-v0.6-impl.md
Transaction Science 448bb75339 joulecontract: round-four implementation diff — wire edge, vector pack, and four bypass repairs
Run the standard's adversarial-diff method against its own reference
implementation (provenance/joulecontract-diff-v0.6-impl.md): two blind
lenses, 28 findings, four guarantee bypasses — case-variant key forgery,
the entity-omission Party hole, caller-supplied generator key sets, and
reseal-under-any-key — all repaired:

- schema: content addresses are canonical (lowercase, digest-width
  enforced); canonicalizer depth-bounded; non-finite joules rejected at
  the typed boundary before they can null-collide under addressing.
- receipt: independence computed over the effective generator key set
  (declared ∪ receipt signer ∪ attestation signer); Party requires both
  entities bound, normalized, unequal; receipt-local validate() honestly
  caps at JR-2, with the new composed validate_sealed() evaluating the
  full JR-3/JR-4 ladder over sealed evidence resolution, the office key
  log, anchor gates, quorum, and loop governance; receipts bind their
  allowance and EnB to the contract ceiling (residual minting is
  malformed); claims emit the exact §15 token vocabulary.
- seal: verify_offline takes the expected verifier-of-record key —
  integrity is no longer mistaken for authenticity.
- amend: the key log evaluates events chronologically (out-of-order
  appends cannot resurrect a revoked key); classify takes the named
  covering amendment, not "any amendment whatsoever".
- wire (new crate): the receipt as an in-toto Statement v1 with the
  JouleContract predicate type in a DSSE envelope, exact PAE, Ed25519.
- conformance: shared fixtures, the four rounds' attacks as tests, and a
  regenerable conformance/v1 vector pack (13 vectors + MANIFEST) whose
  expected verdicts come from the repaired validators, including both
  the local JR-2 and composed JR-3 grades and two malformed cases.

64 tests green, clippy zero warnings. READMEs state what is implemented
and what is named-but-open (composition, ratchet, recourse bindings).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 08:09:15 -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: 64 tests green, clippy zero warnings, 13 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`.*