#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Eddycoin (EDDY) Y-Chain verifier - SANDBOX / TESTNET ONLY ========================================================== Checks Eddycoin miner result files and/or a chain export from the demo page, fully offline (Python 3.8+ standard library only, no network access, nothing is sent anywhere). For every miner results file (eddycoin_echain_results.json / eddycoin_dchain_results.json): * file checksum (SHA-256 over the canonical JSON) - detects any edit * every block: hash recomputed from its header, hash meets the difficulty target * prev-hash links and heights inside the file Then it pairs E-Chain and D-Chain blocks that commit to the same Y candidate id and builds Y checkpoints: checkpoint id = SHA-256("EDDY-CHECKPOINT-v1" | prev checkpoint | Y state root | E evidence | D evidence) Rewards are created only through Y checkpoints under the fixed split (10.00 EDDY: 50 % E lane, 50 % D lane) and the issuance cap of 900,000,000 EDDY is enforced. For a chain export (eddycoin_chain_export.json from the demo's "Export chain (JSON)" button) it replays the whole E/D/Y history: proof-of-work, links, checkpoint ids, Y candidate ids, ECDSA P-256 signatures, UTXO double-spend checks, value conservation, state roots, issuance cap and registry consistency. Usage: py eddycoin_ychain_verifier.py eddycoin_echain_results.json eddycoin_dchain_results.json py eddycoin_ychain_verifier.py --chain-export eddycoin_chain_export.json (no arguments = use the default file names found in the current folder) Output: a PASS/FAIL report on screen and eddycoin_ychain_verified.json (importable in the demo). Exit code 0 = everything PASS, 1 = something FAILED. """ import argparse, hashlib, json, os, re, sys, time NETWORK = "eddycoin-testnet-sandbox" ZERO = "0" * 64 DEC, COIN = 8, 100000000 # 1 EDDY = 100,000,000 minor units MAX_SUPPLY = 900000000 * COIN # 900,000,000.00000000 EDDY # emission: R0 per checkpoint for EPOCH checkpoints, then *NUM//DEN per epoch; cumulative clamped at MAX_SUPPLY (~year 100) EM_R0, EM_EPOCH, EM_NUM, EM_DEN = 14985005307, 10000, 998335, 1000000 SUBSIDY = EM_R0 E_SPLIT_BPS = 5000 GENESIS_FAUCET = 1000000 * COIN MIN_FEE = 1000000 # 0.01 EDDY MIN_BITS = 8 IMPORT_MIN_BITS = 16 D_CP, D_CAND, D_STATE, D_TXROOT, D_TX, D_SIG, D_ADDR = ("EDDY-CHECKPOINT-v1", "EDDY-Y-CANDIDATE|v1", "EDDY-Y-STATE|v1", "EDDY-TXROOT|v1", "EDDY-TX|v1", "EDDY-TX-SIG|v1", "EDDY-ADDR|v1") HEX64 = re.compile(r"^[0-9a-f]{64}$") def canon(o): return json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=False) def sha(s): return hashlib.sha256(s.encode("utf-8")).hexdigest() def is_int(n): return isinstance(n, int) and not isinstance(n, bool) and abs(n) <= 2 ** 53 - 1 # amounts (v2): decimal strings of integer minor units in all data; Python ints (arbitrary precision) in arithmetic AMT_RE = re.compile(r"^(0|[1-9][0-9]{0,16})$") def is_amt(x): return isinstance(x, str) and bool(AMT_RE.match(x)) and int(x) <= MAX_SUPPLY def amt_int(x): """amount -> int (accepts decimal string or int)""" if isinstance(x, bool): raise ValueError("bad amount") if isinstance(x, int): return x if isinstance(x, str) and re.match(r"^-?[0-9]{1,24}$", x): return int(x) if x is None: return 0 raise ValueError("bad amount") def lzb(h): n = 0 for c in h: v = int(c, 16) if v == 0: n += 4 continue n += 4 - v.bit_length() break return n def checksum_of(o): d = dict(o) d.pop("checksum", None) return sha(canon(d)) def fmt(m, dec=DEC): m = amt_int(m) s = str(abs(m)).rjust(dec + 1, "0") return ("-" if m < 0 else "") + "{:,}".format(int(s[:-dec])) + "." + s[-dec:] _EM = None def _em_table(): global _EM if _EM is None: r, c, x, cum = [], [], EM_R0, 0 while x > 0 and cum < MAX_SUPPLY: r.append(x); c.append(cum); cum += x * EM_EPOCH; x = x * EM_NUM // EM_DEN _EM = (r, c) return _EM def emission_cum(h): """total emission of checkpoints 1..h, clamped at the cap (identical to eddy-core.js emissionCum)""" if h <= 0: return 0 r, c = _em_table() k = (h - 1) // EM_EPOCH if k >= len(r): return MAX_SUPPLY return min(MAX_SUPPLY, c[k] + (h - k * EM_EPOCH) * r[k]) def emission_reward(h): return 0 if h < 1 else emission_cum(h) - emission_cum(h - 1) # ---------------- ECDSA P-256 verification (pure Python, for transaction signatures) ---------------- P = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff A = P - 3 B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5) def _add(p, q): if p is None: return q if q is None: return p if p[0] == q[0] and (p[1] + q[1]) % P == 0: return None if p == q: l = (3 * p[0] * p[0] + A) * pow(2 * p[1], -1, P) % P else: l = (q[1] - p[1]) * pow(q[0] - p[0], -1, P) % P x = (l * l - p[0] - q[0]) % P return (x, (l * (p[0] - x) - p[1]) % P) def _mul(k, p): r = None while k: if k & 1: r = _add(r, p) p = _add(p, p) k >>= 1 return r def ecdsa_verify(pub_hex, sig_hex, msg): try: pb = bytes.fromhex(pub_hex) sb = bytes.fromhex(sig_hex) if len(pb) != 65 or pb[0] != 4 or len(sb) != 64: return False Q = (int.from_bytes(pb[1:33], "big"), int.from_bytes(pb[33:], "big")) if (Q[1] * Q[1] - (Q[0] ** 3 + A * Q[0] + B)) % P: return False r, s = int.from_bytes(sb[:32], "big"), int.from_bytes(sb[32:], "big") if not (1 <= r < N and 1 <= s < N): return False e = int.from_bytes(hashlib.sha256(msg.encode("utf-8")).digest(), "big") w = pow(s, -1, N) X = _add(_mul(e * w % N, G), _mul(r * w % N, Q)) return X is not None and X[0] % N == r except Exception: return False # ---------------- protocol functions (mirror eddy-core.js) ---------------- def block_header(b): return "EDDY-%s-BLOCK|v1|%s|%s|%s|%s|%s|%s|%s" % (b["chain"], b["height"], b["prev"], b["y_candidate"], b["miner"], b["time"], b["bits"], b["nonce"]) def block_hash(b): # no "alg" = single SHA-256 (server / in-browser lanes); "sha256d" = double SHA-256 (real-compute miners) if b.get("alg") == "sha256d": return hashlib.sha256(hashlib.sha256(block_header(b).encode("utf-8")).digest()).hexdigest() return sha(block_header(b)) def check_block(b, chain, min_bits=MIN_BITS): try: if b.get("chain") != chain: return "wrong chain" if not all(is_int(b.get(k)) for k in ("height", "nonce", "bits", "time")) or b["height"] < 0 or b["nonce"] < 0: return "bad fields" if not (b["prev"] == ZERO or HEX64.match(b["prev"])) or not HEX64.match(b["y_candidate"]) or not HEX64.match(b["hash"]): return "bad hash fields" if not isinstance(b["miner"], str) or len(b["miner"]) > 64: return "bad miner" if b.get("alg") not in (None, "sha256d"): return "unknown hash algorithm" if b["bits"] < min_bits or b["bits"] > 40: return "difficulty %d below minimum %d" % (b["bits"], min_bits) if block_hash(b) != b["hash"]: return "hash mismatch" if lzb(b["hash"]) < b["bits"]: return "hash does not meet target" except Exception as ex: return "malformed block (%s)" % ex return None def address_of(pub): return "eddy1" + sha(D_ADDR + "|" + pub)[:40] def txid_of(t): return sha(D_TX + "|" + canon({k: t.get(k) for k in ("v", "type", "inputs", "outputs", "fee", "time", "memo", "pubkey")})) def validate_transfer(utxos, t, check_sig=True): if not isinstance(t, dict) or t.get("v") != 2 or t.get("type") != "transfer": return "unsupported tx version/type (v2 = string amounts)" ins, outs = t.get("inputs"), t.get("outputs") if not isinstance(ins, list) or not isinstance(outs, list) or not 1 <= len(ins) <= 50 or not 1 <= len(outs) <= 4: return "bad input/output count" if not isinstance(t.get("memo"), str) or len(t["memo"]) > 80 or not is_int(t.get("time")) or not re.match(r"^04[0-9a-f]{128}$", str(t.get("pubkey"))): return "bad serialization" if txid_of(t) != t.get("txid"): return "txid mismatch" if check_sig and not ecdsa_verify(t["pubkey"], t.get("sig", ""), D_SIG + "|" + t["txid"]): return "invalid signature" owner = address_of(t["pubkey"]) seen, sin, sout = set(), 0, 0 for i in ins: k = "%s:%s" % (i["txid"], i["vout"]) if k in seen: return "duplicate input" seen.add(k) u = utxos.get(k) if not u: return "input missing or already spent (double spend)" if u["address"] != owner: return "input not owned by signer" sin += amt_int(u["amount"]) for o in outs: if not is_amt(o.get("amount")) or int(o["amount"]) <= 0: return "output amount must be a positive decimal string" if not re.match(r"^eddy1[0-9a-f]{40}$", str(o.get("address"))): return "bad output address" sout += int(o["amount"]) if not is_amt(t.get("fee")) or int(t["fee"]) < MIN_FEE: return "fee below minimum (or not a decimal string)" if sin != sout + int(t["fee"]): return "value not conserved (in %d != out %d + fee %s)" % (sin, sout, t["fee"]) return None def apply_tx(utxos, t): for i in t["inputs"]: utxos.pop("%s:%s" % (i["txid"], i["vout"]), None) for n, o in enumerate(t["outputs"]): utxos["%s:%d" % (t["txid"], n)] = {"address": o["address"], "amount": str(amt_int(o["amount"]))} def subsidy_at(h, issued): return max(0, min(emission_reward(h), MAX_SUPPLY - amt_int(issued))) def reward_split(sub, fees): """50/50; odd minor units go to the E miner""" sub, fees = amt_int(sub), amt_int(fees) d = sub // 2 + fees // 2 return sub + fees - d, d def tx_root(txids): return sha(D_TXROOT + "|" + ",".join(txids)) def candidate_id(prev, txids, height): return sha(D_CAND + "|" + prev + "|" + tx_root(txids) + "|" + str(height)) def state_root(height, issued, utxos): lst = ["%s:%s:%s" % (k, utxos[k]["address"], utxos[k]["amount"]) for k in sorted(utxos)] return sha(D_STATE + "|" + canon({"height": height, "issued": str(amt_int(issued)), "utxos": lst})) def checkpoint_id(prev, sroot, e, d): return sha(D_CP + "|" + prev + "|" + sroot + "|" + e + "|" + d) def make_reward(outputs, t, memo): tx = {"v": 2, "type": "reward", "inputs": [], "outputs": [{"address": o["address"], "amount": str(amt_int(o["amount"]))} for o in outputs if amt_int(o["amount"]) > 0], "fee": "0", "time": t, "memo": memo, "pubkey": "", "sig": ""} tx["txid"] = txid_of(tx) return tx # ---------------- report helpers ---------------- class Report: def __init__(self): self.checks = [] def add(self, name, ok, detail=""): res = "SKIP" if ok is None else ("PASS" if ok else "FAIL") self.checks.append({"name": name, "result": res, "detail": detail}) print(" [%s] %s%s" % (res, name, (" - " + detail) if detail else "")) return ok @property def ok(self): return all(c["result"] != "FAIL" for c in self.checks) def verify_miner_file(path, f, rep): tag = os.path.basename(path) ok_fmt = isinstance(f, dict) and f.get("format") == "eddycoin-miner-results" and f.get("version") == 1 and f.get("network") == NETWORK \ and f.get("chain") in ("E", "D") and isinstance(f.get("blocks"), list) rep.add("%s: format/network" % tag, ok_fmt, ("%s-Chain, %d blocks, device: %s, compute: %s" % (f["chain"], len(f["blocks"]), f.get("device"), f.get("compute", "n/a"))) if ok_fmt else "not an Eddycoin miner results file") if not ok_fmt: return False cs = checksum_of(f) r1 = rep.add("%s: file checksum" % tag, cs == f.get("checksum"), (cs[:8] + "..." + cs[-4:]) if cs == f.get("checksum") else "checksum mismatch - file was modified") pow_err, link_err, good = "", "", 0 for i, b in enumerate(f["blocks"]): why = check_block(b, f["chain"], IMPORT_MIN_BITS) if why and not pow_err: pow_err = "block %d: %s" % (i, why) if not why: good += 1 prev = f["blocks"][i - 1].get("hash") if i else ZERO if (b.get("prev") != prev or b.get("height") != i) and not link_err: link_err = "block %d: prev/height link broken" % i r2 = rep.add("%s: proof-of-work (hash + target)" % tag, not pow_err and len(f["blocks"]) > 0, pow_err or "%d/%d blocks valid, >= %d bits" % (good, len(f["blocks"]), IMPORT_MIN_BITS)) r3 = rep.add("%s: prev-hash links" % tag, not link_err, link_err or "all links intact") return bool(r1 and r2 and r3) def build_offline_checkpoints(e_blocks, d_blocks): def first_by(arr): m = {} for b in arr: m.setdefault(b["y_candidate"], b) return m em, dm = first_by(e_blocks), first_by(d_blocks) pairs = [(min(e["time"], dm[c]["time"]), c, e, dm[c]) for c, e in em.items() if c in dm] pairs.sort(key=lambda p: (p[0], p[1])) utxos, issued, prev, out = {}, 0, ZERO, [] for h, (t, c, e, d) in enumerate(pairs): sub = subsidy_at(h + 1, issued) se, sd = reward_split(sub, 0) rw = make_reward([{"address": e["miner"], "amount": se}, {"address": d["miner"], "amount": sd}], t, "offline checkpoint %d reward" % h) if rw["outputs"]: apply_tx(utxos, rw) issued += sub sr = state_root(h, issued, utxos) cp = {"height": h, "prev": prev, "y_candidate": c, "e_evidence": e["hash"], "d_evidence": d["hash"], "txs": [rw], "tx_root": tx_root([rw["txid"]]), "state_root": sr, "issued": str(issued), "subsidy": str(sub), "time": t} cp["id"] = checkpoint_id(prev, sr, e["hash"], d["hash"]) out.append(cp) prev = cp["id"] return out, issued, utxos def verify_export(X, rep): for ch in ("E", "D"): arr = X.get(ch) or [] anc = X.get("anchor" + ch) or {"height": -1, "hash": ZERO} bad, lk = "", "" for i, b in enumerate(arr): why = check_block(b, ch) if why and not bad: bad = "height %s: %s" % (b.get("height"), why) p = arr[i - 1] if i else anc if (b.get("prev") != p["hash"] or b.get("height") != p["height"] + 1) and not lk: lk = "height %s: link broken" % b.get("height") rep.add("export: %s-Chain proof-of-work" % ch, not bad, bad or "%d blocks" % len(arr)) rep.add("export: %s-Chain prev-hash links" % ch, not lk, lk or ("from pruned anchor #%d" % anc["height"] if anc["height"] >= 0 else "from genesis")) e_idx = {b["hash"]: b for b in X.get("E") or []} d_idx = {b["hash"]: b for b in X.get("D") or []} anc = X.get("anchor") or {"height": -1, "id": ZERO, "utxos": {}, "issued": "0"} utxo = json.loads(json.dumps(anc.get("utxos") or {})) issued, prev, prev_h = amt_int(anc.get("issued", 0)), anc["id"], anc["height"] pruned_e = (X.get("anchorE") or {}).get("height", -1) >= 0 pruned_d = (X.get("anchorD") or {}).get("height", -1) >= 0 err = dict(link="", id="", ev="", cand="", rep="", sr="", cap="") ds = sigs = 0 in_y = {} gf = amt_int(X.get("genesis_faucet", GENESIS_FAUCET)) for cp in X.get("Y") or []: h = cp["height"] if cp["prev"] != prev or h != prev_h + 1: err["link"] = err["link"] or "checkpoint %d: does not link to previous" % h if checkpoint_id(cp["prev"], cp["state_root"], cp["e_evidence"], cp["d_evidence"]) != cp["id"]: err["id"] = err["id"] or "checkpoint %d: id mismatch" % h genesis = h == 0 if not genesis: e, d = e_idx.get(cp["e_evidence"]), d_idx.get(cp["d_evidence"]) if not e and not pruned_e: err["ev"] = err["ev"] or "checkpoint %d: E evidence missing" % h if not d and not pruned_d: err["ev"] = err["ev"] or "checkpoint %d: D evidence missing" % h if e and e["y_candidate"] != cp["y_candidate"]: err["ev"] = err["ev"] or "checkpoint %d: E block commits to another candidate" % h if d and d["y_candidate"] != cp["y_candidate"]: err["ev"] = err["ev"] or "checkpoint %d: D block commits to another candidate" % h if candidate_id(cp["prev"], cp.get("cand_txids") or [], h) != cp["y_candidate"]: err["cand"] = err["cand"] or "checkpoint %d: candidate id mismatch" % h if [t["txid"] for t in cp["txs"] if t["type"] == "transfer"] != (cp.get("cand_txids") or []): err["cand"] = err["cand"] or "checkpoint %d: tx set differs from candidate" % h if tx_root([t["txid"] for t in cp["txs"]]) != cp["tx_root"]: err["id"] = err["id"] or "checkpoint %d: tx root mismatch" % h fees = 0 for n, t in enumerate(cp["txs"]): in_y[t["txid"]] = h if t["type"] == "transfer": sigs += 1 why = validate_transfer(utxo, t) if why: if "double spend" in why: ds += 1 err["rep"] = err["rep"] or "checkpoint %d tx %s: %s" % (h, t["txid"][:8], why) continue fees += int(t["fee"]) apply_tx(utxo, t) elif t["type"] == "reward" or (t["type"] == "genesis" and genesis): fmt_ok = all(is_amt(o.get("amount")) for o in t["outputs"]) s = sum(int(o["amount"]) for o in t["outputs"]) if fmt_ok else -1 expect = gf if genesis else subsidy_at(cp["height"], issued) + fees if not (fmt_ok and txid_of(t) == t["txid"] and not t["inputs"] and s == expect and n == len(cp["txs"]) - 1): err["rep"] = err["rep"] or "checkpoint %d: reward/issuance invalid (%d vs allowed %d)" % (h, s, expect) apply_tx(utxo, t) else: err["rep"] = err["rep"] or "unknown tx type" issued += gf if genesis else subsidy_at(cp["height"], issued) if not isinstance(cp.get("issued"), str) or issued != amt_int(cp["issued"]): err["rep"] = err["rep"] or "checkpoint %d: issued total mismatch" % h if issued > MAX_SUPPLY: err["cap"] = err["cap"] or "checkpoint %d: issuance exceeds cap" % h if state_root(h, issued, utxo) != cp["state_root"]: err["sr"] = err["sr"] or "checkpoint %d: state root mismatch" % h prev, prev_h = cp["id"], h ny = len(X.get("Y") or []) rep.add("export: Y checkpoint links", not err["link"], err["link"] or "%d checkpoints" % ny) rep.add("export: checkpoint ids = SHA-256(DOMAIN|prev|Y state root|E evidence|D evidence)", not err["id"], err["id"] or "all recomputed") rep.add("export: E + D evidence commit to the Y candidate", not err["ev"], err["ev"] or "every checkpoint has both") rep.add("export: Y candidate ids", not err["cand"], err["cand"] or "recomputed") rep.add("export: ECDSA signatures + UTXO replay (value conservation)", not err["rep"], err["rep"] or "%d signatures verified" % sigs) rep.add("export: no double spends in finalized history", ds == 0, "%d conflicting spends" % ds if ds else "none") rep.add("export: Y state roots", not err["sr"], err["sr"] or "recomputed") rep.add("export: issuance cap (%s EDDY)" % fmt(MAX_SUPPLY), not err["cap"] and issued == amt_int(X.get("issued", issued)), err["cap"] or "issued %s EDDY" % fmt(issued)) pend, pv = "", json.loads(json.dumps(utxo)) pending = list((X.get("cand") or {}).get("txs") or []) + list(X.get("mempool") or []) for t in pending: why = validate_transfer(pv, t) if why: pend = pend or "%s: %s" % (t.get("txid", "?")[:8], why) else: apply_tx(pv, t) rep.add("export: pending txs (candidate + mempool)", not pend, pend or "%d valid" % len(pending)) reg_err = "" pend_ids = set((X.get("cand") or {}).get("txids") or []) | {t.get("txid") for t in X.get("mempool") or []} for e in X.get("registry") or []: tid = e.get("txid") if not tid: continue if e.get("status") == "finalized" and tid not in in_y and (e.get("cp") or 0) > anc["height"]: reg_err = reg_err or "%s marked finalized but not in Y" % tid[:8] if e.get("status") == "rejected" and (tid in in_y or tid in pend_ids): reg_err = reg_err or "%s rejected but included" % tid[:8] rep.add("export: registry consistency", not reg_err, reg_err or "%d entries" % len(X.get("registry") or [])) if X.get("fiat") and X.get("fiat_totals"): te = sum(amt_int(v["EUR"]) for v in X["fiat"].values()) tc = sum(amt_int(v["CZK"]) for v in X["fiat"].values()) rep.add("export: EUR/CZK sandbox bank conservation", te == amt_int(X["fiat_totals"]["EUR"]) and tc == amt_int(X["fiat_totals"]["CZK"]), "EUR %s, CZK %s" % (fmt(te, 2), fmt(tc, 2))) def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("files", nargs="*", help="miner results files (and/or a chain export)") ap.add_argument("--chain-export", action="append", default=[], help="eddycoin_chain_export.json from the demo") ap.add_argument("--out", default="eddycoin_ychain_verified.json") a = ap.parse_args() files = list(a.files) + list(a.chain_export) if not files: files = [f for f in ("eddycoin_echain_results.json", "eddycoin_dchain_results.json", "eddycoin_chain_export.json") if os.path.exists(f)] if not files: sys.exit("No input files. Run a miner first or pass file names (see --help).") print("Eddycoin Y-Chain verifier - sandbox, no real money\n") rep = Report() inputs, e_blocks, d_blocks, export = [], [], [], None for p in files: try: with open(p, encoding="utf-8") as fh: f = json.load(fh) except Exception as ex: rep.add("%s: readable JSON" % os.path.basename(p), False, str(ex)) continue if isinstance(f, dict) and f.get("format") == "eddycoin-chain-export": print("Chain export %s:" % p) verify_export(f, rep) export = {"file": os.path.basename(p), "label": f.get("label"), "checkpoints": len(f.get("Y") or []), "E": len(f.get("E") or []), "D": len(f.get("D") or [])} continue print("Miner results %s:" % p) ok = verify_miner_file(p, f, rep) info = {"file": os.path.basename(p), "chain": f.get("chain") if isinstance(f, dict) else None, "party": f.get("party") if isinstance(f, dict) else None, "device": f.get("device") if isinstance(f, dict) else None, "checksum": f.get("checksum") if isinstance(f, dict) else None, "result": "PASS" if ok else "FAIL", "blocks": len(f.get("blocks") or []) if isinstance(f, dict) else 0, "miner_file": f if ok else None} inputs.append(info) if ok: (e_blocks if f["chain"] == "E" else d_blocks).extend(f["blocks"]) cps, issued = [], 0 if inputs: print("Y-Chain checkpoints from miner results:") cps, issued, _ = build_offline_checkpoints(e_blocks, d_blocks) if e_blocks and d_blocks: rep.add("E/D pairing -> Y checkpoints", True if cps else None, "%d checkpoints (E and D blocks committing to the same Y candidate)" % len(cps) if cps else "no common Y candidate - run both miners at the same time") else: rep.add("E/D pairing -> Y checkpoints", None, "need valid E-Chain and D-Chain results to build checkpoints") prev, bad = ZERO, "" for cp in cps: if cp["prev"] != prev or checkpoint_id(cp["prev"], cp["state_root"], cp["e_evidence"], cp["d_evidence"]) != cp["id"]: bad = "checkpoint %d" % cp["height"] prev = cp["id"] if cps: rep.add("checkpoint ids + links", not bad, bad or "SHA-256(DOMAIN|prev|Y state root|E evidence|D evidence)") per = sum(sum(int(o["amount"]) for o in cp["txs"][0]["outputs"]) for cp in cps) rep.add("rewards only via Y (declining emission), split %d%% E / %d%% D" % (E_SPLIT_BPS // 100, 100 - E_SPLIT_BPS // 100), per == issued, "%s EDDY issued in %d checkpoints" % (fmt(issued), len(cps))) rep.add("issuance cap (%s EDDY)" % fmt(MAX_SUPPLY), issued <= MAX_SUPPLY, "issued %s EDDY" % fmt(issued)) overall = "PASS" if rep.ok else "FAIL" out = {"format": "eddycoin-ychain-verified", "version": 1, "network": NETWORK, "generated": int(time.time() * 1000), "verifier": "eddycoin_ychain_verifier.py v2 (string amounts)", "overall": overall, "checks": rep.checks, "reward_policy": {"emission": {"R0": EM_R0, "epoch": EM_EPOCH, "num": EM_NUM, "den": EM_DEN}, "subsidy": str(SUBSIDY), "e_split_bps": E_SPLIT_BPS, "max_supply": str(MAX_SUPPLY), "amounts": "decimal-string"}, "inputs": inputs, "checkpoints": cps, "issued": str(issued), "chain_export": export} out["checksum"] = sha(canon(out)) with open(a.out, "w", encoding="utf-8") as fh: json.dump(out, fh, indent=1, ensure_ascii=False) print("\nOVERALL: %s (%d checks) -> %s" % (overall, len(rep.checks), os.path.abspath(a.out))) sys.exit(0 if overall == "PASS" else 1) if __name__ == "__main__": main()