open-standards/jcp/conformance/verify_replay.py
Transaction Science 702ec06814
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 5m8s
Payments: every log prefix is a crash point; the ledger runs a reservation's whole life
JCP
- jcp-receipt/tests/crash_points.rs replays the receipt log into a fresh
  runtime after every step of random priced lifecycles and requires it to
  stand where the live one does. It found, on its first run, that a
  settlement refused before it took its hold (wrong currency, already
  resolved, wrong subject) still stated `settles`, so a restart dropped a
  live reservation. A resolution now settles its hold only if it consumed
  it: SPEC 8.1 (MUST), the writer, the Rust reader and both Python readers.
  No published vector changes.
- hold_invariants draws its amounts around the values each bound turns on
  (nothing, the edge, one either side, well past it) instead of evenly.
- The signature arithmetic is built optimized in test builds; the
  per-prefix replay went from over five minutes to eight seconds.

OpenPay
- op-ledger: ReservingStore, a capability beside LedgerStore. An account
  can forbid overdraft (available = posted - reserved; money not yet
  arrived counts for nothing), a pending transaction can be drawn in
  parts, each draw its own immutable posted transaction, what is not
  drawn is released, and one with a last draw time lapses on the sweep.
  Posting a partly drawn reservation whole is refused.
- The store re-validates every transaction it is handed rather than
  trusting a constructor made it, and a negative entry amount is refused:
  a negative debit is a credit under another name.
- tests/invariants.rs runs the store against a separately written model
  that predicts every acceptance, refusal and balance. Eight seeded faults
  in the store are each caught by it.
- op-metered:📒:Booked puts the ledger behind any gate: reserve is a
  pending transaction, capture a draw, resolve a draw and a release,
  refund an entry the other way. The payer's balance bounds what the
  authorization does not. metered-settlement.md 6.6.

Co-Authored-By: Claude <[email protected]>
2026-11-10 22:48:03 -05:00

584 lines
24 KiB
Python

#!/usr/bin/env python3
"""A second reader of the published replay log, conformance/v1/replay.json (SPEC §8.1).
It shares no code with the reference implementation, and needs no package: det-CBOR,
Ed25519 (RFC 8032) and BLAKE3 are written out below from their specifications. For every
entry of the log it checks that
1. the envelope is a COSE_Sign1 under the published steward key, over a canonical core
equal to the published wire form;
2. `seq` counts from zero and `prev` is the BLAKE3 of the previous envelope;
3. what SPEC §8.1's rules read from the core is what the runtime's ledgers moved by:
the joules debited, the money debited, and what a tainted sub-budget accrued. A hold
is read against the whole log: settled by a later entry it restores nothing, and
settled by none it restores its `held_uj`, as a restart holds it. A firing of a
standing grant debits nothing, and counts one fire and its `minted_uj` in the
standing grant's mint ledger (SPEC §4.6).
Then the sums must be the ledgers the runtime and the firings ended with, and each
receipt in `legacy` must verify, state none of the member it was sealed before, and read
as published.
verify_replay.py [--selftest] [DIR]
DIR defaults to conformance/v1 beside this file. --selftest also checks that the reader
fails: a changed debit, a dropped or moved entry, a changed total, and a reader with each
planted fault must all be refused. Exit 0 when everything holds, 1 otherwise.
"""
import hashlib
import json
import os
import struct
import sys
# The two deny reasons the debit point seals (SPEC §4.9, §4.12).
DEBIT_POINT_REASONS = ("caveat-unsatisfied", "window-exceeded")
class Refused(Exception):
pass
# ---- det-CBOR (RFC 8949 §4.2.1), the subset a JCR-1 receipt uses ------------------------
def cbor_read(b, i=0):
head = b[i]
major, info = head >> 5, head & 31
i += 1
if info < 24:
n = info
elif info <= 27:
width = 1 << (info - 24)
n = int.from_bytes(b[i:i + width], "big")
i += width
else:
raise Refused(f"CBOR additional info {info}")
if major == 0:
return n, i
if major == 1:
return -1 - n, i
if major == 2:
return b[i:i + n], i + n
if major == 3:
return b[i:i + n].decode(), i + n
if major == 4:
out = []
for _ in range(n):
v, i = cbor_read(b, i)
out.append(v)
return out, i
if major == 5:
out = {}
for _ in range(n):
k, i = cbor_read(b, i)
out[k], i = cbor_read(b, i)
return out, i
if major == 6:
return cbor_read(b, i) # the tag itself is checked by the caller
raise Refused(f"CBOR major type {major}")
def cbor_head(major, n):
if n < 24:
return bytes([major << 5 | n])
for info, width in ((24, 1), (25, 2), (26, 4), (27, 8)):
if n < 1 << (8 * width):
return bytes([major << 5 | info]) + n.to_bytes(width, "big")
raise Refused("CBOR length past 64 bits")
def cbor_write(v):
if isinstance(v, int):
return cbor_head(0, v) if v >= 0 else cbor_head(1, -1 - v)
if isinstance(v, bytes):
return cbor_head(2, len(v)) + v
if isinstance(v, str):
e = v.encode()
return cbor_head(3, len(e)) + e
if isinstance(v, list):
return cbor_head(4, len(v)) + b"".join(cbor_write(x) for x in v)
if isinstance(v, dict):
keys = sorted(v, key=lambda k: (len(k.encode()), k.encode()))
return cbor_head(5, len(v)) + b"".join(cbor_write(k) + cbor_write(v[k]) for k in keys)
raise Refused(f"no CBOR form for {type(v)}")
# ---- Ed25519 verification (RFC 8032 §5.1) -----------------------------------------------
P = 2**255 - 19
L = 2**252 + 27742317777372353535851937790883648493
D = -121665 * pow(121666, P - 2, P) % P
SQRT_M1 = pow(2, (P - 1) // 4, P)
def point_add(a, b):
x1, y1, z1, t1 = a
x2, y2, z2, t2 = b
e = (y1 - x1) * (y2 - x2) % P
f = (y1 + x1) * (y2 + x2) % P
g = 2 * t1 * t2 * D % P
h = 2 * z1 * z2 % P
i, j, k, m = f - e, h - g, h + g, f + e
return (i * j % P, k * m % P, j * k % P, i * m % P)
def scalar_mul(s, p):
q = (0, 1, 1, 0)
while s:
if s & 1:
q = point_add(q, p)
p = point_add(p, p)
s >>= 1
return q
def decompress(b):
if len(b) != 32:
return None
y = int.from_bytes(b, "little")
sign = y >> 255
y &= (1 << 255) - 1
if y >= P:
return None
u = (y * y - 1) * pow(D * y * y + 1, P - 2, P) % P
x = pow(u, (P + 3) // 8, P)
if (x * x - u) % P:
x = x * SQRT_M1 % P
if (x * x - u) % P:
return None
if x == 0 and sign:
return None
if x & 1 != sign:
x = P - x
return (x, y, 1, x * y % P)
BASE = decompress((4 * pow(5, P - 2, P) % P).to_bytes(32, "little"))
def same_point(a, b):
x1, y1, z1, _ = a
x2, y2, z2, _ = b
return (x1 * z2 - x2 * z1) % P == 0 and (y1 * z2 - y2 * z1) % P == 0
def ed25519_verify(key, message, signature):
a, r = decompress(key), decompress(signature[:32])
s = int.from_bytes(signature[32:], "little")
if a is None or r is None or s >= L:
return False
h = int.from_bytes(hashlib.sha512(signature[:32] + key + message).digest(), "little") % L
return same_point(scalar_mul(s, BASE), point_add(r, scalar_mul(h, a)))
# ---- BLAKE3, hash mode ------------------------------------------------------------------
IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]
PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8]
CHUNK_START, CHUNK_END, PARENT, ROOT = 1, 2, 4, 8
MASK = 0xFFFFFFFF
def rotr(x, n):
return (x >> n | x << (32 - n)) & MASK
def mix(s, a, b, c, d, x, y):
s[a] = (s[a] + s[b] + x) & MASK
s[d] = rotr(s[d] ^ s[a], 16)
s[c] = (s[c] + s[d]) & MASK
s[b] = rotr(s[b] ^ s[c], 12)
s[a] = (s[a] + s[b] + y) & MASK
s[d] = rotr(s[d] ^ s[a], 8)
s[c] = (s[c] + s[d]) & MASK
s[b] = rotr(s[b] ^ s[c], 7)
def compress(node):
cv, words, counter, block_len, flags = node
s = cv[:8] + IV[:4] + [counter & MASK, counter >> 32 & MASK, block_len, flags]
m = list(words)
for _ in range(7):
for i, (a, b, c, d) in enumerate(((0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15),
(0, 5, 10, 15), (1, 6, 11, 12), (2, 7, 8, 13), (3, 4, 9, 14))):
mix(s, a, b, c, d, m[2 * i], m[2 * i + 1])
m = [m[i] for i in PERMUTATION]
return [s[i] ^ s[i + 8] for i in range(8)]
def chunk_node(chunk, index):
blocks = [chunk[i:i + 64] for i in range(0, len(chunk), 64)] or [b""]
cv = IV[:]
for i, block in enumerate(blocks):
flags = (CHUNK_START if i == 0 else 0) | (CHUNK_END if i == len(blocks) - 1 else 0)
words = struct.unpack("<16I", block.ljust(64, b"\0"))
if i == len(blocks) - 1:
return (cv, words, index, len(block), flags)
cv = compress((cv, words, index, 64, flags))
def tree_node(chunks, lo, hi):
if hi - lo == 1:
return chunk_node(chunks[lo], lo)
left = 1 << ((hi - lo - 1).bit_length() - 1)
words = compress(tree_node(chunks, lo, lo + left)) + compress(tree_node(chunks, lo + left, hi))
return (IV[:], words, 0, 64, PARENT)
def blake3(data):
chunks = [data[i:i + 1024] for i in range(0, len(data), 1024)] or [b""]
cv, words, counter, block_len, flags = tree_node(chunks, 0, len(chunks))
return struct.pack("<8I", *compress((cv, words, counter, block_len, flags | ROOT)))
# ---- The receipt ------------------------------------------------------------------------
def open_envelope(envelope, steward, key):
"""The signed core of a COSE_Sign1 envelope, or Refused."""
if envelope[:1] != b"\xd2":
raise Refused("not a tagged COSE_Sign1")
(protected, unprotected, payload, signature), end = cbor_read(envelope)
header, _ = cbor_read(protected)
if end != len(envelope) or unprotected != {}:
raise Refused("not the COSE_Sign1 shape")
if header.get(1) != -8 or header.get(4) != steward.encode():
raise Refused("not this steward's EdDSA envelope")
if not ed25519_verify(key, cbor_write(["Signature1", protected, b"", payload]), signature):
raise Refused("the signature does not verify")
core, end = cbor_read(payload)
if end != len(payload) or cbor_write(core) != payload:
raise Refused("the core is not canonical")
return core
def is_hold(core):
"""An allow stating `held_uj` (SPEC §8.1). A refusal debits nothing, so it holds nothing."""
return "held_uj" in core and core["verdict"] == "allow"
def is_mint(core):
"""A firing of a standing grant: an allow stating `minted_uj` (SPEC §4.6, §8.1). A refusal
mints nothing, so it fires nothing."""
return "minted_uj" in core and core["verdict"] == "allow"
def read_debits(core):
"""What this receipt debited, by SPEC §8.1's rules: [(grant, uj)]. A hold debits nothing
itself: its work is the settling entry's, or, settled by none, read_held's. A firing
debits nothing: it runs no work, and its child's receipts debit what the child spends."""
grants = core.get("chain_ids", [core["grant_id"]])
if is_hold(core) or is_mint(core):
return []
if "chain_debited_uj" in core:
if len(core["chain_debited_uj"]) != len(grants):
raise Refused("chain_debited_uj does not match chain_ids")
return list(zip(grants, core["chain_debited_uj"]))
allowed = core["verdict"] == "allow"
if "debited_uj" in core:
uj = core["debited_uj"]
elif allowed or core.get("deny_reason") in DEBIT_POINT_REASONS:
uj = core["cost_uj"]
else:
uj = 0
return [] if uj == 0 and not allowed else [(g, uj) for g in grants]
def read_money(core):
"""What this receipt debited in money, by SPEC §8.1's rules: [(grant, minor)].
Only a stated figure counts, and a grant debited none is left out."""
grants = core.get("chain_ids", [core["grant_id"]])
if "chain_debited_minor" in core:
per_link = core["chain_debited_minor"]
if len(per_link) != len(grants):
raise Refused("chain_debited_minor does not match chain_ids")
elif "debited_minor" in core:
per_link = [core["debited_minor"]] * len(grants)
else:
return []
return [(g, m) for g, m in zip(grants, per_link) if m]
def read_tainted(core):
"""What this receipt accrued against a tainted sub-budget, by SPEC §8.1: [(grant, uj)].
`tainted_uj` where stated; otherwise a core with a flow block that debited accrued its
cost_uj, and any other core nothing."""
grants = core.get("chain_ids", [core["grant_id"]])
if "tainted_uj" in core:
uj = core["tainted_uj"]
elif "flow" in core and read_debits(core):
uj = core["cost_uj"]
else:
uj = 0
return [(g, uj) for g in grants] if uj else []
def read_mints(core):
"""What this receipt adds to a standing grant's mint ledger, by SPEC §8.1: one fire and its
`minted_uj`, for the standing grant it names. [(grant, fires, uj)]."""
return [(core["grant_id"], 1, core["minted_uj"])] if is_mint(core) else []
def read_settled(cores):
"""The ids of the entries some entry settles (SPEC §8.1): each `settles` member."""
return {c["settles"] for c in cores if "settles" in c and not left_outstanding(c)}
def left_outstanding(core):
"""A resolution refused before it took its hold (SPEC §8.1): the hold is still
outstanding whatever the receipt names, so it settles nothing."""
return core["verdict"] == "deny" and core.get("deny_reason") in (
"already-resolved", "currency-mismatch", "wrong-subject")
def read_held(core, unresolved):
"""What a hold restores, by SPEC §8.1: nothing when an entry settles it; otherwise its
`held_uj` from every grant it names, and as much against each one's tainted
sub-budget where it carries a flow block. ([(grant, uj)], [(grant, uj)])."""
if not unresolved or not is_hold(core):
return [], []
held = [(g, core["held_uj"]) for g in core.get("chain_ids", [core["grant_id"]])]
return held, (held if "flow" in core else [])
# The dimensions a log is read in: the entry's list, its unit, and the total it sums to.
DIMENSIONS = (("debits", "uj", "spent_after"), ("money", "minor", "money_after"), ("tainted", "uj", "tainted_after"))
def published(entry, dim, unit):
return [(d["grant_hex"], d[unit]) for d in entry[dim]]
def published_mints(entry):
return [(d["grant_hex"], d["fires"], d["uj"]) for d in entry["mints"]]
def check(doc, steward, key, read=read_debits, money=read_money, tainted=read_tainted, held=read_held,
settled=read_settled, mints=read_mints):
"""Refused unless the log reads as published. Returns the entry count."""
readers = (read, money, tainted)
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)
done = settled(cores)
ledgers = [{}, {}, {}]
fired = {}
for i, (entry, core, own) in enumerate(zip(doc["entries"], cores, ids)):
held_uj, held_tainted = held(core, own not in done)
reads = (read(core) + held_uj, money(core), tainted(core) + held_tainted)
for got, (dim, unit, _), ledger in zip(reads, DIMENSIONS, ledgers):
want = published(entry, dim, unit)
if got != want:
raise Refused(f"entry {i}: read {dim} {got}, the ledger moved by {want}")
for grant, n in got:
ledger[grant] = ledger.get(grant, 0) + n
got = mints(core)
if got != published_mints(entry):
raise Refused(f"entry {i}: read mints {got}, the mint ledger moved by {published_mints(entry)}")
for grant, fires, uj in got:
was = fired.get(grant, {"fires": 0, "uj": 0})
fired[grant] = {"fires": was["fires"] + fires, "uj": was["uj"] + uj}
for (_, _, total), ledger in zip(DIMENSIONS, ledgers):
if ledger != doc[total]:
raise Refused(f"the log replays to {ledger}, not {total} {doc[total]}")
if fired != doc["mints_after"]:
raise Refused(f"the log's firings rebuild {fired}, not mints_after {doc['mints_after']}")
for i, entry in enumerate(doc["legacy"]):
core = open_envelope(bytes.fromhex(entry["envelope_hex"]), steward, key)
if core != entry["wire"] or entry["sealed_before"] in core:
raise Refused(f"legacy {i}: not a receipt without {entry['sealed_before']}")
for reader, (dim, unit, _) in zip(readers, DIMENSIONS):
if reader(core) != published(entry, dim, unit):
raise Refused(f"legacy {i}: read {dim} {reader(core)}")
if mints(core) != published_mints(entry):
raise Refused(f"legacy {i}: read mints {mints(core)}")
return len(doc["entries"])
def check_coverage(doc):
"""Refused unless the log still holds each form the rules distinguish."""
wires = [e["wire"] for e in doc["entries"]]
denials = [w for w in wires if w["verdict"] == "deny"]
ids = [blake3(bytes.fromhex(e["envelope_hex"])).hex() for e in doc["entries"]]
done = read_settled(wires)
open_holds = [i for i, w in enumerate(wires) if is_hold(w) and ids[i] not in done]
forms = {
"an allow stating nothing": any(w["verdict"] == "allow" and "debited_uj" not in w for w in wires),
"a denial that debited": any(w.get("debited_uj", 0) > 0 for w in denials),
"a refusal stating its zero": any(w.get("debited_uj") == 0 for w in denials),
"a refusal stating nothing": any("debited_uj" not in w for w in denials),
"a debit below its cost": any(w.get("debited_uj", w["cost_uj"]) < w["cost_uj"] and w["debited_uj"] for w in wires),
"a chained receipt": any("chain_ids" in w for w in wires),
"links debited differently": any("chain_debited_uj" in w for w in wires),
"a money debit": any("debited_minor" in w for w in wires),
"links debited money differently": any("chain_debited_minor" in w for w in wires),
"an accrual its flow block says": any("flow" in w and "tainted_uj" not in w and read_tainted(w) for w in wires),
"a tainted refusal with a cost": any("flow" in w and w["verdict"] == "deny" and w["cost_uj"] > 0 for w in wires),
"an accrual stated": any("tainted_uj" in w and "flow" not in w for w in wires),
"a hold an entry settles": any(is_hold(w) and ids[i] in done for i, w in enumerate(wires)),
"a hold nothing settles": any("flow" not in wires[i] for i in open_holds),
"a tainted hold nothing settles": any("flow" in wires[i] for i in open_holds),
"an entry after an unsettled hold, not settling it": any(i + 1 < len(wires) for i in open_holds),
"a firing of a standing grant": any(is_mint(w) for w in wires),
"two firings of one standing grant": any(
sum(is_mint(v) and v["grant_id"] == w["grant_id"] for v in wires) > 1 for w in wires if is_mint(w)),
"a call under a child a firing minted": any(
not is_mint(w) and w.get("chain_ids", [None])[0] in {v["grant_id"] for v in wires if is_mint(v)}
for w in wires),
"a legacy receipt for each member": {e["sealed_before"] for e in doc["legacy"]}
== {"debited_uj", "debited_minor", "tainted_uj", "settles"},
}
missing = [name for name, held in forms.items() if not held]
if missing:
raise Refused(f"the log no longer holds: {', '.join(missing)}")
# ---- Self-test: the reader must be able to fail -----------------------------------------
def refuses(name, doc, steward, key, **readers):
try:
check(doc, steward, key, **readers)
except Refused:
return
raise SystemExit(f"self-test: {name} was accepted")
def with_changed_member(doc, at, member):
"""`doc` with entry `at`'s stated `member` changed in place, wire form to match."""
out = json.loads(json.dumps(doc))
entry = out["entries"][at]
key = cbor_write(member)
envelope = bytearray.fromhex(entry["envelope_hex"])
start = envelope.index(key) + len(key)
value, end = cbor_read(bytes(envelope), start)
# An integer's low bit; a hex id's last digit.
changed = value ^ 1 if isinstance(value, int) else value[:-1] + ("1" if value[-1] == "0" else "0")
envelope[start:end] = cbor_write(changed)
entry["envelope_hex"] = envelope.hex()
entry["wire"][member] = changed
return out
def selftest(doc, steward, key):
if blake3(b"").hex() != "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262":
raise SystemExit("self-test: BLAKE3 of nothing")
if blake3(bytes(i % 251 for i in range(1025))).hex() != "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444":
raise SystemExit("self-test: BLAKE3 past one chunk")
for member in ("debited_uj", "debited_minor", "tainted_uj", "held_uj", "settles", "minted_uj"):
stated = [i for i, e in enumerate(doc["entries"]) if member in e["wire"]]
if not stated:
raise SystemExit(f"self-test: no entry states {member}")
for at in stated:
refuses(f"{member} changed on signed entry {at}", with_changed_member(doc, at, member), steward, key)
breach = next(i for i, e in enumerate(doc["entries"]) if e["wire"].get("debited_uj", 0) > 0 and e["wire"]["verdict"] == "deny")
dropped = json.loads(json.dumps(doc))
del dropped["entries"][breach]
refuses("a breach dropped from the log", dropped, steward, key)
moved = json.loads(json.dumps(doc))
moved["entries"][0], moved["entries"][1] = moved["entries"][1], moved["entries"][0]
refuses("two entries swapped", moved, steward, key)
for _, _, name in DIMENSIONS:
for delta in (1, -1):
total = json.loads(json.dumps(doc))
grant = next(iter(total[name]))
total[name][grant] += delta
refuses(f"{name} changed by {delta}", total, steward, key)
for figure in ("fires", "uj"):
for delta in (1, -1):
total = json.loads(json.dumps(doc))
total["mints_after"][next(iter(total["mints_after"]))][figure] += delta
refuses(f"mints_after {figure} changed by {delta}", total, steward, key)
faults = {
"a reader dropping a breach": lambda c: read_debits(c) if c["verdict"] == "allow" else [],
"a reader counting a refusal": lambda c: [(g, c["cost_uj"]) for g in c.get("chain_ids", [c["grant_id"]])],
"a reader losing an ancestor's debit": lambda c: read_debits(c)[-1:],
"a reader taking one figure for every link": lambda c: [(g, c.get("debited_uj", c["cost_uj"])) for g, _ in read_debits(c)],
"a reader reading an old denial as zero": lambda c: read_debits(c) if "debited_uj" in c or c["verdict"] == "allow" else [],
}
for name, read in faults.items():
refuses(name, doc, steward, key, read=read)
def grants(c):
return c.get("chain_ids", [c["grant_id"]])
money_faults = {
"a reader restoring no money": lambda c: [],
"a reader taking one money figure for every link":
lambda c: [(g, c["debited_minor"]) for g in grants(c)] if c.get("debited_minor") else [],
}
for name, money in money_faults.items():
refuses(name, doc, steward, key, money=money)
tainted_faults = {
"a reader restoring no accrual": lambda c: [],
"a reader ignoring a flow block": lambda c: [(g, c["tainted_uj"]) for g in grants(c)] if c.get("tainted_uj") else [],
"a reader ignoring a stated accrual": lambda c: [] if "tainted_uj" in c else read_tainted(c),
"a reader counting a tainted refusal": lambda c: [(g, c["cost_uj"]) for g in grants(c)] if "flow" in c else read_tainted(c),
}
for name, tainted in tainted_faults.items():
refuses(name, doc, steward, key, tainted=tainted)
held_faults = {
"a reader ignoring a hold nothing settles": lambda c, unresolved: ([], []),
"a reader counting a settled hold": lambda c, unresolved: read_held(c, True),
"a reader ignoring a hold's flow block": lambda c, unresolved: (read_held(c, unresolved)[0], []),
"a reader holding a hold's cost_uj": lambda c, unresolved:
([(g, c["cost_uj"]) for g, _ in read_held(c, unresolved)[0]], read_held(c, unresolved)[1]),
}
for name, held in held_faults.items():
refuses(name, doc, steward, key, held=held)
refuses("a reader taking the next entry as a hold's settle", doc, steward, key,
settled=lambda cores: {c["prev"] for c in cores})
refuses("a reader reading a firing as a call", doc, steward, key,
read=lambda c: [(g, c["cost_uj"]) for g in grants(c)] if is_mint(c) else read_debits(c))
mint_faults = {
"a reader counting no firing": lambda c: [],
"a reader counting a fire but not what it minted": lambda c: [(g, f, 0) for g, f, _ in read_mints(c)],
"a reader counting a chained call as a firing":
lambda c: read_mints(c) or ([(c["chain_ids"][0], 1, 0)] if "chain_ids" in c else []),
}
for name, mints in mint_faults.items():
refuses(name, doc, steward, key, mints=mints)
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, "replay.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_replay: {e}", file=sys.stderr)
return 1
if run_selftest:
selftest(doc, steward, key)
print(f"verify_replay: {count} entries and {len(doc['legacy'])} legacy receipts read as published"
+ (", and every fault was refused" if run_selftest else ""))
return 0
if __name__ == "__main__":
sys.exit(main())