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

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: 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.