open-standards/jcp/jcp-rs/crates/jcp-conformance/tests/pack.rs
Transaction Science 32924ef632 jcp §8.1: a crash cannot refund a debit
Runtime::charge debits before it computes the verdict, so a call that
breaches MaxCostUj or a joule window at the debit point is denied and stays
debited. replay_spend kept only verdict == "allow", so a replayed stream
restored less than was spent: reproduced at the base with three failing
tests (0 restored against 900, 400, and 900 on each link of a chain).

The verdict cannot say what was debited. The same reasons also refuse a call
before any work, and a refusal seals the cost it refused, so cost does not
separate them either. And a two-phase debit saturates at the ceiling, each
link of a chain at its own, so it can be less than cost_uj.

The signed core now says, where the verdict does not. debited_uj is a uint,
since the core holds no bool, and chain_debited_uj lists each link's figure
beside chain_ids where a cap left them differing. Only a capped debit and a
denial naming caveat-unsatisfied or window-exceeded carry a member, the
denial always, 0 on a refusal, so its absence there can only mean a receipt
sealed before the member. Every other receipt keeps its bytes and the tag
stays jcp/2: regenerating the pack changes no published file.

Those older receipts keep verifying. A denial of either reason without the
member reads as its cost_uj, the most it can have debited, since an unknown
debit is not a zero one: an old log restores at least what was debited, and
SPEC §8.1 says exactly what it can no longer prove.

Decision carries the debit, so it is no longer Copy: a chain's per-link
figures are a Vec. aggregate now matches the ledger too, charge no longer
underflows a grant an old log restored past its ceiling, and ReceiptLog gains
append_chained, without which a logged chained decision lost its ancestors.

conformance/v1/replay.json publishes a hash-chained log with chain_ids, seq,
prev and every form of the member, its spend taken from the runtime's ledger
and not from the receipts. conformance/verify_replay.py is a second reader
sharing no code with the reference implementation, run by the repository
checks with its own planted faults.

Changelog: none
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:48:56 -05:00

294 lines
13 KiB
Rust

//! Consume the published `jcp` conformance pack — the files nothing read.
//!
//! jcp was already the best-covered standard here: `jcp-action-receipt`'s
//! `tests/conformance.rs` genuinely reads `action_receipt.json` and
//! `action_receipt_meta.json` from disk. But three of the pack's files had no
//! reader at all:
//!
//! - **`legacy_jcp1.json`** — receipts sealed under the superseded `jcp/1`
//! profile, published, in the pack's own words, "so a verifier can prove the
//! jcp/2 migration path: these MUST still verify".
//!
//! The guarantee is *not* untested: `jcp-receipt`'s own
//! `published_v1_receipts_still_verify` checks it. But it checks **one inlined
//! hex constant**, which is a copy of this pack's *allow* receipt. The pack also
//! publishes a **legacy deny** (`untrusted-control`) that no inlined constant
//! covers, and the file itself is read by nothing — so the published bytes, and
//! the legacy refusal path, were unverified. (`jcp-receipt`'s doc comment titled
//! *The migration guarantee* is also misplaced: it sits ~80 lines above, over an
//! unrelated test about environment bindings.)
//!
//! The mechanism is simpler than that comment suggests, too. There is no version
//! dispatch in `jcr1.rs`; `payload_hash` is merely **optional**, so its absence
//! leaves a legacy preimage byte-identical — which is what the neighbouring
//! comment ("Absent, the field changes nothing") actually says.
//! - **`dual_ceilings.json`** — §4.8's money-beside-joules ceiling, including the
//! two refusals that make it real (`funds-exceeded`, `currency-mismatch`). Those
//! are negative vectors, and nothing executed them.
//! - **`standing.json`** — the standing-grant lineage, and (added with §4.7's
//! remainder rule) the spend-bearing checkpoint whose arithmetic a third
//! party can redo.
//!
//! Note `legacy_jcp1.json` is deliberately absent from the drift check below: it is
//! a frozen historical artifact ("not minted any more"), so regenerating it would
//! destroy the very thing it exists to prove.
use jcp_receipt::{extract, replay_spend};
use serde_json::Value;
use std::path::PathBuf;
fn pack_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../conformance/v1")
.canonicalize()
.expect("the conformance pack must exist next to the spec")
}
fn doc(name: &str) -> Value {
serde_json::from_slice(&std::fs::read(pack_dir().join(name)).expect("read pack file"))
.unwrap_or_else(|e| panic!("{name} must parse: {e}"))
}
/// The steward identity and key the pack publishes.
fn steward_and_key() -> (String, [u8; 32]) {
let m = doc("meta.json");
let steward = m["steward"].as_str().expect("meta names the steward").to_string();
let key: [u8; 32] = hex::decode(m["pubkey_hex"].as_str().expect("meta publishes a key"))
.expect("hex")
.try_into()
.expect("a 32-byte Ed25519 key");
(steward, key)
}
fn envelopes(d: &Value) -> Vec<Vec<u8>> {
d["receipts"]
.as_array()
.expect("the pack lists receipts")
.iter()
.map(|r| hex::decode(r["envelope_hex"].as_str().expect("receipt carries an envelope")).unwrap())
.collect()
}
/// The migration guarantee, over the **published** bytes rather than an inlined copy.
///
/// Every receipt sealed under the superseded `jcp/1` profile must still verify under
/// today's implementation, or the `payload_hash` bump silently invalidated every
/// receipt in the wild. `jcp-receipt` already checks the allow receipt from an
/// inlined constant; what was missing is the file itself and the legacy **deny**,
/// which no inlined constant covers. Both verdicts are required below so the
/// refusal cannot quietly disappear from the pack.
#[test]
fn receipts_sealed_under_the_superseded_profile_still_verify() {
let d = doc("legacy_jcp1.json");
let (steward, key) = steward_and_key();
assert_eq!(d["profile"], "jcp/1", "this pack is the superseded profile's evidence");
let receipts = d["receipts"].as_array().expect("the pack lists legacy receipts");
assert!(!receipts.is_empty(), "a wrong path must not look like a pass");
let verdicts: Vec<&str> = receipts.iter().map(|r| r["verdict"].as_str().unwrap()).collect();
assert!(verdicts.contains(&"allow"), "the legacy pack must keep an allow: {verdicts:?}");
assert!(
verdicts.contains(&"deny"),
"the legacy pack must keep a deny — the refusal path is the half no inlined \
constant covers: {verdicts:?}"
);
for (i, r) in receipts.iter().enumerate() {
let env = hex::decode(r["envelope_hex"].as_str().unwrap()).unwrap();
let core = extract(&env, &steward, &key).unwrap_or_else(|e| {
panic!("legacy receipt {i} no longer verifies ({e:?}) — the profile bump broke migration")
});
assert_eq!(
core.verdict,
r["verdict"].as_str().unwrap(),
"legacy receipt {i}: verdict differs from the published one"
);
assert_eq!(core.steward, steward, "legacy receipt {i}: steward differs");
}
}
/// The migration check is not vacuous: tampering with a legacy envelope, or
/// verifying it under another key, must both fail.
#[test]
fn a_tampered_legacy_receipt_does_not_verify() {
let d = doc("legacy_jcp1.json");
let (steward, key) = steward_and_key();
let mut env = hex::decode(d["receipts"][0]["envelope_hex"].as_str().unwrap()).unwrap();
let stranger = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]).verifying_key().to_bytes();
assert!(
extract(&env, &steward, &stranger).is_err(),
"a legacy receipt verified under a key that did not seal it"
);
assert!(
extract(&env, "did:example:not-this-steward", &key).is_err(),
"a legacy receipt verified against the wrong steward"
);
let mid = env.len() / 2;
env[mid] ^= 0x01;
assert!(extract(&env, &steward, &key).is_err(), "a tampered legacy receipt verified");
}
/// §4.8's dual ceilings, with the refusals actually executed.
///
/// The pack's value is in its two denials: a spend that exceeds the money ceiling,
/// and a spend in the wrong currency. Both are compared as the whole recorded
/// verdict — verdict *and* reason — because "it denied" would pass even if it
/// denied for the wrong reason.
#[test]
fn the_dual_ceiling_refusals_replay_with_their_recorded_reasons() {
let d = doc("dual_ceilings.json");
let (steward, key) = steward_and_key();
let receipts = d["receipts"].as_array().expect("the pack lists receipts");
assert_eq!(receipts.len(), 3, "the scenario is one allow and two refusals");
let mut reasons = Vec::new();
for (i, r) in receipts.iter().enumerate() {
let env = hex::decode(r["envelope_hex"].as_str().unwrap()).unwrap();
let core = extract(&env, &steward, &key)
.unwrap_or_else(|e| panic!("dual-ceiling receipt {i} does not verify: {e:?}"));
assert_eq!(core.verdict, r["verdict"].as_str().unwrap(), "receipt {i}: verdict differs");
let want_reason = r["wire"]["deny_reason"].as_str();
assert_eq!(
core.deny_reason.as_deref(),
want_reason,
"receipt {i}: denied for a different reason than the pack records"
);
if let Some(reason) = want_reason {
reasons.push(reason.to_string());
}
}
// The two rules §4.8 exists to enforce: a money ceiling, and no invented FX.
assert!(reasons.iter().any(|r| r == "funds-exceeded"), "no funds-exceeded refusal: {reasons:?}");
assert!(
reasons.iter().any(|r| r == "currency-mismatch"),
"no currency-mismatch refusal: {reasons:?}"
);
// Replaying the spend must land on the recorded total: only the allow debits.
let envs = envelopes(&d);
let spent: u64 = replay_spend(envs.iter().map(|e| e.as_slice()), &steward, &key)
.into_iter()
.map(|(_grant, uj)| uj)
.sum();
assert_eq!(
spent,
d["spent_after"]["uj"].as_u64().expect("the pack records the joules spent"),
"replaying the receipts does not reproduce the recorded joule spend"
);
assert!(
spent <= d["grant"]["budget_uj"].as_u64().unwrap(),
"the replayed spend exceeds the grant's own joule ceiling"
);
}
/// The standing-grant lineage, as far as the pack makes it checkable.
///
/// One link closes: `minted_child` names `standing_grant` as its parent, and its
/// budget is within the amended ceiling. One does not: `continuation_grant` names a
/// `parent_hex` that is **not among the ids the pack publishes**. Its budget matches
/// the amendment's `new_budget_uj`, so the parent is plausibly the post-amendment
/// grant — but that grant is not in the pack, so a third party cannot close the
/// link from the published files alone. Asserted as the current state, with a
/// message to replace it once the parent is published.
#[test]
fn the_standing_lineage_links_as_far_as_the_pack_allows() {
let d = doc("standing.json");
let id = |k: &str| d[k]["id_hex"].as_str().unwrap_or_default().to_string();
for k in ["standing_grant", "mint_policy", "minted_child", "amendment", "checkpoint", "continuation_grant"] {
let i = id(k);
assert_eq!(i.len(), 64, "{k} has no 32-byte id");
assert!(i.bytes().all(|b| b.is_ascii_hexdigit()), "{k} id is not hex");
}
assert_eq!(
d["minted_child"]["parent_hex"].as_str().unwrap(),
id("standing_grant"),
"the minted child does not descend from the standing grant"
);
let amended = d["amendment"]["new_budget_uj"].as_u64().expect("the amendment sets a budget");
assert_eq!(
d["continuation_grant"]["budget_uj"].as_u64().unwrap(),
amended,
"the continuation does not carry the amended budget"
);
assert!(
d["minted_child"]["budget_uj"].as_u64().unwrap() <= amended,
"a minted child may not exceed the amended ceiling"
);
// The spend-bearing checkpoint (§4.7's remainder rule). Two things are
// checkable from the published files alone: the remainder is arithmetic a
// third party can redo, and the entry's id differs from `checkpoint`'s —
// which is what makes the additivity claim non-vacuous. `checkpoint` was
// generated with no spend and keeps the id it had before the field existed;
// this one carries the field and therefore cannot.
let sp = &d["checkpoint_with_spend"];
let issued = sp["grant_budget_uj"].as_u64().expect("the entry publishes the issued budget");
let spent = sp["spent_uj"].as_u64().expect("the entry publishes the spend");
let remaining = sp["remaining_uj"].as_u64().expect("the entry publishes the remainder");
assert_eq!(
issued.saturating_sub(spent), remaining,
"the published remainder is not the issued budget minus the spend"
);
assert!(remaining < issued, "a vector for the remainder rule must actually have spent something");
assert_ne!(
id("checkpoint_with_spend"), id("checkpoint"),
"a declared spend must change the checkpoint's id — it is signed, not advisory"
);
let published: Vec<String> = ["standing_grant", "mint_policy", "minted_child", "amendment", "checkpoint", "continuation_grant"]
.iter()
.map(|k| id(k))
.collect();
let cont_parent = d["continuation_grant"]["parent_hex"].as_str().unwrap();
assert!(
!published.contains(&cont_parent.to_string()),
"the continuation's parent is now published — replace this with a real lineage \
assertion linking it to that grant"
);
}
/// The generator still emits the files it owns.
///
/// `legacy_jcp1.json` is excluded on purpose: it is frozen pre-`payload_hash`
/// evidence, so regenerating it would erase what it exists to prove.
#[test]
fn the_generator_still_emits_the_files_it_owns() {
let tmp = std::env::temp_dir().join(format!("jcp-pack-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).expect("create temp dir");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_gen-vectors"))
.arg(&tmp)
.output()
.expect("gen-vectors must run");
assert!(out.status.success(), "gen-vectors failed: {}", String::from_utf8_lossy(&out.stderr));
let mut checked = 0;
for f in ["meta.json", "vectors.json", "dual_ceilings.json", "standing.json", "replay.json"] {
let regenerated = tmp.join(f);
if !regenerated.exists() {
continue; // the generator does not own this one
}
assert_eq!(
std::fs::read(&regenerated).unwrap(),
std::fs::read(pack_dir().join(f)).unwrap(),
"{f} has drifted from what the generator emits"
);
checked += 1;
}
assert!(checked >= 3, "expected the generator to own at least three files, saw {checked}");
assert!(
!tmp.join("legacy_jcp1.json").exists(),
"the generator now rewrites the frozen legacy pack — that would erase the \
pre-payload_hash evidence the migration guarantee rests on"
);
let _ = std::fs::remove_dir_all(&tmp);
}