jcp: C26 — the one-writer requirement gets a conformance clause with teeth #352
2 changed files with 48 additions and 0 deletions
|
|
@ -1375,3 +1375,4 @@ gate.
|
|||
- **C24 — caveats:** a caveat embedded in the grant is enforced offline by the decision point; `PayloadIs` makes a grant one-shot (a replay with different arguments is denied `caveat-unsatisfied`); `MaxCostUj` bounds a single invocation independently of the cumulative budget; `ModelIn` admits only a frame naming one of its models, and denies a frame naming another or none; a caveat may narrow below the capability; attenuation inherits every parent caveat and refuses to drop one; and a grant with no caveats encodes byte-identically to one minted before they existed.
|
||||
- **C25 — flow:** a value's propagated label must sit at or below the grant's ceiling and clear the resource's taint floor; an argument whose provenance was **not computed** fails closed against a ceilinged grant rather than passing as clean; several ceilings in a chain compose to the strictest and attenuation never widens one; a flow denial debits nothing, so a tainted call costs zero joules; and a grant with no flow caveat encodes byte-identically to one minted before flow existed.
|
||||
- **C22 — dual ceilings:** a money ceiling beside the joule ceiling is enforced independently — the first to bind denies (`funds-exceeded` / `budget-exceeded`), a foreign-currency cost is refused `currency-mismatch` rather than converted, an allow debits both dimensions and a deny debits neither, and an absent money ceiling encodes byte-identically to a joules-only grant (so its content id and signature are unchanged). Attenuation narrows money independently: dropping, raising, or re-denominating a bounded parent's ceiling is refused; a plain attenuation inherits it. A priced chained decision applies the parent's money ceiling to its child's spend, and a delegated grant decided without its chain is denied `chain-broken`.
|
||||
- **C26 — one writer per record:** a durable decision record admits one writer. A second holder of the same record is refused rather than opening it, because two would each replay it, each re-establish the same remaining budget and each debit against it — spending it twice while every entry still verifies (§5). The exclusive claim is released when the holder goes away, so a restart can reopen the record it previously held, and a platform that cannot provide exclusivity fails the open rather than proceeding.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@
|
|||
//! - **C9 — energy honesty:** the meter ladder always yields a figure and its
|
||||
//! provenance is internally consistent (HwShunt ⇒ measured present;
|
||||
//! Estimator ⇒ measured absent).
|
||||
//! - **C26 — one writer per record:** a durable decision record admits one
|
||||
//! writer; a second holder is refused, the claim is released with the holder
|
||||
//! so a restart can reopen it, and a platform that cannot lock fails the
|
||||
//! open rather than proceeding.
|
||||
//! - **C-neg:** expired grants, forged grants, and wrong callers are denied with
|
||||
//! the precise reason; canonical CBOR rejects floats/bools.
|
||||
|
||||
|
|
@ -68,6 +72,7 @@ pub fn checks() -> Vec<Check> {
|
|||
Check { id: "C22 dual ceilings: money beside joules, no rate invented", run: c22_dual_ceilings },
|
||||
Check { id: "C23 receipt binds its payload + jcp/1 still verifies", run: c23_payload_binding },
|
||||
Check { id: "C24 caveats: one-shot, per-call cost, model, monotone attenuation", run: c24_caveats },
|
||||
Check { id: "C26 one writer per record: a second holder is refused", run: c26_single_writer },
|
||||
Check { id: "C-neg expired / forged / wrong-caller", run: cneg_denials },
|
||||
Check { id: "C-neg canonical rejects floats", run: cneg_canonical },
|
||||
]
|
||||
|
|
@ -889,6 +894,48 @@ fn c13_signed_caller() -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// C26 — §5's one-writer requirement, on the reference `FileJournal`.
|
||||
///
|
||||
/// Two endpoints holding one record would each replay it, each re-establish
|
||||
/// the *same* remaining budget and each debit against it: the budget is spent
|
||||
/// twice while every entry still verifies, so nothing in the record shows it.
|
||||
/// The record must therefore admit one writer and refuse the second.
|
||||
///
|
||||
/// Also checks the two properties that make the refusal usable rather than a
|
||||
/// trap: the claim is released when the holder goes away, so a restart can
|
||||
/// reopen the record it held; and the refusal is specifically a contention,
|
||||
/// not some other I/O failure a caller might paper over.
|
||||
fn c26_single_writer() -> Result<(), String> {
|
||||
let path = std::env::temp_dir()
|
||||
.join(format!("jcp-conformance-c26-{}.journal", std::process::id()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let (held, records) =
|
||||
jcp_wire::FileJournal::open(&path).map_err(|e| format!("a fresh record did not open: {e}"))?;
|
||||
if !records.is_empty() {
|
||||
return Err("a fresh record replayed entries".into());
|
||||
}
|
||||
|
||||
match jcp_wire::FileJournal::open(&path) {
|
||||
Ok(_) => {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
return Err("a second writer opened a record another endpoint holds".into());
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
|
||||
Err(e) => {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
return Err(format!("a contended record failed for the wrong reason: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Released with the holder, or a restart could never reopen its own record.
|
||||
drop(held);
|
||||
jcp_wire::FileJournal::open(&path)
|
||||
.map_err(|e| format!("the record did not reopen after its holder went away: {e}"))?;
|
||||
let _ = std::fs::remove_file(&path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn c14_replay() -> Result<(), String> {
|
||||
let issuer = sk(1);
|
||||
let (worker_a, worker_b) = (sk(3), sk(4));
|
||||
|
|
|
|||
Loading…
Reference in a new issue