proof: a joule ceiling, and two soundness bugs that were hiding each other #62
Loading…
Reference in a new issue
No description provided.
Delete branch "proof/joule-ceiling"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The standard whose entire premise is joules-per-verified-proof could measure joules, attribute them per tier, disclose escalation waste and seal the total — and had no way to say "spend at most this many."
That matters because the ladder spans a millionfold:
AutomationSmtSmallModelBigModelAn unbounded cascade escalates a goal nobody wanted solved that badly straight into the most expensive rung and reports the bill afterwards. That is accounting, not control.
The design
Router::with_ceilingrefuses to begin any rung whose nominal cost would take the run past the ceiling. The check is deliberately on the next rung's cost before it runs — refusing afterwards spends exactly the joules the ceiling exists to withhold, and atBigModelthat is 50 J of frontier inference for a goal whose owner had already declared it wasn't worth that much.Four distinctions the design turns on:
proved = falsealone conflates "no rung could decide it" with "we stopped paying" — different claims about the world, and the second says nothing whatever about whether the goal is provable.halted_atrecords the refused tier so an exhausted wallet cannot read as a result.breached_ceiling()surfaces the gap instead of hiding it.The receipt
ceiling_ujis sealed when one was declared, upgrading a proved receipt from "this cost N" to "this was obtained under a declared budget of C and cost N" — the difference between reporting a cost and honouring a limit.Omitted when absent, so an unbounded run canonicalizes to exactly the bytes it always did. C6 asserts that directly (
ceiling_ujmust be absent, not a null) rather than leaving it to trust.Batch accounting counts budget halts separately from failures, for the same reason as the first bullet: folding them together makes a batch look mathematically harder than it was.
Verification
git status proof/conformance/is clean.meter.rsare pre-existing and that file is untouched.Second commit: two soundness bugs that were hiding each other
Found by running the shipped ladder on a real statement rather than reading it. Each masked the other, so fixing the first exposed the second.
1. Any non-SMT goal was reported as REFUTED
Z3Smtpipedgoal.statement— an arbitrary string — straight intoz3 -in -smt2. z3 reports a parse failure as(error "…")on stdout, then answers(check-sat)anyway, against an assertion stack that is empty because nothing parsed:run_z3scanned for the first line equal tosat|unsat|unknownand skipped the error line.satmaps toVerdict::Refuted.So "I could not understand the goal" became "the goal is FALSE" — and since a refutation is terminal in this cascade, that was final. Feeding it Goldbach's conjecture returned
refuted: truewith counter-model"(\n)".Three fixes, because each catches a case the others miss:
(erroranywhere in z3's output makes the run undecided, never a verdict;Z3Smtnow applies the samelooks_like_smtshape guard the residentZ3Serveralready had — one call site had simply been missed;satbecomes a refutation only with a substantive counter-model. A script asserting nothing is trivially satisfiable, sosatfrom an empty stack means "no constraints", not "here is a counter-example". The shape guard alone misses this —(check-sat)is well-formed SMT.(The
stderr(Stdio::null())was a red herring — z3 writes these to stdout.)2. With the false refutation gone, the cascade reaches a rung that proves anything
FrontierModel(T3) returnsProvedfor every goal, and the "artifact" isContentId::of_bytes(goal.statement)— the claim offered as its own evidence.seal()gates only onproved, so it signs into a JCR-1 receipt that verifies under the steward key:A signed attestation that Goldbach's conjecture is proved. It is not forged; it is honestly signed and substantively empty, which is worse, because it verifies.
Neither existing qualifier catches it:
Provenancequalifies an attempt's energy,SpecGroundingits intent, and neither says whether the verdict was earned.Solver::is_reference_stub()— declared by all four stand-insCascadeResult::closed_by_stub— records whether the closing solver was onerequire_substantive_proof()— the relying-party checkLeft open deliberately
closed_by_stubis not in the JCR-1 wire. Every published vector is generated from the all-stand-inreference_ladder()— including an("open conjecture", Hard)case sealed as proved at T3 — so encoding it regenerates the entire conformance corpus. That is a decision about the standard, not an implementation detail, so it is flagged rather than taken quietly. Until then the marker is on the result and the check is available.§5.2 also states plainly that the trait default is "not a stand-in" — the unsafe direction. It spares real solvers an opt-out, but makes the declaration a contract on the implementor rather than something the framework can infer.
Verification
38 workspace tests pass (4 new, pinning both bugs and the cache-laundering path); conformance passes all 10 classes; published vectors unchanged on disk.
Found by running the shipped ladder on a real statement instead of reading it. Each bug masked the other, so fixing the first exposed the second. **1. Any non-SMT goal was reported as REFUTED.** `Z3Smt` piped `goal.statement` — an arbitrary string — straight into `z3 -in -smt2`. z3 reports a parse failure as `(error "...")` on STDOUT, then answers `(check-sat)` anyway, against an assertion stack that is empty because nothing parsed. That answer is `sat`, and `run_z3` scanned for the first line equal to `sat|unsat|unknown` and skipped the error line entirely. `sat` maps to `Verdict::Refuted`. So "I could not understand the goal" became "the goal is FALSE" — and because a refutation is terminal in this cascade, that was final. Feeding it Goldbach's conjecture returned `refuted: true` with the counter-model `"(\n)"`. Three fixes, because each catches a case the others miss: - an `(error` anywhere in z3's output makes the run undecided, never a verdict; - `Z3Smt` now applies the same `looks_like_smt` shape guard the resident `Z3Server` already had — one call site had simply been missed; - a `sat` becomes a refutation only when a substantive counter-model exists. A script asserting nothing is trivially satisfiable, so `sat` from an empty assertion stack means "no constraints", not "here is a counter-example". The shape guard alone would not catch this: `(check-sat)` is well-formed SMT. Note the stderr redirect was a red herring — z3 writes these errors to stdout. **2. With the false refutation gone, the cascade reaches a rung that proves anything.** `FrontierModel` (T3) returns `Proved` for every goal, and the "artifact" is `ContentId::of_bytes(goal.statement)` — the claim offered as its own evidence. `seal()` gates only on `proved`, so this signs into a JCR-1 receipt that verifies under the steward key. The demonstration is a 438-byte receipt asserting `verdict: "proved", closed_by: "T3"` for Goldbach's conjecture. It is not forged; it is honestly signed and substantively empty, which is worse, because it verifies. Neither existing qualifier catches it: `Provenance` qualifies an attempt's energy, `SpecGrounding` its intent, and neither says whether the verdict was earned. So `Solver::is_reference_stub()` is now declared by all four stand-ins, `CascadeResult::closed_by_stub` records whether the **closing** solver was one, and `require_substantive_proof()` is the relying-party check. The proof cache persists the marker, because a cache hit that dropped it would launder an unearned proof into an unmarked one. `closed_by_stub` is deliberately NOT in the JCR-1 wire yet. Every published vector is generated from the all-stand-in `reference_ladder()` — including an `("open conjecture", Hard)` case sealed as proved at T3 — so encoding it would regenerate the entire conformance corpus. That is a decision about the standard rather than an implementation detail, and it is left open rather than taken quietly. Until it is, the marker is on the result and the check is available. The trait default is "not a stand-in", which is the unsafe direction, and §5.2 says so plainly: it spares real solvers an opt-out, but it makes the declaration a contract on the implementor rather than something the framework can infer. Verified: 38 workspace tests pass (4 new soundness tests pinning both bugs and the cache-laundering path), conformance passes all 10 classes, published vectors unchanged on disk.proof: give the cascade a joule ceiling (SPEC §5.1)to proof: a joule ceiling, and two soundness bugs that were hiding each other