# NEB stratum protocol (neb.suprnova.cc)

Version 1 · as implemented by the neb.suprnova.cc pool (a NOMP-based stratum server) and by suprminer `-a neb`.

This document is meant to be complete: a miner or pool author (or their AI agent) should be able to implement a working NEB stratum client or server from it alone, plus the official NEB specification for the proof of work itself. Everything here is public; the pool needs no keys or credentials from the miner.

- Human-readable version: https://neb.suprnova.cc/CoinInfos.html#stratum
- This text (Markdown): https://neb.suprnova.cc/neb-stratum.md
- Full captured session and a verified share (test vector, JSON): https://neb.suprnova.cc/neb-stratum-example.json (v1 network)
- The same after the nebhash v2 fork (KEP1 job, dataset index 2): https://neb.suprnova.cc/neb-stratum-example-v2.json
- Official NEB node, PoW reference and specification: https://github.com/SilasEigen/neb (`spec/node.md`, `spec/shares.md`, `pow/`)

## 1. Overview

NEB is a Proof-of-Training chain. An *attempt* is not a plain hash: the attempt key `BLAKE3(header)` selects 16 training samples from a dataset, the miner computes a gradient of a small integer neural network (784 → 128 → 10, 4-bit weights) on them, and the result is hashed into a 256-bit *ticket*. A ticket that is at or below a target is a share or a block.

The stratum protocol is Bitcoin-style stratum v1 (JSON lines over TCP, `mining.subscribe` / `mining.authorize` / `mining.notify` / `mining.submit`, extranonce1 + extranonce2 in the coinbase, nTime rolling, difficulty via `mining.set_difficulty`) with three NEB-specific changes:

1. **`mining.set_model`**: the pool pushes the network's current model weights (base64; 50 816 bytes for the v1 network, 13 000 / 11 140 bytes for the nebhash v2 networks KEP1 / RET2 from block 8800) that the attempts of the following jobs are evaluated on. The model changes with every block.
2. **`mining.notify` carries NEB fields**: block height, the model's `weights_hash`, the dataset index (`slot`), the body's `tx_count`, `shares_root`, and the upper nTime bound `ntime_max`. The header is NEB's 80-byte header, and the Merkle tree and body commitment use domain-separated BLAKE3.
3. **Difficulty means expected attempts per share**: `share_target = floor((2^256 - 1) / difficulty)`, compared with the ticket as 256-bit little-endian integers.

There are no other extensions. The miner needs the public NEB dataset file (section 3) and a PoW implementation; the pool sends everything else.

## 2. Connection

| | |
|---|---|
| Host | `neb.suprnova.cc` |
| Transport | plain TCP, no TLS; one JSON object per line, terminated by `\n` (UTF-8) |
| Port 7801 | VarDiff, start 20 000, min 500 (CPU and small GPUs); custom difficulty allowed |
| Port 7802 | fixed difficulty 20 000 |
| Port 7803 | fixed difficulty 2 000 000 |
| Port 7804 | VarDiff for big GPUs, start 5·10^8, min 10^5 (recommended for GPUs); custom difficulty allowed |
| Port 7805 | SOLO: VarDiff, start 5·10^7, min 500; custom difficulty allowed. Same protocol; a block found here is credited to its finder alone (99 %), shares only measure hashrate |
| VarDiff | aims at one share per 30 s; retargets after at least 90 s and 3 accepted shares (at once after 12 fast shares), at most ×4 or ÷4 per step; lowers the difficulty when a session has no accepted share for 90 s |
| Username | `<neb1 address>` or `<neb1 address>.<worker>`; worker = 1–32 characters of `A–Z a–z 0–9 _ -` |
| Password | anything (`x`); optionally `d=<number>` for a fixed difficulty on ports 7801, 7804 and 7805, within the port's limits |

The address is a NEB testnet address: `neb1` followed by bech32m data, 62 lowercase characters in total. Payouts go to that address; there is no registration.

## 3. What a miner needs locally

- **The dataset file `neb-datasets.bin` of NEB 0.3.0** (release asset, 259 897 080 bytes, also as a 26 MB zip). Its BLAKE3 hash is pinned by the network: `296a174edbf67339fa0af341f063154292bba224b3d2cacf5b18b31c70da80eb`. It holds 4 datasets (format: `spec/shares.md` section 11.6, `spec/nebhash-v2.md` section 2): index 0 Kepler transit signals and index 1 retinal OCT scans (`NMD1`, the v1 network, used up to block 8799), index 2 Kepler signals for **KEP1** and index 3 retinal scans at 64 × 64 for **RET2** (`NMD2`, from block 8800 / 8801). The pool names the dataset index of every job. The old 2-dataset file of NEB 0.2.1 (`b1008b97…`) still works until block 8799 only.
- **A ticket function** `ticket(model, dataset[index], key)`: for an `NMD1` dataset exactly as in `spec/shares.md` sections 3–5 (BLAKE3, ChaCha8, sample selection from the key, integer forward/backward pass, gradient digest, `ticket = BLAKE3(digest || key)`); for an `NMD2` dataset nebhash v2 (`spec/nebhash-v2.md`, the small convolutional networks KEP1 and RET2, same sample selection, digest and ticket rule). The C reference in the NEB repository (`pow/ref/shares.c`, `pow/ref/nebhash2.c`) is normative; the official CPU and GPU miners and suprminer are bit-exact with it. Any mismatch makes all your shares invalid, so test against the vectors in section 9.

## 4. Message flow

```
client                                                   pool
  | --- mining.subscribe ----------------------------------> |
  | <-- result [subscriptions, extranonce1, 8] ------------- |
  | <-- mining.set_difficulty [D] -------------------------- |
  | --- mining.authorize [user, pass] ---------------------> |
  | <-- mining.set_difficulty [D]   (only if it changed) --- |
  | <-- mining.set_model [weights_hash, index, base64] ----- |
  | <-- mining.notify [job ...] ---------------------------- |
  | <-- result true ---------------------------------------- |   (authorize result comes AFTER the first job)
  |                                                          |
  | --- mining.submit [user, job_id, en2, ntime, nonce] ---> |
  | <-- result true  /  error [code, message, null] -------- |
  |          ...                                             |
  | <-- mining.set_model (when the model changes) ---------- |
  | <-- mining.notify (new block: clean_jobs = true) ------- |
  | <-- mining.notify (same block, refresh: clean = false) - |
```

Rules a client can rely on:

- Responses carry the request `id`. Notifications have `"id": null` and may arrive at any time, before or after any response. Never assume the next line is the answer to your last request.
- The first `set_difficulty`, `set_model` and `notify` of a session are sent **before** the `mining.authorize` result.
- A `mining.set_model` is sent before a `mining.notify` whenever that job's `weights_hash` differs from the one of the previous job on this session. In practice this happens with every new block (the model is trained by every block, and the dataset alternates). The pool sends it at most once per change; cache it.
- A `mining.set_difficulty` is sent right before a `mining.notify` and applies from then on. A share that meets only the difficulty in force before the last change is still accepted, so work in flight is never lost.
- The pool sends a `mining.notify` at least every 75 seconds (a new block, a refreshed job, or the current job re-sent). A client may treat 180 seconds without any data as a dead connection and reconnect.
- Line sizes: pool → client lines are at most about 70 KB (`mining.set_model` is about 68 KB); client → pool lines must stay below 10 KB.

### Reading the socket (important)

Read like cpuminer or XMRig: one network thread does blocking reads into a persistent buffer and hands every complete line (ending in `\n`) to the miner. A 68 KB `set_model` line arrives in many TCP segments; a client that polls with short timeouts and discards a partially received line will drop it and loop forever. Treat a read timeout as "no data yet" (on Windows it is `WSAETIMEDOUT` 10060 / `TimedOut`, on Unix `EAGAIN` / `WouldBlock`), never as a lost connection. Reconnect with a backoff (for example 1, 2, 4 … 60 s).

## 5. Methods

All hex strings are lowercase, without `0x`.

### 5.1 mining.subscribe

```json
{"id":1,"method":"mining.subscribe","params":["myminer/1.0"]}
{"id":1,"result":[[["mining.set_difficulty","neb0000000001e4"],["mining.notify","neb0000000001e4"]],"4e0000012317f237",8],"error":null}
```

- `result[1]` = **extranonce1**, 8 bytes (16 hex), unique per session.
- `result[2]` = **extranonce2 size** = 8 bytes. The miner chooses extranonce2 (16 hex) freely, for example a per-GPU counter.
- params are optional and ignored. Subscribing twice on one connection is an error. `mining.extranonce.subscribe` is answered with `false` (the extranonce never changes within a session).

### 5.2 mining.authorize

```json
{"id":2,"method":"mining.authorize","params":["neb1…your-address….rig01","x"]}
{"id":2,"result":true,"error":null}
```

`false` means the address is not a valid `neb1` address or the worker name breaks the rules in section 2. One worker per connection: run one connection per worker name.

### 5.3 mining.set_difficulty (pool → miner)

```json
{"id":null,"method":"mining.set_difficulty","params":[20000]}
```

`D` = expected number of attempts per share (a number, may be fractional). Share target:

```
share_target = floor( (2^256 - 1) / D )
```

In exact integer arithmetic. The pool computes `floor((2^256 - 1) * 10^6 / round(D * 10^6))`, which is the same for any D with at most 6 decimals. For D = 20000 the target as 32 little-endian bytes is:

```
21fdf675e09c11a5bdc117265305a3923a014d840d4faf946588635ddc460300
```

### 5.4 mining.set_model (pool → miner)

```json
{"id":null,"method":"mining.set_model","params":["a0022b7e3683ae94695b79f06005484ec98ddd98372e9f540672a50365cc3606",1,"IB4hPQ/wACv+ETBN…(67 756 base64 characters)…"]}
```

| # | field | meaning |
|---|---|---|
| 0 | weights_hash | 32 bytes hex: `BLAKE3("neb-model-weights" ‖ pack(W1) ‖ pack(W2))` (`spec/shares.md` section 7) |
| 1 | index | index of the dataset in the network file this model trains: height mod 2 (0 or 1) up to block 8799, then 2 (KEP1) at even and 3 (RET2) at odd heights (`spec/shares.md` 11.7) |
| 2 | weights | base64 of the packed weights. **v1 (index 0/1): 50 816 bytes**, `pack(W1)` (50 176 bytes, W1 is 128 × 784) followed by `pack(W2)` (640 bytes, W2 is 10 × 128). **KEP1 (index 2): 13 000 bytes, RET2 (index 3): 11 140 bytes**: all weights of the architecture's layers in layer order, each tensor row-major (`spec/nebhash-v2.md` sections 3 and 6). Always signed 4-bit values −8..7, element `e` in the low nibble of byte `e/2` when `e` is even and in the high nibble when odd (`spec/shares.md` section 3.4) |

Check: decode, verify `BLAKE3("neb-model-weights" ‖ bytes) == weights_hash`, and key the model by `weights_hash`. Jobs name the model they need by its `weights_hash` (notify field 10).

### 5.5 mining.notify (pool → miner)

```json
{"id":null,"method":"mining.notify","params":[
 "10985",
 "513d8d34b62ba1aa3ec4495006dfca9256560e07791c931d9483dccc2121e76c",
 "01010000000000000000000000000000000000000000000000000000000000000000ffffffff1441150000",
 "0200b33f71000000000104290170dc4dc035fdc27c5a1db5b7bc7c0e09aee08a28f37abb6256a01827b700e1f5050000000002270bbd6380c0c7022176e202083d5671ed6bb7bc0c27350bcbd134846ce87dd5",
 [],
 "00000001","c3ce4480","6af447ee",true,
 5441,
 "a0022b7e3683ae94695b79f06005484ec98ddd98372e9f540672a50365cc3606",1,
 "00000001",
 "81e74225b9436f0fcd0ed90b31817f2acebb2e2b3d28577b90a7e46ad736c5f9",
 "6af455fe"]}
```

| # | field | type | meaning |
|---|---|---|---|
| 0 | job_id | string | opaque job id, echo it in `mining.submit` |
| 1 | parent | hex 32 B | block id of the parent, in header byte order (copy as is) |
| 2 | coinb1 | hex | coinbase transaction up to the extranonce |
| 3 | coinb2 | hex | coinbase transaction after the extranonce |
| 4 | merkle_branch | array of hex 32 B | siblings of the coinbase leaf, bottom-up (empty when the block has only the coinbase) |
| 5 | version | hex u32 | header version (big-endian hex of the value; write it little-endian) |
| 6 | nbits | hex u32 | compact block target (big-endian hex of the value; write it little-endian) |
| 7 | ntime | hex u32 | job start time = lowest allowed nTime (big-endian hex) |
| 8 | clean_jobs | bool | `true` = new parent block: drop all older jobs, their shares are stale |
| 9 | height | number | height of the block being mined |
| 10 | weights_hash | hex 32 B | the model (from `mining.set_model`) to evaluate attempts on |
| 11 | slot | number | dataset index of the job's model (same as `set_model` param 1): height mod 2 up to block 8799, then 2 / 3 |
| 12 | tx_count | hex u32 | number of transactions in the body including the coinbase (big-endian hex) |
| 13 | shares_root | hex 32 B | commitment to the block's list of training shares (copy as is) |
| 14 | ntime_max | hex u32 | highest allowed nTime for this job (big-endian hex) |

Jobs with `clean_jobs = false` refresh the block template on the same parent (the node refreshes every 30 s). Shares on the older jobs of the same parent stay valid, up to 16 jobs back.

### 5.6 mining.submit

```json
{"id":4,"method":"mining.submit","params":["neb1…your-address….rig01","10985","0000000000000000","6af447ee","00002eb1"]}
{"id":4,"result":true,"error":null}
```

| # | field | format |
|---|---|---|
| 0 | user | the authorized username (informational) |
| 1 | job_id | from notify field 0 |
| 2 | extranonce2 | 16 hex = the 8 bytes you placed in the coinbase, in byte order |
| 3 | ntime | 8 hex = big-endian hex of the u32 nTime you placed in the header |
| 4 | nonce | 8 hex = big-endian hex of the u32 nonce you placed in the header |

Submit every attempt whose ticket meets the share target. A ticket that also meets the block target (`nbits`) is a block; submit it the same way and the pool forwards it to the node at once. At most 16 submits may be unanswered at a time; send further ones after replies arrive.

### 5.7 Other methods

| method | answer |
|---|---|
| `mining.suggest_difficulty [D]` | `true`; on ports 7801/7804/7805 the difficulty is set to D clamped to the port's limits |
| `mining.configure` | `{}` (no extensions) |
| `mining.extranonce.subscribe` | `false` with error `[20, "Not supported.", null]` |
| anything else | error `[20, "Unknown method", null]`; the connection stays open |

## 6. Building an attempt

Notation: `‖` = concatenation; `H(tag, a, b, …) = BLAKE3(ASCII(tag) ‖ a ‖ b ‖ …)` with no separator or length prefix (`spec/node.md` section 3); `LE32(x)` = 4 bytes little-endian; all 256-bit numbers (targets, tickets) are little-endian byte strings.

```
coinbase    = coinb1 ‖ extranonce1 (8 B) ‖ extranonce2 (8 B) ‖ coinb2
leaf        = H("leaf", BLAKE3(coinbase))
acc         = leaf;  for each sibling s in merkle_branch:  acc = H("node", acc ‖ s)
body_root   = H("body", LE32(tx_count) ‖ acc ‖ shares_root)
header (80) = LE32(version) ‖ parent (32) ‖ body_root (32) ‖ LE32(ntime) ‖ LE32(nbits) ‖ LE32(nonce)
key         = BLAKE3(header)                 (this is also the block id)
ticket      = NEB ticket of key on (model of weights_hash, dataset[index])  (v1: spec/shares.md 5.4, KEP1/RET2: spec/nebhash-v2.md 4)
share       if ticket <= share_target        (256-bit little-endian compare)
block       if ticket <= target(nbits)       target = mantissa << shift, nbits = shift << 24 | mantissa
```

- The coinbase leaf is always the leftmost leaf, so it is always the left input of `H("node", …)`.
- Treat coinb1 and coinb2 as opaque bytes. For reference: the coinbase witness is `LE32(height) ‖ extranonce1 ‖ extranonce2`; coinb1 ends with the witness length and `LE32(height)`, and coinb2 holds the outputs (pool and project share).
- **Work space**: the 32-bit nonce gives 2^32 attempts per header. For more, change extranonce2 (a new `body_root`) or nTime. An attempt costs far more than a hash, so a GPU rarely exhausts a nonce range within one job.
- **nTime** must satisfy `ntime ≤ ntime_value ≤ ntime_max` from the job, and must not be more than 90 s ahead of the pool's clock. Using the job's `ntime` unchanged is always fine.
- The ticket depends on the model and the dataset of the job; the weights size and the dataset's `NMD1`/`NMD2` header tell which network to run. After `clean_jobs = true` (new block) the model changes, so all in-flight attempts on older jobs are worthless; stop them at once.

## 7. Errors

`error` is `[code, message, null]`.

| code | message (examples) | meaning |
|---|---|---|
| 20 | `incorrect size or hex of extranonce2` / `ntime` / `nonce`, `ntime out of range`, `malformed submit params`, `too many shares in flight`, `Unknown method` | malformed or out-of-range request |
| 21 | `job not found` | stale: the job belongs to an older block (or is more than 16 jobs old) |
| 22 | `duplicate share` | the same extranonce2/ntime/nonce was already submitted, or the same header on this parent |
| 23 | `low difficulty share of 1234.56` | ticket above the share target (the number is the ticket's difficulty) |
| 24 | `unauthorized worker` | submit before a successful authorize |
| 25 | `not subscribed` | submit before subscribe |

A few stale or duplicate shares around block changes are normal. Code 23 on every share means the ticket computation, the header assembly or a byte order is wrong; check them against section 9.

## 8. Limits and protection

- At most 50 messages per second and 16 unanswered submits per connection; a client line longer than 10 KB, invalid JSON or more than 50 messages per second closes the connection.
- A client that stops reading (more than 4 MB of unsent data) is disconnected.
- If 50 % or more of the last 100 shares of a connection are invalid, its IP is banned for 10 minutes.
- A connection that has sent no share for 600 s is closed at the next job.
- Idle TCP connections are closed after 600 s.

## 9. Test vector

The session above (the pool on port 7802, difficulty 20000) with `extranonce2 = 0000000000000000` and `ntime = 6af447ee`. The full messages, including the complete `set_model` line, are in https://neb.suprnova.cc/neb-stratum-example.json.

```
extranonce1  4e0000012317f237
coinbase     01010000000000000000000000000000000000000000000000000000000000000000ffffffff14411500004e0000012317f23700000000000000000200b33f71000000000104290170dc4dc035fdc27c5a1db5b7bc7c0e09aee08a28f37abb6256a01827b700e1f5050000000002270bbd6380c0c7022176e202083d5671ed6bb7bc0c27350bcbd134846ce87dd5
merkle root  480b7029aa6d97b2cc61edafb5ab59a8b1cd999b637e9b73e9fa5c1dfdccdba7   (= leaf, the branch is empty)
body_root    022746bed909155d8814ac149e210f23cdd7040f996dc242d092b215a2c92977

nonce 00000000
  header     01000000513d8d34b62ba1aa3ec4495006dfca9256560e07791c931d9483dccc2121e76c022746bed909155d8814ac149e210f23cdd7040f996dc242d092b215a2c92977ee47f46a8044cec300000000
  key        b1ca9209fee3ecb5a4f6579af92dea2021eda429fdf676132192dba783387b81
  ticket     44ec6b4e59d8c0b6d159588155687d709fbf6499d027b55d56276259932a1c8f   (not a share)

nonce 00002eb1   (the first share, found at the 11 954th attempt)
  header     01000000513d8d34b62ba1aa3ec4495006dfca9256560e07791c931d9483dccc2121e76c022746bed909155d8814ac149e210f23cdd7040f996dc242d092b215a2c92977ee47f46a8044cec3b12e0000
  key        06343b25ea077e3e5b322fa11b632433969add41d74e0e4784b38ec5e166c7fc
  ticket     eb366f7a7fa33f35b8138e5347533af03e0697726dc1a19b46fe288df0700200   (difficulty 26846.14 ≥ 20000: a share)
  submit     ["<address>.<worker>","10985","0000000000000000","6af447ee","00002eb1"]
```

Check your implementation step by step: the coinbase, then `body_root`, then the header bytes (note how `ntime` `6af447ee` and `nbits` `c3ce4480` appear little-endian in the header), then the key, and finally the ticket with the model from the example file and dataset slot 1.

### 9.1 nebhash v2 test vector

https://neb.suprnova.cc/neb-stratum-example-v2.json is the same exercise after the fork: a session with a KEP1 job (height
8800, dataset index 2, the fork model = the initial state of the upgrade seed `BLAKE3("neb-model-upgrade" || LE32(8800) || LE32(0))`,
13 000-byte `set_model`), the coinbase, body_root, header, key and ticket of nonce 0 and of the first share at difficulty 200, and the
accepted submit. Check your KEP1 ticket against it and the official vectors `vectors/nebhash2.txt` of NEB 0.3.0.

## 10. Checklist for a new miner

1. Load `neb-datasets.bin` and check its BLAKE3 hash.
2. Connect, subscribe, authorize; read lines with a persistent buffer (section 4).
3. On `set_model`: decode the base64, check the hash, store the model under `weights_hash`.
4. On `notify`: build coinbase → body_root → header for your extranonce2; take the model by `weights_hash` and the dataset by its index (field 11); run v1 or nebhash v2 by the dataset's network; on `clean_jobs` abort old work.
5. Roll the nonce (then extranonce2); compute `key = BLAKE3(header)` and the ticket; submit when `ticket ≤ share_target`.
6. Reproduce the test vector of section 9 before mining on the pool.

## 11. NEB 0.3.0 network upgrade (nebhash v2 from block 8800)

From block 8800 (about Friday 13 November 2026, 19:00 UTC; the height decides) both models of the chain are small
convolutional networks, **KEP1** (Kepler signals, from block 8800) and **RET2** (retinal scans at 64 × 64, normal or abnormal,
from block 8801), each starting from a new model. On this pool's protocol only the values change, not the messages:
`set_model` and `notify` name dataset index 2 or 3, the weights are 13 000 or 11 140 bytes, and the ticket is computed with
nebhash v2. One attempt costs much more than a v1 attempt (CPU about 2 ms for KEP1 and 7 ms for RET2 instead of 0.4 ms), so
hash rates drop by that factor and the difficulty follows; VarDiff adjusts each connection. A miner that only knows the v1
network or the old dataset file cannot mine after block 8799. The pool also hands every share that meets the chain's share
target to its node, so the training steps of this pool's miners become part of the chain (`spec/pool.md` section 7.6).
