open-standards/jcp/jcp-rs/crates/jcp-conformance/tests/lifecycle_pack.rs
Transaction Science 910b359d4d
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 6m16s
Payments: a capture reversal through a delegation chain, and across a restart
A reversal returned money on a lone grant only, and a hold taken up again
after a restart began with nothing drawn, so a capture made before the
restart could no longer be reversed after it.

- Runtime: reverse_capture_chained returns the money to the hold on every
  link that carries a money ceiling, checking every link before changing
  any, so it is refused whole or applied whole. reverse_capture is now the
  one-link case of the same rule. adopt_hold takes what captures drew and
  reversals have not returned.
- Receipts: append_reversal_chained; an open hold reports drawn_minor, the
  money its captures drew net of reversals.
- Agreed terms: a reopened quote-bound hold carries drawn_minor, and
  ConditionalHolds::reverse takes a reversed part out of what counts as
  paid toward the payee's minimum.
- SPEC 8.1: a reversal applies to a chain as to a lone grant; a hold taken
  up again keeps what was drawn. C32 checks the chain case.
- lifecycle.json gains a delegate's hold captured, reversed through the
  chain and settled; verify_lifecycle.py has a fault for a reader that
  returns a chained reversal on the leaf only, and refuses it.

Seeded faults, each caught: a chained reversal on the leaf only; a
reopened hold forgetting what was drawn; a quote-bound reversal still
counted as paid; a restore recounting drawn money; a chained reversal
past what one link drew.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 14:08:09 -05:00

100 lines
4.5 KiB
Rust

//! The published hold-lifecycle log (`lifecycle.json`, SPEC §8.1, C32), read
//! through the reference library. `conformance/verify_lifecycle.py` reads the
//! same file without it.
use jcp_receipt::{extract, replay_ledger, unresolved_holds, verify_log};
use jcp_runtime::Runtime;
use jcp_schema::ContentId;
use serde_json::Value;
use std::path::PathBuf;
fn pack_file(name: &str) -> Value {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../conformance/v1").join(name);
serde_json::from_slice(&std::fs::read(path).expect("read pack file")).expect("the pack file parses")
}
fn steward_and_key() -> (String, [u8; 32]) {
let m = pack_file("meta.json");
let key = hex::decode(m["pubkey_hex"].as_str().unwrap()).unwrap().try_into().unwrap();
(m["steward"].as_str().unwrap().to_string(), key)
}
fn entries(doc: &Value) -> Vec<Vec<u8>> {
doc["entries"]
.as_array()
.expect("the log lists entries")
.iter()
.map(|e| hex::decode(e["envelope_hex"].as_str().unwrap()).unwrap())
.collect()
}
fn published(doc: &Value, name: &str) -> Vec<(ContentId, u64)> {
doc[name]
.as_object()
.unwrap_or_else(|| panic!("the pack publishes {name}"))
.iter()
.map(|(g, n)| (ContentId::from_hex(g).unwrap(), n.as_u64().unwrap()))
.collect()
}
/// The committed log is a valid chain, and restores exactly the ledger it
/// publishes — joules, gross money, and refunds, each from its own receipts.
#[test]
fn the_published_lifecycle_log_restores_the_ledger_it_publishes() {
let doc = pack_file("lifecycle.json");
let (steward, key) = steward_and_key();
let log = entries(&doc);
assert_eq!(verify_log(&log, &steward, &key), Ok(log.len()));
let mut rt = Runtime::new();
rt.restore_ledger(replay_ledger(log.iter().map(Vec::as_slice), &steward, &key));
let mut seen = 0;
for (g, want) in published(&doc, "spent_after") {
assert_eq!(rt.spent(&g), want, "joules restored for {}", g.to_hex());
seen += 1;
}
for (g, want) in published(&doc, "money_after") {
assert_eq!(rt.spent_minor(&g), want, "gross money restored for {}", g.to_hex());
}
for (g, want) in published(&doc, "refunded_after") {
assert_eq!(rt.refunded_minor(&g), want, "refunds restored for {}", g.to_hex());
assert!(rt.net_minor(&g) < rt.spent_minor(&g), "a refund lowers the net, not the gross");
}
assert!(seen >= 2, "the pack covers more than one grant");
}
/// The log holds each form the lifecycle rules distinguish, so a pack that
/// lost one would fail here rather than quietly prove less.
#[test]
fn the_published_lifecycle_log_holds_every_form_the_rules_distinguish() {
let doc = pack_file("lifecycle.json");
let (steward, key) = steward_and_key();
let log = entries(&doc);
let cores: Vec<_> = log.iter().map(|e| extract(e, &steward, &key).expect("verifies")).collect();
assert!(cores.iter().any(|c| c.is_hold() && c.held_minor.is_some()), "a hold stating money");
assert!(cores.iter().any(|c| c.captures.is_some() && c.verdict == "allow"), "an allowed capture");
assert!(cores.iter().any(|c| c.captures.is_some() && c.verdict == "deny"), "a refused capture");
assert!(cores.iter().any(|c| c.reclaims.is_some() && c.verdict == "allow"), "an allowed reclaim");
assert!(cores.iter().any(|c| c.refunded_minor.is_some()), "an allowed refund");
assert!(
cores.iter().any(|c| c.deny_reason.as_deref() == Some("wrong-subject") && c.refunded_minor.is_none()),
"a refused refund stating no amount"
);
assert!(cores.iter().any(|c| c.known_through.is_some()), "a stated revocation marker");
assert!(cores.iter().any(|c| c.known_through.is_none()), "and a receipt stating none");
assert!(cores.iter().any(|c| c.reverses.is_some() && c.reversed_minor.is_some() && c.verdict == "allow"), "an allowed reversal");
assert!(
cores.iter().any(|c| c.reverses.is_some() && c.reversed_minor.is_none() && c.verdict == "deny"),
"a refused reversal stating no amount"
);
assert!(
cores.iter().any(|c| c.reverses.is_some() && c.verdict == "allow" && c.chain_ids.as_ref().is_some_and(|ids| ids.len() > 1)),
"an allowed reversal through a delegation chain"
);
// two holds are settled (one through a chain) and two never are: one
// reclaimed with its joules still held, one outstanding after its capture
// was reversed
assert_eq!(unresolved_holds(log.iter().map(Vec::as_slice), &steward, &key).len(), 2);
}