//! 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> { 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); }