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

7.3 KiB
Raw Permalink Blame History

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.