parano1d-stratum-v1 — the NOID stratum protocol on noid.suprnova.cc

Everything a miner developer or an AI coding agent needs to make a miner compatible with this pool. Exact JSON, exact byte orders, exact error strings.

0. Summary for implementers
  1. Connect over TCP, or TLS on 3341 in every region. Newline-delimited JSON-RPC 2.0, one object per line.
  2. mining.subscribe → store session_namespace (8 bytes hex). mining.authorize with "o1<address>.<worker>".
  3. Each mining.notify carries one job object with its own share_target_hex. There is no mining.set_difficulty.
  4. Build the 256-byte header from pow_fields_hex; field #10 (bytes 160..175) = low 8 bytes your counter, high 8 bytes the namespace.
  5. Hash with Poseidon2b (tag POWHDR__); share iff digest < share_target as little-endian 256-bit integers.
  6. mining.submit with [job_id, nonce_32hex, work_domain_id] — the nonce is the whole 16-byte field #10.
  7. On "clean": true drop older jobs immediately. Password is ignored (except d=<n>).
1. Transport and endpoints
HostPortTransportStart difficultyNotes
noid.suprnova.cc (EU)3337plain TCP50 Mrecommended for everyone; VarDiff adjusts within a minute
noid.suprnova.cc (EU)3338 / 3339 / 3340plain TCP200 M / 800 M / 3.2 Ghigher starting difficulty only
noid.suprnova.cc (EU)3341TLS (stratum+ssl)3.2 GTLS terminated at the edge, same protocol inside
stratum-us.suprnova.cc (US)3337 – 3340plain TCPas aboveUS relay, same pool
stratum-us.suprnova.cc (US)3341TLS (stratum+ssl)3.2 GTLS terminated on the US relay
stratum-apac.suprnova.cc (APAC)3337 – 3340plain TCPas aboveAPAC relay, same pool
stratum-apac.suprnova.cc (APAC)3341TLS (stratum+ssl)3.2 GTLS terminated on the APAC relay
  • UTF-8 JSON, \n-terminated, one object per line. Requests carry an integer id; responses echo it; server notifications use "id": null.
  • Errors: {"id":<id>,"result":null,"error":{"code":<int>,"message":"<english>"}}. Codes: -32000 rejection, -32601 unknown method, -32602 invalid params. Message strings are stable (section 6).
  • Keep-alive is optional (mining.ping"pong"); the pool does not drop idle connections.
2. Handshake

mining.subscribe

→ {"id":1,"method":"mining.subscribe","params":["yourminer/1.0.0"]}
← {"id":1,"error":null,
   "hashrate_report":["miner.stats","eth_submitHashrate","mining.hashrate"],
   "result":{"nonce_bits":64,"protocol":"parano1d-stratum-v1","session_namespace":"0100003f53555052"}}
  • result.protocol must be parano1d-stratum-v1; abort otherwise.
  • session_namespace: 8 bytes / 16 hex, unique per connection = the high 8 bytes of every nonce you submit. Also repeated in each notify as nonce_prefix_hex.
  • nonce_bits = 64: the low 8 bytes of field #10 are your search space.

mining.authorize

→ {"id":2,"method":"mining.authorize","params":["o1YOUR_NOID_ADDRESS.rig01","x"]}
← {"id":2,"result":true,"error":null}
← {"id":2,"result":false,"error":{"code":-32000,"message":"invalid address format (expected bech32m o1...)"}}
  • Username = bech32m address starting with o1, optional .worker ([A-Za-z0-9_-], max 32).
  • Password ignored; extension d=<difficulty> requests a static share difficulty (clamped to the pool range).
3. Jobs: mining.notify

Pushed right after subscribe and within milliseconds of every network block. params is an array holding exactly one object:

← {"id":null,"method":"mining.notify","params":[{
     "assignment_id":"1",
     "job_id":"1e2b0000000fe9d400000012000000000",
     "height":54452,
     "clean":true,
     "pow_fields_hex":"<512 hex = 16 fields x 16 bytes, field 10 zeroed>",
     "nonce_field_index":10,
     "nonce_bits":64,
     "nonce_prefix_hex":"0100003f53555052",
     "share_target_hex":"<64 hex, little-endian 256-bit>",
     "block_target_hex":"<64 hex, little-endian 256-bit>",
     "work_domain_id":"<64 hex>",
     "expires_in_seconds":88,
     "coinbase_value_micronoid":45000000
   }]}
FieldTypeMeaning
job_idhex stringOpaque; echo it back exactly. New on every block and every difficulty change.
heightintHeight this job would produce.
cleanbooltrue: abandon all older jobs now. Always true from this pool.
pow_fields_hex512 hex16 header fields of 16 bytes (little-endian). Field #10 is zeroed; you fill it.
nonce_field_indexintAlways 10.
nonce_bitsintAlways 64.
nonce_prefix_hex16 hexGoes into the HIGH 8 bytes (field bytes 8..15) of field #10. Equals your session namespace.
share_target_hex64 hexLittle-endian 256-bit share target for this job.
block_target_hex64 hexNetwork target; informational, the pool detects blocks itself.
work_domain_id64 hexOpaque token bound to the job; echo it back exactly.
expires_in_secondsintTemplate lifetime on the node; just follow notifies.
coinbase_value_micronoidintBlock reward in µNOID (1 NOID = 1,000,000 µNOID).
  • Work only on the most recent job; older job_ids are rejected as stale.
  • VarDiff arrives as a new notify (new job_id, work_domain_id, share_target_hex, same template). Shares in flight for the previous job are still accepted at their own target.
  • Difficulty = floor((2256 − 1) / share_target).
4. Building the header and hashing
  • Header = 256 bytes exactly as pow_fields_hex decodes; field i occupies bytes 16·i .. 16·i+15.
  • Field #10 (bytes 160..175): bytes 160..167 = your 64-bit value, little-endian; bytes 168..175 = nonce_prefix_hex verbatim. Changing the prefix invalidates every share.
  • Hash: Poseidon2b sponge over the 16 fields, domain IV from the ASCII tag POWHDR__, arithmetic in GF(2128). Consensus code: github.com/ignotusnemo/parano1d (crate noid_poseidon2b). Vectors: noid-poseidon2b-vectors.json.
  • Compare digest and target as little-endian unsigned 256-bit integers: share iff digest < share_target (strict). Same with block_target_hex for a block.
  • The submitted nonce is the whole 16-byte field #10 as 32 lowercase hex chars in field byte order: first 16 hex = your low 8 bytes, last 16 hex = the prefix.
5. Submitting shares: mining.submit
→ {"id":7,"method":"mining.submit","params":["<job_id>","<32 hex nonce = field 10>","<work_domain_id>"]}
← {"id":7,"result":true,"error":null}
← {"id":7,"result":null,"error":{"code":-32000,"message":"rejected (below share target)"}}

Exactly three positional params, no worker field. Tolerated but not recommended: [worker, job_id, nonce, work_domain_id] and a single {"job_id","nonce","work_domain_id"} object.

6. Validation order and error messages

Checked in this order; the first failure is returned. Nothing the miner sends is trusted — difficulty comes from the pool, the hash is recomputed by the pool, credited work is the sum of issued difficulties of accepted shares.

#CheckError message (code -32000)
1nonce is exactly 16 bytes of hexnonce must be 16-byte little-endian hex
2job_id was issued to this connection (last 100 remembered)stale share (job not found)
3work_domain_id equals the issued onework_domain_id mismatch
4job still acceptable (current or grace window)stale share (job superseded)
5nonce high 8 bytes == session namespacenonce does not belong to this session namespace
6not a duplicate on this jobduplicate share
7recomputed digest below the pool-issued share targetrejected (below share target)
8connection authorizedunauthorized worker

A share that is also below the network target is submitted to the node by the pool automatically.

7. Optional methods
MethodParamsReplyNotes
mining.hashrate["<hashrate>","<worker>"]trueinformational
eth_submitHashrate["0x<hex>","<worker>"]trueinformational
miner.stats[{"hashrate":n,"worker":"name"}]trueinformational
mining.ping[]"pong"keep-alive
mining.extranonce.subscribe, mining.suggest_difficulty, mining.resumeanytrueaccepted, no effect

Server → miner (currently disabled): {"id":null,"method":"mining.pause","params":[{"reason":"<text>"}]} = stop hashing until the next notify. A no-op implementation is acceptable.

8. VarDiff

Target one share every 15 s per connection; range 3.125 M – 6.4 G; start = port difficulty (50 M on 3337); retarget at most every 60 s, ±30% tolerance, max ×4 per step; always delivered as a new job.

9. Minimal event loop
connect(host, port)                       # TLS if port == 3341
send subscribe  -> ns = result.session_namespace
send authorize("o1address.worker", "x")
loop:
  msg = read_line()
  if msg.method == "mining.notify":
      job = msg.params[0]
      header = hex_decode(job.pow_fields_hex)          # 256 bytes
      header[168:176] = hex_decode(job.nonce_prefix_hex)
      target = le256(job.share_target_hex)
      restart_search(job)                              # clean == true: drop old work
  for each candidate c (uint64):
      header[160:168] = le64(c)
      d = poseidon2b_powhdr(header)                    # 32 bytes
      if le256(d) < target:
          send submit([job.job_id, hex(header[160:176]), job.work_domain_id])
  if msg is a submit response and msg.error: log(msg.error.message)
10. Compatibility checklist
  • subscribe → protocol == parano1d-stratum-v1, namespace stored
  • authorize with an o1… address, with and without .worker
  • first notify handled; field #10 high bytes == namespace; share accepted
  • new notify with clean: true → old job dropped within 1 s
  • VarDiff notify (same height, new job_id/target) handled without reconnect
  • TLS on 3341 works (SNI = host); reconnect with backoff and re-subscribe (new namespace each time)

Raw spec: stratum/stratum-protocol.md · Reference client: stratum/reference-miner.js · Vectors: stratum/noid-poseidon2b-vectors.json · Questions: contact suprnova.