#!/usr/bin/env python3 """Verify the signed Eddycoin emission spec (Kompromis-120b, planned schedule for the new chain). Python 3.8+, standard library only (Ed25519 verification per RFC 8032, pure Python). python3 verify_emission.py # download from the Eddycoin node and verify python3 verify_emission.py --dir ./emission # verify local copies (json, .sig, .sha256, emission-signing.pub) Checks: (1) the public key matches the pinned fingerprint, (2) SHA-256 of the JSON matches the .sha256 file, (3) the detached Ed25519 signature is valid, (4) the whole integer schedule recomputed from the consensus constants equals the signed tables, sums to exactly the cap, and yearly issuance falls every year. Exit code 0 = all checks passed.""" import hashlib, json, sys, os, urllib.request URL = "https://129-159-188-73.sslip.io/emission/" NAME = "kompromis-120b.json" PINNED_KEY_SHA256 = "b62ce8abf98a9c3d2416bee74766ed4d847afb5f3a9cd999bf4f07c4f3d591a5" # ---- Ed25519 verify (RFC 8032 section 5.1.7) ---- p = 2**255 - 19; L = 2**252 + 27742317777372353535851937790883648493 d = -121665 * pow(121666, p - 2, p) % p; I = pow(2, (p - 1) // 4, p) def _add(P, Q): x1, y1, z1, t1 = P; x2, y2, z2, t2 = Q a = (y1 - x1) * (y2 - x2) % p; b = (y1 + x1) * (y2 + x2) % p; c = 2 * t1 * t2 * d % p; dd = 2 * z1 * z2 % p e, f, g, h = b - a, dd - c, dd + c, b + a return (e * f % p, g * h % p, f * g % p, e * h % p) def _mul(s, P): Q = (0, 1, 1, 0) while s: if s & 1: Q = _add(Q, P) P = _add(P, P); s >>= 1 return Q def _eq(P, Q): return (P[0] * Q[2] - Q[0] * P[2]) % p == 0 and (P[1] * Q[2] - Q[1] * P[2]) % p == 0 def _xrec(y, sign): if y >= p: return None x2 = (y * y - 1) * pow(d * y * y + 1, p - 2, p) % p if x2 == 0: return None if sign else 0 x = pow(x2, (p + 3) // 8, p) if (x * x - x2) % p: x = x * I % p if (x * x - x2) % p: return None if (x & 1) != sign: x = p - x return x def _dec(b): y = int.from_bytes(b, "little"); sign = y >> 255; y &= (1 << 255) - 1; x = _xrec(y, sign) return None if x is None else (x, y, 1, x * y % p) _gy = 4 * pow(5, p - 2, p) % p; G = (_xrec(_gy, 0), _gy, 1, _xrec(_gy, 0) * _gy % p) def ed25519_verify(pub, msg, sig): if len(pub) != 32 or len(sig) != 64: return False A = _dec(pub); R = _dec(sig[:32]); s = int.from_bytes(sig[32:], "little") if A is None or R is None or s >= L: return False h = int.from_bytes(hashlib.sha512(sig[:32] + pub + msg).digest(), "little") % L return _eq(_mul(s, G), _add(R, _mul(h, A))) # ---- schedule recomputation ---- def check_schedule(j): c = j["consensus"]; N, E, Q, SH, C, F = c["epochs"], c["epoch_length"], int(c["Q"]), c["shift"], int(c["C"]), int(c["F0"]) CAP, CPY = int(j["cap_units"]), j["checkpoints_per_year"]; s = [] for k in range(N): m = 2 * N - 2 * k - 1; x = (F + C) * m * m // (4 * N * N); x -= x & 1; s.append(x); F = F * Q >> SH pre = [0] for x in s: pre.append(pre[-1] + x * E) END = N * E; D = CAP - pre[-1] assert END == j["end_checkpoint"] and END == j["years"] * CPY, "end checkpoint" assert D == int(c["final_remainder_units"]) and D >= 0 and D % 2 == 0, "final remainder" assert s[0] == int(c["initial_reward_units"]) and s[0] // 2 == int(c["initial_reward_per_side_units"]), "initial reward" assert s[-1] + D == int(c["final_reward_units"]), "final reward" assert all(x % 2 == 0 for x in s), "rewards must be even (exact 50/50 split)" def cum(h): if h <= 0: return 0 if h >= END: return CAP k = (h - 1) // E; return pre[k] + (h - k * E) * s[k] annual = [cum(y * CPY) - cum((y - 1) * CPY) for y in range(1, j["years"] + 1)] assert [str(a) for a in annual] == j["annual_issuance_units"], "annual table" assert sum(annual) == CAP and int(j["premine_units"]) == 0, "cap / premine" assert all(annual[i] > annual[i + 1] for i in range(len(annual) - 1)), "issuance must fall every year" for m in j["milestones"]: assert str(cum(m["checkpoint"])) == m["cumulative_units"] and m["share_of_cap_ppm"] == cum(m["checkpoint"]) * 10**6 // CAP, "milestone %s" % m["year"] return annual def main(): a = sys.argv[1:] if "--dir" in a: base = a[a.index("--dir") + 1]; rd = lambda f: open(os.path.join(base, f), "rb").read() else: rd = lambda f: urllib.request.urlopen(URL + f, timeout=30).read() blob, sig, pub, sha = rd(NAME), bytes.fromhex(rd(NAME + ".sig").decode().strip()), bytes.fromhex(rd("emission-signing.pub").decode().strip()), rd(NAME + ".sha256").decode() fp = hashlib.sha256(pub).hexdigest(); h = hashlib.sha256(blob).hexdigest() print("key fingerprint (sha256 of raw key):", fp); print("spec sha256:", h) ok = True if fp != PINNED_KEY_SHA256: print("FAIL: signing key does not match the pinned fingerprint"); ok = False if sha.split()[0].lower() != h: print("FAIL: .sha256 file does not match"); ok = False if not ed25519_verify(pub, blob, sig): print("FAIL: Ed25519 signature invalid"); ok = False else: print("OK: Ed25519 signature valid") j = json.loads(blob) try: annual = check_schedule(j); print("OK: schedule recomputed from constants, sum = cap, %d years, falls every year" % len(annual)) except AssertionError as e: print("FAIL: schedule check:", e); ok = False for m in j["milestones"]: print(" year %3d %15.2f EDDY %8.4f %%" % (m["year"], int(m["cumulative_units"]) / 1e8, m["share_of_cap_ppm"] / 1e4)) print("ALL CHECKS PASSED" if ok else "VERIFICATION FAILED"); sys.exit(0 if ok else 1) if __name__ == "__main__": main()