All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 6m16s
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]>
100 lines
4.5 KiB
Rust
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);
|
|
}
|