upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlog <log@invalid>2026-10-03 20:31:28 +0000
committerlog <log@invalid>2026-10-03 20:31:28 +0000
commit75aeda8d2167a29e26c17bc76cf6b094187f0c53 (patch)
treedf4ca665df281daee7aa85e52609dc7632880bad
parent20d6dcb461c763036c75bb2b1c0c6e1c94171c37 (diff)
size 45286
-rw-r--r--reveal/v5-000001/check.py92
-rw-r--r--reveal/v5-000001/manifest.txt.otsbin689 -> 2621 bytes
-rw-r--r--reveal/v5-000001/seal.json2
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
10 10
11Each SEAL_DIR is one seal: `manifest.txt` (the exact text the seal's digest is the sha256 of), `seal.json`, and per 11Each SEAL_DIR is one seal: `manifest.txt` (the exact text the seal's digest is the sha256 of), `seal.json`, and per
12reveal group `r<ROUND>.tlock` (the group's key, time-locked to that drand round) and `r<ROUND>.ct` (the group, 12reveal group `r<ROUND>.tlock` (the group's key, time-locked to that drand round) and `r<ROUND>.ct` (the group,
13encrypted under that key). `manifest.txt.ots`, when present, is an OpenTimestamps proof over the manifest: 13encrypted under that key), and, once the group has been revealed, `r<ROUND>.held.json` (how many of its withheld
14`ots verify manifest.txt.ots` checks it against Bitcoin. 14lines Echo publishes no salt for, and why) and any number of `r<ROUND>.salts.<N>.json`: each the salts of withheld
15lines Echo decided to open and had not erased, each with its line. `manifest.txt.ots`, when present, is an
16OpenTimestamps proof over the manifest: `ots verify manifest.txt.ots` checks it against Bitcoin.
15 17
16What it checks, from the files alone and the beacon: 18What it checks, from the files alone and the beacon:
17 1. the manifest hashes to the seal's digest, and each seal names the one before it (when several are given); 19 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:
22 counted against the committed counts, its lines in id order, and every line's reveal day recomputed from its 24 counted against the committed counts, its lines in id order, and every line's reveal day recomputed from its
23 own settling instant under the seal's named rule (`utc-day-after-close`: the first 00:00 UTC after 25 own settling instant under the seal's named rule (`utc-day-after-close`: the first 00:00 UTC after
24 `settles_at`, else after the deadline); 26 `settles_at`, else after the deadline);
25 4. once every group of every seal from the first is open and nothing is withheld: the union of their v4 lines, 27 4. for every opened group with releases: each names this seal, round and group and its own number; each released
26 sorted by id, rehashes to the v4 digest the last seal follows, so no sealed forecast was left out of a reveal. 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.
27 35
28A group whose round is not yet signed is reported as pending, not as a failure. Exit 0 when everything checkable 36A group whose round is not yet signed, or whose withheld lines have no release yet, is reported as pending or held,
29passed, 1 when anything did not match. 37not as a failure. Exit 0 when everything checkable passed, 1 when anything did not match.
30""" 38"""
31from __future__ import annotations 39from __future__ import annotations
32 40
@@ -146,10 +154,56 @@ def unlock(tle: str, beacon: dict, locked: str) -> bytes | None:
146 raise RuntimeError("; ".join(errors)) 154 raise RuntimeError("; ".join(errors))
147 155
148 156
157def 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
149def check(dirs: list[Path], tle: str = "tle", offline: bool = False) -> int: 202def check(dirs: list[Path], tle: str = "tle", offline: bool = False) -> int:
150 from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 203 from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
151 204
152 failed, pending, withheld, union = 0, 0, 0, {} 205 failed, pending, withheld, held, erased, union = 0, 0, 0, 0, 0, {}
206 reasons: set[str] = set()
153 first_prev = "unset" 207 first_prev = "unset"
154 208
155 def say(ok: bool, what: str) -> None: 209 def say(ok: bool, what: str) -> None:
@@ -216,20 +270,32 @@ def check(dirs: list[Path], tle: str = "tle", offline: bool = False) -> int:
216 else: 270 else:
217 print(f" .... round {r}: reveal rule {m['variant'].get('reveal')!r} is not one this checker " 271 print(f" .... round {r}: reveal rule {m['variant'].get('reveal')!r} is not one this checker "
218 "recomputes; which lines belong to the day is unchecked") 272 "recomputes; which lines belong to the day is unchecked")
273 opened, gone, why = release(d, row["digest"], g, lines, say)
219 for x in lines: 274 for x in lines:
220 if x["id"] in union: 275 if x["id"] in union:
221 say(False, f"forecast {x['id']} is in two groups") 276 say(False, f"forecast {x['id']} is in two groups")
222 union[x["id"]] = x.get("v4") 277 union[x["id"]] = x.get("v4") or opened.get(x["id"])
223 withheld += "withheld" in x 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)
224 if last is not None: 285 if last is not None:
225 if first_prev is not None: 286 if first_prev is not None:
226 print(" .... completeness: the first seal given is not the chain's first; give every seal from it") 287 print(" .... completeness: the first seal given is not the chain's first; give every seal from it")
227 elif pending: 288 elif pending:
228 print(f" .... completeness: {pending} groups still locked; it is checkable once all have opened") 289 print(f" .... completeness: {pending} groups still locked; it is checkable once all have opened")
229 elif withheld: 290 elif withheld or held or erased:
230 print(f" .... completeness: {withheld} text layers are withheld, so the v4 lines cannot all be rebuilt;" 291 told = [f"{withheld} withheld lines not yet released"] if withheld else []
231 f" {len(union)} forecasts against the v4 seal's {last['follows']['n']}") 292 told += ([f"{held} withheld lines held, for which Echo publishes no salt ({'; '.join(sorted(reasons))})"]
232 say(len(union) == last["follows"]["n"], "every forecast the v4 seal covers is in a group") 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")
233 else: 299 else:
234 got = sha("\n".join(union[k] for k in sorted(union)).encode()) 300 got = sha("\n".join(union[k] for k in sorted(union)).encode())
235 say(got == last["follows"]["digest"] and len(union) == last["follows"]["n"], 301 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
--- a/reveal/v5-000001/manifest.txt.ots
+++ b/reveal/v5-000001/manifest.txt.ots
Binary files 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 @@
1{ 1{
2 "algorithm": "sha256-live-v5", 2 "algorithm": "sha256-live-v5",
3 "anchor_ref": "sha256:ab1c64ee208cedf8ce14e1e4a1568de02e03578c9af283db73392ad2e61c90e8", 3 "anchor_ref": "bitcoin:block:969687",
4 "covers_after": null, 4 "covers_after": null,
5 "covers_through": "2026-10-03T01:35:21.431130Z", 5 "covers_through": "2026-10-03T01:35:21.431130Z",
6 "digest": "ab1c64ee208cedf8ce14e1e4a1568de02e03578c9af283db73392ad2e61c90e8", 6 "digest": "ab1c64ee208cedf8ce14e1e4a1568de02e03578c9af283db73392ad2e61c90e8",