#!/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 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"))) def make_engine(np, cl, plat, dev, batch, intensity): """compile the OpenCL kernel for one device; returns (device label, search, rate, stats)""" ctx = cl.Context([dev]) qu = cl.CommandQueue(ctx) try: prg = cl.Program(ctx, KERNEL).build() except Exception as ex: raise RuntimeError("the OpenCL kernel could not be built on %s (%s: %s)%s" % (ascii_label(dev.name, 40), type(ex).__name__, str(ex)[:300], " - Apple has deprecated OpenCL; use the CPU miner." if sys.platform == "darwin" else "")) kern = cl.Kernel(prg, "mine") kind = "GPU" if is_gpu(cl, dev) else "OpenCL CPU device (test)" device = "%s: %s (OpenCL, %s)" % (kind, ascii_label(dev.name, 40), ascii_label(plat.name, 30)) max_out = 64 mf = cl.mem_flags count_buf = cl.Buffer(ctx, mf.READ_WRITE, 4) out_buf = cl.Buffer(ctx, mf.WRITE_ONLY, 8 * max_out) cnt_host = np.zeros(1, dtype=np.uint32) stats = {"hashes": 0, "gpu_reports": 0, "host_rejects": 0, "active": 0.0} t_start = time.perf_counter() def search(prefix, bits, stop): """run kernel batches over nonce ranges until a host-verified nonce is found or stop() is True""" pb = prefix.encode("utf-8") full = len(pb) - len(pb) % 64 mid = np.array(midstate(pb), dtype=np.uint32) tail = np.frombuffer(pb[full:] + b"\0", dtype=np.uint8) mid_buf = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=mid) tail_buf = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=tail) base = 0 while base < MAX_NONCE and not stop(): t0 = time.perf_counter() cl.enqueue_copy(qu, count_buf, np.zeros(1, dtype=np.uint32)) kern(qu, (batch,), None, mid_buf, tail_buf, np.uint32(len(pb) - full), np.uint64(full), np.uint64(base), np.uint32(bits), count_buf, out_buf, np.uint32(max_out)) cl.enqueue_copy(qu, cnt_host, count_buf) qu.finish() stats["hashes"] += batch stats["active"] += time.perf_counter() - t0 found = None if cnt_host[0]: n = min(int(cnt_host[0]), max_out) outs = np.zeros(max_out, dtype=np.uint64) cl.enqueue_copy(qu, outs, out_buf) qu.finish() stats["gpu_reports"] += n for nonce in sorted(int(v) for v in outs[:n]): d = sha256d_hex(prefix + str(nonce)) # host-side verification of every reported nonce if (int(d, 16) >> (256 - bits)) == 0: found = found or (nonce, d) else: stats["host_rejects"] += 1 base += batch if intensity < 100: time.sleep((time.perf_counter() - t0) * (100 - intensity) / intensity) if found: return found return None def rate(): # measured while the kernel is running (waiting time excluded) return stats["hashes"] / max(stats["active"], 1e-6) return device, search, rate, stats def gui_main(): import eddycoin_miner_gui as G env = {} def list_devices(): try: import numpy as np import pyopencl as cl except Exception as ex: raise SystemExit("PyOpenCL could not be loaded (%s: %s)." % (type(ex).__name__, ex)) env.update(np=np, cl=cl) devs = all_devices(cl) if not devs: raise SystemExit("No OpenCL devices found." + INSTALL_HELP) env["devs"] = devs return [{"idx": i, "gpu": is_gpu(cl, d), "label": "[%d] %s - %s%s" % (i, ascii_label(d.name, 40), ascii_label(p.name, 24), "" if is_gpu(cl, d) else " (CPU/other, test only)")} for i, (p, d) in enumerate(devs)] def start(cfg, out, stop_ev, st): plat, dev = env["devs"][cfg.device["idx"]] device, search, rate, stats = make_engine(env["np"], env["cl"], plat, dev, 1 << 22 if cfg.device["gpu"] else 1 << 16, int(cfg.intensity)) out("device: " + device) live(cfg, device, search, rate, stats, out=out, stop_ev=stop_ev, st=st) G.run("gpu", "Eddycoin GPU Miner (D-Chain)", DEFAULT_SERVER, api, start, list_devices=list_devices, install_help=INSTALL_HELP, 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_dchain_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("--list-devices", action="store_true", help="list OpenCL devices and exit") ap.add_argument("--device", default=None, help="device number from --list-devices, or part of its name (default: first GPU)") ap.add_argument("--batch", type=int, default=1 << 22, help="nonces per kernel launch (default 4194304)") ap.add_argument("--intensity", type=int, default=100, help="10..100 %% duty cycle (default 100; lower = less heat)") ap.add_argument("--bits", type=int, default=30, help="offline mode: difficulty in leading zero bits (20-48, default 30; live: set by the server)") ap.add_argument("--max-blocks", type=int, default=500, help="stop after this many blocks (default 500)") ap.add_argument("--allow-cpu-device", action="store_true", help="allow an OpenCL CPU device (testing only)") a = ap.parse_args() if not a.list_devices: check_live_args(a) np, cl = load_cl() devs = all_devices(cl) if a.list_devices: if not devs: print("No OpenCL devices found." + INSTALL_HELP) sys.exit(2) for i, (p, d) in enumerate(devs): print("[%d] %s | %s | %s | %d compute units | %d MB" % (i, "GPU" if is_gpu(cl, d) else "CPU/other", ascii_label(d.name, 60), ascii_label(p.name, 40), d.max_compute_units, d.global_mem_size // (1024 * 1024))) return if not 10 <= a.intensity <= 100: sys.exit("--intensity must be between 10 and 100") batch = max(1 << 12, min(a.batch, 1 << 28)) sel = pick_device(cl, devs, a.device) if sel is None: print("ERROR: no OpenCL GPU found%s." % ((" matching '%s'" % a.device) if a.device else "")) print("Available devices: " + (", ".join("[%d] %s" % (i, ascii_label(d.name, 40)) for i, (p, d) in enumerate(devs)) or "none")) print(INSTALL_HELP) sys.exit(2) plat, dev = sel if not is_gpu(cl, dev) and not a.allow_cpu_device: print("ERROR: device '%s' is not a GPU. Choose a GPU with --device (see --list-devices)." % ascii_label(dev.name)) sys.exit(2) device, search, rate, stats = make_engine(np, cl, plat, dev, batch, a.intensity) t_start = time.perf_counter() print("NOTE: the GPU runs at high load while mining - keep an eye on temperatures.") if not a.offline: return live(a, device, search, rate, stats) if a.minutes is None: a.minutes = 2.0 if not 20 <= a.bits <= 48: sys.exit("--bits must be between 20 and 48") miner = "party:" + a.party print("Eddycoin D-Chain GPU miner - OFFLINE, real OpenCL double-SHA-256 compute, test coin only (no real money, no network)") print("device: %s | party %s | difficulty %d bits | batch %d | intensity %d%% | %.1f min" % (device, a.party, a.bits, batch, a.intensity, a.minutes)) started = int(time.time() * 1000) deadline = time.time() + max(0.05, a.minutes) * 60 blocks, prev = [], ZERO def save(): el = max(time.perf_counter() - t_start, 1e-6) 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 OpenCL double-SHA-256 work on the device above (stand-in for ETChash); every GPU nonce re-checked with hashlib. Test coin only, no real money.", "batch": batch, "intensity": a.intensity, "bits": a.bits, "started": started, "finished": int(time.time() * 1000), "seconds": round(el, 3), "hashes": stats["hashes"], "hashrate_hs": int(stats["hashes"] / el), "gpu_reported_nonces": stats["gpu_reports"], "host_rejected_nonces": stats["host_rejects"], "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 try: while time.time() < deadline and len(blocks) < a.max_blocks: t = int(time.time() * 1000) cand = candidate_for(int(time.time()) // 10) prefix = "EDDY-%s-BLOCK|v1|%d|%s|%s|%s|%d|%d|" % (CHAIN, len(blocks), prev, cand, miner, t, a.bits) found = search(prefix, a.bits, lambda: time.time() >= deadline) 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(" D-block #%-4d nonce %-14d %s %.1f MH/s" % (b["height"], b["nonce"], b["hash"][:16], rate() / 1e6)) except KeyboardInterrupt: print("stopped by user") res = save() print("done: %d blocks, %d hashes in %.1f s = %.1f MH/s (measured); GPU nonces re-checked on host: %d, rejected: %d -> %s" % (len(blocks), stats["hashes"], res["seconds"], res["hashrate_hs"] / 1e6, stats["gpu_reports"], stats["host_rejects"], os.path.abspath(OUT))) print("Next: import this file on the Eddycoin demo page, or run: py eddycoin_ychain_verifier.py %s" % OUT) def live(a, device, search, rate, stats, 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 D-Chain GPU miner - LIVE on %s - real OpenCL 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)) 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=D" + ("&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 E-Chain CPU 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 = "EDDY-%s-BLOCK|v1|%d|%s|%s|%s|%d|%d|" % (CHAIN, w["height"], w["prev"], w["y_candidate"], head_miner, t, w["bits"]) out(" work: D-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 "")) chk = {"t": time.time(), "changed": False} def stop(): st.update(hashrate=rate(), bits=w["bits"], height=w["height"]) if (end and time.time() > end) or stop_ev.is_set(): return True if time.time() * 1000 - t_loc > (12000 if w.get("max_time_age_ms") else 90000): return True # refresh the timestamp (server tells max_time_age_ms once block times must be within +-30 s of arrival) if time.time() - chk["t"] > 10: chk["t"] = 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"); return True return False st["hashing"] = True found = search(prefix, tbits, 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/d", b) 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 D #%d" % b["height"] cp = r.get("checkpoint") out(" ACCEPTED D-block #%d %s %.1f 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 an E-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 D-block #%d: %s" % (b["height"], r.get("reason") or r.get("error"))) except KeyboardInterrupt: out("stopped by user") out("done: %d accepted, %d rejected, %.1f MH/s average (measured); GPU nonces re-checked on host: %d, rejected: %d. Check your wallet on %s" % (acc, rej, rate() / 1e6, stats["gpu_reports"], stats["host_rejects"], EXPLORER_URL if srv.cur in pool_srv else srv.cur + "/live")) if __name__ == "__main__": if len(sys.argv) == 1 and os.environ.get("EDDY_NO_GUI") != "1": try: gui_main() except ImportError as ex: print("GUI not available (%s); use the command line, see --help" % ex) else: if "--cli" in sys.argv: sys.argv.remove("--cli") main()