//! SPEC §8.1: a crash cannot refund a debit. //! //! Each test runs real decisions through a runtime, drops it, and rebuilds a //! fresh one from nothing but the sealed receipts. use ed25519_dalek::SigningKey; use jcp_grant::{attenuate, issue}; use jcp_grant::standing::{issue_policy, mint_on_trigger, MintError, MintLedger, SignedMintPolicy, TriggerEvent}; use jcp_receipt::{ aggregate, extract, jcr1, replay_ledger, replay_mints, replay_spend, seal, seal_chained, to_jcr1_wire, to_jcr1_wire_chained, verify, verify_chained, verify_log, FlowSeal, ReceiptError, ReceiptLog, }; use jcp_runtime::{Decision, Runtime, Price}; use jcp_schema::{ Capability, Caveat, ContentId, DenyReason, Did, Energy, FlowFacts, Frame, FrameKind, Grant, Integrity, Label, Origin, Origins, Provenance, Readers, SignedGrant, Trust, Verdict, Window, WindowLimit, }; use serde_json::{json, Value}; const STEWARD: &str = "did:example:replay"; fn sk(s: u8) -> SigningKey { SigningKey::from_bytes(&[s; 32]) } fn did(k: &SigningKey) -> Did { Did::from_verifying_key(&k.verifying_key()) } fn resource() -> ContentId { ContentId::of_bytes(b"db") } fn grant(budget_uj: u64, caveats: Vec) -> SignedGrant { issue( &sk(1), Grant { issuer: did(&sk(1)), subject: did(&sk(2)), capability: Capability::new(resource(), ["read"]), budget_uj, budget_funds: None, caveats, expiry: 0, parent: None, delegate_depth: None, nonce: [7; 16], }, ) .unwrap() } fn frame(sg: &SignedGrant) -> Frame { Frame { kind: FrameKind::ToolCall, trust: Trust::Control, grant_id: sg.grant.id(), action: "read".into(), resource: resource(), payload_hash: ContentId::of_bytes(b"q"), model: None, } } fn log() -> ReceiptLog { ReceiptLog::new(sk(7), STEWARD) } /// A fresh runtime holding only what the stream replays to, in every /// dimension it records. fn restored(entries: &[Vec]) -> Runtime { let mut rt = Runtime::new(); rt.restore_ledger(replay_ledger(entries.iter().map(Vec::as_slice), STEWARD, &pubkey())); rt } fn pubkey() -> [u8; 32] { sk(7).verifying_key().to_bytes() } fn joule_window(max: u64) -> Caveat { Caveat::WithinWindow(Window { secs: 60, limit: WindowLimit::MaxUj(max) }) } /// A root under `caveats` and a delegate of it holding `leaf_budget`. fn chain(root_budget: u64, leaf_budget: u64, caveats: Vec) -> (Vec, Vec) { let root = grant(root_budget, caveats); let kid = attenuate(&root, &sk(2), did(&sk(3)), Capability::new(resource(), ["read"]), leaf_budget, 0, [8; 16]).unwrap(); let links = vec![root, kid]; let ids = links.iter().map(|sg| sg.grant.id()).collect(); (links, ids) } /// The two-phase path: a call whose measured cost breaches `MaxCostUj`. The /// work ran, so the joules are debited and the verdict is a denial (§4.9). #[test] fn a_cost_breach_survives_a_crash() { let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let holder = sg.grant.subject; let mut log = log(); let mut rt = Runtime::new(); assert!(rt.authorize(&f, &sg, holder, 10).is_allow()); let d = rt.charge(&sg.grant, Energy::measured(900)); assert!(!d.is_allow(), "the breach is a denial"); assert_eq!(rt.spent(&sg.grant.id()), 900, "and it stays debited"); log.append(&f, &sg.grant, &d); drop(rt); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 900, "the crash refunded the breach"); } /// The same for a joule window (§4.12). #[test] fn a_window_breach_survives_a_crash() { let sg = grant(1_000, vec![joule_window(100)]); let f = frame(&sg); let holder = sg.grant.subject; let mut log = log(); let mut rt = Runtime::new(); assert!(rt.authorize(&f, &sg, holder, 10).is_allow()); let d = rt.charge(&sg.grant, Energy::measured(400)); assert!(!d.is_allow(), "the overrun is a denial"); assert_eq!(rt.spent(&sg.grant.id()), 400); log.append(&f, &sg.grant, &d); drop(rt); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 400, "the crash refunded the overrun"); } /// A tainted call seals `jcp/3`, the same core with a flow block, and a breach /// there debits as any other does. #[test] fn a_tainted_breach_survives_a_crash() { let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let facts = FlowFacts { label: Label { integrity: Integrity::Untrusted, origins: Origins::one(Origin::channel(ContentId::of_bytes(b"mail:read"))), readers: Readers::Public, }, witness_id: ContentId::of_bytes(b"a witness"), eval_uj: 42, eval_prov: Provenance::ModelBased, }; let mut rt = Runtime::new(); assert!(rt.authorize_flow(&f, &sg, sg.grant.subject, 10, &facts).is_allow()); let d = rt.charge_flow(&sg.grant, Energy::measured(900), &facts); assert!(!d.is_allow()); let mut log = log(); log.append_flow(&f, &sg.grant, &d, &FlowSeal { facts: Some(facts), ceiling: None }); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 900, "the crash refunded the breach"); } /// Over a chain, a breach debits every link, ancestors included. #[test] fn a_chained_breach_survives_a_crash_on_every_link() { let (links, ids) = chain(10_000, 5_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&links[1]); let mut rt = Runtime::new(); assert!(rt.authorize_chained(&f, &links, did(&sk(3)), 10).is_allow()); let d = rt.charge_chained(&links, Energy::measured(900)); assert!(!d.is_allow(), "the breach is a denial"); let env = seal_chained(&f, &ids, &links[1].grant, &d, &sk(7), STEWARD).unwrap(); drop(rt); let back = restored(&[env]); assert_eq!(back.spent(&ids[1]), 900, "the leaf's debit was refunded"); assert_eq!(back.spent(&ids[0]), 900, "the ancestor's debit was refunded"); } /// A refusal debits nothing (C3), though `decide` seals the cost it refused, and /// a replay must not find a debit there. Both reasons a breach also carries. #[test] fn a_refusal_restores_nothing() { let caveated = grant(1_000, vec![Caveat::MaxCostUj(100)]); let windowed = grant(1_000, vec![joule_window(100)]); let mut log = log(); let mut rt = Runtime::new(); for sg in [&caveated, &windowed] { let f = frame(sg); let d = rt.decide(&f, sg, sg.grant.subject, Energy::measured(900), 10); assert!(!d.is_allow()); assert_eq!(d.cost.uj(), 900, "the refused cost is on the receipt"); assert_eq!(rt.spent(&sg.grant.id()), 0, "C3"); log.append(&f, &sg.grant, &d); } assert!(replay_spend(log.entries().iter().map(Vec::as_slice), STEWARD, &pubkey()).is_empty()); } /// The same refusal over a chain: no link was debited. #[test] fn a_chained_refusal_restores_nothing() { let (links, ids) = chain(10_000, 5_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&links[1]); let d = Runtime::new().decide_chained(&f, &links, did(&sk(3)), Energy::measured(900), 10); assert_eq!(d.verdict, Verdict::Deny(DenyReason::CaveatUnsatisfied)); let env = seal_chained(&f, &ids, &links[1].grant, &d, &sk(7), STEWARD).unwrap(); assert!(replay_spend([env.as_slice()], STEWARD, &pubkey()).is_empty()); } /// A two-phase debit saturates at the ceiling, so `cost_uj` is more than was /// debited. Replay restores the debit: the grant lands on its ceiling, not past it. #[test] fn a_capped_debit_restores_what_was_debited() { let sg = grant(100, Vec::new()); let f = frame(&sg); let mut rt = Runtime::new(); rt.authorize(&f, &sg, sg.grant.subject, 10); let d = rt.charge(&sg.grant, Energy::measured(150)); assert!(d.is_allow()); assert_eq!((d.cost.uj(), d.debited_uj, rt.spent(&sg.grant.id())), (150, 100, 100)); let mut log = log(); log.append(&f, &sg.grant, &d); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 100); } /// Each link saturates at its own ceiling, so a capped debit leaves the links /// differing, and one figure on the receipt could not be exact for both. #[test] fn a_capped_chain_restores_each_links_own_debit() { let (links, ids) = chain(10_000, 100, Vec::new()); let f = frame(&links[1]); let mut rt = Runtime::new(); rt.authorize_chained(&f, &links, did(&sk(3)), 10); let d = rt.charge_chained(&links, Energy::measured(150)); assert!(d.is_allow()); assert_eq!(d.chain_debited_uj, vec![150, 100], "root first"); assert_eq!(d.debited_uj, 100, "the leaf's"); let mut log = log(); log.append_chained(&f, &ids, &links[1].grant, &d); let back = restored(log.entries()); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (rt.spent(&ids[0]), rt.spent(&ids[1]))); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (150, 100)); } /// The ancestor can be the link that binds: siblings share it, so a leaf with /// room left is debited more than a root at its ceiling. #[test] fn a_chain_capped_at_its_root_restores_each_links_own_debit() { let (links, ids) = chain(100, 100, Vec::new()); let f = frame(&links[1]); let mut rt = Runtime::new(); rt.restore([(ids[0], 60)]); rt.resume_windows_at(0); rt.authorize_chained(&f, &links, did(&sk(3)), 10); let d = rt.charge_chained(&links, Energy::measured(70)); assert_eq!(d.chain_debited_uj, vec![40, 70]); let mut log = log(); log.append_chained(&f, &ids, &links[1].grant, &d); let back = restored(log.entries()); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (40, 70)); } /// Links that all saturate to one figure need no list: `debited_uj` says it. #[test] fn a_chain_capped_alike_states_one_figure() { let (links, ids) = chain(100, 100, Vec::new()); let f = frame(&links[1]); let mut rt = Runtime::new(); rt.authorize_chained(&f, &links, did(&sk(3)), 10); let d = rt.charge_chained(&links, Energy::measured(150)); assert!(d.chain_debited_uj.is_empty()); let mut log = log(); log.append_chained(&f, &ids, &links[1].grant, &d); let back = restored(log.entries()); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (100, 100)); } /// The whole property at once: a log of every kind of decision, across a chain /// and two grants, restores the runtime's ledger to the microjoule, and the /// restarted runtime carries on from it. #[test] fn a_replayed_log_is_the_ledger() { let single = grant(1_000, vec![Caveat::MaxCostUj(100)]); let (links, ids) = chain(5_000, 900, vec![Caveat::MaxCostUj(300)]); let delegate = did(&sk(3)); let (fs, fc) = (frame(&single), frame(&links[1])); let leaf = &links[1].grant; let holder = single.grant.subject; let mut log = log(); let mut rt = Runtime::new(); let d = rt.decide(&fs, &single, holder, Energy::measured(40), 10); log.append(&fs, &single.grant, &d); let d = rt.decide(&fs, &single, holder, Energy::measured(800), 10); log.append(&fs, &single.grant, &d); rt.authorize(&fs, &single, holder, 11); let d = rt.charge(&single.grant, Energy::measured(800)); log.append_flow(&fs, &single.grant, &d, &FlowSeal::clean()); let d = rt.decide_chained(&fc, &links, delegate, Energy::measured(200), 10); log.append_chained(&fc, &ids, leaf, &d); let d = rt.decide_chained(&fc, &links, delegate, Energy::measured(800), 10); log.append_chained(&fc, &ids, leaf, &d); rt.authorize_chained(&fc, &links, delegate, 11); let d = rt.charge_chained(&links, Energy::measured(500)); log.append_chained(&fc, &ids, leaf, &d); rt.authorize_chained(&fc, &links, delegate, 12); let d = rt.charge_chained(&links, Energy::measured(250)); log.append_chained(&fc, &ids, leaf, &d); assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(7)); let mut back = restored(log.entries()); assert_eq!(rt.spent(&single.grant.id()), 840, "the fixture really debited"); assert_eq!((rt.spent(&ids[0]), rt.spent(&ids[1])), (950, 900), "and capped the leaf"); for g in [single.grant.id(), ids[0], ids[1]] { assert_eq!(back.spent(&g), rt.spent(&g), "{}", g.to_hex()); } // The next decision sees the same budget it would have seen. let after = back.decide(&fs, &single, holder, Energy::measured(100), 20); assert_eq!(after, rt.decide(&fs, &single, holder, Energy::measured(100), 20)); assert_eq!(after.budget_remaining_uj, 60); } /// §8.2: a worker answers for the joules of a breach, and for a capped debit's /// figure, or the attribution stops matching the ledger. #[test] fn aggregation_matches_the_ledger() { let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let holder = sg.grant.subject; let mut log = log(); let mut rt = Runtime::new(); let d = rt.decide(&f, &sg, holder, Energy::measured(40), 10); log.append(&f, &sg.grant, &d); let d = rt.decide(&f, &sg, holder, Energy::measured(800), 10); log.append(&f, &sg.grant, &d); rt.authorize(&f, &sg, holder, 11); let d = rt.charge(&sg.grant, Energy::measured(1_100)); log.append(&f, &sg.grant, &d); let (totals, bad) = aggregate(log.entries().iter().map(Vec::as_slice), STEWARD, &pubkey()); assert_eq!(bad, 0); let t = &totals[&(sg.grant.id().to_hex(), holder.to_did_key())]; assert_eq!((t.allows, t.denies), (1, 2)); assert_eq!(t.spent_uj, rt.spent(&sg.grant.id())); assert_eq!(t.spent_uj, 1_000); } /// A receipt whose debit the verdict already says keeps the bytes it always had: /// nothing that was sealed before this member, and nothing published, moves. #[test] fn a_debit_the_verdict_says_is_not_stated() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let holder = sg.grant.subject; let mut rt = Runtime::new(); let allowed = rt.decide(&f, &sg, holder, Energy::measured(40), 10); let wire = to_jcr1_wire(&f, &sg.grant, &allowed, STEWARD); assert_eq!(wire.get("debited_uj"), None, "an allow that debited its cost"); let mut data = f.clone(); data.trust = Trust::Data; let denied = rt.decide(&data, &sg, holder, Energy::measured(40), 10); let wire = to_jcr1_wire(&data, &sg.grant, &denied, STEWARD); assert_eq!(wire.get("debited_uj"), None, "a denial no debit point can seal"); let (links, ids) = chain(1_000, 500, Vec::new()); let fc = frame(&links[1]); let d = rt.decide_chained(&fc, &links, did(&sk(3)), Energy::measured(40), 10); let wire = to_jcr1_wire_chained(&fc, &ids, &links[1].grant, &d, STEWARD); assert_eq!((wire.get("debited_uj"), wire.get("chain_debited_uj")), (None, None)); assert!(wire.get("chain_ids").is_some()); } /// Either reason the debit point seals always states the debit, zero included. /// That is what lets a reader tell a refusal from a receipt too old to say. #[test] fn a_refusal_for_a_reason_the_debit_point_seals_states_its_zero() { let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let mut rt = Runtime::new(); let d = rt.decide(&f, &sg, sg.grant.subject, Energy::measured(900), 10); assert_eq!(to_jcr1_wire(&f, &sg.grant, &d, STEWARD).get("debited_uj"), Some(&json!(0))); let env = seal(&f, &sg.grant, &d, &sk(7), STEWARD).unwrap(); assert_eq!(extract(&env, STEWARD, &pubkey()).unwrap().debited_uj, Some(0)); } /// A breach's envelope and the byte offset of its stated debit's value. fn breach() -> (Frame, SignedGrant, Decision, Vec, usize) { let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let mut rt = Runtime::new(); rt.authorize(&f, &sg, sg.grant.subject, 10); let d = rt.charge(&sg.grant, Energy::measured(900)); let env = seal(&f, &sg.grant, &d, &sk(7), STEWARD).unwrap(); let key = b"jdebited_uj"; // CBOR text(10), then the key let at = env.windows(key.len()).position(|w| w == key).expect("the core states the debit") + key.len(); assert_eq!(&env[at..at + 3], &[0x19, 0x03, 0x84], "uint16 900 follows the key"); (f, sg, d, env, at) } /// The debit is inside the signature: a stated figure changed on a sealed /// receipt, to nothing or to more, makes the receipt fail, and a failed receipt /// contributes nothing. #[test] fn a_debit_changed_on_a_signed_receipt_does_not_verify() { let (f, sg, d, env, at) = breach(); assert!(extract(&env, STEWARD, &pubkey()).is_ok()); for changed in [[0x19, 0x00, 0x00], [0x19, 0x03, 0x85], [0x19, 0x83, 0x84]] { let mut flipped = env.clone(); flipped[at..at + 3].copy_from_slice(&changed); assert!(extract(&flipped, STEWARD, &pubkey()).is_err(), "{changed:x?}"); assert!(verify(&f, &sg.grant, &d, &flipped, STEWARD, &pubkey()).is_err(), "{changed:x?}"); assert!(replay_spend([flipped.as_slice()], STEWARD, &pubkey()).is_empty(), "{changed:x?}"); } } /// In a log the same change also breaks the chain at that entry, so a restart /// refuses the stream rather than losing the debit from it. #[test] fn a_debit_changed_in_a_log_breaks_the_chain_there() { let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let mut rt = Runtime::new(); let mut log = log(); let d = rt.decide(&f, &sg, sg.grant.subject, Energy::measured(40), 10); log.append(&f, &sg.grant, &d); rt.authorize(&f, &sg, sg.grant.subject, 11); let d = rt.charge(&sg.grant, Energy::measured(900)); log.append(&f, &sg.grant, &d); let mut entries = log.entries().to_vec(); let key = b"jdebited_uj"; let at = entries[1].windows(key.len()).position(|w| w == key).unwrap() + key.len(); entries[1][at + 2] ^= 0x01; assert_eq!(verify_log(&entries, STEWARD, &pubkey()), Err(1)); assert!(ReceiptLog::new(sk(7), STEWARD).resume(entries).is_err()); } /// A decision whose debit differs from the receipt's is not that receipt's /// decision, in either direction, and the check does not fall back to the form /// without the member when the envelope carries it. #[test] fn a_decision_with_another_debit_is_not_this_receipts() { let (f, sg, d, env, _) = breach(); assert!(verify(&f, &sg.grant, &d, &env, STEWARD, &pubkey()).is_ok()); for debited_uj in [0, 899, 901] { let other = Decision { debited_uj, ..d.clone() }; assert!( matches!(verify(&f, &sg.grant, &other, &env, STEWARD, &pubkey()), Err(ReceiptError::PayloadMismatch)), "{debited_uj}" ); } let (links, ids) = chain(10_000, 100, Vec::new()); let fc = frame(&links[1]); let mut rt = Runtime::new(); rt.authorize_chained(&fc, &links, did(&sk(3)), 10); let d = rt.charge_chained(&links, Energy::measured(150)); let env = seal_chained(&fc, &ids, &links[1].grant, &d, &sk(7), STEWARD).unwrap(); assert!(verify_chained(&fc, &ids, &links[1].grant, &d, &env, STEWARD, &pubkey()).is_ok()); let other = Decision { chain_debited_uj: vec![100, 100], ..d.clone() }; assert!(verify_chained(&fc, &ids, &links[1].grant, &other, &env, STEWARD, &pubkey()).is_err()); } /// Seal `wire` as it stands, as a receipt whose form this implementation would /// not produce itself. fn seal_wire(wire: &Value) -> Vec { jcr1::cose_sign1_envelope(&jcr1::signed_core_bytes(wire).unwrap(), &sk(7), STEWARD) } /// A receipt sealed before the member exists: it keeps verifying and reading, /// and an unknown debit is not read as a zero one. A breach and a refusal of /// that age look alike, so both read as the most either can have debited. #[test] fn a_receipt_sealed_before_the_member_reads_as_the_most_it_can_have_debited() { let (f, sg, d, env, _) = breach(); let mut wire = to_jcr1_wire(&f, &sg.grant, &d, STEWARD); wire.as_object_mut().unwrap().remove("debited_uj"); let old = seal_wire(&wire); assert_ne!(old, env); let core = extract(&old, STEWARD, &pubkey()).expect("still readable"); assert_eq!(core.debited_uj, None); assert!(verify(&f, &sg.grant, &d, &old, STEWARD, &pubkey()).is_ok(), "still verifies"); assert_eq!(replay_spend([old.as_slice()], STEWARD, &pubkey()), vec![(sg.grant.id(), 900)]); let mut rt = Runtime::new(); let refused = rt.decide(&f, &sg, sg.grant.subject, Energy::measured(800), 10); let mut wire = to_jcr1_wire(&f, &sg.grant, &refused, STEWARD); wire.as_object_mut().unwrap().remove("debited_uj"); let old = seal_wire(&wire); assert!(verify(&f, &sg.grant, &refused, &old, STEWARD, &pubkey()).is_ok()); assert_eq!(replay_spend([old.as_slice()], STEWARD, &pubkey()), vec![(sg.grant.id(), 800)]); } /// Every other old receipt reads as it always did. #[test] fn an_old_receipt_the_verdict_settles_reads_as_it_did() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let mut data = f.clone(); data.trust = Trust::Data; let mut rt = Runtime::new(); let allowed = rt.decide(&f, &sg, sg.grant.subject, Energy::measured(40), 10); let denied = rt.decide(&data, &sg, sg.grant.subject, Energy::measured(40), 10); let allow = seal(&f, &sg.grant, &allowed, &sk(7), STEWARD).unwrap(); let deny = seal(&data, &sg.grant, &denied, &sk(7), STEWARD).unwrap(); assert_eq!(extract(&allow, STEWARD, &pubkey()).unwrap().debited_uj, None); assert_eq!(extract(&deny, STEWARD, &pubkey()).unwrap().debited_uj, None); assert_eq!(replay_spend([allow.as_slice(), deny.as_slice()], STEWARD, &pubkey()), vec![(sg.grant.id(), 40)]); } /// An old log can restore a grant past its ceiling: a capped allow reads at its /// full cost. A debit against that spend must not underflow the remainder. #[test] fn a_charge_after_an_over_restored_grant_leaves_nothing_remaining() { let sg = grant(100, Vec::new()); let mut rt = Runtime::new(); rt.restore([(sg.grant.id(), 150)]); let d = rt.charge(&sg.grant, Energy::measured(10)); assert_eq!((d.budget_remaining_uj, d.debited_uj), (0, 0)); } /// A debit member that does not parse must not read as absent, which means /// something else, and a list that does not match the chain must not shed a /// link's debit by zipping short. #[test] fn a_malformed_debit_member_is_refused() { let (f, sg, d, _, _) = breach(); let mut wire = to_jcr1_wire(&f, &sg.grant, &d, STEWARD); wire["debited_uj"] = json!("900"); assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).is_err(), "a text figure"); let mut wire = to_jcr1_wire(&f, &sg.grant, &d, STEWARD); wire["chain_debited_uj"] = json!([900]); assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).is_err(), "a list with no chain"); let (links, ids) = chain(10_000, 100, Vec::new()); let fc = frame(&links[1]); let mut rt = Runtime::new(); rt.authorize_chained(&fc, &links, did(&sk(3)), 10); let d = rt.charge_chained(&links, Energy::measured(150)); let mut wire = to_jcr1_wire_chained(&fc, &ids, &links[1].grant, &d, STEWARD); assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).is_ok()); wire["chain_debited_uj"] = json!([150]); assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).is_err(), "one entry for two links"); wire["chain_debited_uj"] = json!([150, "100"]); assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).is_err(), "a text entry"); } /// A one-link chain appends what `append` would, as its seal does. #[test] fn a_one_link_chain_appends_a_plain_entry() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let d = Runtime::new().decide(&f, &sg, sg.grant.subject, Energy::measured(40), 10); let (mut plain, mut chained) = (log(), log()); assert_eq!(chained.append_chained(&f, &[sg.grant.id()], &sg.grant, &d), plain.append(&f, &sg.grant, &d)); } // ── P1: a replay restores money and the tainted sub-budget too ────────────── // // Each test below once reproduced a restart that handed back money or a // tainted wallet, since a replay restored joules alone. The core now states // both (SPEC §8.1), and a replay restores them. The positive control before // each fault assertion holds that the replay itself ran. /// A grant bounded in money as well as joules (SPEC §4.8). fn priced_grant(budget_uj: u64, minor: u64) -> SignedGrant { issue( &sk(1), Grant { issuer: did(&sk(1)), subject: did(&sk(2)), capability: Capability::new(resource(), ["read"]), budget_uj, budget_funds: Some(jcp_schema::Funds::new("USD", minor)), caveats: Vec::new(), expiry: 0, parent: None, delegate_depth: None, nonce: [9; 16], }, ) .unwrap() } /// Fault P1, money: a restart through receipt replay handed back a grant's /// money spend, since the core recorded joules only. It now states the money /// a decision debited, so a spent money ceiling stays spent. #[test] fn p1_a_replayed_restart_refunds_money_spend() { let sg = priced_grant(1_000, 300); let f = frame(&sg); let holder = sg.grant.subject; let usd = |minor| jcp_runtime::Price::new("USD", minor); let mut log = log(); let mut live = Runtime::new(); let d = live.decide_priced(&f, &sg, holder, Energy::measured(10), &usd(300), 10); assert!(d.is_allow()); log.append(&f, &sg.grant, &d); assert_eq!(live.spent_minor(&sg.grant.id()), 300, "the money ceiling is spent"); let next = live.decide_priced(&f, &sg, holder, Energy::measured(10), &usd(1), 10).verdict; assert_eq!(next, Verdict::Deny(DenyReason::FundsExceeded)); let mut back = restored(log.entries()); assert_eq!(back.spent(&sg.grant.id()), 10, "the replay restored the joules"); let spent_minor = back.spent_minor(&sg.grant.id()); let after = back.decide_priced(&f, &sg, holder, Energy::measured(10), &usd(1), 10).verdict; assert_eq!( after, next, "fault P1: the restart refunded money spend: it restored {spent_minor} of 300 minor units, so a spent \ money ceiling admits another priced call" ); } /// Fault P1, the tainted sub-budget: a restart through receipt replay handed /// back what tainted calls accrued against a grant's tainted sub-budget /// (SPEC §4.10), so an exhausted tainted wallet admitted tainted work again. /// The accrual is now read off the receipt's flow block. #[test] fn p1_a_replayed_restart_refunds_the_tainted_sub_budget() { let wallet = jcp_schema::FlowCeiling { ceiling: Label::top(), tainted_budget_uj: Some(40) }; let sg = grant(1_000, vec![Caveat::FlowBelow(wallet)]); let f = frame(&sg); let holder = sg.grant.subject; let facts = FlowFacts { label: Label { integrity: Integrity::Untrusted, origins: Origins::one(Origin::channel(ContentId::of_bytes(b"mail:read"))), readers: Readers::Public, }, witness_id: ContentId::of_bytes(b"a witness"), eval_uj: 42, eval_prov: Provenance::ModelBased, }; let seal = FlowSeal { facts: Some(facts.clone()), ceiling: None }; let mut log = log(); let mut live = Runtime::new(); assert!(live.authorize_flow(&f, &sg, holder, 10, &facts).is_allow()); let d = live.charge_flow(&sg.grant, Energy::measured(40), &facts); assert!(d.is_allow()); log.append_flow(&f, &sg.grant, &d, &seal); assert_eq!(live.spent_tainted(&sg.grant.id()), 40, "the tainted wallet is spent"); let next = live.authorize_flow(&f, &sg, holder, 10, &facts).verdict; assert_eq!(next, Verdict::Deny(DenyReason::TaintBudgetExceeded)); let mut back = restored(log.entries()); assert_eq!(back.spent(&sg.grant.id()), 40, "the replay restored the joules"); let tainted = back.spent_tainted(&sg.grant.id()); let after = back.authorize_flow(&f, &sg, holder, 10, &facts).verdict; assert_eq!( after, next, "fault P1: the restart refunded the tainted sub-budget: it restored {tainted} of 40 µJ, so an exhausted \ tainted wallet admits another tainted call" ); } // ── the members that close P1 (SPEC §8.1) ─────────────────────────────────── /// An argument from an untrusted channel. fn tainted_facts() -> FlowFacts { FlowFacts { label: Label { integrity: Integrity::Untrusted, origins: Origins::one(Origin::channel(ContentId::of_bytes(b"mail:read"))), readers: Readers::Public, }, witness_id: ContentId::of_bytes(b"a witness"), eval_uj: 42, eval_prov: Provenance::ModelBased, } } /// Money over a chain: every link with a money ceiling is debited, and one /// without is debited none, so the receipt says what each link was and a /// replay restores exactly the runtime's money ledger. #[test] fn a_priced_chain_states_each_links_money() { let usd = |minor| jcp_runtime::Price::new("USD", minor); let root = grant(10_000, Vec::new()); let kid = jcp_grant::attenuate_priced( &root, &sk(2), did(&sk(3)), Capability::new(resource(), ["read"]), 5_000, Some(jcp_schema::Funds::new("USD", 500)), 0, [8; 16], ) .unwrap(); let links = vec![root, kid]; let ids: Vec = links.iter().map(|sg| sg.grant.id()).collect(); let f = frame(&links[1]); let mut rt = Runtime::new(); let d = rt.decide_chained_priced(&f, &links, did(&sk(3)), Energy::measured(40), &usd(200), 10); assert!(d.is_allow()); assert_eq!((d.debited_minor, d.chain_debited_minor.clone()), (200, vec![0, 200])); let wire = to_jcr1_wire_chained(&f, &ids, &links[1].grant, &d, STEWARD); assert_eq!((wire.get("debited_minor"), wire.get("chain_debited_minor")), (Some(&json!(200)), Some(&json!([0, 200])))); let env = seal_chained(&f, &ids, &links[1].grant, &d, &sk(7), STEWARD).unwrap(); assert!(verify_chained(&f, &ids, &links[1].grant, &d, &env, STEWARD, &pubkey()).is_ok()); let back = restored(&[env]); assert_eq!((back.spent_minor(&ids[0]), back.spent_minor(&ids[1])), (0, 200)); assert_eq!((rt.spent_minor(&ids[0]), rt.spent_minor(&ids[1])), (0, 200)); // Both links bounded: one figure for every link, and no per-link list. let bounded = priced_grant(10_000, 500); let heir = attenuate(&bounded, &sk(2), did(&sk(3)), Capability::new(resource(), ["read"]), 5_000, 0, [8; 16]).unwrap(); let both = vec![bounded, heir]; let ids: Vec = both.iter().map(|sg| sg.grant.id()).collect(); let f = frame(&both[1]); let mut rt = Runtime::new(); let d = rt.decide_chained_priced(&f, &both, did(&sk(3)), Energy::measured(40), &usd(200), 10); assert!(d.is_allow() && d.chain_debited_minor.is_empty()); let env = seal_chained(&f, &ids, &both[1].grant, &d, &sk(7), STEWARD).unwrap(); let back = restored(&[env]); assert_eq!((back.spent_minor(&ids[0]), back.spent_minor(&ids[1])), (200, 200)); } /// A priced receipt sealed before `debited_minor` existed keeps verifying, and /// cannot show its money debit: it reads as its joules and no money. One that /// carries the member binds it, in either direction. #[test] fn a_priced_receipt_sealed_before_the_money_member_cannot_show_its_money() { let sg = priced_grant(1_000, 300); let f = frame(&sg); let mut rt = Runtime::new(); let d = rt.decide_priced(&f, &sg, sg.grant.subject, Energy::measured(10), &jcp_runtime::Price::new("USD", 300), 10); let mut wire = to_jcr1_wire(&f, &sg.grant, &d, STEWARD); assert_eq!(wire.get("debited_minor"), Some(&json!(300))); wire.as_object_mut().unwrap().remove("debited_minor"); let old = seal_wire(&wire); assert!(verify(&f, &sg.grant, &d, &old, STEWARD, &pubkey()).is_ok(), "still verifies"); let core = extract(&old, STEWARD, &pubkey()).unwrap(); assert_eq!((core.debits(), core.money_debits()), (vec![(sg.grant.id(), 10)], vec![])); let env = seal(&f, &sg.grant, &d, &sk(7), STEWARD).unwrap(); assert!(verify(&f, &sg.grant, &d, &env, STEWARD, &pubkey()).is_ok()); for debited_minor in [0, 299, 301] { let other = Decision { debited_minor, ..d.clone() }; assert!( matches!(verify(&f, &sg.grant, &other, &env, STEWARD, &pubkey()), Err(ReceiptError::PayloadMismatch)), "{debited_minor}" ); } } /// A tainted call sealed with its flow block keeps the bytes it always had: /// the block and the debit say what it accrued. A refusal there accrued /// nothing, though it records the cost it refused. #[test] fn a_flow_block_says_what_a_tainted_call_accrued() { let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let facts = tainted_facts(); let seal_t = FlowSeal { facts: Some(facts.clone()), ceiling: None }; let mut rt = Runtime::new(); rt.authorize_flow(&f, &sg, sg.grant.subject, 10, &facts); let d = rt.charge_flow(&sg.grant, Energy::measured(40), &facts); assert_eq!(d.tainted_uj, 40); let wire = jcp_receipt::to_jcr1_wire_flow(&f, &sg.grant, &d, STEWARD, &seal_t); assert_eq!((wire["jcp_receipt"].as_str(), wire.get("tainted_uj")), (Some("jcp/3"), None)); let env = jcp_receipt::seal_flow(&f, &sg.grant, &d, &sk(7), STEWARD, &seal_t).unwrap(); assert_eq!(extract(&env, STEWARD, &pubkey()).unwrap().tainted_accruals(), vec![(sg.grant.id(), 40)]); let refused = rt.decide_flow(&f, &sg, sg.grant.subject, Energy::measured(900), 10, &facts); assert_eq!(refused.verdict, Verdict::Deny(DenyReason::CaveatUnsatisfied)); let env = jcp_receipt::seal_flow(&f, &sg.grant, &refused, &sk(7), STEWARD, &seal_t).unwrap(); let core = extract(&env, STEWARD, &pubkey()).unwrap(); assert!(core.flow && core.tainted_accruals().is_empty(), "a refusal accrued nothing"); // A flow block on a decision that accrued nothing states its zero. let untainted = Decision { tainted_uj: 0, ..d.clone() }; let wire = jcp_receipt::to_jcr1_wire_flow(&f, &sg.grant, &untainted, STEWARD, &seal_t); assert_eq!(wire.get("tainted_uj"), Some(&json!(0))); } /// Where no flow block says it, the receipt states the accrual: a chained /// tainted decision, on every link, and a tainted call sealed plain. #[test] fn a_tainted_accrual_no_flow_block_says_is_stated() { let facts = tainted_facts(); let (links, ids) = chain(10_000, 5_000, Vec::new()); let f = frame(&links[1]); let mut rt = Runtime::new(); let d = rt.decide_chained_flow(&f, &links, did(&sk(3)), Energy::measured(30), 10, &facts); assert!(d.is_allow()); let env = seal_chained(&f, &ids, &links[1].grant, &d, &sk(7), STEWARD).unwrap(); assert_eq!(extract(&env, STEWARD, &pubkey()).unwrap().tainted_uj, Some(30)); let back = restored(&[env]); for id in &ids { assert_eq!((back.spent_tainted(id), rt.spent_tainted(id)), (30, 30)); } // A breach sealed plain states both its debit and its accrual. Sealed // before `tainted_uj` existed, it still verifies, and reads as no accrual. let sg = grant(1_000, vec![Caveat::MaxCostUj(100)]); let f = frame(&sg); let mut rt = Runtime::new(); rt.authorize_flow(&f, &sg, sg.grant.subject, 10, &facts); let d = rt.charge_flow(&sg.grant, Energy::measured(900), &facts); let mut wire = to_jcr1_wire(&f, &sg.grant, &d, STEWARD); assert_eq!((wire.get("debited_uj"), wire.get("tainted_uj")), (Some(&json!(900)), Some(&json!(900)))); wire.as_object_mut().unwrap().remove("tainted_uj"); let old = seal_wire(&wire); assert!(verify(&f, &sg.grant, &d, &old, STEWARD, &pubkey()).is_ok()); assert!(extract(&old, STEWARD, &pubkey()).unwrap().tainted_accruals().is_empty()); let other = Decision { tainted_uj: 899, ..d.clone() }; assert!(verify(&f, &sg.grant, &other, &seal(&f, &sg.grant, &d, &sk(7), STEWARD).unwrap(), STEWARD, &pubkey()).is_err()); } /// A money or accrual member that does not parse is refused rather than read /// as absent, and a per-link money list must match the chain. #[test] fn a_malformed_money_or_accrual_member_is_refused() { let sg = priced_grant(1_000, 300); let f = frame(&sg); let d = Runtime::new().decide_priced(&f, &sg, sg.grant.subject, Energy::measured(10), &jcp_runtime::Price::new("USD", 300), 10); for (member, value) in [("debited_minor", json!("300")), ("tainted_uj", json!("40")), ("chain_debited_minor", json!([300]))] { let mut wire = to_jcr1_wire(&f, &sg.grant, &d, STEWARD); wire[member] = value; assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).is_err(), "{member}"); } } // ── holds: the decision recorded before the work (SPEC §8.1, §4.11) ──────── fn unresolved(entries: &[Vec]) -> Vec { jcp_receipt::unresolved_holds(entries.iter().map(Vec::as_slice), STEWARD, &pubkey()) } /// The two-phase path seals its allow as a hold before the work runs. A /// stream that ends at the hold, as one does when the enforcement point stops /// while the work runs, restores what the hold states; one holding the /// receipt that settles it restores the work's debit alone, in whatever /// order the two are read. #[test] fn a_hold_restores_its_figure_until_a_receipt_settles_it() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let mut rt = Runtime::new(); let mut log = log(); let auth = rt.authorize(&f, &sg, sg.grant.subject, 10); let bound = rt.hold_bound(&sg.grant, &FlowFacts::bottom()); assert_eq!(bound, 1_000, "the most a charge can debit: what the grant has left"); log.append_hold(&f, &sg.grant, &auth, &FlowSeal::clean(), bound, 0); let hold = log.head(); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 1_000, "a stop while the work ran refunded it"); assert_eq!(unresolved(log.entries()), vec![hold]); let d = rt.charge(&sg.grant, Energy::measured(70)); log.append_settle(&f, &sg.grant, &d, &FlowSeal::clean(), hold); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 70, "a settled hold added to its work's debit"); assert!(unresolved(log.entries()).is_empty()); let reversed: Vec> = log.entries().iter().rev().cloned().collect(); assert_eq!(restored(&reversed).spent(&sg.grant.id()), 70, "the settle read first still settles the hold"); let core = extract(&log.entries()[1], STEWARD, &pubkey()).unwrap(); assert_eq!((core.settles, core.debits()), (Some(hold), vec![(sg.grant.id(), 70)])); assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(2)); } /// **An unresolved hold restores its money, not only its joules.** A replay /// that restored one dimension and dropped the other would rebuild a state in /// which the interrupted charge can be paid for a second time. #[test] fn a_hold_restores_the_money_it_reserved_until_it_is_settled() { let g = issue( &sk(1), Grant { issuer: did(&sk(1)), subject: did(&sk(2)), capability: Capability::new(resource(), ["read"]), budget_uj: 1_000, budget_funds: Some(jcp_schema::Funds::new("USD", 500)), caveats: vec![Caveat::MaxCostUj(100)], expiry: 0, parent: None, delegate_depth: None, nonce: [9; 16], }, ) .unwrap(); let f = frame(&g); let mut rt = Runtime::new(); let mut log = log(); let gid = g.grant.id(); let (auth, held) = rt.hold_priced(&f, &g, g.grant.subject, 10, &FlowFacts::bottom(), &Price::new("USD", 400)); let h = held.expect("the hold was allowed"); assert_eq!((h.uj, h.minor), (100, 400)); log.append_hold(&f, &g.grant, &auth, &FlowSeal::clean(), h.uj, h.minor); let hold = log.head(); // a stop while the work ran: the replay restores BOTH reservations let back = restored(log.entries()); assert_eq!(back.spent(&gid), 100, "the joule reservation came back"); assert_eq!(back.spent_minor(&gid), 400, "the money reservation came back"); assert_eq!(unresolved(log.entries()), vec![hold]); // settled for a tenth of the maximum: the replay restores what it cost let d = rt.settle_priced(&g.grant, h.id, Energy::measured(40), &Price::new("USD", 40), &FlowFacts::bottom()); assert!(d.is_allow()); log.append_settle(&f, &g.grant, &d, &FlowSeal::clean(), hold); let back = restored(log.entries()); assert_eq!((back.spent(&gid), back.spent_minor(&gid)), (40, 40)); assert!(unresolved(log.entries()).is_empty()); assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(2)); let core = extract(&log.entries()[0], STEWARD, &pubkey()).unwrap(); assert_eq!(core.held_minor, Some(400)); assert_eq!(core.held_money(), vec![(gid, 400)]); } /// A hold reserving no money omits `held_minor` entirely, so a joules-only /// hold's receipt is byte-identical to one sealed before the money dimension /// existed — which is what keeps every published vector valid. #[test] fn a_joules_only_hold_states_no_money_member() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let mut rt = Runtime::new(); let mut log = log(); let auth = rt.authorize(&f, &sg, sg.grant.subject, 10); let bound = rt.hold_bound(&sg.grant, &FlowFacts::bottom()); let with_zero = log.append_hold(&f, &sg.grant, &auth, &FlowSeal::clean(), bound, 0); let core = extract(&with_zero, STEWARD, &pubkey()).unwrap(); assert_eq!(core.held_minor, None, "a zero reservation is omitted, not stated as 0"); assert!(core.held_money().is_empty()); assert_eq!(core.held(), vec![(sg.grant.id(), bound)]); } /// **The marker reaches the audit trail.** §4.3's whole point is that an allow /// by a point that had not yet heard of a revocation is explicable, while one /// by a point that heard and ignored it is a violation — and that nothing in a /// receipt told them apart. A receipt from a point that declares its marker now /// states it, under the same signature as the verdict. #[test] fn a_receipt_states_the_sealing_points_revocation_marker() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let mut rt = Runtime::new(); let mut log = log(); assert_eq!(log.revocation_marker(), None, "a fresh log has not said"); let silent = log.append(&f, &sg.grant, &rt.decide(&f, &sg, sg.grant.subject, Energy::measured(10), 10)); let core = extract(&silent, STEWARD, &pubkey()).unwrap(); assert_eq!(core.known_through, None, "a point that has not said states nothing"); log.declare_revocation_marker(7); let stated = log.append(&f, &sg.grant, &rt.decide(&f, &sg, sg.grant.subject, Energy::measured(10), 11)); let core = extract(&stated, STEWARD, &pubkey()).unwrap(); assert_eq!(core.known_through, Some(7)); assert_eq!(core.verdict, "allow", "the marker rides beside the verdict, not instead of it"); // A revocation issued at marker 9 is not explained by a point at 7 having // heard it; one at 11 would be. The auditor's comparison, now possible. assert!(core.known_through.is_some_and(|m| m < 9), "an allow at 7 is explicable against M=9"); log.declare_revocation_marker(11); let later = log.append(&f, &sg.grant, &rt.decide(&f, &sg, sg.grant.subject, Energy::measured(10), 12)); let core = extract(&later, STEWARD, &pubkey()).unwrap(); assert!(core.known_through.is_some_and(|m| m >= 9), "an allow at 11 is not"); assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(3)); } /// A point that declares nothing seals exactly what it sealed before the member /// existed — so "did not say" stays distinguishable from "said 0", and every /// published vector stays valid. #[test] fn an_undeclared_marker_is_omitted_rather_than_stated_as_zero() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let mut rt = Runtime::new(); let d = rt.decide(&f, &sg, sg.grant.subject, Energy::measured(10), 10); let silent = log().append(&f, &sg.grant, &d); let mut zero = log(); zero.declare_revocation_marker(0); let claimed = zero.append(&f, &sg.grant, &d); assert_ne!(silent, claimed, "saying 0 is a claim; saying nothing is not"); assert_eq!(extract(&claimed, STEWARD, &pubkey()).unwrap().known_through, Some(0)); assert_eq!(extract(&silent, STEWARD, &pubkey()).unwrap().known_through, None); } fn lifecycle_grant(nonce: u8) -> SignedGrant { issue( &sk(1), Grant { issuer: did(&sk(1)), subject: did(&sk(2)), capability: Capability::new(resource(), ["read"]), budget_uj: 10_000, budget_funds: Some(jcp_schema::Funds::new("USD", 1_000)), caveats: vec![Caveat::MaxCostUj(500)], expiry: 0, parent: None, delegate_depth: None, nonce: [nonce; 16], }, ) .unwrap() } /// **A partially captured hold restores what it still reserves, not what it /// first reserved.** The captures restore their own debits; restoring the whole /// reservation beside them would count the captured part twice and leave a /// restarted point refusing calls it has room for. #[test] fn a_replay_restores_a_partly_captured_hold_less_what_was_captured() { let g = lifecycle_grant(21); let f = frame(&g); let gid = g.grant.id(); let facts = FlowFacts::bottom(); let mut rt = Runtime::new(); let mut log = log(); let (auth, h) = rt.hold_priced(&f, &g, g.grant.subject, 10, &facts, &Price::new("USD", 600)); let h = h.expect("held"); log.append_hold(&f, &g.grant, &auth, &FlowSeal::clean(), h.uj, h.minor); let hold = log.head(); let c = rt.capture_priced(&g.grant, h.id, Energy::measured(100), &Price::new("USD", 200), 11, &facts); assert!(c.is_allow()); log.append_capture(&f, &g.grant, &c, &FlowSeal::clean(), hold); // a stop here: 200 captured + 400 still reserved = 600, not 800 let back = restored(log.entries()); assert_eq!(back.spent_minor(&gid), 600, "the captured part was counted twice"); assert_eq!(back.spent(&gid), 500, "100 captured + 400 joules still reserved"); assert_eq!(unresolved(log.entries()), vec![hold], "a capture does not settle its hold"); let core = extract(&log.entries()[1], STEWARD, &pubkey()).unwrap(); assert_eq!((core.captures, core.settles), (Some(hold), None)); // settled: only what the captures and the final draw debited let d = rt.settle_priced(&g.grant, h.id, Energy::measured(10), &Price::new("USD", 50), &facts); log.append_settle(&f, &g.grant, &d, &FlowSeal::clean(), hold); let back = restored(log.entries()); assert_eq!((back.spent(&gid), back.spent_minor(&gid)), (110, 250)); assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(3)); } /// A reclaimed hold restores no money and still its joules; a refund restores /// as a refund, beside a gross debit that never decreases. #[test] fn a_replay_restores_a_reclaim_and_a_refund_as_what_they_were() { let g = lifecycle_grant(22); let f = frame(&g); let gid = g.grant.id(); let facts = FlowFacts::bottom(); let mut rt = Runtime::new(); let mut log = log(); let (auth, h) = rt.hold_priced_until(&f, &g, g.grant.subject, 10, &facts, &Price::new("USD", 600), 100); let h = h.expect("held"); log.append_hold(&f, &g.grant, &auth, &FlowSeal::clean(), h.uj, h.minor); let hold = log.head(); let (d, released) = rt.reclaim(&g.grant, h.id, 101); assert_eq!((d.is_allow(), released), (true, 600)); log.append_reclaim(&f, &g.grant, &d, hold); let back = restored(log.entries()); assert_eq!(back.spent_minor(&gid), 0, "a reclaimed reservation was restored"); assert_eq!(back.spent(&gid), 500, "the joule reservation is not reclaimed"); // spend, then the issuer refunds part let spend = rt.decide_priced(&f, &g, g.grant.subject, Energy::measured(1), &Price::new("USD", 300), 102); assert!(spend.is_allow() || spend.verdict == Verdict::Deny(DenyReason::BudgetExceeded)); let mut log2 = self::log(); let mut rt2 = Runtime::new(); let spend = rt2.decide_priced(&f, &g, g.grant.subject, Energy::measured(1), &Price::new("USD", 300), 10); assert!(spend.is_allow()); log2.append(&f, &g.grant, &spend); let r = rt2.refund(&g.grant, g.grant.issuer, &Price::new("USD", 120)); assert!(r.is_allow()); log2.append_refund(&f, &g.grant, &r, 120); let refused = rt2.refund(&g.grant, g.grant.subject, &Price::new("USD", 50)); log2.append_refund(&f, &g.grant, &refused, 50); let back = restored(log2.entries()); assert_eq!((back.spent_minor(&gid), back.refunded_minor(&gid), back.net_minor(&gid)), (300, 120, 180)); let core = extract(&log2.entries()[1], STEWARD, &pubkey()).unwrap(); assert_eq!(core.refunded_minor, Some(120)); let core = extract(&log2.entries()[2], STEWARD, &pubkey()).unwrap(); assert_eq!(core.refunded_minor, None, "a refused refund states no amount"); } /// **A chained hold, captured and reclaimed, replays on every link.** Each /// link restores its joules less what was captured, and no money once the /// reclaim is read — the same rules as a lone grant, applied per link. #[test] fn a_replay_restores_a_chained_capture_and_reclaim_on_every_link() { let root = issue( &sk(1), Grant { issuer: did(&sk(1)), subject: did(&sk(2)), capability: Capability::new(resource(), ["read"]), budget_uj: 5_000, budget_funds: Some(jcp_schema::Funds::new("USD", 1_000)), caveats: vec![Caveat::MaxCostUj(400)], expiry: 0, parent: None, delegate_depth: None, nonce: [41; 16], }, ) .unwrap(); let kid = attenuate(&root, &sk(2), did(&sk(3)), Capability::new(resource(), ["read"]), 3_000, 0, [42; 16]).unwrap(); let chain = [root.clone(), kid.clone()]; let ids = [root.grant.id(), kid.grant.id()]; let f = frame(&kid); let mut rt = Runtime::new(); let mut log = log(); let (auth, h) = rt.hold_chained_priced_until(&f, &chain, did(&sk(3)), 10, &Price::new("USD", 600), 100); let h = h.expect("held"); log.append_hold_chained(&f, &ids, &kid.grant, &auth, h.uj, h.minor); let hold = log.head(); let back = restored(log.entries()); assert_eq!((back.spent(&ids[0]), back.spent_minor(&ids[0])), (400, 600), "the parent restores the reservation"); assert_eq!((back.spent(&ids[1]), back.spent_minor(&ids[1])), (400, 600), "and so does the delegate"); let c = rt.capture_chained_priced(&chain, h.id, Energy::measured(150), &Price::new("USD", 200), 50); assert!(c.is_allow()); log.append_capture_chained(&f, &ids, &kid.grant, &c, hold); let back = restored(log.entries()); for g in &ids { assert_eq!((back.spent(g), back.spent_minor(g)), (400, 600), "150+250 joules, 200+400 money: nothing counted twice"); } let (d, released) = rt.reclaim_chained(&chain, h.id, 101); assert_eq!((d.is_allow(), released), (true, 400)); log.append_reclaim_chained(&f, &ids, &kid.grant, &d, hold); let back = restored(log.entries()); for g in &ids { assert_eq!(back.spent_minor(g), 200, "only the captured money remains on each link"); assert_eq!(back.spent(g), 400, "the joule reservation is not reclaimed on any link"); } assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(3)); } /// Following a hold is not settling it: the receipt a restarted enforcement /// point seals next links to the hold by `prev`, names no hold, and leaves the /// hold restoring its figure. #[test] fn a_receipt_after_a_hold_does_not_settle_it() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let mut rt = Runtime::new(); let mut log = log(); let auth = rt.authorize(&f, &sg, sg.grant.subject, 10); log.append_hold(&f, &sg.grant, &auth, &FlowSeal::clean(), rt.hold_bound(&sg.grant, &FlowFacts::bottom()), 0); let mut back = restored(log.entries()); let refused = back.authorize(&f, &sg, sg.grant.subject, 11); assert_eq!(refused.verdict, Verdict::Deny(DenyReason::BudgetExceeded), "the held grant admits nothing more"); log.append(&f, &sg.grant, &refused); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 1_000); assert_eq!(unresolved(log.entries()).len(), 1); } /// A tainted hold carries its call's flow block, so a hold nothing settles /// holds the tainted sub-budget as well as the joules; settled, it accrues /// nothing and its settle accrues the call's cost. #[test] fn a_tainted_hold_holds_the_wallet_too() { let wallet = jcp_schema::FlowCeiling { ceiling: Label::top(), tainted_budget_uj: Some(40) }; let sg = grant(1_000, vec![Caveat::FlowBelow(wallet)]); let f = frame(&sg); let facts = tainted_facts(); let seal = FlowSeal { facts: Some(facts.clone()), ceiling: None }; let mut rt = Runtime::new(); let mut log = log(); let auth = rt.authorize_flow(&f, &sg, sg.grant.subject, 10, &facts); assert!(auth.is_allow()); let bound = rt.hold_bound(&sg.grant, &facts); log.append_hold(&f, &sg.grant, &auth, &seal, bound, 0); let hold = log.head(); let mut back = restored(log.entries()); assert_eq!((back.spent(&sg.grant.id()), back.spent_tainted(&sg.grant.id())), (bound, bound)); assert!(!back.authorize_flow(&f, &sg, sg.grant.subject, 11, &facts).is_allow()); let d = rt.charge_flow(&sg.grant, Energy::measured(30), &facts); log.append_settle(&f, &sg.grant, &d, &seal, hold); let back = restored(log.entries()); assert_eq!((back.spent(&sg.grant.id()), back.spent_tainted(&sg.grant.id())), (30, 30)); } /// §8.2: a call sealed as a hold and its settle is one action, counted once /// at the settle's debit; a hold no receipt settles is not attributed at all, /// though a replay restores its `held_uj`. An aggregator that read a hold as /// an allow would count each call twice, and one that read an unsettled hold /// as a debit would attribute a figure no receipt shows was spent. #[test] fn aggregation_counts_a_held_call_once_and_an_unsettled_hold_not_at_all() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let holder = sg.grant.subject; let mut rt = Runtime::new(); let mut log = log(); let totals = |entries: &[Vec]| { let (totals, bad) = aggregate(entries.iter().map(Vec::as_slice), STEWARD, &pubkey()); assert_eq!(bad, 0); totals[&(sg.grant.id().to_hex(), holder.to_did_key())].clone() }; let auth = rt.authorize(&f, &sg, holder, 10); log.append_hold(&f, &sg.grant, &auth, &FlowSeal::clean(), rt.hold_bound(&sg.grant, &FlowFacts::bottom()), 0); let hold = log.head(); let d = rt.charge(&sg.grant, Energy::measured(70)); log.append_settle(&f, &sg.grant, &d, &FlowSeal::clean(), hold); let t = totals(log.entries()); assert_eq!((t.allows, t.denies, t.spent_uj), (1, 0, 70), "a hold and its settle are one call"); assert_eq!(t.spent_uj, rt.spent(&sg.grant.id()), "and the attribution matches the ledger"); // The next call stops between its hold and its outcome. let auth = rt.authorize(&f, &sg, holder, 11); let bound = rt.hold_bound(&sg.grant, &FlowFacts::bottom()); assert_eq!(bound, 930, "what the grant has left"); log.append_hold(&f, &sg.grant, &auth, &FlowSeal::clean(), bound, 0); let t = totals(log.entries()); assert_eq!((t.allows, t.denies, t.spent_uj), (1, 0, 70), "a hold no receipt settles is not attributed"); assert_eq!(restored(log.entries()).spent(&sg.grant.id()), 70 + bound, "though a replay restores its figure"); } /// A refusal debits nothing (C3) and lets nothing run, so it holds nothing: /// `append_hold` seals it without `held_uj`, and a refusal that carries one /// reads by the ordinary rules. #[test] fn a_refusal_holds_nothing() { let sg = grant(100, Vec::new()); let f = frame(&sg); let refused = Runtime::new().decide(&f, &sg, sg.grant.subject, Energy::measured(500), 10); assert!(!refused.is_allow()); let mut log = log(); log.append_hold(&f, &sg.grant, &refused, &FlowSeal::clean(), 100, 0); let core = extract(&log.entries()[0], STEWARD, &pubkey()).unwrap(); assert_eq!(core.held_uj, None); assert!(replay_spend(log.entries().iter().map(Vec::as_slice), STEWARD, &pubkey()).is_empty()); let mut wire = to_jcr1_wire(&f, &sg.grant, &refused, STEWARD); wire["held_uj"] = json!(100); let env = seal_wire(&wire); let core = extract(&env, STEWARD, &pubkey()).unwrap(); assert!(!core.is_hold() && core.held().is_empty()); assert!(replay_spend([env.as_slice()], STEWARD, &pubkey()).is_empty()); } /// A hold placed through a delegation chain names every link (SPEC §8.1). /// Live, it counts against each, so a sibling delegate's call through the /// held root is refused; a stream that ends at it reads every link as debited /// its `held_uj`, and one holding its settle restores each link's own debit. #[test] fn a_chained_hold_counts_and_restores_on_every_link() { let (links, ids) = chain(1_000, 400, Vec::new()); let sibling = vec![ links[0].clone(), attenuate(&links[0], &sk(2), did(&sk(4)), Capability::new(resource(), ["read"]), 300, 0, [9; 16]).unwrap(), ]; let (f, fs) = (frame(&links[1]), frame(&sibling[1])); let full = Verdict::Deny(DenyReason::BudgetExceeded); let mut rt = Runtime::new(); let mut log = log(); let (auth, held) = rt.hold_chained(&f, &links, did(&sk(3)), 10); let held = held.expect("a hold on fresh links"); assert_eq!(held.uj, 1_000, "the most the work can debit any link: the root's remainder"); assert_eq!(rt.decide_chained(&fs, &sibling, did(&sk(4)), Energy::measured(1), 10).verdict, full, "live, the sibling waits"); log.append_hold_chained(&f, &ids, &links[1].grant, &auth, held.uj, held.minor); let hold = log.head(); let core = extract(&log.entries()[0], STEWARD, &pubkey()).unwrap(); assert_eq!((core.chain_ids.as_deref(), core.held_uj), (Some(ids.as_slice()), Some(1_000))); let mut back = restored(log.entries()); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (1_000, 1_000), "a stop while the work ran refunded a link"); assert_eq!(back.decide_chained(&fs, &sibling, did(&sk(4)), Energy::measured(1), 11).verdict, full, "restored, the sibling waits"); assert_eq!(unresolved(log.entries()), vec![hold]); let d = rt.settle_chained(&links, held.id, Energy::measured(500)); assert_eq!(d.chain_debited_uj, vec![500, 400], "each link saturates at its own ceiling"); log.append_settle_chained(&f, &ids, &links[1].grant, &d, hold); let back = restored(log.entries()); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (500, 400), "a settled hold added to its work's debit"); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (rt.spent(&ids[0]), rt.spent(&ids[1]))); assert!(unresolved(log.entries()).is_empty()); assert!(rt.decide_chained(&fs, &sibling, did(&sk(4)), Energy::measured(300), 12).is_allow(), "the root has 500 left"); assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(2)); } /// A hold member that does not parse is refused rather than read as absent. #[test] fn a_malformed_hold_member_is_refused() { let sg = grant(1_000, Vec::new()); let f = frame(&sg); let auth = Runtime::new().authorize(&f, &sg, sg.grant.subject, 10); for (member, value) in [("held_uj", json!("1000")), ("settles", json!(7)), ("settles", json!("not hex"))] { let mut wire = to_jcr1_wire(&f, &sg.grant, &auth, STEWARD); wire[member] = value; assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).is_err(), "{member}"); } } // ── standing grants: the firing recorded before the child leaves (SPEC §4.6, §8.1) ── /// A standing grant the issuer holds and fires itself, with a policy allowing /// three firings of 200 µJ each. fn standing() -> (SignedGrant, SignedMintPolicy) { let sg = issue( &sk(1), Grant { issuer: did(&sk(1)), subject: did(&sk(1)), capability: Capability::new(resource(), ["read", "write"]), budget_uj: 1_000, budget_funds: None, caveats: Vec::new(), expiry: 0, parent: None, delegate_depth: None, nonce: [8; 16], }, ) .unwrap(); let policy = issue_policy(&sk(1), &sg, ContentId::of_bytes(b"webhook:builds"), 200, 3, Capability::new(resource(), ["read"]), 0, [9; 16]) .unwrap(); (sg, policy) } /// Fire the standing grant on event `n` and seal the firing, as an /// enforcement point does before the child leaves it. fn fire( sg: &SignedGrant, policy: &SignedMintPolicy, ledger: &mut MintLedger, log: &mut ReceiptLog, n: u8, ) -> Result { let event = TriggerEvent { class: ContentId::of_bytes(b"webhook:builds"), payload_id: ContentId::of_bytes(&[n]) }; let child = mint_on_trigger(sg, policy, ledger, &sk(1), did(&sk(2)), &event, [n; 16])?; let policy_id = ContentId::of(&policy.policy).unwrap(); let remaining = sg.grant.budget_uj.saturating_sub(ledger.spent_uj); log.append_mint(&sg.grant, policy_id, event.payload_id, policy.policy.per_fire_budget_uj, remaining); Ok(child) } /// The mint ledger a restart holds: the copy it persisted, restored with what /// its receipt stream rebuilds for the standing grant. fn restarted(persisted: MintLedger, entries: &[Vec], standing: ContentId) -> MintLedger { let minted = replay_mints(entries.iter().map(Vec::as_slice), STEWARD, &pubkey()); let mut ledger = persisted; ledger.restore(&MintLedger::of_firings(minted.into_iter().filter(|(g, _)| *g == standing).map(|(_, uj)| uj))); ledger } /// Fault P3: a mint ledger reading fewer fires than happened looked the same /// as one that fired less, so a standing grant restarted from a stale ledger /// fired past `max_fires`. Nothing a firing left behind rebuilt the ledger: a /// mint sealed no receipt, and a child never exercised left no trace. Each /// firing now seals a receipt stating `minted_uj` before the child leaves, /// and a restart restores the ledger it persisted with what its receipt /// stream rebuilds, keeping the higher figures. #[test] fn p3_a_stale_mint_ledger_fires_a_standing_grant_past_max_fires() { let (sg, policy) = standing(); // max_fires 3, 200 µJ a fire let mut log = log(); let mut ledger = MintLedger::default(); fire(&sg, &policy, &mut ledger, &mut log, 1).unwrap(); // The enforcement point persists the ledger here, and no later. let persisted = ledger.clone(); fire(&sg, &policy, &mut ledger, &mut log, 2).unwrap(); fire(&sg, &policy, &mut ledger, &mut log, 3).unwrap(); assert!(matches!(fire(&sg, &policy, &mut ledger, &mut log, 4), Err(MintError::RateLimited)), "the policy is spent"); assert_eq!(verify_log(log.entries(), STEWARD, &pubkey()), Ok(3), "the stream holds the three firings"); // A restart reads the ledger it persisted, and its receipt stream. let mut back = restarted(persisted, log.entries(), sg.grant.id()); let refired = (5..8).filter(|&n| fire(&sg, &policy, &mut back, &mut log, n).is_ok()).count(); assert_eq!( refired, 0, "fault P3: a standing grant with max_fires 3 that fired 3 times fired {refired} more after a restart \ from a ledger reading 1 fire" ); } /// The stream rebuilds the ledger the firings left, whatever copy the /// restart holds: none at all, a stale one, or the live one. #[test] fn a_firing_stream_rebuilds_the_mint_ledger() { let (sg, policy) = standing(); let mut log = log(); let mut ledger = MintLedger::default(); for n in 1..=2 { fire(&sg, &policy, &mut ledger, &mut log, n).unwrap(); } assert_eq!(ledger, MintLedger { fires: 2, spent_uj: 400 }); for persisted in [MintLedger::default(), MintLedger { fires: 1, spent_uj: 200 }, ledger.clone()] { assert_eq!(restarted(persisted.clone(), log.entries(), sg.grant.id()), ledger, "from {persisted:?}"); } let other = grant(1_000, Vec::new()).grant.id(); assert_eq!(restarted(MintLedger::default(), log.entries(), other), MintLedger::default(), "another grant's firings"); } /// A firing's receipt is an allow on the standing grant that runs no work: it /// debits nothing and accrues nothing, group accounting does not count it as /// an action, and the call its child makes debits the child and the standing /// grant as any chained call does, and is no firing. #[test] fn a_firing_debits_nothing_and_its_child_debits_the_chain() { let (sg, policy) = standing(); let mut log = log(); let mut ledger = MintLedger::default(); let child = fire(&sg, &policy, &mut ledger, &mut log, 1).unwrap(); let core = extract(&log.entries()[0], STEWARD, &pubkey()).unwrap(); assert!(core.is_mint()); assert_eq!((core.minted_uj, core.action.as_str(), core.cost_uj, core.budget_remaining_uj), (Some(200), "mint", 0, 800)); assert_eq!(core.grant_id, sg.grant.id()); assert!(core.debits().is_empty() && core.money_debits().is_empty() && core.tainted_accruals().is_empty()); assert_eq!(replay_ledger(log.entries().iter().map(Vec::as_slice), STEWARD, &pubkey()), jcp_runtime::Ledger::default()); let (totals, bad) = aggregate(log.entries().iter().map(Vec::as_slice), STEWARD, &pubkey()); assert!(totals.is_empty() && bad == 0, "a firing is not an action: {totals:?}"); let links = vec![sg.clone(), child.clone()]; let ids: Vec = links.iter().map(|g| g.grant.id()).collect(); let f = frame(&child); let d = Runtime::new().decide_chained(&f, &links, did(&sk(2)), Energy::measured(50), 10); assert!(d.is_allow()); log.append_chained(&f, &ids, &child.grant, &d); let back = restored(log.entries()); assert_eq!((back.spent(&ids[0]), back.spent(&ids[1])), (50, 50)); assert_eq!(replay_mints(log.entries().iter().map(Vec::as_slice), STEWARD, &pubkey()), vec![(ids[0], 200)]); } /// A refusal mints nothing (C3), so a denial stating `minted_uj` fires /// nothing; a member that does not parse is refused rather than read as /// absent; and a firing's member, changed on its signed receipt, does not /// verify. #[test] fn a_refusal_fires_nothing_and_a_malformed_member_is_refused() { let (sg, policy) = standing(); let mut log = log(); fire(&sg, &policy, &mut MintLedger::default(), &mut log, 1).unwrap(); let payload = jcr1::verify_cose_sign1(&log.entries()[0], STEWARD, &pubkey()).unwrap(); let mut wire: Value = jcp_schema::from_canonical_cbor(&payload).unwrap(); for m in ["seq", "prev"] { wire.as_object_mut().unwrap().remove(m); } assert!(extract(&seal_wire(&wire), STEWARD, &pubkey()).unwrap().is_mint()); let mut refused = wire.clone(); refused["verdict"] = json!("deny"); refused["deny_reason"] = json!("rate-limited"); let env = seal_wire(&refused); assert!(!extract(&env, STEWARD, &pubkey()).unwrap().is_mint()); assert!(replay_mints([env.as_slice()], STEWARD, &pubkey()).is_empty()); let mut malformed = wire.clone(); malformed["minted_uj"] = json!("200"); assert!(extract(&seal_wire(&malformed), STEWARD, &pubkey()).is_err()); let name = [&[0x60 | 9u8][..], b"minted_uj"].concat(); let mut changed = log.entries().to_vec(); let at = changed[0].windows(name.len()).position(|w| w == name).unwrap() + name.len(); changed[0][at] ^= 0x01; assert!(extract(&changed[0], STEWARD, &pubkey()).is_err()); assert!(replay_mints(changed.iter().map(Vec::as_slice), STEWARD, &pubkey()).is_empty()); }