#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Eddycoin (EDDY) E-Chain CPU miner - REAL COMPUTE, test coin only ================================================================ Does real double-SHA-256 proof-of-work on your CPU cores (multiprocessing) and records E-Chain blocks for the Eddycoin Testnet (sandbox). SHA-256d is a stand-in: RandomX is not implemented. * Real CPU work, real measured hash rate - but EDDY is a TEST COIN: no real money, no value. * LIVE mode (default): fetches work from the Eddycoin Testnet server (HTTPS, only the --server you give, default https://eddycoin-demo.pages.dev), mines, and submits every found block immediately. Rewards are paid via Y-Chain checkpoints to --wallet (needs a D-Chain block too: run the GPU miner). * --offline: no network at all; writes eddycoin_echain_results.json (import it in /demo or check it with eddycoin_ychain_verifier.py). Usage (Windows): py eddycoin_echain_cpu_miner.py --wallet eddy1... (address from /live > Create my wallet) Offline: py eddycoin_echain_cpu_miner.py --offline --minutes 2 --party A Options: --threads N (default: half of the logical cores) --intensity 10..100 (default 100, lower = pauses between work slices, less heat) --minutes N (live default: until Ctrl+C) Python 3.8+ standard library only. """ import argparse, hashlib, json, multiprocessing as mp, os, platform, queue, sys, time # 'spawn' on every OS (the default on Windows and macOS): workers re-import this file, so all start-up code # stays under `if __name__ == "__main__":`. No fork-related problems on macOS. MPC = mp.get_context("spawn") NETWORK = "eddycoin-testnet-sandbox" CHAIN = "E" ZERO = "0" * 64 OUT = "eddycoin_echain_results.json" SLICE = 4096 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 ascii_label(s, n=48): s = "".join(c for c in str(s) if 32 <= ord(c) < 127) return " ".join(s.split())[:n] or "unknown" def candidate_for(epoch): # Offline Y-candidate id: miners running in the same 10-second window commit to the same candidate, # so an E-Chain (CPU) run and a D-Chain (GPU) run can be paired into Y checkpoints by the verifier. return sha("EDDY-Y-CANDIDATE|v1|offline|%s|%d" % (NETWORK, epoch)) def default_threads(): """Sensible default worker count: on Macs the performance cores (Apple Silicon) or the physical cores (Intel), elsewhere half of the logical cores. Always at least 1 and leaves one logical core free.""" logical = os.cpu_count() or 2 n = 0 if sys.platform == "darwin": import subprocess for key in ("hw.perflevel0.physicalcpu", "hw.physicalcpu"): try: n = int(subprocess.run(["sysctl", "-n", key], capture_output=True, text=True, timeout=3).stdout.strip() or 0) except Exception: n = 0 if n > 0: break if n <= 0: n = logical // 2 return max(1, min(n, logical - 1 if logical > 1 else 1)) def cpu_name(): n = "" try: if sys.platform.startswith("win"): import winreg k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0") n = winreg.QueryValueEx(k, "ProcessorNameString")[0] elif sys.platform == "darwin": import subprocess n = subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, text=True, timeout=3).stdout.strip() elif os.path.exists("/proc/cpuinfo"): for line in open("/proc/cpuinfo"): if line.lower().startswith("model name"): n = line.split(":", 1)[1] break except Exception: pass return ascii_label(n or platform.processor() or platform.machine() or "CPU", 40) def worker(idx, stride, task_q, res_q, job, counts, intensity): while True: t = task_q.get() if t is None: return jid, prefix, bits, deadline = t base = hashlib.sha256(prefix.encode("utf-8")) shift = 256 - bits nonce = idx s256 = hashlib.sha256 while job.value == jid and time.time() < deadline and nonce < 2 ** 53: t0 = time.perf_counter() for _ in range(SLICE): h = base.copy() h.update(str(nonce).encode("ascii")) d = s256(h.digest()).digest() if (int.from_bytes(d, "big") >> shift) == 0: res_q.put((jid, nonce)) nonce += stride with counts.get_lock(): counts[idx] += SLICE if intensity < 100: el = time.perf_counter() - t0 time.sleep(el * (100 - intensity) / intensity) def header_prefix(height, prev, cand, miner, t, bits): return "EDDY-%s-BLOCK|v1|%d|%s|%s|%s|%d|%d|" % (CHAIN, height, prev, cand, miner, t, bits) HTTP_WARNING = "plain http:// is unencrypted - fine for a test-coin node, but others on the network could read or change the traffic" def check_server(url): """Return (ok, message). Allowed: any https:// URL; http:// only for this PC (localhost / 127.x), private LAN IPs (10.x, 172.16-31.x, 192.168.x) or an explicit node URL with a port (e.g. http://name.duckdns.org:8770). For http:// to another computer the message is a warning (unencrypted).""" import ipaddress, urllib.parse try: u = urllib.parse.urlsplit(url.strip()) host = (u.hostname or "").lower(); port = u.port except ValueError: return False, "invalid server URL" if u.scheme == "https" and host: return True, "" if u.scheme != "http" or not host: return False, "server must be an https:// URL or an http:// node URL (e.g. http://192.168.1.20:8770)" if host == "localhost": return True, "" try: ip = ipaddress.ip_address(host) except ValueError: ip = None if ip is not None and ip.is_loopback: return True, "" lan = ip is not None and ((ip.version == 4 and any(ip in ipaddress.ip_network(n) for n in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"))) or (ip.version == 6 and (ip.is_private or ip.is_link_local))) if lan or port is not None: return True, HTTP_WARNING return False, "http:// is allowed only for this PC, a LAN IP (10.x / 172.16-31.x / 192.168.x) or a node URL with a port, e.g. http://name.duckdns.org:8770 - otherwise use https://" def normalize_server(text, default_port=8770): """Turn what the user typed into a server URL: 192.168.1.5:8770 -> http://192.168.1.5:8770 host.example:8770 -> http://host.example:8770 [fd00::5]:8770 -> http://[fd00::5]:8770 192.168.1.5 / raspberrypi / localhost -> http://...:8770 eddycoin-demo.pages.dev -> https://eddycoin-demo.pages.dev (plain domain without port = https) Full URLs (http:// or https://) are kept; a trailing slash / path is removed.""" import ipaddress, re as _re, urllib.parse t = _re.sub(r"\s+", "", str(text or "")) if not t: return "" if "://" in t: try: u = urllib.parse.urlsplit(t) except ValueError: return t return "%s://%s" % (u.scheme.lower(), u.netloc.lower()) if u.netloc else t hp = t.split("/")[0] m = _re.match(r"^\[([0-9A-Fa-f:.]+)\](?::(\d{1,5}))?$", hp) if m: return "http://[%s]:%s" % (m.group(1).lower(), m.group(2) or default_port) try: ip = ipaddress.ip_address(hp) return ("http://[%s]:%d" if ip.version == 6 else "http://%s:%d") % (str(ip), default_port) except ValueError: pass m = _re.match(r"^([A-Za-z0-9.\-]+):(\d{1,5})$", hp) if m: return "http://%s:%s" % (m.group(1).lower(), m.group(2)) if _re.match(r"^[A-Za-z0-9\-]+$", hp): return "http://%s:%d" % (hp.lower(), default_port) if _re.match(r"^[A-Za-z0-9.\-]+$", hp): return "https://" + hp.lower() return t def is_addr(a): return isinstance(a, str) and len(a) == 45 and a.startswith("eddy1") and all(c in "0123456789abcdef" for c in a[5:]) LIVE_SERVER = "https://eddycoin-demo.pages.dev" # miner 1.3: the official Eddycoin pool is the default for NEW installs / the CLI without --server; any normal server/node (solo) or another pool still works. DEFAULT_POOL = os.environ.get("EDDY_DEFAULT_POOL") or "https://pool.129-159-188-73.sslip.io" DEFAULT_SERVER = LIVE_SERVER EXPLORER_URL = "https://eddycoin-demo.pages.dev/eddycoin.html" # balance lookup for pool payouts (the pool host has no /live page) DEFAULT_SERVERS = [DEFAULT_POOL, LIVE_SERVER] # pool first; if the pool is unreachable for 60 s the miner solo-mines on the live server def pool_of(w): """work from a pool carries {"pool": {"address", "share_bits", "fee_bps"}}; a normal server/node never sends it (= solo)""" p = w.get("pool") if isinstance(w, dict) else None if isinstance(p, dict) and is_addr(str(p.get("address", ""))) and isinstance(p.get("share_bits"), int): return p return None def fmt_amt(m): try: m = int(m) except Exception: return "?" return "%d.%08d" % (m // 100000000, m % 100000000) def api(server, path, body=None, timeout=20): """HTTPS call to the given Eddycoin server only (GET without body, POST with JSON body).""" import ssl, urllib.request, urllib.error url = server.rstrip("/") + path data = None if body is None else json.dumps(body, separators=(",", ":")).encode("utf-8") req = urllib.request.Request(url, data=data, method="GET" if body is None else "POST", headers={"content-type": "application/json", "accept": "application/json", "user-agent": "eddycoin-miner/2 (test coin)"}) try: with urllib.request.urlopen(req, timeout=timeout, context=ssl.create_default_context()) as r: return json.loads(r.read().decode("utf-8")) except urllib.error.HTTPError as e: try: return json.loads(e.read().decode("utf-8")) except Exception: return {"ok": False, "reason": "HTTP %d" % e.code, "error": "HTTP %d" % e.code} except Exception as ex: return {"ok": False, "reason": "network error: %s" % ex, "error": "network error: %s" % ex, "_neterr": True} def parse_servers(values): """--server may be repeated and/or comma-separated: returns the ordered list without duplicates.""" res = [] for v in ([values] if isinstance(values, str) else (values or [])): for s in str(v).split(","): s = normalize_server(s) if s and s not in res: res.append(s) return res or list(DEFAULT_SERVERS) def probe_server(url, timeout=8): """Ask a server/node for /api/state. Returns (ok, text) e.g. (True, 'Y #69, E #68, D #68').""" r = api(url, "/api/state", timeout=timeout) if str(r.get("network", "")).startswith("eddycoin-testnet") and isinstance(r.get("tips"), dict): t = r["tips"] return True, "Y #%s, E #%s, D #%s" % (t.get("Y"), t.get("E"), t.get("D")) return False, str(r.get("error") or r.get("reason") or "not an Eddycoin server")[:120] class ServerList: """Server list with automatic failover: if the server in use is unreachable for FAILOVER_S seconds, the miner switches to the next reachable server in the list (it does not switch back by itself).""" FAILOVER_S = 60 def __init__(self, servers, out=print, st=None): self.servers = list(servers) or list(DEFAULT_SERVERS) self.i = 0; self.down_since = None; self.out = out self.st = st if st is not None else {} self.st["server"] = self.cur @property def cur(self): return self.servers[self.i] def call(self, path, body=None): r = api(self.cur, path, body) if r.get("_neterr") or str(r.get("error", "")).startswith(("HTTP 502", "HTTP 503", "HTTP 504", "HTTP 521", "HTTP 522", "HTTP 523", "HTTP 530")): now = time.time(); self.down_since = self.down_since or now if len(self.servers) > 1 and now - self.down_since >= self.FAILOVER_S and self.failover(): return api(self.cur, path, body) else: self.down_since = None return r def failover(self): n = len(self.servers) for k in range(1, n): j = (self.i + k) % n ok, info = probe_server(self.servers[j]) if ok: self.out(" server %s unreachable for %d s - switching to %s (%s)" % (self.cur, self.FAILOVER_S, self.servers[j], info)) self.i = j; self.down_since = None; self.st["server"] = self.cur return True self.out(" server %s unreachable - no other server in the list is reachable either, still trying" % self.cur) self.down_since = time.time() return False def check_live_args(a): if a.offline: return if a.wallet_file: try: with open(a.wallet_file, encoding="utf-8") as f: wf = json.load(f) except Exception as ex: sys.exit("cannot read --wallet-file: %s" % ex) if wf.get("format") != "eddycoin-wallet": sys.exit("--wallet-file is not an Eddycoin wallet file") if a.wallet and a.wallet != wf.get("address"): sys.exit("--wallet and --wallet-file disagree") a.wallet = wf.get("address") # only the public address is read; no password or private key needed a.servers = parse_servers(getattr(a, "servers", None) or a.server) for sv in a.servers: ok, msg = check_server(sv) if not ok: sys.exit("--server %s: %s" % (sv, msg)) if msg: print("NOTE (%s): %s" % (sv, msg), file=sys.stderr) a.server = a.servers[0] if not is_addr(a.wallet or ""): sys.exit("--wallet
or --wallet-file is required for live mining (eddy1 + 40 hex). Open %s/live, click 'Create my wallet' and copy the address.\n" "Or use --offline to only write a results file." % DEFAULT_SERVER) def work_key(w): return (w.get("height"), w.get("prev"), w.get("y_candidate"), w.get("bits"), bool(w.get("waiting"))) class Pool: def __init__(self, threads, intensity): self.threads = threads self.counts = MPC.Array("Q", threads) self.job = MPC.Value("Q", 0) self.res_q = MPC.Queue() self.tqs = [MPC.Queue() for _ in range(threads)] self.procs = [MPC.Process(target=worker, args=(i, threads, self.tqs[i], self.res_q, self.job, self.counts, intensity), daemon=True) for i in range(threads)] for p in self.procs: p.start() self.jid = 0 def start(self, prefix, bits, deadline): self.t_on = time.perf_counter() self.jid += 1 self.prefix, self.bits = prefix, bits self.job.value = self.jid for q in self.tqs: q.put((self.jid, prefix, bits, deadline)) def stop(self): if getattr(self, "t_on", None): self.active = getattr(self, "active", 0.0) + time.perf_counter() - self.t_on; self.t_on = None self.job.value = self.jid + 1000000 def rate(self): act = getattr(self, "active", 0.0) + ((time.perf_counter() - self.t_on) if getattr(self, "t_on", None) else 0.0) return self.hashes() / max(act, 1e-6) def poll(self, timeout): """returns a host-verified (nonce, hash) for the current job, or None""" end = time.time() + timeout while time.time() < end: try: r = self.res_q.get(timeout=max(0.01, end - time.time())) except queue.Empty: return None if r[0] != self.jid: continue d = hashlib.sha256(hashlib.sha256((self.prefix + str(r[1])).encode("utf-8")).digest()).hexdigest() if (int(d, 16) >> (256 - self.bits)) == 0: return r[1], d return None def hashes(self): with self.counts.get_lock(): return int(sum(self.counts)) def close(self): self.job.value = 2 ** 63 for q in self.tqs: q.put(None) for p in self.procs: p.join(timeout=3) if p.is_alive(): p.terminate() def live(a, threads, device, out=print, stop_ev=None, st=None): import threading stop_ev = stop_ev or threading.Event() st = st if st is not None else {} srv = ServerList(getattr(a, "servers", None) or parse_servers(getattr(a, "server", None)), out, st) out("Eddycoin E-Chain CPU miner - LIVE on %s - real double-SHA-256, test coin only (no real money)" % (srv.cur + (" (failover list: %s)" % ", ".join(srv.servers[1:]) if len(srv.servers) > 1 else ""))) out("device: %s | wallet %s | intensity %d%%" % (device, a.wallet, a.intensity)) pool = Pool(threads, a.intensity) t0 = time.perf_counter() end = time.time() + a.minutes * 60 if a.minutes else None acc = rej = 0 last_msg = "" pool_srv = set() # servers that answered as a pool nshare = 0 def wurl(): return "/api/work?chain=E" + ("&payout=" + a.wallet if srv.cur in pool_srv else "") try: while (end is None or time.time() < end) and not stop_ev.is_set(): w = srv.call(wurl()) if "y_candidate" not in w: out(" server: %s - retrying in 15 s" % (w.get("error") or w.get("reason") or w)); stop_ev.wait(15); continue if w.get("waiting"): if w.get("message") != last_msg: last_msg = w.get("message"); out(" " + last_msg + " (start the D-Chain GPU miner)") stop_ev.wait(5); continue last_msg = "" P = pool_of(w) if P and srv.cur not in pool_srv: pool_srv.add(srv.cur); out(" POOL mode on %s: fee %.2f %%, %s, your payouts go to %s (min %s EDDY)" % (srv.cur, P.get("fee_bps", 0) / 100, P.get("scheme", "PPLNS"), a.wallet, fmt_amt(P.get("min_payout", "0")))) elif not P and srv.cur in pool_srv: pool_srv.discard(srv.cur) if st.get("mode") != ("pool" if P else "solo"): st["mode"] = "pool" if P else "solo" if not P: out(" SOLO mode on %s: whole block rewards go to %s" % (srv.cur, a.wallet)) head_miner = P["address"] if P else a.wallet tbits = max(1, min(P["share_bits"], w["bits"])) if P else w["bits"] t_loc = time.time() * 1000 t = int(t_loc + (w["server_time"] - t_loc if isinstance(w.get("server_time"), int) and abs(w["server_time"] - t_loc) < 3600000 else 0)) # server clock prefix = header_prefix(w["height"], w["prev"], w["y_candidate"], head_miner, t, w["bits"]) pool.start(prefix, tbits, time.time() + 3600) out(" work: E-block #%d for Y candidate #%s, difficulty %d bits%s" % (w["height"], w.get("y_height"), w["bits"], (", pool share target %d bits" % tbits) if P else "")) found, last_check = None, time.time() st["hashing"] = True while found is None: found = pool.poll(0.5) st.update(hashrate=pool.rate(), bits=w["bits"], height=w["height"]) if found or (end and time.time() > end) or stop_ev.is_set(): break if time.time() * 1000 - t_loc > (12000 if w.get("max_time_age_ms") else 90000): break # refresh the timestamp (server tells max_time_age_ms once block times must be within +-30 s of arrival) if time.time() - last_check > 10: last_check = time.time() w2 = srv.call(wurl()) if "y_candidate" in w2 and work_key(w2) != work_key(w): out(" work changed (new tip / candidate / difficulty) - restarting"); break pool.stop(); st["hashing"] = False if not found: continue b = {"chain": CHAIN, "height": w["height"], "prev": w["prev"], "y_candidate": w["y_candidate"], "miner": head_miner, "time": t, "bits": w["bits"], "nonce": found[0], "alg": "sha256d", "hash": found[1]} if P: b["payout"] = a.wallet r = srv.call("/api/submit/e", b) rate = pool.rate() st["found"] = st.get("found", 0) + 1 if P: # pool: every hash below the share target counts; the pool passes real blocks on to the node if r.get("ok"): nshare += 1; acc += 1; st["accepted"] = acc; st["shares"] = nshare blk = r.get("block_result") if r.get("block") else None st["last"] = ("POOL BLOCK %s #%d" % (CHAIN, b["height"])) if blk and blk.get("ok") else "share %d bits" % r.get("share_bits", tbits) out(" share accepted (%d bits)%s" % (r.get("share_bits", tbits), (" ** BLOCK %s #%d found for the pool: %s **" % (CHAIN, b["height"], "accepted" if blk.get("ok") else blk.get("reason"))) if blk else "")) else: rej += 1; st["rejected"] = rej; st["last"] = "share rejected: %s" % (r.get("reason") or r.get("error")) out(" share REJECTED: %s" % (r.get("reason") or r.get("error"))) continue if r.get("ok"): acc += 1; st["accepted"] = acc; st["last"] = "accepted E #%d" % b["height"] cp = r.get("checkpoint") out(" ACCEPTED E-block #%d %s %.2f MH/s%s" % (b["height"], b["hash"][:16], rate / 1e6, (" -> Y checkpoint #%d finalized, reward paid to your wallet" % cp["height"]) if cp else " (waiting for a D-Chain block to finalize the checkpoint)")) if r.get("retarget"): out(" difficulty retarget: %s -> %s bits" % (r["retarget"]["from"], r["retarget"]["to"])) else: rej += 1; st["rejected"] = rej; st["last"] = "rejected: %s" % (r.get("reason") or r.get("error")) out(" REJECTED E-block #%d: %s" % (b["height"], r.get("reason") or r.get("error"))) except KeyboardInterrupt: out("stopped by user") pool.close() el = max(time.perf_counter() - t0, 1e-6) out("done: %d accepted, %d rejected, %.2f MH/s while hashing (measured). Check your wallet on %s" % (acc, rej, pool.rate() / 1e6, EXPLORER_URL if srv.cur in pool_srv else srv.cur + "/live")) def gui_main(): import eddycoin_miner_gui as G def start(cfg, out, stop_ev, st): threads = max(1, min(int(cfg.threads), 256)) device = "CPU: %s (%d threads, %d logical cores)" % (cpu_name(), threads, os.cpu_count() or 0) live(cfg, threads, device, out=out, stop_ev=stop_ev, st=st) G.run("cpu", "Eddycoin CPU Miner (E-Chain)", DEFAULT_SERVER, api, start, pool_default=DEFAULT_POOL) def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--wallet", default=None, help="your wallet address (live mode, required): eddy1...") ap.add_argument("--wallet-file", default=None, help="read your address from an Eddycoin wallet file (no password needed)") ap.add_argument("--server", action="append", default=None, help="Eddycoin server or node; repeat it or separate with commas for a failover list (switches after 60 s unreachable). Default: %s (pool) then %s (solo). Solo: give a normal server/node, e.g. https://129-159-188-73.sslip.io or http://192.168.1.20:8770; http:// only for this PC, LAN IPs or node URLs with a port)" % (DEFAULT_POOL, LIVE_SERVER)) ap.add_argument("--offline", action="store_true", help="no network: only write eddycoin_echain_results.json") ap.add_argument("--minutes", type=float, default=None, help="how long to mine (live default: until Ctrl+C, offline default 2)") ap.add_argument("--party", choices=["A", "B"], default="A", help="offline mode: sandbox party for the work credit") ap.add_argument("--threads", type=int, default=None, help="worker processes (default: Mac performance/physical cores, otherwise half of the logical cores)") ap.add_argument("--intensity", type=int, default=100, help="10..100 %% duty cycle per worker (default 100)") ap.add_argument("--bits", type=int, default=22, help="offline mode: difficulty in leading zero bits (16-40, default 22; live: set by the server)") ap.add_argument("--max-blocks", type=int, default=500, help="stop after this many blocks (default 500)") a = ap.parse_args() if not 16 <= a.bits <= 40: sys.exit("--bits must be between 16 and 40") if not 10 <= a.intensity <= 100: sys.exit("--intensity must be between 10 and 100") check_live_args(a) threads = max(1, min(a.threads or default_threads(), 256)) device = "CPU: %s (%d threads, %d logical cores)" % (cpu_name(), threads, os.cpu_count() or 0) if not a.offline: return live(a, threads, device) if a.minutes is None: a.minutes = 2.0 miner = "party:" + a.party print("Eddycoin E-Chain CPU miner - real double-SHA-256 compute, test coin only (no real money, no network)") print("device: %s | party %s | difficulty %d bits | intensity %d%% | %.1f min" % (device, a.party, a.bits, a.intensity, a.minutes)) counts = MPC.Array("Q", threads) job = MPC.Value("Q", 0) res_q = MPC.Queue() tqs = [MPC.Queue() for _ in range(threads)] procs = [MPC.Process(target=worker, args=(i, threads, tqs[i], res_q, job, counts, a.intensity), daemon=True) for i in range(threads)] for p in procs: p.start() started = int(time.time() * 1000) t_start = time.perf_counter() deadline = time.time() + max(0.05, a.minutes) * 60 blocks, prev = [], ZERO state = {"hashes": 0, "rate": 0.0} def total_hashes(): with counts.get_lock(): return int(sum(counts)) def save(): state["hashes"] = total_hashes() el = max(time.perf_counter() - t_start, 1e-6) state["rate"] = state["hashes"] / el res = {"format": "eddycoin-miner-results", "version": 1, "network": NETWORK, "chain": CHAIN, "party": a.party, "device": device, "compute": "real", "hash_alg": "sha256d", "simulated_gpu": False, "note": "Real double-SHA-256 work on %d CPU worker processes (stand-in for RandomX). Test coin only, no real money." % threads, "threads": threads, "intensity": a.intensity, "bits": a.bits, "started": started, "finished": int(time.time() * 1000), "seconds": round(el, 3), "hashes": state["hashes"], "hashrate_hs": int(state["rate"]), "blocks": blocks} res["checksum"] = sha(canon(res)) tmp = OUT + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(res, f, indent=1, ensure_ascii=False) os.replace(tmp, OUT) return res jid = 0 try: while time.time() < deadline and len(blocks) < a.max_blocks: jid += 1 t = int(time.time() * 1000) cand = candidate_for(int(time.time()) // 10) prefix = header_prefix(len(blocks), prev, cand, miner, t, a.bits) job.value = jid for q in tqs: q.put((jid, prefix, a.bits, deadline)) found = None while time.time() < deadline and found is None: try: r = res_q.get(timeout=0.5) except queue.Empty: continue if r[0] != jid: continue nonce = r[1] # host-side re-check of the found nonce d = hashlib.sha256(hashlib.sha256((prefix + str(nonce)).encode("utf-8")).digest()).hexdigest() if (int(d, 16) >> (256 - a.bits)) == 0: found = (nonce, d) job.value = jid + 1000000 # stop workers on this job if found is None: break b = {"chain": CHAIN, "height": len(blocks), "prev": prev, "y_candidate": cand, "miner": miner, "time": t, "bits": a.bits, "nonce": found[0], "alg": "sha256d", "hash": found[1]} blocks.append(b) prev = b["hash"] save() print(" E-block #%-4d nonce %-12d %s %.2f MH/s" % (b["height"], b["nonce"], b["hash"][:16], state["rate"] / 1e6)) except KeyboardInterrupt: print("stopped by user") job.value = 2 ** 63 for q in tqs: q.put(None) for p in procs: p.join(timeout=3) if p.is_alive(): p.terminate() res = save() print("done: %d blocks, %d hashes in %.1f s = %.2f MH/s (measured) -> %s" % (len(blocks), res["hashes"], res["seconds"], res["hashrate_hs"] / 1e6, os.path.abspath(OUT))) print("Next: import this file on the Eddycoin demo page, or run: py eddycoin_ychain_verifier.py %s" % OUT) if __name__ == "__main__": mp.freeze_support() if len(sys.argv) == 1 and os.environ.get("EDDY_NO_GUI") != "1": try: gui_main() except ImportError as ex: # no tkinter: fall back to the command line help print("GUI not available (%s); use the command line, see --help" % ex) else: if "--cli" in sys.argv: sys.argv.remove("--cli") main()