- 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]>
7.3 KiB
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)
- R1 — case-variant key forgery.
ContentAddressequality was case-sensitive on the hex string whileof_bytesemitted lowercase: writing one key asABCD…in the generator set andabcd…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). - R2/F2 — the entity-omission Party hole.
compute_independencegrantedPartywhen the generator carried no entity binding at all — omittinggenerator.entity(which triggered no demotion) bought the strongest independence level with one self-asserted verifier string. Fix:Partyrequires both sides bound, normalized (trim + case-fold; round-four R8), and unequal; undecidable floors atKeyOnly, never ceilings atParty. - 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. - R5 — reseal-under-any-key.
seal::verify_offlineauthenticated 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 isWrongSignerbefore 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 /managedand 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_stalenow 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 forcertified(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→nullcontent-address collision; the canonicalizer is depth-bounded against adversarial nesting. - R7 — the receipt's
joules_allowedandenb_refnow 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_ofevaluates 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 —
classifyno 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_byis 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;
ModelBasedwithout a sealed anchor demotes toUnaccounted. - F4 — the receipt now carries
execution,register_pin,evidence(bundle address; absent serializesevidence:external),energy_anchor, and optionalgrid_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_unattributedis not yet producible. No code claims otherwise. recourse_undeclaredandtarget_self_assignedhave no carrying fields yet (no recourse-regime binding on verifier credentials; no sector-equivalence claim on the contract).evidence_lostvsunresolvable— the distinction exists inseal::evidence_state, but composed validation currently treats a missing object asunresolvable; 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.
residualcarried 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
sha256name at the edge while the core stays algorithm-prefixed; stated injoulecontract-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.