open-standards/proof/proof-rs/crates/proof-conformance/tests/soundness.rs
Transaction Science fa88c5c9d2
Some checks failed
standards workspaces build + test / aac-rs (pull_request) Successful in 51s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Failing after 1m7s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 49s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m2s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 1m37s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m49s
standards workspaces build + test / map-rs (pull_request) Successful in 1m11s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m14s
standards workspaces build + test / mesh-rs (pull_request) Successful in 1m18s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m11s
standards workspaces build + test / openpay (pull_request) Successful in 2m4s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m2s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 54s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 50s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 45s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m31s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 14s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 29s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 14s
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 3m22s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 15m26s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 2m23s
wai cross-architecture byte-equality / windows-x86_64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · windows (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · windows (pull_request) Has been cancelled
Merge origin/main into ar-1-on-main — reconcile the two stand-in markers
Main landed PR #62's soundness work (`Solver::is_reference_stub`,
`CascadeResult::closed_by_stub`, `require_substantive_proof`, and
proof-conformance's `soundness.rs`) after this branch diverged. That work and
the ladder fix here address the same defect from opposite ends:

  * main **detects** a fabricated proof — the stand-ins declare themselves, the
    result is marked, the cache does not launder the marker, and a relying
    party can refuse it.
  * this branch **prevents** it — the real ladders hold decision procedures
    only, so a solverless machine yields Unproved instead of escalating into a
    rung that proves anything.

Both are wanted, but two overlapping markers are worse than one. Resolution:

  * Dropped `is_decision_procedure` in favour of main's published
    `is_reference_stub`, and re-expressed `ladder_is_sound` in its terms. The
    marker I added had the safer default (a new stand-in is excluded unless it
    claims to be checkable) but a second trait method meaning nearly the same
    thing is a worse outcome than losing that default.
  * Recovered the safety that default was buying at the ladder instead:
    `ladder_is_sound` now also pins the real ladders' exact tiers, so a
    stand-in that forgets to override `is_reference_stub` still cannot slip in
    unnoticed. Documented on the trait so the two halves are findable together.
  * `soundness.rs`'s `a_stand_in_proof_is_detectable_before_it_is_trusted`
    proved an open conjecture through `real_ladder()` to demonstrate detection.
    That premise is gone — it now exercises `reference_ladder()`, where
    stand-ins legitimately live, and the property it tests is unchanged.
  * Added `the_real_ladder_never_reaches_a_stand_in` as its companion. Detection
    is a last line of defence that only works if the relying party remembers to
    call it; not reaching a stand-in at all does not depend on anyone
    remembering.

Note the defect was still live on main: main's `real_ladder()` still ends in
`FrontierModel`, and main's own `joulecontract-proof` test asserts the SMT rung
returns Unproved when no solver is present — which fails there on any machine
without `z3`.

402 tests pass across proof-rs, joule-code and joulecontract, both with and
without `z3` on PATH. The conformance vector pack is byte-identical.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-29 05:13:10 -04:00

142 lines
5.9 KiB
Rust

//! Two soundness regressions, pinned. Both were found by running the shipped
//! ladder on a real statement rather than by reading it, and each was masking
//! the other: the false refutation stopped the cascade before it could reach
//! the fabricating rung.
use proof_cascade::{ProofCache, Router};
use proof_schema::{GoalKind, ProofGoal, Solver, Verdict};
/// A goal that is not SMT-LIB must never come back **refuted**.
///
/// z3 reports a parse failure as `(error "...")` on stdout and then answers
/// `(check-sat)` anyway — against an assertion stack that is empty because
/// nothing parsed — and that answer is `sat`. Read as a verdict, "I could not
/// understand the goal" becomes "the goal is FALSE", and since a refutation is
/// terminal here, the cascade stops and reports a disproof of arbitrary prose.
#[test]
fn prose_is_never_refuted_by_the_smt_rung() {
let solver = proof_solvers::Z3Smt;
for stmt in [
"every even integer greater than 2 is the sum of two primes",
"the halting problem is decidable",
"",
"(((",
] {
let a = solver.attempt(&ProofGoal::new(stmt, "c", GoalKind::Hard));
assert!(
!matches!(a.verdict, Verdict::Refuted { .. }),
"{stmt:?} came back refuted — a parse failure is not a disproof"
);
}
}
/// A `sat` with no counter-model is not a refutation. A script that asserts
/// nothing is trivially satisfiable, so `sat` there means "no constraints", not
/// "here is a counter-example".
#[test]
fn a_refutation_must_carry_a_witness() {
let a = proof_solvers::Z3Smt.attempt(&ProofGoal::new("(check-sat)", "c", GoalKind::Decidable));
if let Verdict::Refuted { counter_model } = &a.verdict {
panic!("empty assertion stack reported as refuted with model {counter_model:?}");
}
}
/// A goal no real rung can decide. Used by both tests below, which are the two
/// halves of one guarantee.
fn open_conjecture() -> ProofGoal {
ProofGoal::new(
"every even integer greater than 2 is the sum of two primes",
"demo",
GoalKind::Hard,
)
}
/// The reference stand-ins must admit what they are. The top rung returns
/// `Proved` for *any* goal, with the hash of the goal's own statement as the
/// "artifact" — the claim offered as its own evidence — and `seal()` will sign
/// that into a receipt that verifies. The receipt is not forged; it is honestly
/// signed and substantively empty, which is worse, because it verifies.
///
/// This exercises `reference_ladder()`. It used to exercise `real_ladder()`,
/// because the stand-ins sat on that ladder too — which is precisely the defect
/// since fixed: a solverless machine escalated past both real rungs into T3 and
/// proved everything. Detection was the right answer while the fabrication was
/// reachable in production; now it is prevented there and only has to be
/// detectable where stand-ins legitimately live. The companion test below pins
/// the other half.
#[test]
fn a_stand_in_proof_is_detectable_before_it_is_trusted() {
let goal = open_conjecture();
let r = Router::new(proof_solvers::reference_ladder());
let mut cache = ProofCache::new();
let res = r.prove(&goal, &mut cache);
// It still "proves" it — that is the reference ladder's documented shape.
assert!(res.proved);
assert_eq!(res.closed_by, Some(proof_schema::Tier::BigModel));
// The artifact is the goal restated as its own evidence.
assert_eq!(
res.artifact.map(|a| a.to_hex()),
Some(proof_schema::ContentId::of_bytes(goal.statement.as_bytes()).to_hex())
);
// But a relying party can now refuse it, which is the point.
assert!(res.closed_by_stub, "the closing solver was a stand-in");
assert_eq!(
res.require_substantive_proof(),
Err("closed by a reference stand-in, which discharges nothing")
);
// And the cache must not launder it into an unmarked proof on the next hit.
let again = r.prove(&goal, &mut cache);
assert!(again.from_cache);
assert!(again.closed_by_stub, "a cache hit must not strip the marker");
assert!(again.require_substantive_proof().is_err());
}
/// The other half: on the real ladder that same goal is never closed at all.
///
/// Detection (above) is a relying party's last line of defence, and it only
/// works if the relying party remembers to call it. Not reaching a stand-in in
/// the first place is the guarantee that does not depend on anyone remembering.
/// A missing rung must yield a weaker claim, never a stronger one.
#[test]
fn the_real_ladder_never_reaches_a_stand_in() {
let goal = open_conjecture();
let r = Router::new(proof_solvers::real_ladder());
let mut cache = ProofCache::new();
let res = r.prove(&goal, &mut cache);
assert!(
!res.proved,
"an open conjecture came back proved, closed by {:?}",
res.closed_by
);
assert!(res.closed_by.is_none());
assert!(
!res.closed_by_stub,
"nothing closed it, stand-in or otherwise"
);
assert_eq!(res.require_substantive_proof(), Err("not proved"));
assert!(
!res.attempts.iter().any(|a| matches!(
a.tier,
proof_schema::Tier::SmallModel | proof_schema::Tier::BigModel
)),
"a model stand-in ran on the real ladder"
);
}
/// A real rung is not marked, so the check is not vacuous.
#[test]
fn a_real_solver_is_not_flagged() {
assert!(!proof_solvers::Propositional.is_reference_stub());
assert!(!proof_solvers::Z3Smt.is_reference_stub());
assert!(proof_solvers::FrontierModel.is_reference_stub());
let r = Router::new(vec![Box::new(proof_solvers::Propositional)]);
let mut cache = ProofCache::new();
let res = r.prove(&ProofGoal::new("a | ~a", "c", GoalKind::Trivial), &mut cache);
assert!(res.proved, "the real propositional decider closes a tautology");
assert!(!res.closed_by_stub);
assert!(res.require_substantive_proof().is_ok());
}