Pearl (PRL) Stratum Protocol
v1.0Pool: prl.suprnova.cc Β· Algorithm: pearlhash (PoUW L1, GEMM + Plonky2 ZK) Β· Published: 2026-06-03
This document specifies the wire protocol the suprnova Pearl pool speaks on its stratum ports so third-party miner authors can implement compatible mining clients alongside the existing reference miners (SRBMiner-MULTI, pearl-miner).
The protocol is a custom dialect of JSON-RPC over plaintext or TLS-wrapped TCP, modeled on the named-parameters form used by Monero / XMRig but adapted to Pearl's PoUW workload. It is the same dialect used across the broader Pearl pool ecosystem, so a single miner implementation works against any compatible pool.
β Download canonical markdown source (STRATUM-PROTOCOL.md)
1. Endpoints#
| Region | Host | Ports |
|---|---|---|
| EU | prl.suprnova.cc | 3370 β 3374 |
| US | us.prl.suprnova.cc | 3370 β 3374 |
| APAC | apac.prl.suprnova.cc | 3370 β 3374 |
| Port | Difficulty mode | Starting share-difficulty | Encryption |
|---|---|---|---|
3370 | Fixed | 1,000,000 | Plain TCP |
3371 | Fixed | 4,000,000 | Plain TCP |
3372 | Fixed | 16,000,000 | Plain TCP |
3373 | VarDiff | 2,000,000 (adaptive 500 K β 10 T) | Plain TCP |
3374 | VarDiff | 2,000,000 (adaptive 500 K β 10 T) | TLS (use stratum+ssl://) |
Recommended for new miners: port3373(or3374for TLS). The VarDiff retargets every 30 s toward a 10-second-per-share cadence and has been tuned to handle anything from a single laptop GPU up to multi-petahash farms.
2. Transport#
- TCP, single long-lived connection per worker. No connection pooling, no multiplexing.
- TLS only on port
3374(TLS 1.2+, modern cipher suite β same TLS stack as nginx). Plain TCP on the others. Usestratum+ssl://in miner URLs that distinguish the two. - Framing: newline-delimited JSON. One JSON value per line, terminated with
\n(0x0A). No length prefix. - Encoding: UTF-8.
- Maximum line length: 600 KiB. The pool accepts any
mining.submit.params.plain_proofpayload up to 600,000 base-64 characters (~ 450 KB raw). The reference miner produces ~137 KB base64 (~102 KB raw); other miners (e.g. SRBMiner-MULTI 3.3.7 on H100) that choose larger(m, n, k, rank)tuples within the Β§7.1 sanity bounds may produce substantially larger PlainProofs and are accepted. The pool's compressed Plonky2 ZKProof — never the raw PlainProof — is what goes on-chain, and that must still satisfy the network'sMaxZKProofSize = 60,000byte cap. - Idle / keepalive: there is no application-level keepalive. The pool will not close an idle connection on its own; TCP keepalive on the OS is sufficient. Reconnection is at the miner's discretion.
3. JSON-RPC dialect#
The protocol is a custom flavour of JSON-RPC 2.0:
- The
"jsonrpc": "2.0"field is optional and typically omitted by both client and server. - Request IDs are numeric integers. They do not need to be monotonic; you may reuse IDs (each method has only one outstanding response at a time per connection).
- Server-pushed notifications use
"id": nullto signal "no response expected". paramsis always an object (named parameters), never an array. Positional-array params will be rejected with error code20(params must be an object).- Method names are dotted lowercase:
mining.authorize,mining.notify,mining.submit.
Response shapes
Success:
{"error": null, "id": <req_id>, "result": <value>}
Error:
{"error": {"code": <int>, "msg": "<text>"}, "id": <req_id>, "result": null}
Standard error codes
| Code | msg | Cause |
|---|---|---|
| 20 | method not supported | Unknown method value |
| 20 | params must be an object | params was an array, not a named object |
| 21 | stale job | job_id no longer matches the current template |
| 22 | duplicate share | Identical (job_id, plain_proof_digest) already submitted |
| 23 | low difficulty share | Submitted proof's jackpot hash does not meet the issued target |
| 24 | wallet is missing | mining.authorize called without a wallet field |
| 25 | invalid wallet | wallet is not a valid bech32m PRL address |
| 26 | invalid proof | plain_proof failed sanity / Merkle / sample-GEMM verification |
| 27 | unauthorized | mining.submit received before successful mining.authorize |
4. Client β Server messages#
4.1 mining.authorize
Sent once per connection, immediately after TCP connect (and after TLS handshake if applicable). There is no mining.subscribe step β go straight to authorize.
{
"id": 1,
"method": "mining.authorize",
"params": {
"wallet": "prl1pa9xwhukl2zse55lgvphxkjejl7draruuls22x2p2kwvqfhk7t2rqkj2cpl",
"worker": "rig1",
"pass": "x",
"agent": "my-pearl-miner/0.1.0"
}
}
Required
wallet(string) β bech32m PRL address (HRPprl, taproot witness v1). Server validates with error code 25 if malformed, 24 if absent.
Optional
worker(string) β display label for this rig / GPU group. Defaults todefaultif omitted.pass(string) β see Β§6 for thed=Ncustom-difficulty syntax. May be omitted (treated as empty).agent(string) β miner user-agent. Cosmetic; used for per-miner statistics.
Gluedwallet.workerform (compatibility): some miners (e.g. SRBMiner-MULTI) place the worker name in thewalletfield separated by a dot βprl1...44v.rtx3090. The pool splits at the first.and treats the suffix as the worker name; any explicitly providedworkerfield still wins. This makes the pool drop-in compatible with miners that do not expose a separate worker CLI flag.
Authorization timeout: the pool gives clients 300 seconds (5 minutes) between TCP connect and the mining.authorize request. This generous window accommodates Plonky2 circuit cold-start (90β180 s on most Blackwell/Ada GPUs). Connections that have not authorized within 300 s are closed.
Pool's response sequence after authorize
- Pool pushes a
mining.notifywith the current job ("id": null) β before the authorize ack. - Pool sends the
mining.authorizeack:{"error": null, "id": 1, "result": true}.
A correct miner implementation must accept an unsolicited mining.notify before the authorize ack and stash the job. This is the same ordering as most XMRig-dialect pools.
4.2 mining.submit
{
"id": 10,
"method": "mining.submit",
"params": {
"job_id": "32fc29f1_500000",
"plain_proof": "AAACAAAAAAAAAAIAAAAAAAAQAAAAAAAAAAEAAAAAAAA...<base64>..."
}
}
Required
job_id(string) β must match the most recentmining.notify.params.job_idthe client received. Submitting against a previousjob_idyields error 21 (stale job).plain_proof(base64 string) β base64-encoded binaryPlainProofstructure. See Β§7 for the binary layout. The pool re-verifies the proof's sanity, Merkle commitments, and jackpot hash before accepting.
Notes
- There is no separate
nonce,extranonce, orresult_hashfield. The randomness lives inside thePlainProof(in the sampled rows/columns and thenoise_rankβnoise_seedderivation). The miner picks its own randomness; the pool re-derives the public-data commitment from the header bytes it gave the miner and checks consistency. - One
mining.submitper accepted share. The pool does not accept multi-share batches. - The pool responds before queueing the share for downstream accounting; ack latency is ~5β10 ms on a healthy connection.
4.3 No other client β server methods
The pool does not implement mining.subscribe, mining.set_difficulty, mining.ping, keepalived, or XMRig-style login. All four return error code 20 (method not supported).
5. Server β Client messages#
5.1 mining.notify
{
"id": null,
"method": "mining.notify",
"params": {
"header": "00004020b6ed5da6...3aedfdbd4eb7c8a7...200e480bc06a1d6ae1820118",
"height": 67204,
"job_id": "32fc29f1_500000",
"target": "000000000000218dcdb37c99ae924f227d028a1dfb9389b52007dd441355475a"
}
}
Fields
header(76-byte hex string, lowercase, unspaced) β incomplete Pearl block header. The full header is 108 bytes; the trailing 32-byteProofCommitmentis what the miner constructs while mining (from the public-data section of its PlainProof). The pool supplies only the first 76 bytes.Layout (all multi-byte fields are little-endian on the wire):
Offset Size Field Notes 0 4 versionu32 LE. 0x20400000for PoUW V1 blocks.4 32 prev_block_hashInternal LE storage; displayed big-endian by block explorers. 36 32 merkle_rootCoinbase + tx merkle root. 68 4 timestampu32 LE Unix seconds. Miner MAY roll within Β±2 hours. 72 4 bitsu32 LE compact nBits β the network target. height(integer) β height of the block being mined (current tip + 1).job_id(string) β opaque identifier of the form<8 hex chars>_<integer>. The leading 8 hex characters are a per-template nonce; the trailing integer reflects the difficulty band the pool selected for this push. Miners should treat the entire value as opaque and echo it back unchanged inmining.submit.target(64-hex-char string, U256 big-endian) β share-acceptance threshold. A share is accepted iff the PlainProof's computed jackpot hash, interpreted as little-endian U256, is β€ the target interpreted as little-endian U256. On VarDiff ports (3373,3374), this target adapts per worker. On fixed ports (3370β3372), it is constant for the duration of the connection and reflects the port's published difficulty.
Cadence
- One
mining.notifyis pushed immediately aftermining.authorize(before the auth ack). - A new
mining.notifyis pushed whenever:- The pool advances to a new block template (new tip or BIP22 long-poll trigger from
pearld). - The pool's coinbase or memory pool composition changes meaningfully.
- The pool's VarDiff algorithm retargets the per-worker share difficulty.
- The pool advances to a new block template (new tip or BIP22 long-poll trigger from
- The pool does not issue
mining.set_difficultyas a separate method. New difficulty is always conveyed by the newtargetin amining.notify.
5.2 Response acks
Both mining.authorize and mining.submit are acknowledged with:
{"error": null, "id": <req_id>, "result": true}
A result: true means accepted. Rejections come back with the error shape from Β§3 β for instance, a stale share looks like:
{"error": {"code": 21, "msg": "stale job"}, "id": 10, "result": null}
6. Custom difficulty via the pass field#
On the VarDiff ports (3373, 3374), miners may pin their share difficulty by passing a d=N directive in the pass field of mining.authorize:
"pass": "d=50000000" // pin at 50,000,000
Range: 500,000 β€ N β€ 10,000,000,000,000 (500 K to 10 T). Values outside this range are clamped silently.
Effect
- The pool uses
Nas the starting share difficulty for this worker. - VarDiff retargeting continues to operate from that starting point β i.e. if the miner is much faster or slower than
Nimplies, VarDiff will still drift toward the 10-second-per-share target. - To suppress VarDiff entirely, prefer one of the fixed ports (
3370β3372).
SRBMiner-MULTI example
./SRBMiner-MULTI --algorithm pearlhash \
--pool prl.suprnova.cc:3373 \
--wallet prl1pa9xwhukl...rqkj2cpl \
--worker rtx4090-rig1 \
--password "d=8000000"
The pool also accepts d=N on fixed ports but ignores it there.
7. PlainProof binary structure#
The plain_proof field in mining.submit carries a base64-encoded binary blob. Size depends on the mining-config dimensions (m, n, k, rank) chosen by the miner — any tuple within the Β§7.1 sanity bounds is legal. At the reference config m=512, n=512, k=4096, noise_rank=256 the decoded length is exactly 102,512 bytes (~137 KB base64). Larger-rank / larger-k tuples (e.g. those preferred by SRBMiner-MULTI 3.3.7 on H100) can produce decoded sizes up to ~450 KB (~ 600,000 base-64 chars). The pool accepts the full range; any submission whose base-64 representation exceeds 600,000 chars is rejected with code 25 for DoS protection. Note: the compressed Plonky2 ZKProof derived from this PlainProof — not the PlainProof itself — is what is embedded in the on-chain certificate, and that compressed form must satisfy the network's MaxZKProofSize = 60,000 byte cap (see pearl/node/wire/certificate.go).
Top-level layout (little-endian throughout)
pub struct PlainProof {
m: u64, // 8 bytes LE
n: u64, // 8 bytes LE
k: u64, // 8 bytes LE
noise_rank: u64, // 8 bytes LE
a: MatrixMerkleProof, // sampled rows of matrix A
bt: MatrixMerkleProof, // sampled columns of B^T
}
The first 32 bytes are the dimensional preamble. Example decoded preamble at the standard config:
00 02 00 00 00 00 00 00 β m = 512
00 02 00 00 00 00 00 00 β n = 512
00 10 00 00 00 00 00 00 β k = 4096
00 01 00 00 00 00 00 00 β noise_rank = 256
MatrixMerkleProof layout
Each MatrixMerkleProof is a variable-length structure encoding (a) the sampled int8 entries of the relevant matrix and (b) the Blake3 Merkle inclusion proofs binding those entries to the public commitment in the block header.
struct MatrixMerkleProof {
leaf_count: u64 LE, // number of leaves committed to (rows for A, cols for B^T)
sample_count: u64 LE, // number of samples in this proof
samples: [Sample; sample_count],
siblings: [Blake3; variable], // packed Merkle siblings (Blake3 32-byte hashes)
}
struct Sample {
index: u64 LE, // row or column index in the original matrix
values: [i8; k], // the actual int8 vector (k entries)
}
type Blake3 = [u8; 32]; // Blake3 hash, raw bytes
The samples block in a carries sample_count rows of A (each k int8s long). The samples block in bt carries sample_count columns of B (each k int8s long, presented row-major over B^T). The siblings arrays carry just enough Blake3 sibling hashes for a verifier to reconstruct the Merkle root over all leaves and compare it to the public-data commitment in the block header.
Sanity constraints
The pool rejects with error code 26 (invalid proof) if any of these constraints fail:
noise_rankis a power of two in{32, 64, 128, 256, 512, 1024}.k β₯ 1024andk % 64 == 0.k β₯ 16 Γ noise_rank(covers the noise-injection capacity).k β€ 4 Γ noise_rankΒ²(covers the noise-injection budget).m, n β€ 2Β²β΄.- Decoded byte size matches the template the pool currently issues (the size is deterministic given
m, n, k, noise_rank).
What the pool verifies
The pool performs share-time verification that is intentionally cheap (~5β10 ms per share):
- Sanity-check the preamble (above).
- Reconstruct the
job_keyfrom the header bytes the miner used. - Re-derive the noise seeds from
job_key. - Verify the
MatrixMerkleProofforAβ every sampled row's Blake3 leaves chain to the public commitment in the header. - Same for
B^T. - Re-compute the jackpot value from the sampled rows/columns plus the derived noise (a small
sample_count Γ sample_count Γ kint8 GEMM β milliseconds on CPU). - Re-hash the jackpot with Blake3 keyed by the noise seed.
- Compare against the issued
target.
The full m Γ n Γ k GEMM and the heavy Plonky2 proof generation stay with the miner. The pool only generates the full Plonky2 proof when a share also clears the network target β i.e. it is a real block, at which point the pool calls submitblock on pearld.
Reference implementation pointers
The canonical PlainProof layout, sanity constraints, and verification logic live in the open-source Pearl node source tree:
- Rust struct:
pearl/zk-pow/src/ffi/plain_proof.rs - Sanity checks:
pearl/zk-pow/src/api/sanity_checks.rs - Public-data derivation:
pearl/zk-pow/src/api/proof.rs::PublicProofParams - Noise seed derivation:
pearl/zk-pow/src/circuit/pearl_noise.rs
A working serializer can be lifted from the SRBMiner-MULTI source or from the open-source pearl-miner reference implementation; both produce byte-identical output for the same input.
8. Connection lifecycle#
1. Client TCP-connects (and TLS-handshakes on port 3374)
2. Client β mining.authorize (NO subscribe step; submit within 300 s)
3. Server β mining.notify (pushed FIRST, "id": null)
4. Server β authorize ack ({"error":null, "id":<from 2>, "result":true})
--- now mining ---
5. Server β mining.notify (on new template / VarDiff retarget)
6. Client β mining.submit (when miner finds a share that meets target)
7. Server β submit ack (true or error code 21/22/23/26)
loop 5β7
8. Either side closes TCP. Client SHOULD reconnect with exponential backoff
(1 s, 2 s, 4 s, capped at ~30 s).
Reconnection policy for well-behaved miners
- On clean connection close: reconnect after 1 s.
- On TCP error or TLS error: reconnect with exponential backoff (1, 2, 4, 8, 16, 30, 30, β¦).
- On error code 25 (
invalid wallet): do not auto-reconnect. The wallet is wrong; surface the error to the user. - On any other server-side error: continue with the standard backoff.
Grace period: for 30 s after a VarDiff retarget, the pool will accept shares at the previous difficulty as well as the new one. This avoids losing the work-in-flight when a retarget races with an outbound share.
9. Worked example#
A complete authorize β notify β submit β ack transcript (whitespace added for readability β the wire is one line per JSON value):
[client β server]
{"id":1,"method":"mining.authorize","params":{
"wallet":"prl1pa9xwhukl2zse55lgvphxkjejl7draruuls22x2p2kwvqfhk7t2rqkj2cpl",
"worker":"rtx4090-1",
"pass":"d=8000000",
"agent":"my-pearl-miner/0.1.0"
}}
[server β client]
{"id":null,"method":"mining.notify","params":{
"header":"00004020b6ed5da6...c06a1d6ae1820118",
"height":67204,
"job_id":"32fc29f1_8000000",
"target":"00000000000010cda8a44d2b...3f"
}}
[server β client]
{"error":null,"id":1,"result":true}
... miner crunches GEMM ...
[client β server]
{"id":10,"method":"mining.submit","params":{
"job_id":"32fc29f1_8000000",
"plain_proof":"AAACAAAAAAAAAAIAAAAAAAAQ...<~137 KB base64>...=="
}}
[server β client]
{"error":null,"id":10,"result":true}
10. Compatibility & versioning#
This document describes stratum protocol version 1.0, the same dialect SRBMiner-MULTI 3.3.2+ speaks and the same dialect used by pearl-miner v11+. A miner implementing this spec will work against any compatible pool in the Pearl ecosystem.
Stability guarantees
- The four field names in
mining.authorize.params(wallet,worker,pass,agent) and the four field names inmining.notify.params(header,height,job_id,target) and the two field names inmining.submit.params(job_id,plain_proof) will not be renamed within v1.x. - New optional fields may be added; clients MUST ignore unrecognised fields.
- The PlainProof binary layout is governed by the Pearl protocol itself (not the pool); changes there are driven by upstream Pearl PoUW upgrades and will be coordinated through the Pearl developer channels.
- Error codes 20β29 are reserved for the protocol. Pool-specific codes start at 100.
Suggested minimum miner behaviours for spec conformance
- Accept an unsolicited
mining.notifybefore the authorize ack. - Re-target whenever a new
targetarrives (do not drop in-flight shares β submit them anyway; the 30 s grace period covers a single retarget overlap). - Reconnect with exponential backoff on any error other than code 25.
- Honour the 600 KiB max line length when buffering server messages (was 256 KiB — raised 2026-06-10 to accommodate larger-rank PlainProofs from miners like SRBMiner-MULTI 3.3.7).
11. Reference miners#
These are the currently known open / publicly distributed Pearl miners. They all speak this exact protocol against the suprnova pool:
- SRBMiner-MULTI β CPU+GPU, closed-source, the de-facto reference.
--algorithm pearlhash. - pearl-miner β open-source CUDA miner.
If you ship a new miner, we'd love to add it here. Open a PR against this document or email mining@suprnova.cc (subject: "Stratum spec β new miner").
12. Changelog#
| Version | Date | Notes |
|---|---|---|
| 1.0 | 2026-06-03 | Initial public specification. |
Copyright Β© suprnova.cc. This document is published openly under CC-BY-4.0 to encourage development of additional Pearl mining clients.