Solo miner in pure Python: suggest_difficulty only works after authorize

A pure-Python solo miner built to watch mining happen, and a Stratum quirk that is in no spec: mining.suggest_difficulty is ignored before authorize and honoured after it, on CKPool and Public Pool.

I wanted to actually see what a miner does, not just read about it. So I wrote a small solo miner in plain Python: its own Stratum V1 client over a raw socket (no cgminer, no cpuminer), real SHA256d work on real jobs from a real solo pool, and a terminal dashboard that shows the 80-byte header being hashed, colour-coded field by field, with the nonce as ????????.

It is written to be read, not to earn. A 4-core laptop does about 0.8 MH/s in pure Python, which works out to roughly 15 billion years per block. The dashboard has an “odds” panel that says so.

The thing I did not expect

Solo pools open with a share difficulty meant for ASICs: CKPool starts you at 10000, Public Pool at 100000. At difficulty 10000 a single share needs about 43 trillion hashes, i.e. more than a year of my laptop. So the pool would never see a share from me and I would never learn whether my header construction was even correct.

Stratum has mining.suggest_difficulty for this. I sent [1] right after mining.subscribe, the way most examples do it, and it did nothing on either pool. Difficulty stayed at 10000 / 100000.

Sending the same message after mining.authorize succeeded worked on both pools immediately: mining.set_difficulty came back with 1, and now the laptop lands about one share per hour. Shares are worth nothing, but each one is proof from the real network that my coinbase assembly, merkle root and header bytes are right.

Order matters, and I could not find this documented anywhere. If anyone knows why the implementations drop the suggestion before authorize (state not created yet? per-user difficulty attached at authorize?), I would like to hear it.

Other small things I learned while building it

  • mining.subscribe gives you extranonce1 and the size of extranonce2. Your extranonce2 is your private search space. I hand each CPU core its own value, so the cores never hash the same header.
  • Network difficulty and network hashrate can both be derived from the nbits field of the job itself. No API call needed.
  • Address validation before starting is worth it: bech32 and base58check both carry a checksum, so a typo is refused instead of being mined to for hours. It still cannot tell whether a valid address is yours, so paste, do not retype.
  • A --sim mode with a tiny local target (shares every few seconds) made the whole thing debuggable. Everything else, header construction, hash loop, share check, is identical to real mining.
  • Both pools use the payout address as the username. No account, nothing to register, a found block pays the address directly.

Happy to share more details about the header layout or the Stratum message flow if anyone is curious. Written mostly as a learning tool for myself, and the “you will never find a block” panel is the most honest part of the UI.


Write a comment