Some checks failed
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 6m0s
Five receipt members added in the last two days — `held_minor`, `known_through`, `captures`, `reclaims`, `refunded_minor` — had no published vector and no independent reader. By this repository's own bar that means their canonical form was pinned by nothing but the implementation that wrote them. `conformance/v1/lifecycle.json` is one hash-chained log holding the life of a money hold: a hold captured in part and then reclaimed past its deadline, a hold captured and settled, a spend refunded by its issuer, a refund refused to its subject, and a point that states its revocation marker on some receipts and not others. It publishes the joules, gross money and refunds a reader must restore. `verify_lifecycle.py` reads it by the specification's rules, sharing no code with the reference implementation, and restored the same ledger on its first run. Its selftest re-reads the log with each rule broken — restoring a captured hold's whole reservation, restoring a reclaimed hold's money, subtracting a refund from the gross debit — and requires every one refused, so a pass means the log distinguishes the rules. It runs in CI beside the replay reader, and a Rust test reads the committed pack through the library. Regenerating the vectors produced exactly one new file. Every existing pack came out byte-identical, which is independent confirmation that none of the receipt changes altered a published vector. The x402 profile now spans the lifecycle, and this is where our side differs from the wire it rides: **every capture seals its own measurement.** A stream paid per chunk gets a receipt per chunk, each committing to that draw's amount and the energy metered since the last — so a dispute over the third step has the third step's evidence, not one total sealed afterwards. `capture`, `reclaim` and `refund` are on the profile; ingest reads `auth-capture` and `batch-settlement` and takes the capture deadline from the requirement; and the refund goes through the runtime's rule, so the agent cannot refund itself. Co-Authored-By: Claude <[email protected]>
182 lines
8 KiB
Python
182 lines
8 KiB
Python
#!/usr/bin/env python3
|
|
"""A second reader of JCP's published hold-lifecycle log (SPEC §8.1, C32).
|
|
|
|
`v1/lifecycle.json` is one hash-chained log holding the life of a money hold: a hold
|
|
captured in part and then reclaimed past its deadline, a hold captured and settled, a
|
|
spend refunded by its issuer, and a refund refused to its subject. It publishes the
|
|
ledger a reader must restore from it. This reads that log by the specification's rules
|
|
and refuses unless it restores the same ledger.
|
|
|
|
It shares no code with the reference implementation. The envelope, CBOR, Ed25519 and
|
|
BLAKE3 come from `verify_replay.py`, which is itself independent of it; the rules below
|
|
are written from the specification:
|
|
|
|
- every entry restores its own debits, in joules and in money;
|
|
- a hold no entry settles restores what it STILL reserves: its `held_uj` and
|
|
`held_minor` less what the entries naming it in `captures` debited, since those
|
|
entries restore their own debits and the whole reservation beside them would count
|
|
the captured part twice;
|
|
- a hold an allowed entry names in `reclaims` restores no money, and still its joules;
|
|
- an allowed entry stating `refunded_minor` restores a refund, beside a gross debit
|
|
that never decreases. A refused one refunds nothing.
|
|
|
|
`--selftest` then re-reads the log with each rule broken in turn and requires every
|
|
one of those readers to be refused, so a pass means the log distinguishes the rules.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from verify_replay import Refused, blake3, open_envelope, read_debits, read_money # noqa: E402
|
|
|
|
|
|
def is_hold(core):
|
|
return "held_uj" in core and core["verdict"] == "allow"
|
|
|
|
|
|
def grants(core):
|
|
return core.get("chain_ids", [core["grant_id"]])
|
|
|
|
|
|
def read_log(doc, steward, key):
|
|
"""The verified cores and their ids, or Refused where the chain breaks."""
|
|
prev = "0" * 64
|
|
cores, ids = [], []
|
|
for i, entry in enumerate(doc["entries"]):
|
|
envelope = bytes.fromhex(entry["envelope_hex"])
|
|
core = open_envelope(envelope, steward, key)
|
|
if core != entry["wire"]:
|
|
raise Refused(f"entry {i}: the signed core is not the published wire form")
|
|
if core["seq"] != i or core["prev"] != prev:
|
|
raise Refused(f"entry {i}: the chain is broken here")
|
|
prev = blake3(envelope).hex()
|
|
cores.append(core)
|
|
ids.append(prev)
|
|
return cores, ids
|
|
|
|
|
|
def restore(cores, ids, captured_less=True, reclaim_releases=True, refused_refunds=False,
|
|
refund_lowers_gross=False):
|
|
"""The ledger the log restores: (joules, money, refunded), each {grant: n}.
|
|
|
|
The keyword flags break one rule each, for the selftest."""
|
|
settled = {c["settles"] for c in cores if "settles" in c}
|
|
reclaimed = {c["reclaims"] for c in cores if "reclaims" in c and c["verdict"] == "allow"}
|
|
drawn_uj, drawn_minor = {}, {}
|
|
for c in cores:
|
|
if "captures" in c:
|
|
for g, n in read_debits(c):
|
|
drawn_uj[(c["captures"], g)] = drawn_uj.get((c["captures"], g), 0) + n
|
|
for g, n in read_money(c):
|
|
drawn_minor[(c["captures"], g)] = drawn_minor.get((c["captures"], g), 0) + n
|
|
uj, minor, refunded = {}, {}, {}
|
|
|
|
def add(ledger, grant, n):
|
|
if n:
|
|
ledger[grant] = ledger.get(grant, 0) + n
|
|
|
|
for core, own in zip(cores, ids):
|
|
for g, n in read_debits(core):
|
|
add(uj, g, n)
|
|
for g, n in read_money(core):
|
|
add(minor, g, n)
|
|
if "refunded_minor" in core and (core["verdict"] == "allow" or refused_refunds):
|
|
if refund_lowers_gross:
|
|
minor[core["grant_id"]] = minor.get(core["grant_id"], 0) - core["refunded_minor"]
|
|
else:
|
|
add(refunded, core["grant_id"], core["refunded_minor"])
|
|
if is_hold(core) and own not in settled:
|
|
for g in grants(core):
|
|
less = drawn_uj.get((own, g), 0) if captured_less else 0
|
|
add(uj, g, max(0, core["held_uj"] - less))
|
|
if "held_minor" in core and not (reclaim_releases and own in reclaimed):
|
|
less = drawn_minor.get((own, g), 0) if captured_less else 0
|
|
add(minor, g, max(0, core["held_minor"] - less))
|
|
return uj, minor, refunded
|
|
|
|
|
|
def check(doc, steward, key, **rules):
|
|
cores, ids = read_log(doc, steward, key)
|
|
got = restore(cores, ids, **rules)
|
|
for name, ledger in zip(("spent_after", "money_after", "refunded_after"), got):
|
|
if ledger != doc[name]:
|
|
raise Refused(f"the log restores {name} {ledger}, not the published {doc[name]}")
|
|
return len(cores)
|
|
|
|
|
|
def check_coverage(doc):
|
|
"""Refused unless the log still holds each form the rules distinguish."""
|
|
wires = [e["wire"] for e in doc["entries"]]
|
|
ids = [blake3(bytes.fromhex(e["envelope_hex"])).hex() for e in doc["entries"]]
|
|
settled = {w["settles"] for w in wires if "settles" in w}
|
|
forms = {
|
|
"a hold stating money": any(is_hold(w) and "held_minor" in w for w in wires),
|
|
"a hold no entry settles": any(is_hold(w) and i not in settled for w, i in zip(wires, ids)),
|
|
"a hold an entry settles": any(is_hold(w) and i in settled for w, i in zip(wires, ids)),
|
|
"an allowed capture": any("captures" in w and w["verdict"] == "allow" for w in wires),
|
|
"a refused capture": any("captures" in w and w["verdict"] == "deny" for w in wires),
|
|
"a capture of an unsettled hold": any(
|
|
"captures" in w and w["verdict"] == "allow" and w["captures"] not in settled for w in wires),
|
|
"an allowed reclaim": any("reclaims" in w and w["verdict"] == "allow" for w in wires),
|
|
"an allowed refund": any("refunded_minor" in w and w["verdict"] == "allow" for w in wires),
|
|
"a refused refund stating no amount": any(
|
|
w["verdict"] == "deny" and w.get("deny_reason") == "wrong-subject" and "refunded_minor" not in w
|
|
for w in wires),
|
|
"a receipt stating its revocation marker": any("known_through" in w for w in wires),
|
|
"a receipt stating none": any("known_through" not in w for w in wires),
|
|
}
|
|
missing = [name for name, present in forms.items() if not present]
|
|
if missing:
|
|
raise Refused(f"the log no longer holds: {', '.join(missing)}")
|
|
|
|
|
|
def selftest(doc, steward, key):
|
|
faults = {
|
|
"a reader restoring a captured hold's whole reservation": {"captured_less": False},
|
|
"a reader restoring a reclaimed hold's money": {"reclaim_releases": False},
|
|
"a reader subtracting a refund from the gross debit": {"refund_lowers_gross": True},
|
|
}
|
|
for name, rules in faults.items():
|
|
try:
|
|
check(doc, steward, key, **rules)
|
|
except Refused:
|
|
continue
|
|
raise SystemExit(f"verify_lifecycle: selftest: {name} was not refused")
|
|
# A tampered envelope must not verify.
|
|
broken = json.loads(json.dumps(doc))
|
|
env = bytearray.fromhex(broken["entries"][1]["envelope_hex"])
|
|
env[-1] ^= 1
|
|
broken["entries"][1]["envelope_hex"] = env.hex()
|
|
try:
|
|
check(broken, steward, key)
|
|
except Refused:
|
|
return
|
|
raise SystemExit("verify_lifecycle: selftest: a tampered envelope was not refused")
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
run_selftest = "--selftest" in args
|
|
args = [a for a in args if a != "--selftest"]
|
|
pack = args[0] if args else os.path.join(os.path.dirname(os.path.abspath(__file__)), "v1")
|
|
with open(os.path.join(pack, "meta.json"), encoding="utf-8") as f:
|
|
meta = json.load(f)
|
|
with open(os.path.join(pack, "lifecycle.json"), encoding="utf-8") as f:
|
|
doc = json.load(f)
|
|
steward, key = meta["steward"], bytes.fromhex(meta["pubkey_hex"])
|
|
try:
|
|
count = check(doc, steward, key)
|
|
check_coverage(doc)
|
|
except Refused as e:
|
|
print(f"verify_lifecycle: {e}", file=sys.stderr)
|
|
return 1
|
|
if run_selftest:
|
|
selftest(doc, steward, key)
|
|
print(f"verify_lifecycle: {count} entries read as published"
|
|
+ (", and every fault was refused" if run_selftest else ""))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|