# parano1d-stratum-v1 — NOID (Parano1d) stratum protocol at noid.suprnova.cc

Version: 1 · Pool: suprnova.cc · Last updated: 2026-09-04 · Human page: https://noid.suprnova.cc/StratumProtocol

This document is written so that a developer OR an AI coding agent can implement a compatible miner from it alone.
Everything a miner must do is stated as a rule. Exact JSON examples are authoritative. Byte orders are explicit.

## 0. Summary for implementers (read this first)

1. Open a TCP connection (or TLS on port 3341, available in every region). Messages are JSON-RPC 2.0 objects, one per line, `\n` terminated.
2. Send `mining.subscribe`, read back `session_namespace` (8 bytes hex). Send `mining.authorize` with `"<NOID address>.<worker>"`.
3. Wait for `mining.notify`. Each notify is ONE job object (not an array of positional fields).
4. Build the 256-byte PoW header from `pow_fields_hex`, put your 16-byte nonce into field #10 (bytes 160..175):
   low 8 bytes = your counter, high 8 bytes = `nonce_prefix_hex` (the session namespace). Never change the high 8 bytes.
5. Hash with Poseidon2b (`digest = Poseidon2b_POWHDR(fields[16])`). Compare `digest < share_target_hex` as little-endian 256-bit integers.
6. Submit `mining.submit` with `[job_id, nonce_32hex, work_domain_id]` (nonce = the WHOLE 16-byte field #10, LE, as hex).
7. On a notify with `"clean": true` drop all work on older jobs immediately. There is no `mining.set_difficulty`;
   the share target is per job (`share_target_hex`). A difficulty change arrives as a new job.
8. Do not send a password (or send anything; it is ignored except the `d=<n>` static-difficulty extension).

## 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 |

- Encoding: UTF-8 JSON, newline-delimited (`\n`). One JSON object per line. No batching required.
- Every request carries an integer `id`; every response echoes it. Server notifications use `"id": null`.
- Error format: `{"id": <id>, "result": null, "error": {"code": <int>, "message": "<english text>"}}`.
  Codes: `-32000` general/rejection, `-32601` method not found, `-32602` invalid params. Key your logic on the code and
  on the message strings listed in section 6; messages are stable.
- Keep-alive: send `mining.ping` (reply `"pong"`) if you want; the pool does not require it. Idle connections are not dropped by the pool.

## 2. Handshake

### 2.1 mining.subscribe

Request:
```json
{"id":1,"method":"mining.subscribe","params":["yourminer/1.0.0"]}
```
Response:
```json
{"id":1,"error":null,
 "hashrate_report":["miner.stats","eth_submitHashrate","mining.hashrate"],
 "result":{"nonce_bits":64,"protocol":"parano1d-stratum-v1","session_namespace":"0100003f53555052"}}
```
Rules:
- `result.protocol` MUST equal `parano1d-stratum-v1`. If it does not, abort.
- `result.session_namespace` is 8 bytes as 16 lowercase hex chars. It is unique per connection and is the HIGH 8 bytes of
  every nonce you will submit on this connection. Store it. It is also repeated in every notify as `nonce_prefix_hex`.
- `result.nonce_bits` is 64: you own the LOW 8 bytes of field #10 (a 64-bit search space per job).
- `hashrate_report` lists the method names the pool accepts for optional hashrate reporting (informational only).

### 2.2 mining.authorize

Request:
```json
{"id":2,"method":"mining.authorize","params":["o1YOUR_NOID_ADDRESS.rig01","x"]}
```
Response: `{"id":2,"result":true,"error":null}`

Rules:
- Username = bech32m NOID address starting with `o1`, optionally followed by `.` and a worker name (`[A-Za-z0-9_-]`, up to 32 chars).
- Password is ignored. Extension: password `d=<difficulty>` requests a static share difficulty (clamped to the pool's range).
- Failure: `{"id":2,"result":false,"error":{"code":-32000,"message":"invalid address format (expected bech32m o1...)"}}`.
- You must be authorized before submitting; submits before authorization return `"unauthorized worker"`.

## 3. Jobs: mining.notify

The pool pushes a job as soon as you are subscribed, and a new one within milliseconds of every network block.
`params` is an array with exactly ONE object:
```json
{"id":null,"method":"mining.notify","params":[{
  "assignment_id":"1",
  "job_id":"1e2b0000000fe9d400000012000000000",
  "height":54452,
  "clean":true,
  "pow_fields_hex":"<512 hex chars = 16 fields x 16 bytes, field 10 is all zero>",
  "nonce_field_index":10,
  "nonce_bits":64,
  "nonce_prefix_hex":"0100003f53555052",
  "share_target_hex":"<64 hex chars, little-endian 256-bit>",
  "block_target_hex":"<64 hex chars, little-endian 256-bit>",
  "work_domain_id":"<64 hex chars>",
  "expires_in_seconds":88,
  "coinbase_value_micronoid":45000000
}]}
```
| Field | Type | Meaning |
|---|---|---|
| `job_id` | hex string | Opaque. Echo it back exactly in `mining.submit`. Unique per connection and per difficulty change. |
| `height` | int | Block height this job would produce. |
| `clean` | bool | `true`: abandon all previous jobs now (new network block or new difficulty). Always true from this pool. |
| `pow_fields_hex` | 512 hex | The 16 header fields (16 bytes each, little-endian). Field #10 is zeroed; you fill it. |
| `nonce_field_index` | int | Always 10. |
| `nonce_bits` | int | Always 64 (low 8 bytes of field 10 are yours). |
| `nonce_prefix_hex` | 16 hex | Must be placed in the HIGH 8 bytes (bytes 8..15) of field 10, in field byte order. Equals your session namespace. |
| `share_target_hex` | 64 hex | Little-endian 256-bit share target for THIS job. A digest below it is a share. |
| `block_target_hex` | 64 hex | Little-endian 256-bit network target. Informational; the pool detects blocks itself. |
| `work_domain_id` | 64 hex | Opaque token bound to this job. Echo it back exactly in `mining.submit`. |
| `expires_in_seconds` | int | The node's template lifetime. The pool refreshes before expiry; you only need to follow notifies. |
| `coinbase_value_micronoid` | int | Block reward in µNOID (1 NOID = 1,000,000 µNOID). Informational. |

Rules:
- Work only on the most recent job. Shares for older `job_id`s are rejected as stale.
- A difficulty change (VarDiff) arrives as a NEW notify with a new `job_id`, new `work_domain_id` and new `share_target_hex`
  on the same template. Switch to it; shares already in flight for the previous job are still accepted at their own target.
- There is no `mining.set_difficulty` message. Difficulty = floor((2^256 − 1) / share_target).

## 4. Building the header and hashing

- Header = 16 fields × 16 bytes = 256 bytes, laid out exactly as `pow_fields_hex` decodes (field i occupies bytes 16·i .. 16·i+15).
- Field #10 (bytes 160..175) is the nonce field:
  - bytes 160..167 (field bytes 0..7): YOUR 64-bit search value, little-endian. Any strategy (counter, random) is fine.
  - bytes 168..175 (field bytes 8..15): `nonce_prefix_hex` bytes verbatim. Changing them makes every share invalid
    ("nonce does not belong to this session namespace").
- Hash: Poseidon2b sponge over the 16 fields with the domain IV derived from the ASCII tag `POWHDR__`. Field arithmetic is
  GF(2^128); the consensus implementation is in the Parano1d repository (crate `noid_poseidon2b`, "native/permutation").
  Test vectors: https://noid.suprnova.cc/stratum/noid-poseidon2b-vectors.json
- Compare: interpret `digest` (32 bytes) and `share_target_hex` (32 bytes) both as little-endian unsigned 256-bit integers.
  A share is valid iff `digest < share_target` (strictly less). Same rule with `block_target_hex` for a block.
- The nonce you submit is the WHOLE 16-byte field #10, hex-encoded in field byte order (32 lowercase hex chars):
  the first 16 hex chars are your low 8 bytes, the last 16 hex chars are the prefix.

## 5. Submitting shares: mining.submit

Request (exactly three positional params, no worker field):
```json
{"id":7,"method":"mining.submit","params":["<job_id>","<32 hex nonce = field 10>","<work_domain_id>"]}
```
Accepted: `{"id":7,"result":true,"error":null}`
Rejected: `{"id":7,"result":null,"error":{"code":-32000,"message":"rejected (below share target)"}}`

Also tolerated (do not rely on them): `["<worker>","<job_id>","<nonce>","<work_domain_id>"]` and
`{"job_id":..,"nonce":..,"work_domain_id":..}` as a single object.

## 6. Validation order and error messages

Every submit is checked in this order; the first failure is returned. Nothing the miner sends is trusted.

| # | 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 jobs remembered) | `stale share (job not found)` |
| 3 | work_domain_id equals the one issued with that job | `work_domain_id mismatch` |
| 4 | job still acceptable (current, or within the short grace window after a clean notify) | `stale share (job superseded)` |
| 5 | high 8 bytes of the nonce equal your session namespace | `nonce does not belong to this session namespace` |
| 6 | not submitted before on this job | `duplicate share` |
| 7 | Poseidon2b digest recomputed by the pool is below the share target the POOL issued for that job | `rejected (below share target)` |
| 8 | connection authorized | `unauthorized worker` |

Difficulty is never taken from the miner. Credited work = Σ issued difficulty of accepted shares.
A share below the network target is submitted to the node by the pool automatically; you do nothing special.

## 7. Optional methods

| Method | Params | Reply | Notes |
|---|---|---|---|
| `mining.hashrate` | `["<hashrate as string or int>","<worker>"]` | `true` | informational only |
| `eth_submitHashrate` | `["0x<hex hashrate>","<worker>"]` | `true` | informational only |
| `miner.stats` | `[{"hashrate":<int>,"worker":"<name>"}]` | `true` | informational only |
| `mining.ping` | `[]` | `"pong"` | keep-alive |
| `mining.extranonce.subscribe` | `[]` | `true` | accepted, no effect |
| `mining.suggest_difficulty` | `[<n>]` | `true` | accepted; VarDiff still governs |
| `mining.resume` | `[]` | `true` | |

Server → miner (optional, currently disabled): `{"id":null,"method":"mining.pause","params":[{"reason":"<text>"}]}` —
stop hashing until the next `mining.notify`. Implement it as a no-op if you prefer.

## 8. VarDiff and share rate

Target: one share every 15 s per connection. Range 3.125 M – 6.4 G. Start: the port's difficulty (50 M on 3337).
Retarget every 60 s at most, ±30% tolerance, max ×4 / ÷4 per step, always delivered as a new job (see section 3).

## 9. Reference implementation and test data

- Reference client (protocol state machine, Node.js): https://noid.suprnova.cc/stratum/reference-miner.js
- Poseidon2b / GF(2^128) test vectors (tower and flat basis, permutation, full PoW digests): https://noid.suprnova.cc/stratum/noid-poseidon2b-vectors.json
- Consensus source: https://github.com/ignotusnemo/parano1d (crates `noid_poseidon2b`, `noid_core`)

## 10. Minimal event loop (pseudo-code)

```
connect(host, port)                       # TLS if port is the TLS port
send subscribe -> ns = result.session_namespace (8 bytes)
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
  on each candidate counter c (uint64):
      header[160:168] = le64(c)
      d = poseidon2b_powhdr(header)                # 32 bytes
      if le256(d) < target:
          nonce_hex = hex(header[160:176])
          send submit([job.job_id, nonce_hex, job.work_domain_id])
  if msg is a response to a submit and msg.error: log(msg.error.message)
```

## 11. Compatibility checklist (what the pool tests)

- [ ] subscribe → protocol == parano1d-stratum-v1, namespace stored
- [ ] authorize with `o1…` address (with and without `.worker`)
- [ ] first notify handled; header built; field 10 high bytes == namespace
- [ ] share accepted (`result: true`)
- [ ] new notify with `clean: true` → old job dropped within 1 s
- [ ] VarDiff notify (same height, new job_id/target) handled without restart
- [ ] TLS on 3341 works (SNI = host)
- [ ] reconnect with backoff on socket close; re-subscribe (new namespace each time)
