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.
https://noid.suprnova.cc/stratum/stratum-protocol.md
— plus the Poseidon2b test vectors
and the reference client.
Prompt suggestion: “Implement parano1d-stratum-v1 as specified in this document; verify the hash against the test vectors before connecting.”
0. Summary for implementers
- Connect over TCP, or TLS on 3341 in every region. Newline-delimited JSON-RPC 2.0, one object per line.
mining.subscribe→ storesession_namespace(8 bytes hex).mining.authorizewith"o1<address>.<worker>".- Each
mining.notifycarries one job object with its ownshare_target_hex. There is nomining.set_difficulty. - Build the 256-byte header from
pow_fields_hex; field #10 (bytes 160..175) = low 8 bytes your counter, high 8 bytes the namespace. - Hash with Poseidon2b (tag
POWHDR__); share iffdigest < share_targetas little-endian 256-bit integers. mining.submitwith[job_id, nonce_32hex, work_domain_id]— the nonce is the whole 16-byte field #10.- On
"clean": truedrop older jobs immediately. Password is ignored (exceptd=<n>).
1. Transport and endpoints
| Host | Port | Transport | Start difficulty | Notes |
|---|---|---|---|---|
| noid.suprnova.cc (EU) | 3337 | plain TCP | 50 M | recommended for everyone; VarDiff adjusts within a minute |
| noid.suprnova.cc (EU) | 3338 / 3339 / 3340 | plain TCP | 200 M / 800 M / 3.2 G | higher starting difficulty only |
| noid.suprnova.cc (EU) | 3341 | TLS (stratum+ssl) | 3.2 G | TLS terminated at the edge, same protocol inside |
| stratum-us.suprnova.cc (US) | 3337 – 3340 | plain TCP | as above | US relay, same pool |
| stratum-us.suprnova.cc (US) | 3341 | TLS (stratum+ssl) | 3.2 G | TLS terminated on the US relay |
| stratum-apac.suprnova.cc (APAC) | 3337 – 3340 | plain TCP | as above | APAC relay, same pool |
| stratum-apac.suprnova.cc (APAC) | 3341 | TLS (stratum+ssl) | 3.2 G | TLS terminated on the APAC relay |
- UTF-8 JSON,
\n-terminated, one object per line. Requests carry an integerid; responses echo it; server notifications use"id": null. - Errors:
{"id":<id>,"result":null,"error":{"code":<int>,"message":"<english>"}}. Codes:-32000rejection,-32601unknown method,-32602invalid 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.protocolmust beparano1d-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 asnonce_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
}]}
| Field | Type | Meaning |
|---|---|---|
job_id | hex string | Opaque; echo it back exactly. New on every block and every difficulty change. |
height | int | Height this job would produce. |
clean | bool | true: abandon all older jobs now. Always true from this pool. |
pow_fields_hex | 512 hex | 16 header fields of 16 bytes (little-endian). Field #10 is zeroed; you fill it. |
nonce_field_index | int | Always 10. |
nonce_bits | int | Always 64. |
nonce_prefix_hex | 16 hex | Goes into the HIGH 8 bytes (field bytes 8..15) of field #10. Equals your session namespace. |
share_target_hex | 64 hex | Little-endian 256-bit share target for this job. |
block_target_hex | 64 hex | Network target; informational, the pool detects blocks itself. |
work_domain_id | 64 hex | Opaque token bound to the job; echo it back exactly. |
expires_in_seconds | int | Template lifetime on the node; just follow notifies. |
coinbase_value_micronoid | int | Block 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_hexdecodes; 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_hexverbatim. 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 (cratenoid_poseidon2b). Vectors: noid-poseidon2b-vectors.json. - Compare digest and target as little-endian unsigned 256-bit integers: share iff
digest < share_target(strict). Same withblock_target_hexfor 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.
| # | Check | Error message (code -32000) |
|---|---|---|
| 1 | nonce is exactly 16 bytes of hex | nonce must be 16-byte little-endian hex |
| 2 | job_id was issued to this connection (last 100 remembered) | stale share (job not found) |
| 3 | work_domain_id equals the issued one | work_domain_id mismatch |
| 4 | job still acceptable (current or grace window) | stale share (job superseded) |
| 5 | nonce high 8 bytes == session namespace | nonce does not belong to this session namespace |
| 6 | not a duplicate on this job | duplicate share |
| 7 | recomputed digest below the pool-issued share target | rejected (below share target) |
| 8 | connection authorized | unauthorized worker |
A share that is also below the network target is submitted to the node by the pool automatically.
7. Optional methods
| Method | Params | Reply | Notes |
|---|---|---|---|
mining.hashrate | ["<hashrate>","<worker>"] | true | informational |
eth_submitHashrate | ["0x<hex>","<worker>"] | true | informational |
miner.stats | [{"hashrate":n,"worker":"name"}] | true | informational |
mining.ping | [] | "pong" | keep-alive |
mining.extranonce.subscribe, mining.suggest_difficulty, mining.resume | any | true | accepted, 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.