From 75aeda8d2167a29e26c17bc76cf6b094187f0c53 Mon Sep 17 00:00:00 2001 From: log Date: Sat, 3 Oct 2026 20:31:28 +0000 Subject: size 45286 --- reveal/v5-000001/check.py | 92 ++++++++++++++++++++++++++++++++------ reveal/v5-000001/manifest.txt.ots | Bin 689 -> 2621 bytes reveal/v5-000001/seal.json | 2 +- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/reveal/v5-000001/check.py b/reveal/v5-000001/check.py index e0f55c2..7f86b8d 100644 --- a/reveal/v5-000001/check.py +++ b/reveal/v5-000001/check.py @@ -10,8 +10,10 @@ checks is not a checker, which is why it is copied, whole, into every seal direc Each SEAL_DIR is one seal: `manifest.txt` (the exact text the seal's digest is the sha256 of), `seal.json`, and per reveal group `r.tlock` (the group's key, time-locked to that drand round) and `r.ct` (the group, -encrypted under that key). `manifest.txt.ots`, when present, is an OpenTimestamps proof over the manifest: -`ots verify manifest.txt.ots` checks it against Bitcoin. +encrypted under that key), and, once the group has been revealed, `r.held.json` (how many of its withheld +lines Echo publishes no salt for, and why) and any number of `r.salts..json`: each the salts of withheld +lines Echo decided to open and had not erased, each with its line. `manifest.txt.ots`, when present, is an +OpenTimestamps proof over the manifest: `ots verify manifest.txt.ots` checks it against Bitcoin. What it checks, from the files alone and the beacon: 1. the manifest hashes to the seal's digest, and each seal names the one before it (when several are given); @@ -22,11 +24,17 @@ What it checks, from the files alone and the beacon: counted against the committed counts, its lines in id order, and every line's reveal day recomputed from its own settling instant under the seal's named rule (`utc-day-after-close`: the first 00:00 UTC after `settles_at`, else after the deadline); - 4. once every group of every seal from the first is open and nothing is withheld: the union of their v4 lines, - sorted by id, rehashes to the v4 digest the last seal follows, so no sealed forecast was left out of a reveal. + 4. for every opened group with releases: each names this seal, round and group and its own number; each released + salt and line hash to the withheld line's committed hash; no line is settled twice; and a hold, where there is + one, names the group and counts no more lines than are withheld; + 5. once every group of every seal from the first is open and every withheld line released: the union of their v4 + lines, sorted by id, rehashes to the v4 digest the last seal follows, so no sealed forecast was left out of a + reveal. Where a withheld line has no released salt (held, erased, or not yet released) that digest cannot be + rebuilt, and this says so and why: what is checked then is only that every forecast the v4 seal covers is in a + group, counted. A withheld line is never called verified. -A group whose round is not yet signed is reported as pending, not as a failure. Exit 0 when everything checkable -passed, 1 when anything did not match. +A group whose round is not yet signed, or whose withheld lines have no release yet, is reported as pending or held, +not as a failure. Exit 0 when everything checkable passed, 1 when anything did not match. """ from __future__ import annotations @@ -146,10 +154,56 @@ def unlock(tle: str, beacon: dict, locked: str) -> bytes | None: raise RuntimeError("; ".join(errors)) +def release(d: Path, seal_digest: str, g: dict, lines: list[dict], say) -> tuple[dict[int, str], int, str | None]: + """An opened group's releases and hold, checked against the group: (id -> the v4 line each released salt opens, + how many withheld lines were opened and found erased, and the hold's reason where Echo published one).""" + r = g["round"] + shut = {x["id"]: x["withheld"] for x in lines if "withheld" in x} + out, erased, seen, bad = {}, set(), set(), [] + files = sorted(d.glob(f"r{r}.salts.*.json"), key=lambda p: int(p.name.split(".")[-2])) + for k, path in enumerate(files): + rel = json.loads(path.read_text()) + say(rel.get("format") == "echo-reveal-salts/1" and rel.get("seal") == seal_digest + and rel.get("round") == r and rel.get("group") == g["digest"] and rel.get("n") == k + and path.name == f"r{r}.salts.{k}.json", + f"round {r}: release {k} names this seal, round and group, and follows the one before") + for x in rel.get("released", []): + i, line = x["id"], x["line"] + opens = (i in shut and i not in seen and line.startswith(f"{i}|") + and sha(bytes.fromhex(x["salt"]) + line.encode()) == shut[i]) + if opens: + out[i] = line + else: + bad.append(i) + seen.add(i) + for i in rel.get("erased", []): + if i not in shut or i in seen: + bad.append(i) + erased.add(i) + seen.add(i) + if files: + say(not bad, f"round {r}: {len(out)} released lines open their committed hashes, {len(erased)} opened lines " + f"found erased, none settled twice" + (f" ({len(bad)} do not, e.g. {bad[:3]})" if bad else "")) + why = None + hold = d / f"r{r}.held.json" + if hold.exists(): + h = json.loads(hold.read_text()) + say(h.get("format") == "echo-reveal-held/1" and h.get("seal") == seal_digest and h.get("round") == r + and h.get("group") == g["digest"] and 0 <= h.get("held", -1) <= len(shut), + f"round {r}: the hold names this seal, round and group, and counts at most its withheld lines") + why = str(h.get("why") or "no reason given") + left = len(shut) - len(out) - len(erased) + if left: + print(f" .... round {r}: {left} withheld lines have no released salt: counted, not rehashed" + + (f" (Echo publishes none: {why})" if why else " (no release or hold published yet)")) + return out, len(erased), why + + def check(dirs: list[Path], tle: str = "tle", offline: bool = False) -> int: from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 - failed, pending, withheld, union = 0, 0, 0, {} + failed, pending, withheld, held, erased, union = 0, 0, 0, 0, 0, {} + reasons: set[str] = set() first_prev = "unset" def say(ok: bool, what: str) -> None: @@ -216,20 +270,32 @@ def check(dirs: list[Path], tle: str = "tle", offline: bool = False) -> int: else: print(f" .... round {r}: reveal rule {m['variant'].get('reveal')!r} is not one this checker " "recomputes; which lines belong to the day is unchecked") + opened, gone, why = release(d, row["digest"], g, lines, say) for x in lines: if x["id"] in union: say(False, f"forecast {x['id']} is in two groups") - union[x["id"]] = x.get("v4") - withheld += "withheld" in x + union[x["id"]] = x.get("v4") or opened.get(x["id"]) + shut = sum(1 for x in lines if "withheld" in x and x["id"] not in opened) - gone + erased += gone + if why is None: + withheld += shut + else: + held += shut + reasons.add(why) if last is not None: if first_prev is not None: print(" .... completeness: the first seal given is not the chain's first; give every seal from it") elif pending: print(f" .... completeness: {pending} groups still locked; it is checkable once all have opened") - elif withheld: - print(f" .... completeness: {withheld} text layers are withheld, so the v4 lines cannot all be rebuilt;" - f" {len(union)} forecasts against the v4 seal's {last['follows']['n']}") - say(len(union) == last["follows"]["n"], "every forecast the v4 seal covers is in a group") + elif withheld or held or erased: + told = [f"{withheld} withheld lines not yet released"] if withheld else [] + told += ([f"{held} withheld lines held, for which Echo publishes no salt ({'; '.join(sorted(reasons))})"] + if held else []) + told += [f"{erased} erased stay hashes"] if erased else [] + print(f" .... completeness: {', '.join(told)}: those lines can be counted but not rehashed, so the v4 " + f"lines cannot all be rebuilt and the v4 digest is NOT checked; {len(union)} forecasts against " + f"the v4 seal's {last['follows']['n']}") + say(len(union) == last["follows"]["n"], "every forecast the v4 seal covers is in a group, counted") else: got = sha("\n".join(union[k] for k in sorted(union)).encode()) say(got == last["follows"]["digest"] and len(union) == last["follows"]["n"], diff --git a/reveal/v5-000001/manifest.txt.ots b/reveal/v5-000001/manifest.txt.ots index ec78929..06e31c6 100644 Binary files a/reveal/v5-000001/manifest.txt.ots and b/reveal/v5-000001/manifest.txt.ots differ diff --git a/reveal/v5-000001/seal.json b/reveal/v5-000001/seal.json index 9963ee8..a452f46 100644 --- a/reveal/v5-000001/seal.json +++ b/reveal/v5-000001/seal.json @@ -1,6 +1,6 @@ { "algorithm": "sha256-live-v5", - "anchor_ref": "sha256:ab1c64ee208cedf8ce14e1e4a1568de02e03578c9af283db73392ad2e61c90e8", + "anchor_ref": "bitcoin:block:969687", "covers_after": null, "covers_through": "2026-10-03T01:35:21.431130Z", "digest": "ab1c64ee208cedf8ce14e1e4a1568de02e03578c9af283db73392ad2e61c90e8", -- cgit v1.2.3