Pearl (PRL) Stratum Protocol

v1.0

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

RegionHostPorts
EU prl.suprnova.cc 3370 – 3374
US us.prl.suprnova.cc 3370 – 3374
APAC apac.prl.suprnova.cc 3370 – 3374
PortDifficulty modeStarting share-difficultyEncryption
3370Fixed 1,000,000 Plain TCP
3371Fixed 4,000,000 Plain TCP
3372Fixed 16,000,000 Plain TCP
3373VarDiff 2,000,000 (adaptive 500 K β€” 10 T)Plain TCP
3374VarDiff 2,000,000 (adaptive 500 K β€” 10 T)TLS (use stratum+ssl://)
Recommended for new miners: port 3373 (or 3374 for 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. Use stratum+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_proof payload 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's MaxZKProofSize = 60,000 byte 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": null to signal "no response expected".
  • params is always an object (named parameters), never an array. Positional-array params will be rejected with error code 20 (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

CodemsgCause
20method not supportedUnknown method value
20params must be an objectparams was an array, not a named object
21stale jobjob_id no longer matches the current template
22duplicate shareIdentical (job_id, plain_proof_digest) already submitted
23low difficulty shareSubmitted proof's jackpot hash does not meet the issued target
24wallet is missingmining.authorize called without a wallet field
25invalid walletwallet is not a valid bech32m PRL address
26invalid proofplain_proof failed sanity / Merkle / sample-GEMM verification
27unauthorizedmining.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 (HRP prl, 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 to default if omitted.
  • pass (string) β€” see Β§6 for the d=N custom-difficulty syntax. May be omitted (treated as empty).
  • agent (string) β€” miner user-agent. Cosmetic; used for per-miner statistics.
Glued wallet.worker form (compatibility): some miners (e.g. SRBMiner-MULTI) place the worker name in the wallet field separated by a dot β€” prl1...44v.rtx3090. The pool splits at the first . and treats the suffix as the worker name; any explicitly provided worker field 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

  1. Pool pushes a mining.notify with the current job ("id": null) β€” before the authorize ack.
  2. Pool sends the mining.authorize ack: {"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 recent mining.notify.params.job_id the client received. Submitting against a previous job_id yields error 21 (stale job).
  • plain_proof (base64 string) β€” base64-encoded binary PlainProof structure. 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, or result_hash field. The randomness lives inside the PlainProof (in the sampled rows/columns and the noise_rank β‡’ noise_seed derivation). 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.submit per 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-byte ProofCommitment is 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):

    OffsetSizeFieldNotes
    0 4 version u32 LE. 0x20400000 for PoUW V1 blocks.
    4 32 prev_block_hash Internal LE storage; displayed big-endian by block explorers.
    36 32 merkle_root Coinbase + tx merkle root.
    68 4 timestamp u32 LE Unix seconds. Miner MAY roll within Β±2 hours.
    72 4 bits u32 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 in mining.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.notify is pushed immediately after mining.authorize (before the auth ack).
  • A new mining.notify is 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 does not issue mining.set_difficulty as a separate method. New difficulty is always conveyed by the new target in a mining.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 N as 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 N implies, 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_rank is a power of two in {32, 64, 128, 256, 512, 1024}.
  • k β‰₯ 1024 and k % 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):

  1. Sanity-check the preamble (above).
  2. Reconstruct the job_key from the header bytes the miner used.
  3. Re-derive the noise seeds from job_key.
  4. Verify the MatrixMerkleProof for A β€” every sampled row's Blake3 leaves chain to the public commitment in the header.
  5. Same for B^T.
  6. Re-compute the jackpot value from the sampled rows/columns plus the derived noise (a small sample_count Γ— sample_count Γ— k int8 GEMM β€” milliseconds on CPU).
  7. Re-hash the jackpot with Blake3 keyed by the noise seed.
  8. 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 in mining.notify.params (header, height, job_id, target) and the two field names in mining.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.notify before the authorize ack.
  • Re-target whenever a new target arrives (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#

VersionDateNotes
1.02026-06-03Initial public specification.

Copyright Β© suprnova.cc. This document is published openly under CC-BY-4.0 to encourage development of additional Pearl mining clients.