#!/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()