#!/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 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 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 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) DEFAULT_SERVER = "https://eddycoin-demo.pages.dev" 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:]) 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 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 local = a.server.startswith(("http://127.0.0.1", "http://localhost")) # developer testing only if not (a.server.startswith("https://") or local): sys.exit("--server must be an https:// URL") 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 = mp.Array("Q", threads) self.job = mp.Value("Q", 0) self.res_q = mp.Queue() self.tqs = [mp.Queue() for _ in range(threads)] self.procs = [mp.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 {} out("Eddycoin E-Chain CPU miner - LIVE on %s - real double-SHA-256, test coin only (no real money)" % a.server) 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 = "" try: while (end is None or time.time() < end) and not stop_ev.is_set(): w = api(a.server, "/api/work?chain=E") if "y_candidate" not in w: out(" server: %s - retrying in 20 s" % (w.get("error") or w.get("reason") or w)); stop_ev.wait(20); 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 = "" t = int(time.time() * 1000) prefix = header_prefix(w["height"], w["prev"], w["y_candidate"], a.wallet, t, w["bits"]) pool.start(prefix, w["bits"], time.time() + 3600) out(" work: E-block #%d for Y candidate #%s, difficulty %d bits" % (w["height"], w.get("y_height"), w["bits"])) found, last_check = None, time.time() 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() - last_check > 10: last_check = time.time() w2 = api(a.server, "/api/work?chain=E") if "y_candidate" in w2 and work_key(w2) != work_key(w): out(" work changed (new tip / candidate / difficulty) - restarting"); break if time.time() * 1000 - t > 90000: break # refresh the timestamp (server accepts +-2 min) pool.stop() if not found: continue b = {"chain": CHAIN, "height": w["height"], "prev": w["prev"], "y_candidate": w["y_candidate"], "miner": a.wallet, "time": t, "bits": w["bits"], "nonce": found[0], "alg": "sha256d", "hash": found[1]} r = api(a.server, "/api/submit/e", b) rate = pool.rate() st["found"] = st.get("found", 0) + 1 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/live" % (acc, rej, pool.rate() / 1e6, a.server)) 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) 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", default=DEFAULT_SERVER, help="Eddycoin server (https only, default %s)" % DEFAULT_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=max(1, (os.cpu_count() or 2) // 2), help="worker processes (default: 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, 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 = mp.Array("Q", threads) job = mp.Value("Q", 0) res_q = mp.Queue() tqs = [mp.Queue() for _ in range(threads)] procs = [mp.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()