diff options
| author | log <log@invalid> | 2026-10-04 01:11:02 +0000 |
|---|---|---|
| committer | log <log@invalid> | 2026-10-04 01:11:02 +0000 |
| commit | 9333aefa19a55050cdd20fbeaed41efd42e5bacc (patch) | |
| tree | e1dd758c1e458ba2096162057ae93f1dabc66a98 | |
| parent | b7805a6feb361809831e89eed55a25548491431b (diff) | |
size 48428
| -rw-r--r-- | reveal/v5-000003/manifest.txt.ots | bin | 584 -> 2518 bytes | |||
| -rw-r--r-- | reveal/v5-000003/seal.json | 2 | ||||
| -rw-r--r-- | reveal/v5-000004/check.py | 319 | ||||
| -rw-r--r-- | reveal/v5-000004/manifest.txt | 8 | ||||
| -rw-r--r-- | reveal/v5-000004/manifest.txt.ots | bin | 0 -> 759 bytes | |||
| -rw-r--r-- | reveal/v5-000004/r33389812.ct | bin | 0 -> 1013 bytes | |||
| -rw-r--r-- | reveal/v5-000004/r33389812.nostr.json | 1 | ||||
| -rw-r--r-- | reveal/v5-000004/r33389812.tlock | 11 | ||||
| -rw-r--r-- | reveal/v5-000004/r34541812.ct | bin | 0 -> 1016 bytes | |||
| -rw-r--r-- | reveal/v5-000004/r34541812.nostr.json | 1 | ||||
| -rw-r--r-- | reveal/v5-000004/r34541812.tlock | 11 | ||||
| -rw-r--r-- | reveal/v5-000004/seal.json | 43 |
12 files changed, 395 insertions, 1 deletions
diff --git a/reveal/v5-000003/manifest.txt.ots b/reveal/v5-000003/manifest.txt.ots index 50d510b..f4798e5 100644 --- a/reveal/v5-000003/manifest.txt.ots +++ b/reveal/v5-000003/manifest.txt.ots | |||
| Binary files differ | |||
diff --git a/reveal/v5-000003/seal.json b/reveal/v5-000003/seal.json index c82ed7a..9ebd7bf 100644 --- a/reveal/v5-000003/seal.json +++ b/reveal/v5-000003/seal.json | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | { | 1 | { |
| 2 | "algorithm": "sha256-live-v5", | 2 | "algorithm": "sha256-live-v5", |
| 3 | "anchor_ref": "sha256:baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19", | 3 | "anchor_ref": "bitcoin:block:969776", |
| 4 | "covers_after": "2026-10-03T22:17:20.526176Z", | 4 | "covers_after": "2026-10-03T22:17:20.526176Z", |
| 5 | "covers_through": "2026-10-03T23:07:43.366835Z", | 5 | "covers_through": "2026-10-03T23:07:43.366835Z", |
| 6 | "digest": "baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19", | 6 | "digest": "baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19", |
diff --git a/reveal/v5-000004/check.py b/reveal/v5-000004/check.py new file mode 100644 index 0000000..7f86b8d --- /dev/null +++ b/reveal/v5-000004/check.py | |||
| @@ -0,0 +1,319 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """ | ||
| 3 | Check Echo's self-revealing ledger seals (`sha256-live-v5`) without trusting Echo. | ||
| 4 | |||
| 5 | python3 check.py SEAL_DIR [SEAL_DIR ...] [--tle PATH] [--offline] | ||
| 6 | |||
| 7 | Needs Python 3.11+, the `cryptography` package (ChaCha20-Poly1305) and drand's `tle` binary | ||
| 8 | (`go install github.com/drand/tlock/cmd/tle@v1.2.0`). It imports nothing of Echo's: a checker that needs the thing it | ||
| 9 | checks is not a checker, which is why it is copied, whole, into every seal directory. | ||
| 10 | |||
| 11 | Each SEAL_DIR is one seal: `manifest.txt` (the exact text the seal's digest is the sha256 of), `seal.json`, and per | ||
| 12 | reveal group `r<ROUND>.tlock` (the group's key, time-locked to that drand round) and `r<ROUND>.ct` (the group, | ||
| 13 | encrypted under that key), and, once the group has been revealed, `r<ROUND>.held.json` (how many of its withheld | ||
| 14 | lines Echo publishes no salt for, and why) and any number of `r<ROUND>.salts.<N>.json`: each the salts of withheld | ||
| 15 | lines Echo decided to open and had not erased, each with its line. `manifest.txt.ots`, when present, is an | ||
| 16 | OpenTimestamps proof over the manifest: `ots verify manifest.txt.ots` checks it against Bitcoin. | ||
| 17 | |||
| 18 | What it checks, from the files alone and the beacon: | ||
| 19 | 1. the manifest hashes to the seal's digest, and each seal names the one before it (when several are given); | ||
| 20 | 2. each group's ciphertext and locked key hash to what the manifest commits, and the key is locked to the | ||
| 21 | manifest's round of the manifest's chain; | ||
| 22 | 3. for every group whose round the beacon has signed: the round's signature as each relay serves it, the key | ||
| 23 | unlocked with it, the group decrypted, its bytes hashed to the committed digest, its lines and withheld lines | ||
| 24 | counted against the committed counts, its lines in id order, and every line's reveal day recomputed from its | ||
| 25 | own settling instant under the seal's named rule (`utc-day-after-close`: the first 00:00 UTC after | ||
| 26 | `settles_at`, else after the deadline); | ||
| 27 | 4. for every opened group with releases: each names this seal, round and group and its own number; each released | ||
| 28 | salt and line hash to the withheld line's committed hash; no line is settled twice; and a hold, where there is | ||
| 29 | one, names the group and counts no more lines than are withheld; | ||
| 30 | 5. once every group of every seal from the first is open and every withheld line released: the union of their v4 | ||
| 31 | lines, sorted by id, rehashes to the v4 digest the last seal follows, so no sealed forecast was left out of a | ||
| 32 | reveal. Where a withheld line has no released salt (held, erased, or not yet released) that digest cannot be | ||
| 33 | rebuilt, and this says so and why: what is checked then is only that every forecast the v4 seal covers is in a | ||
| 34 | group, counted. A withheld line is never called verified. | ||
| 35 | |||
| 36 | A group whose round is not yet signed, or whose withheld lines have no release yet, is reported as pending or held, | ||
| 37 | not as a failure. Exit 0 when everything checkable passed, 1 when anything did not match. | ||
| 38 | """ | ||
| 39 | from __future__ import annotations | ||
| 40 | |||
| 41 | import argparse | ||
| 42 | import base64 | ||
| 43 | import hashlib | ||
| 44 | import json | ||
| 45 | import re | ||
| 46 | import subprocess | ||
| 47 | import sys | ||
| 48 | import urllib.request | ||
| 49 | import zlib | ||
| 50 | from datetime import datetime, timedelta, timezone | ||
| 51 | from pathlib import Path | ||
| 52 | |||
| 53 | |||
| 54 | def sha(b: bytes) -> str: | ||
| 55 | return hashlib.sha256(b).hexdigest() | ||
| 56 | |||
| 57 | |||
| 58 | def parse_manifest(text: str) -> dict: | ||
| 59 | lines = text.split("\n") | ||
| 60 | if lines[0] != "sha256-live-v5": | ||
| 61 | raise ValueError(f"not a v5 manifest: {lines[0]!r}") | ||
| 62 | m = {"groups": []} | ||
| 63 | for ln in lines[1:]: | ||
| 64 | word, _, rest = ln.partition(" ") | ||
| 65 | if word == "prev": | ||
| 66 | m["prev"] = None if rest == "-" else rest | ||
| 67 | elif word == "follows": | ||
| 68 | seq, digest, n = rest.split(" ") | ||
| 69 | m["follows"] = {"seq": int(seq), "digest": digest, "n": int(n)} | ||
| 70 | elif word == "covers": | ||
| 71 | m["covers"] = rest.split(" ") | ||
| 72 | elif word == "variant": | ||
| 73 | m["variant"] = json.loads(rest) | ||
| 74 | elif word == "counts": | ||
| 75 | m["n"], m["n_withheld"], m["n_groups"] = map(int, rest.split(" ")) | ||
| 76 | elif word == "group": | ||
| 77 | r, at, n, w, digest, ct, key, late = rest.split(" ") | ||
| 78 | m["groups"].append({"round": int(r), "reveal_at": at, "n": int(n), "n_withheld": int(w), | ||
| 79 | "digest": digest, "ct": ct, "key": key, "late": late == "1"}) | ||
| 80 | else: | ||
| 81 | raise ValueError(f"unknown manifest line {ln!r}") | ||
| 82 | return m | ||
| 83 | |||
| 84 | |||
| 85 | def stanza(locked: str) -> tuple[int, str] | None: | ||
| 86 | body = "".join(ln for ln in locked.strip().splitlines() if not ln.startswith("-----")) | ||
| 87 | m = re.search(rb"-> tlock (\d+) ([0-9a-f]{64})", base64.b64decode(body)) | ||
| 88 | return (int(m.group(1)), m.group(2).decode()) if m else None | ||
| 89 | |||
| 90 | |||
| 91 | def round_at(beacon: dict, at: datetime) -> int: | ||
| 92 | t = at.timestamp() | ||
| 93 | whole = int(t) + (t != int(t)) | ||
| 94 | if whole <= beacon["genesis_time"]: | ||
| 95 | return 1 | ||
| 96 | return -(-(whole - beacon["genesis_time"]) // beacon["period"]) + 1 | ||
| 97 | |||
| 98 | |||
| 99 | def reveal_day(closes: datetime) -> datetime: | ||
| 100 | d = closes.astimezone(timezone.utc).date() + timedelta(days=1) | ||
| 101 | return datetime(d.year, d.month, d.day, tzinfo=timezone.utc) | ||
| 102 | |||
| 103 | |||
| 104 | def parse_line(ln: str) -> dict: | ||
| 105 | """One line: `id|issued_at|deadline|forecaster|p|settlement|anchor<TAB>open:<v4>` or `...<TAB>withheld:<hex>`.""" | ||
| 106 | numeric, tab, text = ln.partition("\t") | ||
| 107 | if not tab: | ||
| 108 | raise ValueError("a line without its text layer") | ||
| 109 | fid, issued, deadline, forecaster, p, rest = numeric.split("|", 5) | ||
| 110 | dec = json.JSONDecoder() | ||
| 111 | settle, end = dec.raw_decode(rest) | ||
| 112 | if rest[end] != "|": | ||
| 113 | raise ValueError("settlement not followed by the anchor") | ||
| 114 | anchor, end2 = dec.raw_decode(rest, end + 1) | ||
| 115 | if end2 != len(rest): | ||
| 116 | raise ValueError("trailing bytes after the anchor") | ||
| 117 | out = {"id": int(fid), "issued_at": issued, "deadline": deadline, "forecaster": forecaster, "p": float(p), | ||
| 118 | "settlement": settle, "anchor": anchor} | ||
| 119 | if text.startswith("open:"): | ||
| 120 | out["v4"] = text[5:] | ||
| 121 | if not out["v4"].startswith(f"{fid}|"): | ||
| 122 | raise ValueError("the text layer is another forecast's line") | ||
| 123 | elif re.fullmatch(r"withheld:[0-9a-f]{64}", text): | ||
| 124 | out["withheld"] = text[9:] | ||
| 125 | else: | ||
| 126 | raise ValueError(f"unknown text layer {text[:20]!r}") | ||
| 127 | return out | ||
| 128 | |||
| 129 | |||
| 130 | def beacon_round(beacon: dict, rnd: int) -> dict: | ||
| 131 | """What each relay serves for the round: its signature, or why there is none.""" | ||
| 132 | out = {} | ||
| 133 | for relay in beacon["relays"]: | ||
| 134 | url = f"{relay.rstrip('/')}/{beacon['chain_hash']}/public/{rnd}" | ||
| 135 | try: | ||
| 136 | with urllib.request.urlopen(url, timeout=20) as r: # noqa: S310 — the seal's own relays | ||
| 137 | out[relay] = json.loads(r.read()).get("signature") | ||
| 138 | except Exception as exc: # noqa: BLE001 — report, go on | ||
| 139 | out[relay] = f"unavailable ({type(exc).__name__})" | ||
| 140 | return out | ||
| 141 | |||
| 142 | |||
| 143 | def unlock(tle: str, beacon: dict, locked: str) -> bytes | None: | ||
| 144 | """The key, or None while the round is unsigned. Each relay is tried in turn.""" | ||
| 145 | errors = [] | ||
| 146 | for relay in beacon["relays"]: | ||
| 147 | r = subprocess.run([tle, "-d", "-n", relay.rstrip("/") + "/", "-c", beacon["chain_hash"]], | ||
| 148 | input=locked.encode(), capture_output=True, timeout=120, check=False) | ||
| 149 | if r.returncode == 0: | ||
| 150 | return r.stdout | ||
| 151 | if b"too early" in r.stderr: | ||
| 152 | return None | ||
| 153 | errors.append(f"{relay}: {r.stderr.decode(errors='replace').strip()[-160:]}") | ||
| 154 | raise RuntimeError("; ".join(errors)) | ||
| 155 | |||
| 156 | |||
| 157 | def release(d: Path, seal_digest: str, g: dict, lines: list[dict], say) -> tuple[dict[int, str], int, str | None]: | ||
| 158 | """An opened group's releases and hold, checked against the group: (id -> the v4 line each released salt opens, | ||
| 159 | how many withheld lines were opened and found erased, and the hold's reason where Echo published one).""" | ||
| 160 | r = g["round"] | ||
| 161 | shut = {x["id"]: x["withheld"] for x in lines if "withheld" in x} | ||
| 162 | out, erased, seen, bad = {}, set(), set(), [] | ||
| 163 | files = sorted(d.glob(f"r{r}.salts.*.json"), key=lambda p: int(p.name.split(".")[-2])) | ||
| 164 | for k, path in enumerate(files): | ||
| 165 | rel = json.loads(path.read_text()) | ||
| 166 | say(rel.get("format") == "echo-reveal-salts/1" and rel.get("seal") == seal_digest | ||
| 167 | and rel.get("round") == r and rel.get("group") == g["digest"] and rel.get("n") == k | ||
| 168 | and path.name == f"r{r}.salts.{k}.json", | ||
| 169 | f"round {r}: release {k} names this seal, round and group, and follows the one before") | ||
| 170 | for x in rel.get("released", []): | ||
| 171 | i, line = x["id"], x["line"] | ||
| 172 | opens = (i in shut and i not in seen and line.startswith(f"{i}|") | ||
| 173 | and sha(bytes.fromhex(x["salt"]) + line.encode()) == shut[i]) | ||
| 174 | if opens: | ||
| 175 | out[i] = line | ||
| 176 | else: | ||
| 177 | bad.append(i) | ||
| 178 | seen.add(i) | ||
| 179 | for i in rel.get("erased", []): | ||
| 180 | if i not in shut or i in seen: | ||
| 181 | bad.append(i) | ||
| 182 | erased.add(i) | ||
| 183 | seen.add(i) | ||
| 184 | if files: | ||
| 185 | say(not bad, f"round {r}: {len(out)} released lines open their committed hashes, {len(erased)} opened lines " | ||
| 186 | f"found erased, none settled twice" + (f" ({len(bad)} do not, e.g. {bad[:3]})" if bad else "")) | ||
| 187 | why = None | ||
| 188 | hold = d / f"r{r}.held.json" | ||
| 189 | if hold.exists(): | ||
| 190 | h = json.loads(hold.read_text()) | ||
| 191 | say(h.get("format") == "echo-reveal-held/1" and h.get("seal") == seal_digest and h.get("round") == r | ||
| 192 | and h.get("group") == g["digest"] and 0 <= h.get("held", -1) <= len(shut), | ||
| 193 | f"round {r}: the hold names this seal, round and group, and counts at most its withheld lines") | ||
| 194 | why = str(h.get("why") or "no reason given") | ||
| 195 | left = len(shut) - len(out) - len(erased) | ||
| 196 | if left: | ||
| 197 | print(f" .... round {r}: {left} withheld lines have no released salt: counted, not rehashed" | ||
| 198 | + (f" (Echo publishes none: {why})" if why else " (no release or hold published yet)")) | ||
| 199 | return out, len(erased), why | ||
| 200 | |||
| 201 | |||
| 202 | def check(dirs: list[Path], tle: str = "tle", offline: bool = False) -> int: | ||
| 203 | from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 | ||
| 204 | |||
| 205 | failed, pending, withheld, held, erased, union = 0, 0, 0, 0, 0, {} | ||
| 206 | reasons: set[str] = set() | ||
| 207 | first_prev = "unset" | ||
| 208 | |||
| 209 | def say(ok: bool, what: str) -> None: | ||
| 210 | nonlocal failed | ||
| 211 | failed += not ok | ||
| 212 | print(f" {'PASS' if ok else 'FAIL'} {what}") | ||
| 213 | |||
| 214 | prev_digest = None | ||
| 215 | seals = [] | ||
| 216 | for d in dirs: | ||
| 217 | text = (d / "manifest.txt").read_bytes() | ||
| 218 | row = json.loads((d / "seal.json").read_text()) | ||
| 219 | seals.append((d, text, row, parse_manifest(text.decode()))) | ||
| 220 | seals.sort(key=lambda s: s[2]["seq"]) | ||
| 221 | last = None | ||
| 222 | for i, (d, text, row, m) in enumerate(seals): | ||
| 223 | print(f"v5 seal {row['seq']}: {m['n']} forecasts in {m['n_groups']} reveal groups, {m['n_withheld']} withheld, " | ||
| 224 | f"follows v4 seal {m['follows']['seq']}") | ||
| 225 | say(sha(text) == row["digest"], f"sha256(manifest.txt) is the seal's digest {row['digest'][:16]}…") | ||
| 226 | say(len(m["groups"]) == m["n_groups"] and sum(g["n"] for g in m["groups"]) == m["n"] | ||
| 227 | and sum(g["n_withheld"] for g in m["groups"]) == m["n_withheld"], "the manifest's counts add up") | ||
| 228 | if i == 0: | ||
| 229 | first_prev = m["prev"] | ||
| 230 | else: | ||
| 231 | say(m["prev"] == prev_digest, "it names the seal before it") | ||
| 232 | prev_digest, last = row["digest"], m | ||
| 233 | b = m["variant"]["beacon"] | ||
| 234 | for g in m["groups"]: | ||
| 235 | r = g["round"] | ||
| 236 | ct, locked = (d / f"r{r}.ct").read_bytes(), (d / f"r{r}.tlock").read_text() | ||
| 237 | say(sha(ct) == g["ct"] and sha(locked.encode()) == g["key"], | ||
| 238 | f"round {r}: the ciphertext and the locked key are the committed ones") | ||
| 239 | say(stanza(locked) == (r, b["chain_hash"]), f"round {r}: the key is locked to that round of that chain") | ||
| 240 | say(round_at(b, datetime.fromisoformat(g["reveal_at"])) == r, f"round {r} is signed at {g['reveal_at']}") | ||
| 241 | if not offline: | ||
| 242 | sigs = beacon_round(b, r) | ||
| 243 | good = {s for s in sigs.values() if s and not s.startswith("unavailable")} | ||
| 244 | print(f" .... round {r} signature from {len(sigs)} relays: " | ||
| 245 | + ("not yet signed" if not good else f"{len(good)} distinct: {min(good)[:24]}…")) | ||
| 246 | if len(good) > 1: | ||
| 247 | say(False, f"round {r}: relays disagree on its signature") | ||
| 248 | key = unlock(tle, b, locked) | ||
| 249 | if key is None: | ||
| 250 | pending += 1 | ||
| 251 | print(f" .... round {r} ({g['reveal_at']}) not yet signed: {g['n']} forecasts stay locked") | ||
| 252 | continue | ||
| 253 | try: | ||
| 254 | plain = zlib.decompress(ChaCha20Poly1305(key).decrypt(b"\x00" * 12, ct, None)) | ||
| 255 | except Exception as exc: # noqa: BLE001 — a finding, not a crash | ||
| 256 | say(False, f"round {r}: the ciphertext does not open under its key ({type(exc).__name__})") | ||
| 257 | continue | ||
| 258 | say(sha(plain) == g["digest"], f"round {r}: the opened group hashes to its committed digest") | ||
| 259 | lines = [parse_line(ln) for ln in plain.decode().split("\n")] | ||
| 260 | ids = [x["id"] for x in lines] | ||
| 261 | say(len(lines) == g["n"] and sum("withheld" in x for x in lines) == g["n_withheld"], | ||
| 262 | f"round {r}: {len(lines)} lines, {g['n_withheld']} withheld, as committed") | ||
| 263 | say(ids == sorted(set(ids)), f"round {r}: lines in id order, none twice") | ||
| 264 | due = datetime.fromisoformat(g["reveal_at"]) | ||
| 265 | if m["variant"].get("reveal") == "utc-day-after-close": | ||
| 266 | wrong = [x["id"] for x in lines if reveal_day(datetime.fromisoformat( | ||
| 267 | (x["settlement"] or {}).get("settles_at") or x["deadline"])) != due] | ||
| 268 | say(not wrong, f"round {r}: every line belongs to this reveal day" | ||
| 269 | + (f" ({len(wrong)} do not, e.g. {wrong[:3]})" if wrong else "")) | ||
| 270 | else: | ||
| 271 | print(f" .... round {r}: reveal rule {m['variant'].get('reveal')!r} is not one this checker " | ||
| 272 | "recomputes; which lines belong to the day is unchecked") | ||
| 273 | opened, gone, why = release(d, row["digest"], g, lines, say) | ||
| 274 | for x in lines: | ||
| 275 | if x["id"] in union: | ||
| 276 | say(False, f"forecast {x['id']} is in two groups") | ||
| 277 | union[x["id"]] = x.get("v4") or opened.get(x["id"]) | ||
| 278 | shut = sum(1 for x in lines if "withheld" in x and x["id"] not in opened) - gone | ||
| 279 | erased += gone | ||
| 280 | if why is None: | ||
| 281 | withheld += shut | ||
| 282 | else: | ||
| 283 | held += shut | ||
| 284 | reasons.add(why) | ||
| 285 | if last is not None: | ||
| 286 | if first_prev is not None: | ||
| 287 | print(" .... completeness: the first seal given is not the chain's first; give every seal from it") | ||
| 288 | elif pending: | ||
| 289 | print(f" .... completeness: {pending} groups still locked; it is checkable once all have opened") | ||
| 290 | elif withheld or held or erased: | ||
| 291 | told = [f"{withheld} withheld lines not yet released"] if withheld else [] | ||
| 292 | told += ([f"{held} withheld lines held, for which Echo publishes no salt ({'; '.join(sorted(reasons))})"] | ||
| 293 | if held else []) | ||
| 294 | told += [f"{erased} erased stay hashes"] if erased else [] | ||
| 295 | print(f" .... completeness: {', '.join(told)}: those lines can be counted but not rehashed, so the v4 " | ||
| 296 | f"lines cannot all be rebuilt and the v4 digest is NOT checked; {len(union)} forecasts against " | ||
| 297 | f"the v4 seal's {last['follows']['n']}") | ||
| 298 | say(len(union) == last["follows"]["n"], "every forecast the v4 seal covers is in a group, counted") | ||
| 299 | else: | ||
| 300 | got = sha("\n".join(union[k] for k in sorted(union)).encode()) | ||
| 301 | say(got == last["follows"]["digest"] and len(union) == last["follows"]["n"], | ||
| 302 | f"the union of {len(union)} lines rehashes to v4 seal {last['follows']['seq']}'s digest") | ||
| 303 | print("\nNOT VERIFIED" if failed else f"\nVERIFIED{f' ({pending} groups not yet revealed)' if pending else ''}") | ||
| 304 | return 1 if failed else 0 | ||
| 305 | |||
| 306 | |||
| 307 | def main(argv: list[str]) -> int: | ||
| 308 | ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | ||
| 309 | ap.add_argument("dirs", nargs="*", default=[str(Path(__file__).resolve().parent)], | ||
| 310 | help="seal directories, any order (default: the one this script sits in)") | ||
| 311 | ap.add_argument("--tle", default="tle", help="the tle binary that unlocks a key once its round is signed") | ||
| 312 | ap.add_argument("--offline", action="store_true", | ||
| 313 | help="do not fetch each round's signature from the relays for display; tle still asks them") | ||
| 314 | args = ap.parse_args(argv) | ||
| 315 | return check([Path(d) for d in args.dirs], tle=args.tle, offline=args.offline) | ||
| 316 | |||
| 317 | |||
| 318 | if __name__ == "__main__": | ||
| 319 | raise SystemExit(main(sys.argv[1:])) | ||
diff --git a/reveal/v5-000004/manifest.txt b/reveal/v5-000004/manifest.txt new file mode 100644 index 0000000..6743e18 --- /dev/null +++ b/reveal/v5-000004/manifest.txt | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | sha256-live-v5 | ||
| 2 | prev baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19 | ||
| 3 | follows 34 737e4a1b4b7b4a23f087f974f2f3d9865211a73b0990837791b7a44b71bc3cfa 39128 | ||
| 4 | covers 2026-10-03T23:07:43.366835Z 2026-10-04T00:08:35.532031Z | ||
| 5 | variant {"beacon":{"chain_hash":"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971","genesis_time":1692803367,"period":3,"relays":["https://api.drand.sh","https://api2.drand.sh","https://api3.drand.sh","https://drand.cloudflare.com"],"scheme":"bls-unchained-g1-rfc9380"},"cipher":"chacha20poly1305-zlib6-nonce0","gate":{"default":"withheld: a kept salt can be released later; an opened text can never be withdrawn","opened":"none","why":"no decision by Echo yet"},"line":"numeric<TAB>open:<v4 line> | numeric<TAB>withheld:sha256(salt||v4 line)","reveal":"utc-day-after-close","salts":"key ring: one content a seal, one part a line","tool":{"name":"tle","sha256":"69157d8c14a1d390d1b7744946288f83d4b10748471d5447f9abb77a5902c541","version":"tlock v1.2.0 -- github.com/drand/tlock"}} | ||
| 6 | counts 24 24 2 | ||
| 7 | group 33389812 2026-10-26T00:00:00.000000Z 12 12 c7218af247b604a2dcf44cc788ce5e7180b492a91b0005dd7dbeea118ffeee33 a79f9f6b909b5ce75081420bc9e6dcfc45b4745c6751ef7873172781163b5d47 7667b76cdbbfbceab379de408ef27235a251dec95b736dbe062ff18354638134 0 | ||
| 8 | group 34541812 2026-12-05T00:00:00.000000Z 12 12 71f3b8b4de94f03c6422fdf67d055535e79b0411595168d406729c17f62a988a c056fab6d09a1abf85c6c41fba220e0a5982e32f67cfb67a8e76e5b2a1c3aced 42cede3b2e0c2c04db31256b113203659e9bab903142fc119298ddd2034da7d9 0 \ No newline at end of file | ||
diff --git a/reveal/v5-000004/manifest.txt.ots b/reveal/v5-000004/manifest.txt.ots new file mode 100644 index 0000000..4039a25 --- /dev/null +++ b/reveal/v5-000004/manifest.txt.ots | |||
| Binary files differ | |||
diff --git a/reveal/v5-000004/r33389812.ct b/reveal/v5-000004/r33389812.ct new file mode 100644 index 0000000..b59d533 --- /dev/null +++ b/reveal/v5-000004/r33389812.ct | |||
| Binary files differ | |||
diff --git a/reveal/v5-000004/r33389812.nostr.json b/reveal/v5-000004/r33389812.nostr.json new file mode 100644 index 0000000..059ef4d --- /dev/null +++ b/reveal/v5-000004/r33389812.nostr.json | |||
| @@ -0,0 +1 @@ | |||
| {"content":"-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDMzMzg5ODEyIDUyZGI5YmE3\nMGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw\nMGM4NGU5NzEKc1BqWmV3WTlqUU4xdE9vZXFkb3dtNE92cTNUcllEM2NTWXVxeGQ2\nRndObEhrZ2VSZ3lYSGFhSzh2NnVSNERyTwpGd1dmNjVna1FWdWduUWpDQ29UMDUr\nMm96NGRrUlpDNDFoY1RlL0ZrdXp1WDdQVHBlL0t1YXJiVDVjd2ZCY1I2ClBwanRK\nVzhOTFZ3UUdLUEJ1SGd2RDg1ZUwzYlJURjRpN0M2aDNlNFhXN2cKLS0tIGUwc05o\nMGFjSm9zREtVZXJWUE5wSmZ3MFFYbTVsMVpxNnV1NTYvUkFpMVUK8BWH7JgkFaRs\nSDIU5APiPXldsjSFN3Ae7vEiugR5A91M9WIGzoZBerf7MXDq2EqOp/hkUcSns5Qv\ndGEb/4nx5w==\n-----END AGE ENCRYPTED FILE-----\n","tags":[["t","echo-ledger-reveal"],["round","33389812"],["reveal_at","2026-10-26T00:00:00.000000Z"],["chain","52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"],["digest","c7218af247b604a2dcf44cc788ce5e7180b492a91b0005dd7dbeea118ffeee33"],["ct","a79f9f6b909b5ce75081420bc9e6dcfc45b4745c6751ef7873172781163b5d47"],["seal","0577329be46aad3dc6b6ba6588d255eb95d6137d7abd48f8ad8568134ff4437d"]]} | |||
diff --git a/reveal/v5-000004/r33389812.tlock b/reveal/v5-000004/r33389812.tlock new file mode 100644 index 0000000..c07742b --- /dev/null +++ b/reveal/v5-000004/r33389812.tlock | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | -----BEGIN AGE ENCRYPTED FILE----- | ||
| 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDMzMzg5ODEyIDUyZGI5YmE3 | ||
| 3 | MGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw | ||
| 4 | MGM4NGU5NzEKc1BqWmV3WTlqUU4xdE9vZXFkb3dtNE92cTNUcllEM2NTWXVxeGQ2 | ||
| 5 | RndObEhrZ2VSZ3lYSGFhSzh2NnVSNERyTwpGd1dmNjVna1FWdWduUWpDQ29UMDUr | ||
| 6 | Mm96NGRrUlpDNDFoY1RlL0ZrdXp1WDdQVHBlL0t1YXJiVDVjd2ZCY1I2ClBwanRK | ||
| 7 | VzhOTFZ3UUdLUEJ1SGd2RDg1ZUwzYlJURjRpN0M2aDNlNFhXN2cKLS0tIGUwc05o | ||
| 8 | MGFjSm9zREtVZXJWUE5wSmZ3MFFYbTVsMVpxNnV1NTYvUkFpMVUK8BWH7JgkFaRs | ||
| 9 | SDIU5APiPXldsjSFN3Ae7vEiugR5A91M9WIGzoZBerf7MXDq2EqOp/hkUcSns5Qv | ||
| 10 | dGEb/4nx5w== | ||
| 11 | -----END AGE ENCRYPTED FILE----- | ||
diff --git a/reveal/v5-000004/r34541812.ct b/reveal/v5-000004/r34541812.ct new file mode 100644 index 0000000..e94d924 --- /dev/null +++ b/reveal/v5-000004/r34541812.ct | |||
| Binary files differ | |||
diff --git a/reveal/v5-000004/r34541812.nostr.json b/reveal/v5-000004/r34541812.nostr.json new file mode 100644 index 0000000..618eb16 --- /dev/null +++ b/reveal/v5-000004/r34541812.nostr.json | |||
| @@ -0,0 +1 @@ | |||
| {"content":"-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDM0NTQxODEyIDUyZGI5YmE3\nMGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw\nMGM4NGU5NzEKbUs1MkdILzk3YkRzN3p1S0x0cmo5YnptRXNIN05PVjFRcm9ES0Uv\nNGlOOVBoL21BRjljZmdGR2YrbFFtM0IrKwpEL0dzcXFXR2NVWVZTTjZDMElJbXJm\nQkdXMWkxOU1qL24rNm1oc1dtbEVEbVRyUlJIUmk0eGlHaSt1cjhxeHVkClBCMGZk\naGY3bkh4b3JTVVV4cGtjNjlIaDBiZSt1YUNiUmJFQzdoam9WYnMKLS0tIHZraFpR\nUVM5VnF0VmVhN3k3OVNxVUVzQkF3a1RXczFSWC9FVGhodDhmM2MKxtV6Gr7sJqmI\nbRUKCeBpkNCnRbdE4TEjR4E5HphvFvPhfKyD2jFUxEwYyWyGr/046XtRJHZfr42o\nWnEmZGeEew==\n-----END AGE ENCRYPTED FILE-----\n","tags":[["t","echo-ledger-reveal"],["round","34541812"],["reveal_at","2026-12-05T00:00:00.000000Z"],["chain","52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"],["digest","71f3b8b4de94f03c6422fdf67d055535e79b0411595168d406729c17f62a988a"],["ct","c056fab6d09a1abf85c6c41fba220e0a5982e32f67cfb67a8e76e5b2a1c3aced"],["seal","0577329be46aad3dc6b6ba6588d255eb95d6137d7abd48f8ad8568134ff4437d"]]} | |||
diff --git a/reveal/v5-000004/r34541812.tlock b/reveal/v5-000004/r34541812.tlock new file mode 100644 index 0000000..1912b1d --- /dev/null +++ b/reveal/v5-000004/r34541812.tlock | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | -----BEGIN AGE ENCRYPTED FILE----- | ||
| 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDM0NTQxODEyIDUyZGI5YmE3 | ||
| 3 | MGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw | ||
| 4 | MGM4NGU5NzEKbUs1MkdILzk3YkRzN3p1S0x0cmo5YnptRXNIN05PVjFRcm9ES0Uv | ||
| 5 | NGlOOVBoL21BRjljZmdGR2YrbFFtM0IrKwpEL0dzcXFXR2NVWVZTTjZDMElJbXJm | ||
| 6 | QkdXMWkxOU1qL24rNm1oc1dtbEVEbVRyUlJIUmk0eGlHaSt1cjhxeHVkClBCMGZk | ||
| 7 | aGY3bkh4b3JTVVV4cGtjNjlIaDBiZSt1YUNiUmJFQzdoam9WYnMKLS0tIHZraFpR | ||
| 8 | UVM5VnF0VmVhN3k3OVNxVUVzQkF3a1RXczFSWC9FVGhodDhmM2MKxtV6Gr7sJqmI | ||
| 9 | bRUKCeBpkNCnRbdE4TEjR4E5HphvFvPhfKyD2jFUxEwYyWyGr/046XtRJHZfr42o | ||
| 10 | WnEmZGeEew== | ||
| 11 | -----END AGE ENCRYPTED FILE----- | ||
diff --git a/reveal/v5-000004/seal.json b/reveal/v5-000004/seal.json new file mode 100644 index 0000000..6d21747 --- /dev/null +++ b/reveal/v5-000004/seal.json | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | { | ||
| 2 | "algorithm": "sha256-live-v5", | ||
| 3 | "anchor_ref": "sha256:0577329be46aad3dc6b6ba6588d255eb95d6137d7abd48f8ad8568134ff4437d", | ||
| 4 | "covers_after": "2026-10-03T23:07:43.366835Z", | ||
| 5 | "covers_through": "2026-10-04T00:08:35.532031Z", | ||
| 6 | "digest": "0577329be46aad3dc6b6ba6588d255eb95d6137d7abd48f8ad8568134ff4437d", | ||
| 7 | "follows_seq": 34, | ||
| 8 | "n_forecasts": 24, | ||
| 9 | "n_groups": 2, | ||
| 10 | "n_withheld": 24, | ||
| 11 | "prev_digest": "baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19", | ||
| 12 | "prev_seq": 3, | ||
| 13 | "sealed_at": "2026-10-04T01:10:50.806839Z", | ||
| 14 | "seq": 4, | ||
| 15 | "variant": { | ||
| 16 | "beacon": { | ||
| 17 | "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", | ||
| 18 | "genesis_time": 1692803367, | ||
| 19 | "period": 3, | ||
| 20 | "relays": [ | ||
| 21 | "https://api.drand.sh", | ||
| 22 | "https://api2.drand.sh", | ||
| 23 | "https://api3.drand.sh", | ||
| 24 | "https://drand.cloudflare.com" | ||
| 25 | ], | ||
| 26 | "scheme": "bls-unchained-g1-rfc9380" | ||
| 27 | }, | ||
| 28 | "cipher": "chacha20poly1305-zlib6-nonce0", | ||
| 29 | "gate": { | ||
| 30 | "default": "withheld: a kept salt can be released later; an opened text can never be withdrawn", | ||
| 31 | "opened": "none", | ||
| 32 | "why": "no decision by Echo yet" | ||
| 33 | }, | ||
| 34 | "line": "numeric<TAB>open:<v4 line> | numeric<TAB>withheld:sha256(salt||v4 line)", | ||
| 35 | "reveal": "utc-day-after-close", | ||
| 36 | "salts": "key ring: one content a seal, one part a line", | ||
| 37 | "tool": { | ||
| 38 | "name": "tle", | ||
| 39 | "sha256": "69157d8c14a1d390d1b7744946288f83d4b10748471d5447f9abb77a5902c541", | ||
| 40 | "version": "tlock v1.2.0 -- github.com/drand/tlock" | ||
| 41 | } | ||
| 42 | } | ||
| 43 | } | ||