open-standards/proof/proof-rs/crates/proof-receipt/src/lib.rs
Transaction Science f1f53f53ce
All checks were successful
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m47s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m14s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m15s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m0s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m33s
standards workspaces build + test / aac-rs (pull_request) Successful in 1m5s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m43s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 7m51s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 2m18s
standards workspaces build + test / map-rs (pull_request) Successful in 1m20s
standards workspaces build + test / mesh-rs (pull_request) Successful in 1m20s
standards workspaces build + test / openpay (pull_request) Successful in 2m39s
standards workspaces build + test / arl-rs (pull_request) Successful in 11m1s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 10m38s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 17m35s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 8m50s
standards workspaces build + test / sense-rs (pull_request) Successful in 10m52s
proof: closed_by_stub rides in the signed receipt (SPEC §6.1)
SPEC §5.2 already says a relying party "MUST check" whether the rung that
closed a goal discharged anything. Until now that marker lived only on the
in-process `CascadeResult`, so a party holding **only the receipt** — which is
the entire purpose of sealing one — could not perform the check the spec asks of
them. A verifier that must consult the prover to learn whether a proof is real
is not a verifier.

What the published pack ships today, signed and verifying under the steward key:

    { "verdict": "proved", "closed_by": "T3",
      "artifact": "ae28bd3b…",        <- hash of the goal restated
      "spec_grounding": "Ungrounded" }

That is Goldbach's conjecture, sealed as proved, with nothing in the bytes to
qualify it. Not forged — honestly signed and substantively empty, which is
worse, because it verifies.

No existing field answers it. `closed_by` names a TIER, and the same tag covers
a real decision procedure on one ladder and a stand-in on another (`T0` is the
propositional decider in `real_ladder`, a `GoalKind` gate in `reference_ladder`).
`energy_provenance` qualifies the energy; `spec_grounding` qualifies the intent.
This is the only field that says whether the VERDICT was earned.

Emit-when-true is normative, not an optimisation, and it is what makes this
revision non-breaking:

  * a receipt closed by a real solver canonicalizes to EXACTLY the bytes it did
    before this field existed — verified byte-for-byte against the canonical
    core, not merely by inspecting the map — so every such receipt sealed under
    an earlier revision still verifies untouched;
  * the byte cost falls entirely on receipts that were making an unearned claim.

Measured before committing: all 4 published vectors change (+16 bytes, new ids
and envelopes) because every one of them comes from the all-stand-in
`reference_ladder` — that is the whole accepted cost, and it is confined to
fixtures. Nothing outside `proof/` consumes the pack. 44 tests pass across
proof-rs and all 10 conformance checks (C1–C6, four negatives) still pass
against the regenerated vectors.

Also documents `ceiling_uj` in the §6 core table. The code has emitted it since
the joule-ceiling work; the spec never listed it, and an artifact that says more
than its specification is the same class of defect this commit closes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-30 17:22:46 -04:00

210 lines
9.2 KiB
Rust

//! The Proof receipt (SPEC §6).
//!
//! A verified proof seals as a signed record of *what it cost*: the goal it
//! discharged, the tier that closed it, the joules spent (with `wasted_uj` and
//! the frontier-only `baseline_uj`), the honest energy provenance, and the
//! statement's spec-grounding. Receipts seal as **JCR-1** envelopes — the
//! family's shared signed-receipt format (canonical det-CBOR + COSE_Sign1/EdDSA)
//! — so a Proof receipt verifies with the same tooling as a Map, Sandbox, or
//! Joule Code receipt.
//!
//! The receipt attests the **proof**, not the specification: a goal whose
//! `spec_grounding` is `Ungrounded` says, in the signed artifact itself, that
//! the statement was never validated against intent.
use ed25519_dalek::SigningKey;
use proof_cascade::CascadeResult;
use serde_json::{json, Value};
pub mod jcr1;
/// Errors from projecting, sealing, or verifying a receipt.
#[derive(Debug, thiserror::Error)]
pub enum ReceiptError {
/// A receipt was requested for a goal that was not proved.
#[error("cannot seal a receipt for an unproved goal")]
NotProved,
/// The envelope payload did not match this result's canonical core.
#[error("envelope payload does not match the result")]
PayloadMismatch,
#[error(transparent)]
Jcr1(#[from] jcr1::Jcr1Error),
}
/// Project a cascade result to JCR-1 wire JSON — the `proof/1` profile of the
/// family's receipt format (SPEC §6). The signed core is a flat uint/text map;
/// fields that don't apply (e.g. `closed_by` for an unproved goal) are omitted.
pub fn to_jcr1_wire(r: &CascadeResult, steward: &str) -> Value {
let mut obj = serde_json::Map::new();
obj.insert("proof_receipt".into(), json!("proof/1"));
obj.insert("goal_id".into(), json!(r.goal_id));
obj.insert("verdict".into(), json!(if r.proved { "proved" } else { "unproved" }));
if let Some(t) = r.closed_by {
obj.insert("closed_by".into(), json!(t.wire_tag()));
}
if let Some(a) = r.artifact {
obj.insert("artifact".into(), json!(a.to_hex()));
}
obj.insert("total_uj".into(), json!(r.total_uj));
obj.insert("wasted_uj".into(), json!(r.wasted_uj));
obj.insert("baseline_uj".into(), json!(r.baseline_uj));
obj.insert("energy_provenance".into(), json!(r.provenance.jcr1_label()));
obj.insert("spec_grounding".into(), json!(r.grounding.label()));
obj.insert("from_cache".into(), json!(if r.from_cache { 1 } else { 0 }));
// The declared ceiling, when there was one. Omitted otherwise, so a receipt
// from an unbounded run canonicalizes to exactly the bytes it always did and
// every previously sealed receipt and published vector still verifies.
//
// On a proved receipt this upgrades the claim from "this proof cost N" to
// "this proof was obtained under a declared budget of C and cost N" — the
// difference between reporting a cost and honouring a limit.
if let Some(c) = r.ceiling_uj {
obj.insert("ceiling_uj".into(), json!(c));
}
// Whether the rung that CLOSED the goal discharges nothing (SPEC §5.2).
//
// §5.2 requires a relying party to check this before acting on a proof, but
// until now it lived only on the in-process result — so anyone holding just
// the receipt, which is the whole point of sealing one, could not perform
// the check the spec asks of them. A stand-in-closed receipt is honestly
// signed and substantively empty; without this field nothing in the bytes
// says so, and `closed_by` does not: it names a TIER, and the same tag
// covers a real decision procedure on one ladder and a stand-in on another.
//
// Emitted only when true, so a receipt closed by a real solver
// canonicalizes to exactly the bytes it always did and every such receipt
// sealed before this field existed still verifies unchanged. The cost falls
// entirely on receipts that were making an unearned claim.
if r.closed_by_stub {
obj.insert("closed_by_stub".into(), json!(1));
}
obj.insert("steward".into(), json!(steward));
Value::Object(obj)
}
/// The canonical signed-core bytes (JCR-1 det-CBOR) for a result.
pub fn signed_core(r: &CascadeResult, steward: &str) -> Result<Vec<u8>, ReceiptError> {
Ok(jcr1::signed_core_bytes(&to_jcr1_wire(r, steward))?)
}
/// Seal a *proved* result into a JCR-1 COSE_Sign1 envelope. Refuses to seal a
/// receipt for an unproved goal.
pub fn seal(r: &CascadeResult, key: &SigningKey, steward: &str) -> Result<Vec<u8>, ReceiptError> {
if !r.proved {
return Err(ReceiptError::NotProved);
}
let core = signed_core(r, steward)?;
Ok(jcr1::cose_sign1_envelope(&core, key, steward))
}
/// Verify a JCR-1 envelope against a steward key and confirm its payload is
/// exactly this result's canonical signed core.
pub fn verify(
r: &CascadeResult,
envelope: &[u8],
steward: &str,
pubkey: &[u8; 32],
) -> Result<(), ReceiptError> {
let payload = jcr1::verify_cose_sign1(envelope, steward, pubkey)?;
if payload != signed_core(r, steward)? {
return Err(ReceiptError::PayloadMismatch);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use proof_cascade::{ProofCache, Router};
use proof_schema::{GoalKind, ProofGoal};
use proof_solvers::reference_ladder;
fn prove_one(stmt: &str, kind: GoalKind) -> CascadeResult {
let router = Router::new(reference_ladder());
let mut cache = ProofCache::new();
router.prove(&ProofGoal::new(stmt, "lean4", kind), &mut cache)
}
#[test]
fn proved_receipt_seals_and_verifies() {
let r = prove_one("x = x", GoalKind::Trivial);
let sk = SigningKey::from_bytes(&[7u8; 32]);
let steward = "did:example:prover1";
let env = seal(&r, &sk, steward).unwrap();
assert!(verify(&r, &env, steward, &sk.verifying_key().to_bytes()).is_ok());
// A different steward must not verify.
assert!(verify(&r, &env, "did:example:other", &sk.verifying_key().to_bytes()).is_err());
}
#[test]
fn wire_records_grounding_and_energy_fields() {
let r = prove_one("dec", GoalKind::Decidable);
let wire = to_jcr1_wire(&r, "did:example:p");
let o = wire.as_object().unwrap();
assert_eq!(o["proof_receipt"], json!("proof/1"));
assert_eq!(o["verdict"], json!("proved"));
assert_eq!(o["closed_by"], json!("T1"));
assert_eq!(o["spec_grounding"], json!("Ungrounded")); // honest by default
assert!(o.contains_key("baseline_uj") && o.contains_key("wasted_uj"));
}
/// A stand-in-closed receipt says so in the bytes a third party receives.
///
/// SPEC §5.2 requires a relying party to check whether the closing rung
/// discharged anything; §6.1 puts the marker in the signed core so they can
/// do it holding only the artifact. Without this the pack ships a receipt
/// asserting an open conjecture is proved, with nothing to qualify it.
#[test]
fn a_stand_in_closed_receipt_declares_itself_on_the_wire() {
let r = prove_one("open conjecture", GoalKind::Hard);
assert!(r.proved && r.closed_by_stub, "fixture must be stub-closed");
let wire = to_jcr1_wire(&r, "did:example:p");
assert_eq!(
wire.as_object().unwrap().get("closed_by_stub"),
Some(&json!(1)),
"a receipt closed by a rung that discharges nothing must say so"
);
}
/// ...and the field is free for an honest proof.
///
/// Omit-when-false is what makes this revision non-breaking: a receipt
/// closed by a real decision procedure canonicalizes to exactly the bytes it
/// did before the field existed, so every one sealed earlier still verifies.
/// The byte cost falls only on receipts making an unearned claim.
#[test]
fn a_real_closed_receipt_is_byte_identical_to_before_the_field() {
use proof_solvers::real_ladder;
let router = Router::new(real_ladder());
let mut cache = ProofCache::new();
let r = router.prove(
&ProofGoal::new("a | ~a", "prop", GoalKind::Trivial),
&mut cache,
);
assert!(r.proved && !r.closed_by_stub, "fixture must be closed by a real rung");
let wire = to_jcr1_wire(&r, "did:example:p");
assert!(
wire.as_object().unwrap().get("closed_by_stub").is_none(),
"the field must be absent, not false — an emitted 0 would change the bytes"
);
// Pin it at the level that actually matters: the canonical core is what
// gets signed and content-addressed, so compare bytes, not just the map.
let core = signed_core(&r, "did:example:p").unwrap();
let mut expected = to_jcr1_wire(&r, "did:example:p");
expected.as_object_mut().unwrap().remove("closed_by_stub");
assert_eq!(core, jcr1::signed_core_bytes(&expected).unwrap());
}
#[test]
fn unproved_goal_cannot_be_sealed() {
// No solver in the reference ladder is unable to close General/Hard, so
// build an explicitly unproved result by hand.
let mut r = prove_one("x = x", GoalKind::Trivial);
r.proved = false;
r.closed_by = None;
let sk = SigningKey::from_bytes(&[7u8; 32]);
assert!(matches!(seal(&r, &sk, "s"), Err(ReceiptError::NotProved)));
}
}