live_worlds_verify.py is the corpus's second implementation, written from the extension's text and the sections it cites, sharing no code with wai-rs; it agrees with the reference on every vector. Writing it found readings the text left open, now closed: a CausalOp's integers are little-endian and its signature is over the domain then the preceding bytes (interactive-worlds 5.1); a world track's receipts are a track's, not interactive-worlds 8's session receipts; the order of the descriptor, grant and statement checks; the group total is checked; a private world's first opened object above group 0 starts its chain; the session hash chains each op with its signature; the outcome lists the actors whose ops were admitted. The corpus gains a group-total refusal, and section 12 states which codes it covers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
657 lines
28 KiB
Python
657 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""Independent verifier of the live-worlds corpus (extensions/live-worlds.md).
|
|
|
|
Written from the extension's text and the texts it cites, alone. It shares no code with the
|
|
reference implementation in Rust, which generates the corpus, and never runs it, nor the
|
|
generator. It checks Ed25519 signatures with
|
|
../claim-conformance/_ed25519.py, WAI's strict rule (SPEC §7 *Signatures*) written from
|
|
RFC 8032 with the standard library, and computes BLAKE3 with ../claim-conformance/_blake3.py;
|
|
SHA-256 (FIPS 180-4) is the standard library's. Everything else is here:
|
|
|
|
- the room descriptor (live-rooms §3.1, §3.2): its reader (§3.4 steps 1 to 3), its digest,
|
|
the room id, and its signature under its `authority`;
|
|
- the room grants (live-rooms §4.1, §4.2, §4.4): each grant's reader (§4.3 step 1), its
|
|
signature under the descriptor's authority (step 2), its room (step 4) and its
|
|
attributes (the `attrs` rule of step 5). The other checks of live-rooms §4.3 (step 3's
|
|
exchange linkage, step 5's other field rules, steps 6 to 8) and the descriptor's field
|
|
rules (§3.4, `descriptor_invalid`) are the live-rooms verifier's, live_rooms_verify.py,
|
|
with its own corpus, and are not repeated here;
|
|
- the world statement (live-worlds §3): its reader and rules, in §3.4's order;
|
|
- the world tracks (live-worlds §4, §9.2): the grant attribute `world` (§5.1), the group
|
|
sequence, the receipts track's object (jwp-receipts §2.7, binary form `WJR1`) and its
|
|
verification by jwp-receipts §3 (key, group signature over the legacy or the labelled
|
|
payload of §2.3, the chain kept per track, the content hash of §2.1 over the bytes the
|
|
receipt covers, the class label, the proof's position and the Merkle walk of §2.2, and
|
|
the group total), the op batch (interactive-worlds §5.1), each op's signature, the
|
|
actor, the action body of `wai.world.model`, redelivery, equivocation and the Lamport
|
|
rule (§4.2);
|
|
- private worlds (live-worlds §8, §9.1, §9.2): the receipts cover `payload`, the ops are
|
|
the batch `opened` from it, and a track's first object may be at any group; above group
|
|
0 it starts its chain whatever parent it names (jwp-receipts §3 step 3, *Joining*), and
|
|
at group 0 it starts it as any first group does. Opening is confidentiality §6's, so the
|
|
relation between `payload` and `opened` is not checked here;
|
|
- the session hash (live-worlds §9.3) over the admitted ops in canonical order
|
|
(interactive-worlds §5.2), and the outcome (§9.4).
|
|
|
|
Of the acquisition classes (energy-measurement §2.7) it reads every tag, so that a class
|
|
that does not read is `malformed` as jwp-receipts §2.7.4 says; it holds tags 0 to 3 to
|
|
jwp-receipts §2.4.1 and §2.4.2. A refined hardware class (tags 4 and 5) needs the
|
|
resolution floor of energy-measurement §2.3, which this verifier does not implement: it
|
|
refuses such a receipt as `class_not_supported_here`, a verifier-local outcome that no
|
|
conforming corpus vector states.
|
|
|
|
For every vector it requires the vector's `expect`: for an accepted world its session
|
|
hash, the number of admitted ops and each actor's count; for a refused one its reason and
|
|
where (`"descriptor"`, `"statement"`, `{"grant": i}` or `{"track": i, "group": g}`).
|
|
|
|
python3 -I live_worlds_verify.py <live-conformance/worlds>
|
|
|
|
Exit status 0 only if every vector agrees. Python 3.8 or later; standard library only.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
|
|
from _blake3 import blake3 # noqa: E402
|
|
import _ed25519 as ED # noqa: E402
|
|
|
|
# live-rooms §3.1, §3.2, §4.1, §4.2
|
|
DESCRIPTOR_MAGIC = b"WRD1"
|
|
GRANT_MAGIC = b"WRG1"
|
|
DESCRIPTOR_DOMAIN = b"wai:live-room\x01" # 14 bytes
|
|
GRANT_DOMAIN = b"wai:live-room-grant\x01" # 20 bytes
|
|
# live-worlds §3
|
|
STATEMENT_MAGIC = b"WLW1"
|
|
STATEMENT_DOMAIN = b"wai:live-world\x01" # 15 bytes
|
|
CAPABILITY = b"wai.world.model"
|
|
WORLD_LEN = 52 # §3.2: id 32, seed 8, width, height, rate 4 each
|
|
MIN_OP, MAX_OP = 136, 65536
|
|
MIN_OPS, MAX_OPS = 1, 4096
|
|
# live-worlds §4.3, §9.3
|
|
WORLD_RECEIPT_ALIAS = 4
|
|
SESSION_DOMAIN = b"wai:world-model-session\x01" # 24 bytes
|
|
# interactive-worlds §5.1
|
|
OP_DOMAIN = b"wai:world-op\x01"
|
|
OP_HEAD = 32 + 8 + 8 + 2 # actor, lamport, effect_tick, n_deps
|
|
OP_DEP = 32 + 8
|
|
OP_MID = 16 + 2 + 4 # target_node, mutation_kind, mut_len
|
|
OP_SIG = 64
|
|
ACTION_KIND = 1 # wai.world.model: an action
|
|
ACTION_HEAD = 16 # dx i32, dy i32, buttons u32, payload_len u32
|
|
# jwp-receipts §2.1, §2.2, §2.3, §2.7, §3
|
|
RECEIPTS_MAGIC = b"WJR1"
|
|
SIGN_LEGACY = b"moq-jwp:group-sign\x01" # 19 bytes
|
|
SIGN_LABELLED = b"moq-jwp:group-sign\x02"
|
|
OBJECT_DOMAIN = b"moq-jwp:object\x01"
|
|
LEAF_LEGACY = b"moq-jwp:merkle-leaf\x01"
|
|
LEAF_LABELLED = b"moq-jwp:merkle-leaf\x02"
|
|
NODE_DOMAIN = b"moq-jwp:merkle-node\x01"
|
|
LINK_DOMAIN = b"moq-jwp:receipt-link\x01"
|
|
ZERO32 = bytes(32)
|
|
# energy-measurement §2.7: the tags, and §2's names for those this verifier compares
|
|
UNMETERED, HW_SHUNT, MODEL_BASED, ESTIMATOR, ON_CHIP, CALIBRATED = 0, 1, 2, 3, 4, 5
|
|
|
|
# The reasons the corpus is to refuse with (live-worlds §11 and the codes it names from
|
|
# live-events §9 and jwp-receipts §3.3). `world_incomplete` is a participant's report about
|
|
# its own copy (§7), not a refusal; `bad_key` at the statement is not listed, since a
|
|
# statement is signed by the descriptor's authority, whose key the descriptor's own
|
|
# signature has already held to SPEC §7's steps 1 and 2.
|
|
CORPUS_REASONS = [
|
|
"truncated", "malformed", "unknown_version", "world_capability_unknown",
|
|
"world_statement_invalid", "wrong_room", "bad_signature",
|
|
"world_not_granted", "world_gap", "moqt_mismatch", "signature_verify_failed",
|
|
"content_hash_mismatch", "chain_break", "parent_not_held",
|
|
"op_malformed", "batch_too_large", "op_too_large", "bad_op_signature",
|
|
"actor_not_owner", "op_not_action", "equivocation", "lamport_regress",
|
|
]
|
|
|
|
|
|
class Refuse(Exception):
|
|
def __init__(self, reason, at):
|
|
Exception.__init__(self, reason)
|
|
self.reason, self.at = reason, at
|
|
|
|
|
|
class Short(Exception):
|
|
"""The bytes end before a field the layout requires."""
|
|
|
|
|
|
class Reader:
|
|
"""Reads a record field by field, in either byte order."""
|
|
|
|
def __init__(self, b, order):
|
|
self.b, self.i, self.order = b, 0, order
|
|
|
|
def take(self, n):
|
|
if self.i + n > len(self.b):
|
|
raise Short()
|
|
out = self.b[self.i:self.i + n]
|
|
self.i += n
|
|
return out
|
|
|
|
def uint(self, n):
|
|
return int.from_bytes(self.take(n), self.order)
|
|
|
|
def u8(self):
|
|
return self.uint(1)
|
|
|
|
def short(self): # rooms: a u8 length and that many bytes
|
|
return self.take(self.u8())
|
|
|
|
def long(self): # rooms: a u16 (little-endian) length and that many bytes
|
|
return self.take(self.uint(2))
|
|
|
|
def string(self): # receipts: u32_be(len) and UTF-8
|
|
return self.take(self.uint(4))
|
|
|
|
def left(self):
|
|
return len(self.b) - self.i
|
|
|
|
|
|
def sha256(b):
|
|
return hashlib.sha256(b).digest()
|
|
|
|
|
|
def is_utf8(b):
|
|
try:
|
|
b.decode("utf-8")
|
|
return True
|
|
except UnicodeDecodeError:
|
|
return False
|
|
|
|
|
|
def is_slot_name(b): # live-rooms §2: 1 to 32 bytes of a-z, 0-9 and _
|
|
return 1 <= len(b) <= 32 and all(c in b"abcdefghijklmnopqrstuvwxyz0123456789_" for c in b)
|
|
|
|
|
|
def read_head(b, magic):
|
|
"""live-rooms §3.4 step 1, and live-worlds §3.4 step 1: the magic and the version."""
|
|
if len(b) < 4:
|
|
raise Refuse("truncated", None)
|
|
if b[:4] != magic:
|
|
raise Refuse("malformed", None)
|
|
if len(b) == 4:
|
|
raise Refuse("truncated", None)
|
|
if b[4] != 1:
|
|
raise Refuse("unknown_version", None)
|
|
|
|
|
|
# --- the room descriptor (live-rooms §3) ---------------------------------------------------
|
|
|
|
def read_descriptor(b):
|
|
"""§3.4 steps 1 to 3 and §3.2's signature; the field rules are live-rooms's verifier's."""
|
|
at = "descriptor"
|
|
try:
|
|
read_head(b, DESCRIPTOR_MAGIC)
|
|
r = Reader(b, "little")
|
|
r.take(5)
|
|
authority, _clock = r.take(32), r.take(32)
|
|
r.take(8 + 8) # t0, tick
|
|
r.long() # origin
|
|
r.take(4 + 2 + 4) # max_present, max_publishers, reval
|
|
r.take(65) # ceiling (WEP1)
|
|
for _ in range(r.u8()): # roles
|
|
r.short()
|
|
r.take(1 + 1 + 1 + 1 + 32)
|
|
for _ in range(r.u8()): # slots
|
|
r.short()
|
|
r.take(1 + 1 + 4)
|
|
for _ in range(r.u8()): # publish profiles (§8.1)
|
|
r.short()
|
|
r.take(1) # media
|
|
for _ in range(r.u8()):
|
|
r.short() # codecs
|
|
r.take(1) # layering
|
|
r.short() # mode
|
|
r.take(14 * r.u8()) # layers
|
|
r.take(4 + 1 + 1) # sample_rate, channels, processing
|
|
r.take(37) # review
|
|
r.take(1 + 2 + 4 + 4) # recording
|
|
r.take(2 * r.u8()) # regions
|
|
body_end = r.i
|
|
sig = r.take(64)
|
|
except Short:
|
|
raise Refuse("truncated", at)
|
|
except Refuse as e:
|
|
raise Refuse(e.reason, at)
|
|
if r.left():
|
|
raise Refuse("malformed", at)
|
|
body = b[:body_end]
|
|
bad = ED.verify(authority, DESCRIPTOR_DOMAIN + body, sig)
|
|
if bad:
|
|
raise Refuse(bad, at) # bad_key or bad_signature
|
|
return {"authority": authority, "room": sha256(body)}
|
|
|
|
|
|
# --- the grants (live-rooms §4) -----------------------------------------------------------
|
|
|
|
def read_attrs(b):
|
|
"""§4.4: keys (short, a slot name) in strictly ascending order, values (long) UTF-8,
|
|
at most 1024 bytes. None when the field breaks the rule."""
|
|
if len(b) > 1024:
|
|
return None
|
|
r, out, last = Reader(b, "little"), {}, None
|
|
try:
|
|
while r.left():
|
|
k, v = r.short(), r.long()
|
|
if not is_slot_name(k) or (last is not None and k <= last) or not is_utf8(v):
|
|
return None
|
|
out[k.decode()] = v.decode("utf-8")
|
|
last = k
|
|
except Short:
|
|
return None
|
|
return out
|
|
|
|
|
|
def read_grant(b, d, index):
|
|
at = {"grant": index}
|
|
try:
|
|
read_head(b, GRANT_MAGIC)
|
|
r = Reader(b, "little")
|
|
r.take(5)
|
|
room, principal = r.take(32), r.take(32)
|
|
r.take(32 + 8 + 8 + 8) # prev, effective, nbf, exp
|
|
r.take(1 + 4 + 1) # role, publish..powers, precedence
|
|
r.take(16 + 16 + 65) # once, viewer, policy
|
|
r.take(8 + 8 + 3) # joules_ceiling, funds_ceiling, currency
|
|
r.take(r.u8()) # profiles
|
|
for _ in range(r.u8()):
|
|
r.short() # origins
|
|
attrs = r.long()
|
|
body_end = r.i
|
|
sig = r.take(64)
|
|
except Short:
|
|
raise Refuse("truncated", at)
|
|
except Refuse as e:
|
|
raise Refuse(e.reason, at)
|
|
if r.left():
|
|
raise Refuse("malformed", at)
|
|
if ED.verify(d["authority"], GRANT_DOMAIN + b[:body_end], sig):
|
|
raise Refuse("bad_signature", at) # §4.3 step 2
|
|
if room != d["room"]:
|
|
raise Refuse("wrong_room", at) # §4.3 step 4
|
|
a = read_attrs(attrs)
|
|
if a is None:
|
|
raise Refuse("grant_invalid", at) # §4.3 step 5, the attrs rule
|
|
return {"principal": principal, "attrs": a}
|
|
|
|
|
|
# --- the world statement (live-worlds §3) -------------------------------------------------
|
|
|
|
def read_statement(b, d):
|
|
at = "statement"
|
|
try:
|
|
read_head(b, STATEMENT_MAGIC)
|
|
r = Reader(b, "little")
|
|
r.take(5)
|
|
room = r.take(32)
|
|
capability = r.short()
|
|
world = r.long()
|
|
max_op_bytes = r.uint(4)
|
|
max_ops = r.uint(2)
|
|
body_end = r.i
|
|
sig = r.take(64)
|
|
except Short:
|
|
raise Refuse("truncated", at)
|
|
except Refuse as e:
|
|
raise Refuse(e.reason, at)
|
|
if r.left():
|
|
raise Refuse("malformed", at)
|
|
if capability != CAPABILITY:
|
|
raise Refuse("world_capability_unknown", at)
|
|
if (len(world) != WORLD_LEN or any(int.from_bytes(world[i:i + 4], "little") == 0 for i in (40, 44, 48))
|
|
or not MIN_OP <= max_op_bytes <= MAX_OP or not MIN_OPS <= max_ops <= MAX_OPS):
|
|
raise Refuse("world_statement_invalid", at)
|
|
if room != d["room"]:
|
|
raise Refuse("wrong_room", at)
|
|
bad = ED.verify(d["authority"], STATEMENT_DOMAIN + b[:body_end], sig)
|
|
if bad:
|
|
raise Refuse(bad, at) # bad_key or bad_signature
|
|
return {"world": world, "max_op_bytes": max_op_bytes, "max_ops": max_ops}
|
|
|
|
|
|
# --- receipts (jwp-receipts §2.7, §3) -----------------------------------------------------
|
|
|
|
def read_class(r):
|
|
"""energy-measurement §2.7: a tag byte and its evidence. Returns (tag, raw bytes)."""
|
|
start = r.i
|
|
tag = r.u8()
|
|
if tag in (UNMETERED, HW_SHUNT, MODEL_BASED, ESTIMATOR):
|
|
pass
|
|
elif tag == ON_CHIP:
|
|
r.take(4 + 8)
|
|
if r.u8() > 2:
|
|
raise ValueError("filtering")
|
|
elif tag == CALIBRATED:
|
|
r.take(4 + 8)
|
|
if not is_utf8(r.string()):
|
|
raise ValueError("calibration_ref")
|
|
else:
|
|
raise ValueError("tag")
|
|
return tag, r.b[start:r.i]
|
|
|
|
|
|
def read_flag_parent(r):
|
|
flag, parent = r.u8(), r.take(32)
|
|
if flag not in (0, 1) or (flag == 0 and parent != ZERO32):
|
|
raise ValueError("parent flag")
|
|
return parent if flag else None
|
|
|
|
|
|
def read_receipts(b):
|
|
"""§2.7.3 read by §2.7.4: any failure is `malformed`."""
|
|
try:
|
|
r = Reader(b, "big")
|
|
if r.take(4) != RECEIPTS_MAGIC:
|
|
raise ValueError("magic")
|
|
start = r.i
|
|
domain = r.take(19)
|
|
if domain not in (SIGN_LEGACY, SIGN_LABELLED):
|
|
raise ValueError("domain")
|
|
g = {"labelled": domain == SIGN_LABELLED}
|
|
g["track_alias"], g["group_id"] = r.uint(8), r.uint(8)
|
|
g["root"], g["leaf_count"], g["total"] = r.take(32), r.uint(8), r.uint(8)
|
|
for k in ("group_start", "group_end"):
|
|
if not is_utf8(r.string()):
|
|
raise ValueError(k)
|
|
g["pubkey"] = r.take(32)
|
|
if not is_utf8(r.string()):
|
|
raise ValueError("signer_id")
|
|
if g["labelled"]:
|
|
g["class"] = read_class(r)
|
|
g["coverage"] = r.u8()
|
|
if g["coverage"] not in (0, 1):
|
|
raise ValueError("coverage")
|
|
g["parent"] = read_flag_parent(r)
|
|
payload_end = r.i
|
|
g["sig"] = r.take(64)
|
|
g["payload"] = b[start:payload_end]
|
|
g["canonical"] = b[start:r.i]
|
|
if not g["labelled"]:
|
|
g["parent"] = read_flag_parent(r)
|
|
objects = []
|
|
for _ in range(r.uint(4)):
|
|
o = {"object_id": r.uint(8), "content_hash": r.take(32), "joules": r.uint(8)}
|
|
o["origin"] = r.string()
|
|
if not is_utf8(o["origin"]):
|
|
raise ValueError("origin")
|
|
o["siblings"] = [r.take(32) for _ in range(r.u8())]
|
|
o["direction_bits"], o["leaf_index"] = r.uint(8), r.uint(8)
|
|
o["leaf_version"] = r.u8()
|
|
if o["leaf_version"] not in (1, 2):
|
|
raise ValueError("leaf_version")
|
|
o["class"] = read_class(r) if o["leaf_version"] == 2 else None
|
|
objects.append(o)
|
|
if r.left():
|
|
raise ValueError("trailing bytes")
|
|
except (Short, ValueError):
|
|
return None
|
|
return g, objects
|
|
|
|
|
|
def merkle_leaf(o):
|
|
"""§2.2: the labelled leaf where the receipt carries a label, the legacy one otherwise."""
|
|
if o["class"] is None:
|
|
return blake3(LEAF_LEGACY + o["content_hash"])
|
|
return blake3(LEAF_LABELLED + o["content_hash"] + o["joules"].to_bytes(8, "big") + o["class"][1]
|
|
+ len(o["origin"]).to_bytes(4, "big") + o["origin"])
|
|
|
|
|
|
def check_receipts(rb, covered, owner, group, chain, joins, at):
|
|
"""live-worlds §9.2 step 3. `chain` is the track's chain state: the head's link hash and
|
|
the link hashes of the earlier groups the verifier holds, and the head's group_id.
|
|
`joins` is True for a private world's first opened object at a group above 0."""
|
|
read = read_receipts(rb)
|
|
if read is None:
|
|
raise Refuse("malformed", at)
|
|
g, objects = read
|
|
if len(objects) != 1:
|
|
raise Refuse("malformed", at)
|
|
o = objects[0]
|
|
# moq-streaming-format §4.1: before any signature is checked.
|
|
if g["pubkey"] != owner or g["track_alias"] != WORLD_RECEIPT_ALIAS or g["group_id"] != group or o["object_id"] != 0:
|
|
raise Refuse("moqt_mismatch", at)
|
|
# jwp-receipts §3 step 1.
|
|
if g["labelled"] and g["class"][0] == UNMETERED and g["coverage"] != 0:
|
|
raise Refuse("group_label_invalid", at) # §2.7.4: a coverage beside the marker
|
|
if not ED.strict_key(g["pubkey"]):
|
|
raise Refuse("bad_signer_key", at)
|
|
# Step 2: the group signature, then the class with the group alone (§2.4.1).
|
|
if ED.verify(g["pubkey"], g["payload"], g["sig"]):
|
|
raise Refuse("signature_verify_failed", at)
|
|
if g["labelled"]:
|
|
tag = g["class"][0]
|
|
if tag == HW_SHUNT or (tag == UNMETERED) != (g["total"] == 0):
|
|
raise Refuse("group_label_invalid", at)
|
|
if tag in (ON_CHIP, CALIBRATED):
|
|
raise Refuse("class_not_supported_here", at)
|
|
# Step 3: the chain of the track (live-worlds §9.2 step 3).
|
|
link = blake3(LINK_DOMAIN + g["canonical"])
|
|
parent = g["parent"]
|
|
if chain["head"] is None:
|
|
if not joins and parent is not None:
|
|
raise Refuse("parent_not_held", at)
|
|
else:
|
|
if not g["labelled"] and g["group_id"] <= chain["head_group"]:
|
|
raise Refuse("group_id_not_increasing", at)
|
|
if parent != chain["head"]:
|
|
if parent is None or parent in chain["earlier"]:
|
|
raise Refuse("chain_break", at)
|
|
raise Refuse("parent_not_held", at)
|
|
# Step 4: the content hash over the bytes the receipt covers (§2.1).
|
|
if blake3(OBJECT_DOMAIN + covered) != o["content_hash"]:
|
|
raise Refuse("content_hash_mismatch", at)
|
|
# Step 5: the label (§2.4.2), the position, the walk to the verified root.
|
|
if o["class"] is not None:
|
|
tag = o["class"][0]
|
|
if tag == HW_SHUNT or (tag == UNMETERED) != (o["joules"] == 0):
|
|
raise Refuse("object_label_invalid", at)
|
|
if tag in (ON_CHIP, CALIBRATED):
|
|
raise Refuse("class_not_supported_here", at)
|
|
n = g["leaf_count"]
|
|
depth = (n - 1).bit_length() if n else -1
|
|
if (o["leaf_index"] >= n or len(o["siblings"]) != depth or o["direction_bits"] != o["leaf_index"]
|
|
or o["object_id"] != o["leaf_index"]):
|
|
raise Refuse("proof_position_mismatch", at)
|
|
cur = merkle_leaf(o)
|
|
for i, sib in enumerate(o["siblings"]):
|
|
cur = blake3(NODE_DOMAIN + (sib + cur if (o["direction_bits"] >> i) & 1 else cur + sib))
|
|
if cur != g["root"]:
|
|
raise Refuse("inclusion_proof_failed", at)
|
|
# The group total: with one leaf, the verifier holds an accepted receipt for every index.
|
|
if n == 1:
|
|
if o["joules"] != g["total"]:
|
|
raise Refuse("group_total_mismatch", at)
|
|
if g["labelled"]:
|
|
if o["joules"] != 0 and o["class"] is None:
|
|
raise Refuse("group_class_disagrees", at) # its objects compose to no class
|
|
want = (UNMETERED, bytes([UNMETERED])) if o["joules"] == 0 else o["class"]
|
|
if g["class"] != want:
|
|
raise Refuse("group_class_disagrees", at)
|
|
if g["coverage"] != 0:
|
|
raise Refuse("group_coverage_disagrees", at)
|
|
# Accepted: the group heads the track's chain.
|
|
if chain["head"] is not None:
|
|
chain["earlier"].add(chain["head"])
|
|
chain["head"], chain["head_group"] = link, g["group_id"]
|
|
|
|
|
|
# --- ops (interactive-worlds §5.1) --------------------------------------------------------
|
|
|
|
def read_batch(b):
|
|
"""One or more ops, each whole, concatenated with nothing between them. None when the
|
|
batch is not that."""
|
|
ops, i = [], 0
|
|
while i < len(b):
|
|
start = i
|
|
if len(b) - i < OP_HEAD:
|
|
return None
|
|
actor = b[i:i + 32]
|
|
lamport = int.from_bytes(b[i + 32:i + 40], "little")
|
|
tick = int.from_bytes(b[i + 40:i + 48], "little")
|
|
n_deps = int.from_bytes(b[i + 48:i + 50], "little")
|
|
i += OP_HEAD + OP_DEP * n_deps
|
|
if len(b) < i + OP_MID:
|
|
return None
|
|
kind = int.from_bytes(b[i + 16:i + 18], "little")
|
|
mut_len = int.from_bytes(b[i + 18:i + 22], "little")
|
|
i += OP_MID
|
|
mutation = b[i:i + mut_len]
|
|
i += mut_len
|
|
if len(b) < i + OP_SIG:
|
|
return None
|
|
i += OP_SIG
|
|
raw = b[start:i]
|
|
ops.append({"raw": raw, "actor": actor, "lamport": lamport, "tick": tick, "n_deps": n_deps,
|
|
"kind": kind, "mutation": mutation, "signed": raw[:-OP_SIG], "sig": raw[-OP_SIG:]})
|
|
return ops if ops else None
|
|
|
|
|
|
def is_action(op):
|
|
"""`wai.world.model`'s action: mutation kind 1, no dependency, and a body that reads:
|
|
dx i32, dy i32, buttons u32, payload_len u32 (little-endian), then the payload."""
|
|
m = op["mutation"]
|
|
if op["kind"] != ACTION_KIND or op["n_deps"] != 0 or len(m) < ACTION_HEAD:
|
|
return False
|
|
return len(m) == ACTION_HEAD + int.from_bytes(m[12:16], "little")
|
|
|
|
|
|
# --- a world (live-worlds §9) -------------------------------------------------------------
|
|
|
|
def verify_world(v):
|
|
d = read_descriptor(bytes.fromhex(v["descriptor"]))
|
|
grants = [read_grant(bytes.fromhex(g), d, i) for i, g in enumerate(v["grants"])]
|
|
st = read_statement(bytes.fromhex(v["statement"]), d)
|
|
private = v["private"]
|
|
if private not in (True, False):
|
|
raise ValueError("private is not a boolean")
|
|
actors = {g["principal"] for g in grants if g["attrs"].get("world") == "act"}
|
|
admitted = {} # op id -> op bytes
|
|
for ti, track in enumerate(v["tracks"]):
|
|
owner = bytes.fromhex(track["owner"])
|
|
chain = {"head": None, "head_group": None, "earlier": set()}
|
|
last_group, owner_lamports = None, []
|
|
for obj in track["objects"]:
|
|
group = obj["group"]
|
|
at = {"track": ti, "group": group}
|
|
# Step 1.
|
|
if owner not in actors:
|
|
raise Refuse("world_not_granted", at)
|
|
# Step 2.
|
|
if last_group is None:
|
|
if not private and group != 0:
|
|
raise Refuse("world_gap", at)
|
|
elif group != last_group + 1:
|
|
raise Refuse("world_gap", at)
|
|
joins = private and last_group is None and group > 0
|
|
last_group = group
|
|
# Step 3: the receipts cover `payload`; in a private world the ops are `opened`.
|
|
covered = bytes.fromhex(obj["payload"])
|
|
if private:
|
|
if obj.get("opened") is None:
|
|
raise ValueError("a private world's object without its opened batch")
|
|
batch = bytes.fromhex(obj["opened"])
|
|
else:
|
|
if obj.get("opened") is not None:
|
|
raise ValueError("an opened batch in a world that is not private")
|
|
batch = covered
|
|
check_receipts(bytes.fromhex(obj["receipts"]), covered, owner, group, chain, joins, at)
|
|
# Step 4.
|
|
ops = read_batch(batch)
|
|
if ops is None:
|
|
raise Refuse("op_malformed", at)
|
|
if len(ops) > st["max_ops"]:
|
|
raise Refuse("batch_too_large", at)
|
|
if any(len(op["raw"]) > st["max_op_bytes"] for op in ops):
|
|
raise Refuse("op_too_large", at)
|
|
# Step 5, each op in batch order.
|
|
for op in ops:
|
|
if ED.verify(op["actor"], OP_DOMAIN + op["signed"], op["sig"]):
|
|
raise Refuse("bad_op_signature", at)
|
|
if not private and op["actor"] != owner:
|
|
raise Refuse("actor_not_owner", at)
|
|
if not is_action(op):
|
|
raise Refuse("op_not_action", at)
|
|
oid = (op["actor"], op["lamport"])
|
|
held = admitted.get(oid)
|
|
if held is not None and held != op["raw"]:
|
|
raise Refuse("equivocation", at)
|
|
redelivery = held is not None
|
|
if op["actor"] == owner:
|
|
if not redelivery and owner_lamports and op["lamport"] <= max(owner_lamports):
|
|
raise Refuse("lamport_regress", at)
|
|
owner_lamports.append(op["lamport"])
|
|
if not redelivery:
|
|
admitted[oid] = op["raw"]
|
|
return session(st["world"], admitted)
|
|
|
|
|
|
def session(world, admitted):
|
|
"""§9.3 over the admitted ops in canonical order, and §9.4's outcome."""
|
|
order = sorted(admitted.items(), key=lambda kv: (int.from_bytes(kv[1][40:48], "little"), kv[0][1], kv[0][0]))
|
|
h = blake3(SESSION_DOMAIN + world)
|
|
counts = {}
|
|
for (actor, _lamport), raw in order:
|
|
h = blake3(SESSION_DOMAIN + h + raw)
|
|
counts[actor.hex()] = counts.get(actor.hex(), 0) + 1
|
|
return {"accept": True, "session_hash": h.hex(), "ops": len(order), "actors": counts}
|
|
|
|
|
|
def outcome(v):
|
|
try:
|
|
return verify_world(v)
|
|
except Refuse as e:
|
|
return {"accept": False, "reason": e.reason, "at": e.at}
|
|
|
|
|
|
def main(corpus_dir):
|
|
fails = []
|
|
st = ED._self_test()
|
|
print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st))
|
|
if st:
|
|
fails.append("ed25519 self-test")
|
|
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
|
|
doc = json.load(f)
|
|
reasons = set()
|
|
for v in doc["vectors"]:
|
|
name, want = v["name"], v["expect"]
|
|
try:
|
|
got = outcome(v)
|
|
except (ValueError, KeyError, TypeError) as e:
|
|
got = {"accept": None, "unreadable": repr(e)}
|
|
ok = got == want
|
|
if not want.get("accept"):
|
|
reasons.add(want.get("reason"))
|
|
if got.get("accept"):
|
|
what = "accept %s, %d ops" % (got["session_hash"][:16], got["ops"])
|
|
elif got.get("accept") is False:
|
|
what = "refuse %s at %s" % (got["reason"], json.dumps(got["at"]))
|
|
else:
|
|
what = "unreadable: %s" % got["unreadable"]
|
|
print(("PASS" if ok else "FAIL") + " vector %s: %s" % (name, what))
|
|
if not ok:
|
|
fails.append("%s: verifier reached %s, the corpus states %s" % (name, json.dumps(got), json.dumps(want)))
|
|
missing = [r for r in CORPUS_REASONS if r not in reasons]
|
|
accepted = [v for v in doc["vectors"] if v["expect"].get("accept")]
|
|
kinds = {v["private"] for v in accepted}
|
|
if missing:
|
|
fails.append("no vector refused with %s" % ", ".join(missing))
|
|
if kinds != {True, False}:
|
|
fails.append("the corpus does not accept both a public and a private world")
|
|
print("%s every refusal of §3.4 and §9.2 refuses a vector, and a public and a private world are accepted"
|
|
% ("PASS" if not missing and kinds == {True, False} else "FAIL"))
|
|
for f in fails:
|
|
print("FAIL " + f)
|
|
print(("PASS" if not fails else "FAIL") + " live-worlds corpus: %d vectors" % len(doc["vectors"]))
|
|
return 1 if fails else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
print("usage: live_worlds_verify.py <live-conformance/worlds>", file=sys.stderr)
|
|
sys.exit(2)
|
|
sys.exit(main(sys.argv[1]))
|