#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Eddycoin (EDDY) D-Chain GPU miner - REAL GPU COMPUTE via OpenCL, test coin only ================================================================================ Runs real parallel double-SHA-256 proof-of-work on your graphics card with an OpenCL kernel (PyOpenCL + numpy). Every nonce the GPU reports is re-checked on the host with Python's hashlib before it is accepted. SHA-256d is a stand-in: ETChash is not implemented. * Real GPU work, real measured MH/s - 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) and submits every found D-block immediately. Rewards are paid via Y-Chain checkpoints to --wallet (needs an E-Chain block too: run the CPU miner as well). * --offline: no network at all; writes eddycoin_dchain_results.json (import in /demo or check with the verifier). * The GPU runs at full load while mining: watch temperatures, use --intensity to reduce heat. Setup (Windows): install Python 3.12 from python.org (tick "Add python.exe to PATH"), then py -m pip install pyopencl numpy Check: py eddycoin_dchain_gpu_miner.py --list-devices Run: py eddycoin_dchain_gpu_miner.py --wallet eddy1... (address from /live > Create my wallet) Offline: py eddycoin_dchain_gpu_miner.py --offline --minutes 2 --party A Options: --device N or name text (default: first GPU) --batch N nonces per kernel launch --intensity 10..100 (default 100) --minutes N (live default: until Ctrl+C) """ import argparse, hashlib, json, os, struct, sys, time NETWORK = "eddycoin-testnet-sandbox" CHAIN = "D" ZERO = "0" * 64 OUT = "eddycoin_dchain_results.json" MAX_NONCE = 2 ** 53 - 1 INSTALL_HELP = """ PyOpenCL / numpy are not installed (or no OpenCL driver was found). 1. Install Python 3.12 from https://www.python.org/downloads/ (tick "Add python.exe to PATH") 2. In a command prompt: py -m pip install pyopencl numpy 3. Graphics driver with OpenCL (normal driver install is enough, CUDA Toolkit NOT required): NVIDIA: GeForce/Studio driver from nvidia.com AMD: Adrenalin driver from amd.com Intel: Arc/Iris Xe graphics driver from intel.com 4. Check: py eddycoin_dchain_gpu_miner.py --list-devices See INSTALL_WINDOWS.txt for details. """ if sys.platform == "darwin": INSTALL_HELP = """ OpenCL is not available on this Mac (or PyOpenCL could not be loaded). Apple has deprecated OpenCL; on many Macs (especially Apple Silicon) it may be missing, slow or broken. Use the CPU miner instead: double-click "Eddycoin CPU Miner.command". (The GPU launcher "Eddycoin GPU Miner.command" installs PyOpenCL + numpy automatically.) """ KERNEL = r""" #define ROTR(x,n) rotate((uint)(x),(uint)(32-(n))) __constant uint K[64] = { 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3, 0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13, 0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208, 0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2}; void compress(uint *st, uint *w) { for (int i = 16; i < 64; i++) { uint s0 = ROTR(w[i-15],7) ^ ROTR(w[i-15],18) ^ (w[i-15] >> 3); uint s1 = ROTR(w[i-2],17) ^ ROTR(w[i-2],19) ^ (w[i-2] >> 10); w[i] = w[i-16] + s0 + w[i-7] + s1; } uint a=st[0],b=st[1],c=st[2],d=st[3],e=st[4],f=st[5],g=st[6],h=st[7]; for (int i = 0; i < 64; i++) { uint t1 = h + (ROTR(e,6) ^ ROTR(e,11) ^ ROTR(e,25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]; uint t2 = (ROTR(a,2) ^ ROTR(a,13) ^ ROTR(a,22)) + ((a & b) ^ (a & c) ^ (b & c)); h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; } st[0]+=a; st[1]+=b; st[2]+=c; st[3]+=d; st[4]+=e; st[5]+=f; st[6]+=g; st[7]+=h; } __kernel void mine(__constant uint *mid, __constant uchar *tail, uint tail_len, ulong full_len, ulong base, uint bits, __global uint *count, __global ulong *out, uint max_out) { ulong nonce = base + get_global_id(0); uchar buf[128]; for (uint i = 0; i < tail_len; i++) buf[i] = tail[i]; uchar dig[20]; int nd = 0; ulong x = nonce; do { dig[nd++] = (uchar)('0' + (x % 10)); x /= 10; } while (x); uint p = tail_len; for (int i = nd - 1; i >= 0; i--) buf[p++] = dig[i]; ulong total = full_len + p; buf[p++] = 0x80; uint nblk = (p + 8 <= 64) ? 1 : 2; uint end = nblk * 64; for (; p < end - 8; p++) buf[p] = 0; ulong bl = total * 8; for (int i = 0; i < 8; i++) buf[end - 1 - i] = (uchar)(bl >> (8 * i)); uint st[8]; for (int i = 0; i < 8; i++) st[i] = mid[i]; uint w[64]; for (uint k = 0; k < nblk; k++) { for (int i = 0; i < 16; i++) { uint o = k * 64 + 4 * i; w[i] = ((uint)buf[o] << 24) | ((uint)buf[o+1] << 16) | ((uint)buf[o+2] << 8) | (uint)buf[o+3]; } compress(st, w); } for (int i = 0; i < 8; i++) w[i] = st[i]; w[8] = 0x80000000; for (int i = 9; i < 15; i++) w[i] = 0; w[15] = 256; uint s2[8] = {0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19}; compress(s2, w); int ok = 1; uint rem = bits; int wi = 0; while (rem >= 32 && ok) { if (s2[wi] != 0) ok = 0; rem -= 32; wi++; } if (ok && rem > 0 && (s2[wi] >> (32 - rem)) != 0) ok = 0; if (ok) { uint idx = atomic_inc(count); if (idx < max_out) out[idx] = nonce; } } """ _K = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2] _H0 = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19] def _rotr(x, n): return ((x >> n) | (x << (32 - n))) & 0xFFFFFFFF def midstate(data): """SHA-256 state after the full 64-byte blocks of data (host side, pure Python).""" st = list(_H0) for o in range(0, len(data) - len(data) % 64, 64): w = list(struct.unpack(">16I", data[o:o + 64])) + [0] * 48 for i in range(16, 64): s0 = _rotr(w[i - 15], 7) ^ _rotr(w[i - 15], 18) ^ (w[i - 15] >> 3) s1 = _rotr(w[i - 2], 17) ^ _rotr(w[i - 2], 19) ^ (w[i - 2] >> 10) w[i] = (w[i - 16] + s0 + w[i - 7] + s1) & 0xFFFFFFFF a, b, c, d, e, f, g, h = st for i in range(64): t1 = (h + (_rotr(e, 6) ^ _rotr(e, 11) ^ _rotr(e, 25)) + ((e & f) ^ (~e & g)) + _K[i] + w[i]) & 0xFFFFFFFF t2 = ((_rotr(a, 2) ^ _rotr(a, 13) ^ _rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) & 0xFFFFFFFF h, g, f, e, d, c, b, a = g, f, e, (d + t1) & 0xFFFFFFFF, c, b, a, (t1 + t2) & 0xFFFFFFFF st = [(x + y) & 0xFFFFFFFF for x, y in zip(st, [a, b, c, d, e, f, g, h])] return st 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 sha256d_hex(s): return hashlib.sha256(hashlib.sha256(s.encode("utf-8")).digest()).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): # same 10-second offline Y-candidate windows as the E-Chain CPU miner (so the verifier can pair them) return sha("EDDY-Y-CANDIDATE|v1|offline|%s|%d" % (NETWORK, epoch)) def load_cl(): try: import numpy as np import pyopencl as cl return np, cl except Exception as ex: print("ERROR: %s" % ex) print(INSTALL_HELP) sys.exit(2) def all_devices(cl): out = [] try: plats = cl.get_platforms() except Exception as ex: print("ERROR: no OpenCL platform found (%s)" % ex) print(INSTALL_HELP) sys.exit(2) for p in plats: try: for d in p.get_devices(): out.append((p, d)) except Exception: pass return out def is_gpu(cl, d): return bool(d.type & cl.device_type.GPU) def pick_device(cl, devs, sel): if sel is None: gpus = [x for x in devs if is_gpu(cl, x[1])] return gpus[0] if gpus else None if sel.isdigit() and int(sel) < len(devs): return devs[int(sel)] m = [x for x in devs if sel.lower() in x[1].name.lower()] return m[0] if m else None 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