From 9333aefa19a55050cdd20fbeaed41efd42e5bacc Mon Sep 17 00:00:00 2001 From: log Date: Sun, 4 Oct 2026 01:11:02 +0000 Subject: size 48428 --- reveal/v5-000003/manifest.txt.ots | Bin 584 -> 2518 bytes reveal/v5-000003/seal.json | 2 +- reveal/v5-000004/check.py | 319 ++++++++++++++++++++++++++++++++++ reveal/v5-000004/manifest.txt | 8 + reveal/v5-000004/manifest.txt.ots | Bin 0 -> 759 bytes reveal/v5-000004/r33389812.ct | Bin 0 -> 1013 bytes reveal/v5-000004/r33389812.nostr.json | 1 + reveal/v5-000004/r33389812.tlock | 11 ++ reveal/v5-000004/r34541812.ct | Bin 0 -> 1016 bytes reveal/v5-000004/r34541812.nostr.json | 1 + reveal/v5-000004/r34541812.tlock | 11 ++ reveal/v5-000004/seal.json | 43 +++++ 12 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 reveal/v5-000004/check.py create mode 100644 reveal/v5-000004/manifest.txt create mode 100644 reveal/v5-000004/manifest.txt.ots create mode 100644 reveal/v5-000004/r33389812.ct create mode 100644 reveal/v5-000004/r33389812.nostr.json create mode 100644 reveal/v5-000004/r33389812.tlock create mode 100644 reveal/v5-000004/r34541812.ct create mode 100644 reveal/v5-000004/r34541812.nostr.json create mode 100644 reveal/v5-000004/r34541812.tlock create mode 100644 reveal/v5-000004/seal.json diff --git a/reveal/v5-000003/manifest.txt.ots b/reveal/v5-000003/manifest.txt.ots index 50d510b..f4798e5 100644 Binary files a/reveal/v5-000003/manifest.txt.ots and b/reveal/v5-000003/manifest.txt.ots 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 @@ { "algorithm": "sha256-live-v5", - "anchor_ref": "sha256:baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19", + "anchor_ref": "bitcoin:block:969776", "covers_after": "2026-10-03T22:17:20.526176Z", "covers_through": "2026-10-03T23:07:43.366835Z", "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 @@ +#!/usr/bin/env python3 +""" +Check Echo's self-revealing ledger seals (`sha256-live-v5`) without trusting Echo. + + python3 check.py SEAL_DIR [SEAL_DIR ...] [--tle PATH] [--offline] + +Needs Python 3.11+, the `cryptography` package (ChaCha20-Poly1305) and drand's `tle` binary +(`go install github.com/drand/tlock/cmd/tle@v1.2.0`). It imports nothing of Echo's: a checker that needs the thing it +checks is not a checker, which is why it is copied, whole, into every seal directory. + +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), 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); + 2. each group's ciphertext and locked key hash to what the manifest commits, and the key is locked to the + manifest's round of the manifest's chain; + 3. for every group whose round the beacon has signed: the round's signature as each relay serves it, the key + unlocked with it, the group decrypted, its bytes hashed to the committed digest, its lines and withheld lines + 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. 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, 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 + +import argparse +import base64 +import hashlib +import json +import re +import subprocess +import sys +import urllib.request +import zlib +from datetime import datetime, timedelta, timezone +from pathlib import Path + + +def sha(b: bytes) -> str: + return hashlib.sha256(b).hexdigest() + + +def parse_manifest(text: str) -> dict: + lines = text.split("\n") + if lines[0] != "sha256-live-v5": + raise ValueError(f"not a v5 manifest: {lines[0]!r}") + m = {"groups": []} + for ln in lines[1:]: + word, _, rest = ln.partition(" ") + if word == "prev": + m["prev"] = None if rest == "-" else rest + elif word == "follows": + seq, digest, n = rest.split(" ") + m["follows"] = {"seq": int(seq), "digest": digest, "n": int(n)} + elif word == "covers": + m["covers"] = rest.split(" ") + elif word == "variant": + m["variant"] = json.loads(rest) + elif word == "counts": + m["n"], m["n_withheld"], m["n_groups"] = map(int, rest.split(" ")) + elif word == "group": + r, at, n, w, digest, ct, key, late = rest.split(" ") + m["groups"].append({"round": int(r), "reveal_at": at, "n": int(n), "n_withheld": int(w), + "digest": digest, "ct": ct, "key": key, "late": late == "1"}) + else: + raise ValueError(f"unknown manifest line {ln!r}") + return m + + +def stanza(locked: str) -> tuple[int, str] | None: + body = "".join(ln for ln in locked.strip().splitlines() if not ln.startswith("-----")) + m = re.search(rb"-> tlock (\d+) ([0-9a-f]{64})", base64.b64decode(body)) + return (int(m.group(1)), m.group(2).decode()) if m else None + + +def round_at(beacon: dict, at: datetime) -> int: + t = at.timestamp() + whole = int(t) + (t != int(t)) + if whole <= beacon["genesis_time"]: + return 1 + return -(-(whole - beacon["genesis_time"]) // beacon["period"]) + 1 + + +def reveal_day(closes: datetime) -> datetime: + d = closes.astimezone(timezone.utc).date() + timedelta(days=1) + return datetime(d.year, d.month, d.day, tzinfo=timezone.utc) + + +def parse_line(ln: str) -> dict: + """One line: `id|issued_at|deadline|forecaster|p|settlement|anchoropen:` or `...withheld:`.""" + numeric, tab, text = ln.partition("\t") + if not tab: + raise ValueError("a line without its text layer") + fid, issued, deadline, forecaster, p, rest = numeric.split("|", 5) + dec = json.JSONDecoder() + settle, end = dec.raw_decode(rest) + if rest[end] != "|": + raise ValueError("settlement not followed by the anchor") + anchor, end2 = dec.raw_decode(rest, end + 1) + if end2 != len(rest): + raise ValueError("trailing bytes after the anchor") + out = {"id": int(fid), "issued_at": issued, "deadline": deadline, "forecaster": forecaster, "p": float(p), + "settlement": settle, "anchor": anchor} + if text.startswith("open:"): + out["v4"] = text[5:] + if not out["v4"].startswith(f"{fid}|"): + raise ValueError("the text layer is another forecast's line") + elif re.fullmatch(r"withheld:[0-9a-f]{64}", text): + out["withheld"] = text[9:] + else: + raise ValueError(f"unknown text layer {text[:20]!r}") + return out + + +def beacon_round(beacon: dict, rnd: int) -> dict: + """What each relay serves for the round: its signature, or why there is none.""" + out = {} + for relay in beacon["relays"]: + url = f"{relay.rstrip('/')}/{beacon['chain_hash']}/public/{rnd}" + try: + with urllib.request.urlopen(url, timeout=20) as r: # noqa: S310 — the seal's own relays + out[relay] = json.loads(r.read()).get("signature") + except Exception as exc: # noqa: BLE001 — report, go on + out[relay] = f"unavailable ({type(exc).__name__})" + return out + + +def unlock(tle: str, beacon: dict, locked: str) -> bytes | None: + """The key, or None while the round is unsigned. Each relay is tried in turn.""" + errors = [] + for relay in beacon["relays"]: + r = subprocess.run([tle, "-d", "-n", relay.rstrip("/") + "/", "-c", beacon["chain_hash"]], + input=locked.encode(), capture_output=True, timeout=120, check=False) + if r.returncode == 0: + return r.stdout + if b"too early" in r.stderr: + return None + errors.append(f"{relay}: {r.stderr.decode(errors='replace').strip()[-160:]}") + 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, held, erased, union = 0, 0, 0, 0, 0, {} + reasons: set[str] = set() + first_prev = "unset" + + def say(ok: bool, what: str) -> None: + nonlocal failed + failed += not ok + print(f" {'PASS' if ok else 'FAIL'} {what}") + + prev_digest = None + seals = [] + for d in dirs: + text = (d / "manifest.txt").read_bytes() + row = json.loads((d / "seal.json").read_text()) + seals.append((d, text, row, parse_manifest(text.decode()))) + seals.sort(key=lambda s: s[2]["seq"]) + last = None + for i, (d, text, row, m) in enumerate(seals): + print(f"v5 seal {row['seq']}: {m['n']} forecasts in {m['n_groups']} reveal groups, {m['n_withheld']} withheld, " + f"follows v4 seal {m['follows']['seq']}") + say(sha(text) == row["digest"], f"sha256(manifest.txt) is the seal's digest {row['digest'][:16]}…") + say(len(m["groups"]) == m["n_groups"] and sum(g["n"] for g in m["groups"]) == m["n"] + and sum(g["n_withheld"] for g in m["groups"]) == m["n_withheld"], "the manifest's counts add up") + if i == 0: + first_prev = m["prev"] + else: + say(m["prev"] == prev_digest, "it names the seal before it") + prev_digest, last = row["digest"], m + b = m["variant"]["beacon"] + for g in m["groups"]: + r = g["round"] + ct, locked = (d / f"r{r}.ct").read_bytes(), (d / f"r{r}.tlock").read_text() + say(sha(ct) == g["ct"] and sha(locked.encode()) == g["key"], + f"round {r}: the ciphertext and the locked key are the committed ones") + say(stanza(locked) == (r, b["chain_hash"]), f"round {r}: the key is locked to that round of that chain") + say(round_at(b, datetime.fromisoformat(g["reveal_at"])) == r, f"round {r} is signed at {g['reveal_at']}") + if not offline: + sigs = beacon_round(b, r) + good = {s for s in sigs.values() if s and not s.startswith("unavailable")} + print(f" .... round {r} signature from {len(sigs)} relays: " + + ("not yet signed" if not good else f"{len(good)} distinct: {min(good)[:24]}…")) + if len(good) > 1: + say(False, f"round {r}: relays disagree on its signature") + key = unlock(tle, b, locked) + if key is None: + pending += 1 + print(f" .... round {r} ({g['reveal_at']}) not yet signed: {g['n']} forecasts stay locked") + continue + try: + plain = zlib.decompress(ChaCha20Poly1305(key).decrypt(b"\x00" * 12, ct, None)) + except Exception as exc: # noqa: BLE001 — a finding, not a crash + say(False, f"round {r}: the ciphertext does not open under its key ({type(exc).__name__})") + continue + say(sha(plain) == g["digest"], f"round {r}: the opened group hashes to its committed digest") + lines = [parse_line(ln) for ln in plain.decode().split("\n")] + ids = [x["id"] for x in lines] + say(len(lines) == g["n"] and sum("withheld" in x for x in lines) == g["n_withheld"], + f"round {r}: {len(lines)} lines, {g['n_withheld']} withheld, as committed") + say(ids == sorted(set(ids)), f"round {r}: lines in id order, none twice") + due = datetime.fromisoformat(g["reveal_at"]) + if m["variant"].get("reveal") == "utc-day-after-close": + wrong = [x["id"] for x in lines if reveal_day(datetime.fromisoformat( + (x["settlement"] or {}).get("settles_at") or x["deadline"])) != due] + say(not wrong, f"round {r}: every line belongs to this reveal day" + + (f" ({len(wrong)} do not, e.g. {wrong[:3]})" if wrong else "")) + 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") 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 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"], + f"the union of {len(union)} lines rehashes to v4 seal {last['follows']['seq']}'s digest") + print("\nNOT VERIFIED" if failed else f"\nVERIFIED{f' ({pending} groups not yet revealed)' if pending else ''}") + return 1 if failed else 0 + + +def main(argv: list[str]) -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("dirs", nargs="*", default=[str(Path(__file__).resolve().parent)], + help="seal directories, any order (default: the one this script sits in)") + ap.add_argument("--tle", default="tle", help="the tle binary that unlocks a key once its round is signed") + ap.add_argument("--offline", action="store_true", + help="do not fetch each round's signature from the relays for display; tle still asks them") + args = ap.parse_args(argv) + return check([Path(d) for d in args.dirs], tle=args.tle, offline=args.offline) + + +if __name__ == "__main__": + 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 @@ +sha256-live-v5 +prev baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19 +follows 34 737e4a1b4b7b4a23f087f974f2f3d9865211a73b0990837791b7a44b71bc3cfa 39128 +covers 2026-10-03T23:07:43.366835Z 2026-10-04T00:08:35.532031Z +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":"numericopen: | numericwithheld: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"}} +counts 24 24 2 +group 33389812 2026-10-26T00:00:00.000000Z 12 12 c7218af247b604a2dcf44cc788ce5e7180b492a91b0005dd7dbeea118ffeee33 a79f9f6b909b5ce75081420bc9e6dcfc45b4745c6751ef7873172781163b5d47 7667b76cdbbfbceab379de408ef27235a251dec95b736dbe062ff18354638134 0 +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 Binary files /dev/null and b/reveal/v5-000004/manifest.txt.ots differ diff --git a/reveal/v5-000004/r33389812.ct b/reveal/v5-000004/r33389812.ct new file mode 100644 index 0000000..b59d533 Binary files /dev/null and b/reveal/v5-000004/r33389812.ct 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 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDMzMzg5ODEyIDUyZGI5YmE3 +MGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw +MGM4NGU5NzEKc1BqWmV3WTlqUU4xdE9vZXFkb3dtNE92cTNUcllEM2NTWXVxeGQ2 +RndObEhrZ2VSZ3lYSGFhSzh2NnVSNERyTwpGd1dmNjVna1FWdWduUWpDQ29UMDUr +Mm96NGRrUlpDNDFoY1RlL0ZrdXp1WDdQVHBlL0t1YXJiVDVjd2ZCY1I2ClBwanRK +VzhOTFZ3UUdLUEJ1SGd2RDg1ZUwzYlJURjRpN0M2aDNlNFhXN2cKLS0tIGUwc05o +MGFjSm9zREtVZXJWUE5wSmZ3MFFYbTVsMVpxNnV1NTYvUkFpMVUK8BWH7JgkFaRs +SDIU5APiPXldsjSFN3Ae7vEiugR5A91M9WIGzoZBerf7MXDq2EqOp/hkUcSns5Qv +dGEb/4nx5w== +-----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 Binary files /dev/null and b/reveal/v5-000004/r34541812.ct 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 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDM0NTQxODEyIDUyZGI5YmE3 +MGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw +MGM4NGU5NzEKbUs1MkdILzk3YkRzN3p1S0x0cmo5YnptRXNIN05PVjFRcm9ES0Uv +NGlOOVBoL21BRjljZmdGR2YrbFFtM0IrKwpEL0dzcXFXR2NVWVZTTjZDMElJbXJm +QkdXMWkxOU1qL24rNm1oc1dtbEVEbVRyUlJIUmk0eGlHaSt1cjhxeHVkClBCMGZk +aGY3bkh4b3JTVVV4cGtjNjlIaDBiZSt1YUNiUmJFQzdoam9WYnMKLS0tIHZraFpR +UVM5VnF0VmVhN3k3OVNxVUVzQkF3a1RXczFSWC9FVGhodDhmM2MKxtV6Gr7sJqmI +bRUKCeBpkNCnRbdE4TEjR4E5HphvFvPhfKyD2jFUxEwYyWyGr/046XtRJHZfr42o +WnEmZGeEew== +-----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 @@ +{ + "algorithm": "sha256-live-v5", + "anchor_ref": "sha256:0577329be46aad3dc6b6ba6588d255eb95d6137d7abd48f8ad8568134ff4437d", + "covers_after": "2026-10-03T23:07:43.366835Z", + "covers_through": "2026-10-04T00:08:35.532031Z", + "digest": "0577329be46aad3dc6b6ba6588d255eb95d6137d7abd48f8ad8568134ff4437d", + "follows_seq": 34, + "n_forecasts": 24, + "n_groups": 2, + "n_withheld": 24, + "prev_digest": "baba31b4d3ca99d52c6204fceab089efc398aa3c152bc7305e9cd3ea83997c19", + "prev_seq": 3, + "sealed_at": "2026-10-04T01:10:50.806839Z", + "seq": 4, + "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": "numericopen: | numericwithheld: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" + } + } +} -- cgit v1.2.3