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