Keryx (KRX) Stratum Protocol

v1.2

Pool: krx.suprnova.cc  Β·  Algorithm: keryx-hash (kHeavyHash + KERYX_MATRIX_SALT_V4 + WaveMix ARX, KeryxHash matmul)  Β·  Published: 2026-06-08  Β·  Updated: 2026-07-06 (H3)

This document specifies the wire protocol the suprnova Keryx pool speaks on its stratum ports so third-party miner authors can implement compatible mining clients alongside the reference keryx-miner.

Keryx pools speak the kaspad-dialect stratum β€” JSON-RPC 2.0 with positional-array params (the convention shared by kaspad / nautilus / Karlsenkoin pools), plus a mandatory Optimistic Proof of Inference (OPoI) tag on every submitted share. The OPoI tag is a deterministic 16-hex fingerprint derived from the share nonce via a fixed-point MLP; it is what makes the chain's per-share Proof-of-Useful-Work property work without slowing share verification.


1. Endpoints#

RegionHostPorts
EU krx.suprnova.cc 4401 – 4404
PortDifficulty modeStarting share-difficultyRange
4401VarDiff Low 8 1 – 10,000  (CPU / laptop / small GPU)
4402Fixed 4 β‰ˆ 1.7 GH/s sweet spot
4403Fixed 128 β‰ˆ 55 GH/s sweet spot
4404VarDiff High 1024 64 – 10,000,000  (recommended for GPU rigs)
Recommended for new miners: port 4404. The VarDiff retargets every 60 s toward a 10-second-per-share cadence and handles anything from a single GPU up to multi-rig farms. For very small workers (sub-GH/s) use 4401 instead β€” it has a much lower starting share-difficulty.
No TLS: all four stratum ports are plain TCP. The pool does not currently offer a TLS endpoint. The HAProxy edge in front of the pool injects a PROXY v1 line so the pool sees the real client IP; that wrapping is invisible to miners.

2. Transport#

  • TCP, single long-lived connection per worker. No connection pooling, no multiplexing.
  • Framing: newline-delimited JSON. One JSON value per line, terminated with \n (0x0A). No length prefix.
  • Encoding: UTF-8.
  • Maximum line length: 16 KiB. Stratum lines are small (a few hundred bytes); this limit is just to bound a misbehaving client.
  • Idle / keepalive: the pool will close any connection that goes 15 minutes without a valid share (the standard kaspad-pool watchdog). Miners that legitimately produce slower than one share per 15 min should drop to 4401 where the share-difficulty floor is 1.

3. JSON-RPC dialect (kaspad-style)#

The protocol is a kaspad-dialect of JSON-RPC 2.0 β€” the same dialect used by Kaspa / Karlsenkoin / Nautilus / Pyrin pools and inherited unchanged by the Keryx ecosystem:

  • 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.
  • Server-pushed notifications use "id": null.
  • params is always a positional ARRAY, never an object. (This is the key difference from XMRig-dialect pools β€” get this wrong and the miner fails to parse the very first mining.notify.)
  • Method names are dotted lowercase: mining.subscribe, mining.authorize, mining.notify, mining.set_difficulty, mining.submit.
  • Numeric fields that exceed JavaScript's safe-integer range (the headerSeed u64 words, the daaScore) are emitted as raw JSON numbers, not strings. Parse them with a BigInt-aware JSON parser.

Response shapes

Success:

{"id": <req_id>, "result": <value>, "error": null}

Error:

{"id": <req_id>, "result": null, "error": [<code>, "<message>", null]}

The kaspad error format is a 3-element array ([code, message, data]), not the JSON-RPC-2 object form. The third element is reserved for additional data and is currently always null.

Pool error codes

CodeMessageCause
20Other / unknownUnspecified pool-side error
21Job not foundjob_id no longer matches any cached template (stale)
22Duplicate shareIdentical (jobId, nonceHex, wallet) already submitted
23Low difficulty shareHash does not meet the worker's current share-target
24Unauthorized workermining.submit arrived before successful authorize, or OPoI tag missing/wrong length
25Not subscribedmining.submit arrived before mining.subscribe
26Invalid OPoI tagThe 16-hex opoiTag does not match the pool-recomputed tag_fixed(nonce)
27Invalid / missing PoM proofPost-fork only (DAA β‰₯ 37,780,000): the 6th mining.submit param (pomProofHex) was absent, malformed, or failed verification. See Β§12.

4. Client β†’ Server messages#

4.1 mining.subscribe

Sent first on every fresh connection, before mining.authorize. Carries the miner's user-agent string (and optionally a session-id hint that the pool ignores).

{"id": 1, "method": "mining.subscribe", "params": ["my-keryx-miner/0.3.2"]}

The pool responds with a kaspad-style ack:

{
  "id": 1,
  "result": [
    [ ["mining.set_difficulty", "<subId>"],
      ["mining.notify",         "<subId>"] ],
    "<extranonce_hex>",
    <ext2_size>
  ],
  "error": null
}

Fields in the result tuple (in this exact order):

  1. Subscription channels array (informational; miners ignore it but it must be present).
  2. extranonce_hex β€” currently always "00" (1 byte of extranonce-1 reserved for the pool, 0 used). A miner that wants to be future-proof should still concatenate this with its own extranonce-2 before building the nonce.
  3. ext2_size β€” number of bytes of extranonce the miner controls inside its 64-bit nonce. With extranonce_hex = "00" this is 7.

4.2 mining.authorize

Sent second, immediately after the subscribe ack. Carries the wallet (CashAddr) and password.

{
  "id": 2,
  "method": "mining.authorize",
  "params": [
    "keryx:qp0vrxc0k5w0pcyem6vau2pjgztje880tsm239rywtm7l7uv2pcxzq55n8khs.rtx4090",
    "x"
  ]
}

Params (positional)

  1. username (string) β€” a Keryx CashAddr address optionally followed by .<worker>. See Β§5 for the address format.
  2. password (string) β€” either "x" (no preference, let VarDiff drive) or a directive such as "d=4096" to pin the starting share-difficulty on the VarDiff High port. See Β§7.

The pool responds with {"id":2,"result":true,"error":null}. From this point the pool will start pushing mining.set_difficulty and mining.notify.

4.3 mining.submit

{
  "id": 10,
  "method": "mining.submit",
  "params": [
    "keryx:qp0vr...n8khs.rtx4090",
    "1a7c",
    "00000000deadbeef",
    "8f6cf7a4732dbe13",
    "",
    "0100ad50ad0bd461d8ab44efc0214989eb3..."
  ]
}

Params (positional)

  1. worker (string) β€” the same username that was authorized (with the same optional .<worker> suffix).
  2. jobId (string) β€” must match the latest mining.notify[0] the client received. Stale submissions return error 21.
  3. nonceHex (string) β€” 16 hex characters, little-endian u64 nonce that satisfies the share target.
  4. opoiTag (string) β€” mandatory. 16 hex characters = the 8-byte tag_fixed(nonce) output from the Keryx OPoI fixed-point MLP. The pool recomputes it; a mismatch returns error 26.
  5. ipfsCID (string, optional) β€” empty string or a CID of an inference result the miner has uploaded. Logged for audit; not required, not validated. Send "" if you have nothing to publish.
  6. pomProofHex (string, optional pre-fork; required post-fork) β€” lowercase hex of a Borsh-encoded PomProof blob proving possession of the declared tier's model weights. Required at DAA β‰₯ 37,780,000 (Fri 2026-06-26 18:00 UTC). Empty string or omitted pre-fork is accepted. See Β§12 for the full PoM extension spec.

Computing the OPoI tag

The OPoI tag is a deterministic 3-layer fixed-point MLP (32 β†’ 256 β†’ 128 β†’ 32) over the share nonce. It is the cheapest part of the Proof-of-Useful-Work scheme β€” completing it takes microseconds β€” but a missing or wrong tag invalidates the share. Reference implementations:

  • Rust: keryx-node/inference/src/model_fixed.rs::tag_fixed(nonce: u64) -> [u8; 8]
  • Go (bridge): keryx-stratum-bridge/src/keryxstratum/opoi.go
  • The official keryx-miner bakes it in via the keryx-inference crate.

4.4 No other client β†’ server methods

The pool does not implement mining.suggest_difficulty, mining.ping, keepalived, XMRig-style login, or any object-params variant. All return error 20.

5. Address format#

Keryx uses the CashAddr encoding (the same family Kaspa uses), not Segwit / bech32m. The prefix is colon-delimited from the payload:

NetworkPrefixExample
Mainnet keryx: keryx:qp0vrxc0k5w0pcyem6vau2pjgztje880tsm239rywtm7l7uv2pcxzq55n8khs
Testnet-10keryxtest:keryxtest:qpkzggeyp0v7l7mx9nnems2l474v62la6dwr5kqefuu4s7j3q4cqs5f9f9ta9
Simnet keryxsim: keryxsim:...
Devnet keryxdev: keryxdev:...

The pool refuses non-mainnet prefixes on the production stratum (4401-4404) and returns error 24 (Unauthorized worker) at authorize-time.

Glued address.worker form: kaspad-style miners (including keryx-miner) concatenate the worker name onto the wallet with a . β€” e.g. keryx:qp…n8khs.rtx4090. The pool splits at the first . (CashAddr never contains one) and treats the suffix as the worker name.

6. Server β†’ Client messages#

6.1 mining.set_difficulty

{"id": null, "method": "mining.set_difficulty", "params": [1024]}

Pushed whenever the pool's VarDiff (or the operator-pinned fixed difficulty) changes. The single parameter is the new share-difficulty as a positive integer. The pool always sends one mining.set_difficulty before the first mining.notify on a new connection.

6.2 mining.notify

{
  "id": null,
  "method": "mining.notify",
  "params": [
    "1a7c",
    [
      11181278953413291283,
      4793928374829834912,
      9123847123847128371,
      6234782634872364871
    ],
    1717862400,
    17275842
  ]
}

Params (positional, all required)

  1. jobId (string) β€” short opaque identifier. Echo it back unchanged in mining.submit.
  2. headerSeed (array of 4 raw-JSON u64 numbers) β€” the canonical 32-byte pre_pow_hash split into four little-endian u64 words. Re-emitted by the pool as raw JSON numbers (not strings) and not quoted; use a BigInt-aware parser.
  3. timestamp (integer) β€” Unix seconds at which the template was rolled. Miners MAY roll this within Β±2 hours.
  4. daaScore (integer or raw-JSON u64) β€” the DAA score of the next block, used by the share-target / network-target math. Carries the salt-fork activation signal: at daaScore β‰₯ 21,932,751 the chain is on SALT v4.

Cadence

  • One mining.notify is pushed immediately after mining.authorize succeeds.
  • The pool subscribes to keryxd's newBlockTemplate wRPC notification β€” a fresh template arrives within milliseconds of any tip change.
  • A 250 ms polling backstop guarantees coverage if a notification is missed.
  • There is no "clean" / "force-restart" boolean as the 4th param β€” that's a Stratum-V1 convention from the Bitcoin pool world and is not present here. The miner should always restart its hashing loop on a new mining.notify.

6.3 Acks

mining.submit is acknowledged with one of:

{"id":10,"result":true,"error":null}                                    ← accepted share
{"id":10,"result":null,"error":[21,"Job not found",null]}              ← stale
{"id":10,"result":null,"error":[23,"Low difficulty share",null]}       ← below share target
{"id":10,"result":null,"error":[26,"Invalid OPoI tag",null]}           ← tag mismatch

If the share also clears the chain's network target, the pool internally calls submitBlock on keryxd and emits a Slack-style "Block found!" event to the public block feed (visible on the Blocks page). The miner still just sees {"result":true,"error":null}.

7. Custom difficulty via the password field#

On the VarDiff High port (4404), miners may pin their starting share difficulty by passing a d=N directive in the password parameter of mining.authorize:

"password": "d=4096"   ← pin start at 4096

Range: 64 ≀ N ≀ 10,000,000. Values outside this range are silently ignored and VarDiff takes over from its default starting point.

Effect

  • The pool uses N as the starting share difficulty for this worker.
  • VarDiff retargeting continues to operate; if the miner's actual hashrate doesn't match N, the share-rate will drift toward the 10-second-per-share target.
  • To suppress VarDiff entirely, use one of the fixed ports (4402 d=4 or 4403 d=128).
  • On the fixed ports the d=N directive is parsed but ignored.

keryx-miner example

./keryx-miner --mining-address \
  --pool krx.suprnova.cc:4404 \
  --wallet keryx:qp0vr...n8khs \
  --worker rtx4090 \
  --password "d=4096"

8. Connection lifecycle#

1. Client TCP-connects to krx.suprnova.cc:<port>
2. Client β†’ mining.subscribe          (params=[user_agent])
3. Server β†’ subscribe ack             ([[channels…], extranonce, ext2_size])
4. Client β†’ mining.authorize          (params=[wallet[.worker], password])
5. Server β†’ authorize ack             (result: true)
6. Server β†’ mining.set_difficulty     (params=[diff])
7. Server β†’ mining.notify             (params=[jobId, headerSeed, ts, daaScore])
   --- now mining ---
8. Server β†’ mining.notify             (on every new template / VarDiff retarget)
9. Server β†’ mining.set_difficulty     (only when the difficulty changes; usually
                                       bundled just before a notify)
10. Client β†’ mining.submit            (when miner finds a share that meets target)
11. Server β†’ submit ack               (true or error code 21/22/23/24/26)
    loop 8–11
12. Either side closes TCP.           Pool watchdog closes idle conns after
                                       15 min of no valid share.

Reconnection policy for well-behaved miners

  • On clean connection close: reconnect after 1 s.
  • On TCP error: reconnect with exponential backoff (1, 2, 4, 8, 16, 30, 30, …).
  • On error code 24 (Unauthorized worker) at authorize-time: do not auto-reconnect β€” the wallet is wrong; surface the error.
  • On any other server-side error: continue with the standard backoff.
Grace period: for 60 s after a VarDiff retarget, the pool accepts shares at the previous difficulty as well as the new one. This avoids losing in-flight work when a retarget races with an outbound share.

9. Worked example#

A complete subscribe β†’ authorize β†’ notify β†’ submit β†’ ack transcript (whitespace added for readability β€” the wire is one line per JSON value):

[client β†’ server]
{"id":1,"method":"mining.subscribe","params":["my-keryx-miner/0.3.2"]}

[server β†’ client]
{"id":1,"result":[[["mining.set_difficulty","s1"],["mining.notify","s1"]],"00",7],"error":null}

[client β†’ server]
{"id":2,"method":"mining.authorize","params":["keryx:qp0vr...n8khs.rtx4090","x"]}

[server β†’ client]
{"id":2,"result":true,"error":null}

[server β†’ client]
{"id":null,"method":"mining.set_difficulty","params":[1024]}

[server β†’ client]
{"id":null,"method":"mining.notify","params":[
   "1a7c",
   [11181278953413291283,4793928374829834912,9123847123847128371,6234782634872364871],
   1717862400,
   17275842
]}

  ... miner crunches keryx-hash + tag_fixed(nonce) ...

[client β†’ server]
{"id":10,"method":"mining.submit","params":["keryx:qp0vr...n8khs.rtx4090","1a7c","00000000deadbeef","8f6cf7a4732dbe13"]}

[server β†’ client]
{"id":10,"result":true,"error":null}

10. The OPoI tag in detail#

Keryx's per-share tag_fixed(nonce) is the cheap half of Optimistic Proof of Inference: a deterministic 3-layer fixed-point MLP (32 β†’ 256 β†’ 128 β†’ 32) over the salted share nonce. Output is the lower 8 bytes of the final hidden state, serialised as 16 lowercase hex characters.

  • The MLP weights are baked into the protocol (file keryx-node/inference/src/model_fixed.rs). They do not change between blocks.
  • Computing the tag for one nonce takes microseconds on CPU β€” it is not the work, just a fingerprint. The actual PoW work is the keryx-hash hashloop.
  • Every accepted share must include the correct opoiTag. The pool recomputes it and rejects mismatches with error 26.
  • This is what gives Keryx its Proof-of-Useful-Work property β€” the tag forces the miner to evaluate the inference model on the same nonce it claimed to hash, so it can't sidestep the inference step.

Larger AI inference tasks (the optional AiRequest path) are separate from the per-share tag. They are negotiated out-of-band over the same connection but are not described in this spec; they are not required for normal mining.

11. Reference miners#

Two published miners speak this protocol:

  • keryx-miner (upstream Keryx Labs) β€” CUDA + OpenCL, open source. Implements the base v1.0 protocol (no PoM extension): github.com/Keryx-Labs/keryx-miner.
  • keryx-miner-supr (Suprnova fork) β€” optimized CUDA build, ships the PoM extension since v0.6.1. Required for post-fork mining (Fri 2026-06-26 18:00 UTC, DAA 37,780,000), and v0.6.7+ is required post-H3 (DAA 43,450,000, 2026-07-05 β€” see Β§12.3): github.com/ocminer/keryx-miner-supr.

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. Proof-of-Model (PoM) hardfork extension#

At DAA 37,780,000 (Friday 2026-06-26 18:00 UTC), Keryx hard-forks to Proof-of-Model (PoM): every accepted block must carry a valid PomProof demonstrating the producer held a declared model's weights in VRAM while mining. After the +24 h grace period (Sat 2026-06-27 ~18:00 UTC), the pool refuses any mining.submit without a proof. This section is the wire spec for the proof envelope. Algorithmic details of the proof itself are in keryx-node/consensus/core/src/pom.rs (the canonical verifier β€” bit-exact with chain).

12.1 Wire envelope

The PoM extension is a 6th positional param on mining.submit β€” pomProofHex. Per the v1.x stability guarantees in Β§13, this is an additive extension: pre-v0.6 miners that send only the first 4 params keep working pre-fork. Post-fork (or whenever the operator flips pom.requireOnSubmit=true), submits without a 6th param are rejected with error code 24 or 27.

// pre-fork (still works)
{"id": 10, "method": "mining.submit", "params":
  ["keryx:qp0vr...n8khs.rtx4090", "1a7c", "00000000deadbeef", "8f6cf7a4732dbe13"]}

// pre-fork with 5th + 6th param attached (forward-compat path; pool stores the proof
// in the block body but does not validate it consensus-side yet)
{"id": 10, "method": "mining.submit", "params":
  ["keryx:qp0vr...n8khs.rtx4090", "1a7c", "00000000deadbeef", "8f6cf7a4732dbe13",
   "", "0100ad50ad0bd461d8ab44efc0214989eb3...<~96KB hex>"]}

// post-fork (mandatory β€” same shape; pool now consensus-validates the proof)
{"id": 10, "method": "mining.submit", "params":
  ["keryx:qp0vr...n8khs.rtx4090", "1a7c", "00000000deadbeef", "8f6cf7a4732dbe13",
   "", "0100ad50ad0bd461d8ab44efc0214989eb3...<~96KB hex>"]}

12.2 PomProof encoding

The proof is Borsh-serialized, then lowercase-hex-encoded. The canonical struct (from keryx-node/consensus/core/src/pom.rs):

pub struct PomProof {
    pub tier: u8,                              // index into POM_TIERS
    pub trace_root: [u8; 32],                  // Merkle root of state[0..=K]
    pub pow_value: [u8; 32],                   // claimed PoW value
    pub final_state: u64,                      // state[K]
    pub initial_trace_path: Vec<[u8; 32]>,     // binds seed β†’ trace
    pub final_trace_path:   Vec<[u8; 32]>,     // binds final_state β†’ trace
    pub openings: Vec<PomOpening>,             // POM_OPENINGS Fiat-Shamir openings
}

pub struct PomOpening {
    pub state_before: u64,
    pub chunk: [u8; 32],                       // 32-byte weight chunk
    pub weight_path: Vec<[u8; 32]>,            // Merkle path under R_T
    pub trace_path_before: Vec<[u8; 32]>,
    pub trace_path_after:  Vec<[u8; 32]>,
}

Typical size for the smallest tier (tier-0, EXAONE-4.0-1.2B-abliterated): ~48 KB raw / ~96 KB hex. Larger tiers (Mistral-7B-v0.3, GLM-4-9B, Qwen3.6-27B, Kimi-Linear-48B-abliterated) produce proofs of similar order β€” Merkle paths are logβ‚‚(N) in the model's chunk count.

Pool-side size cap: pom.maxProofHexBytes = 2 MB. Larger blobs are rejected with error 25 before they reach the verifier.

12.3 Tiers + activation

TierModelR_T chunksApprox. proof size (hex)
0EXAONE-4.0-1.2B-abliterated28,943,588~96 KB
1Mistral-7B-Instruct-v0.3-abliterated185,827,840~102 KB
2GLM-4-9B-0414-abliterated258,040,832~103 KB
3Qwen3.6-27B-abliterated516,762,688~106 KB
4Kimi-Linear-48B-A3B-abliterated927,994,064~108 KB

Canonical tier roots (32-byte R_T) and chunk counts (N) are pinned in keryx-node/consensus/core/src/config/params.rs (POM_TIERS). Constants:

  • POM_WALK_STEPS = 256 β€” data-dependent VRAM reads per attempt
  • POM_OPENINGS = 32 β€” Fiat-Shamir openings revealed per proof
  • POM_CHUNK_WORDS = 4 (= 32 bytes per chunk, 4 LE u64 words)
  • POM_SEED_SALT = 0x4B65727978500 ("KeryxP")
  • POM_ACTIVATION_DAA = 37,780,000 (mainnet β€” original PoM fork, 2026-06-26)
  • POM_H3_ACTIVATION_DAA = 43,450,000 (mainnet β€” H3 hardfork, 2026-07-05 ~18:00 UTC)

H3 hardfork update (DAA 43,450,000, 2026-07-05)

At POM_H3_ACTIVATION_DAA = 43,450,000 the chain hard-forks again ("H3"). Two things change in the PoM path:

  • The PoM proof-of-PoW-history (PPH) fold gains a forced-update salt, so the fold β€” and therefore the resulting pow_value and trace β€” differ from the pre-H3 computation.
  • The block header now commits the proof's final_state (a u64) β€” the miner sets it on submit exactly like the nonce, and it is bound into the block hash post-gate.

The wire envelope is unchanged. It is still the 6th positional pomProofHex param carrying the same Borsh PomProof struct (which already contains final_state at byte offset 65). No new params, no format break β€” H3 changes the computation of a valid proof, not its shape. The canonical, bit-exact algorithm (including the H3 salt constant) lives in keryx-node/consensus/core/src/pom.rs; it is not reproduced here.

Miner requirement: to produce valid proofs at DAA β‰₯ 43,450,000 you must run keryx-miner-supr v0.6.7+. Older builds compute the pre-H3 fold; their shares fail verification (error 27 β€” typically BadFinalTracePath / a PoW-value mismatch) and, post-gate, the pool also rejects sub-v0.6.7 clients at mining.submit (error 24) with an upgrade notice.

12.4 Pool-side modes

The pool runs the share-verify subprocess (pool-verifier) in one of three modes, selected by combining pom.enabled and pom.requireOnSubmit in the pool config:

pom.enabledpom.requireOnSubmitVerifier modeBehavior
falseanydisabled6th param parsed only if sent; never forwarded to daemon. Pre-PoM behavior.
truefalsepassthroughShare validation = kHeavyHash (pre-fork). If 6th param present, pool attaches it to RpcRawBlock.body.pom_proof on submitBlock. Daemon stores it (ignored pre-fork, validated post-fork). Current production state.
truetrueverifyShare validation REQUIRES a valid PomProof. Missing β†’ error 24/27. Borsh-decode failure β†’ error 27. Proof-verify failure β†’ error 27. Use this on testnet today; flips on production at fork +24 h.

12.5 How to test your miner against this pool now

Pre-fork, the production pool runs in passthrough. That lets you ship a PoM-enabled miner today and verify the wire envelope without waiting for Friday:

  1. Point your miner at stratum+tcp://krx.suprnova.cc:4401 (or any of 4402-4404).
  2. Send the 6-param mining.submit for every share. The pool accepts pre-v0.6 4-param submits too β€” don't be afraid to ship the 6-param shape from day one.
  3. On a block-difficulty share, the pool will log [KeryxValidator] forwarding submitBlock with pomProofBytes=<N> worker=<your-worker> on its side. You won't see this directly; instead, check the public Blocks page for your worker name as the finder.
  4. To test the full PoM-PoW + verify path (not just envelope), build keryxd with the pom_activation_daa_score param patched to 0 on testnet-10 and point a PoM-PoW miner at it. The verifier source needed for that test is published separately β€” email mining@suprnova.cc for the snapshot.

12.6 PoM-related rejection codes

  • Code 24 β€” Unauthorized worker: after the +24 h grace flip, sent with message starting "⚠ UPGRADE TO keryx-miner-supr/0.6.1+ …" if the miner's UA isn't on a PoM-capable build. Miner's ErrorCode::Unauthorized handler returns Err β†’ connection drops.
  • Code 25 β€” Not subscribed: 6th param exceeded the pom.maxProofHexBytes cap (2 MB), or hex-decode failed (odd length, non-hex chars).
  • Code 27 β€” Invalid / missing PoM proof (post-fork only):
    • pom_proof_hex required in verify mode β€” 6th param empty / absent under verify mode
    • PomProof borsh decode: <error> β€” bytes weren't a valid Borsh PomProof
    • pom_proof.tier N out of range β€” tier index beyond POM_TIERS.len()
    • invalid pom proof: BadInitialState | BadFinalTracePath | BadStateBeforePath | BadWeightPath | BadStateAfterPath β€” the consensus verifier (keryx-consensus-core::pom::verify_pom_proof) rejected the proof structure or paths
    Note: TargetNotMet is not a hard fail; the pool falls through to the share-target check, so a proof that meets share-target but not network-target still earns share credit.

12.7 Minimum miner-side checklist

  • Build the PoM walk + opening generation on the GPU (CUDA / OpenCL / Metal). Bit-identical to keryx-consensus-core::pom.
  • Generate a PomProof on every block-worthy nonce (or on every share, if you're using the pre-fork test loop).
  • Serialize via Borsh, hex-encode lowercase, attach as the 6th mining.submit param.
  • Until the operator-side flip, you may also attach an empty string for the 6th param and the pool will accept the submit β€” but you need real proofs working before Saturday +18:00 UTC.
  • Watch for warn!("Share rejected by pool ... : ⚠ UPGRADE TO keryx-miner-supr/0.6.1+ ...") in your miner log β€” that's the pool's one-shot upgrade notice before the grace-window flip.

13. Upstream source references#

Canonical implementation details live in the open-source Keryx tree:

  • keryx-hash PoW algorithm (kHeavyHash + KERYX_MATRIX_SALT_V4 + WaveMix ARX): keryx-node/consensus/pow/
  • Pre-PoW hash (the 32-byte pre_pow_hash that becomes headerSeed): keryx-node/consensus/core/src/hashing/header.rs::hash_override_nonce_time
  • Salt-fork activation DAA scores: keryx-node/consensus/core/src/config/params.rs β€” v2 at 17,275,000, v4 at 21,932,751.
  • OPoI tag (mandatory per share): keryx-node/inference/src/model_fixed.rs::tag_fixed(nonce: u64) -> [u8; 8]
  • Reference stratum bridge (Go): github.com/Keryx-Labs/keryx-stratum-bridge
  • Node + wRPC daemon: github.com/Keryx-Labs/keryx-node
  • PoM verifier + types (canonical, bit-exact with chain): keryx-node/consensus/core/src/pom.rs β€” exposes PomProof, PomOpening, pom_block_seed(), pom_pow_value(), verify_pom_proof().
  • PoM tier table + activation DAA: keryx-node/consensus/core/src/config/params.rs β€” POM_TIERS, POM_WALK_STEPS, POM_OPENINGS, POM_ACTIVATION_DAA.
  • Daemon-side PoM validation entry-point (mirrors what the pool's verify mode does): keryx-node/consensus/src/pipeline/body_processor/body_validation_in_isolation.rs::check_pom_proof.

14. Compatibility & versioning#

This document describes stratum protocol version 1.0, the kaspad-dialect adopted by Keryx pools and the dialect spoken by keryx-miner v0.3.x. A miner implementing this spec will work against any compatible Keryx pool.

Stability guarantees

  • The positional argument lists for mining.subscribe, mining.authorize, mining.notify, mining.set_difficulty, and mining.submit will not change within v1.x.
  • Additional fields may be appended after the last positional param in either direction; clients MUST ignore extra trailing fields they don't recognise.
  • The OPoI tag_fixed weights are part of the chain consensus rules β€” they can only change via a Keryx hard-fork coordinated through upstream Keryx Labs.
  • Error codes 20–29 are reserved for the protocol. Pool-specific codes start at 100.

Suggested minimum miner behaviours for spec conformance

  • Send mining.subscribe first, then mining.authorize, before the pool will push any work.
  • Compute and attach the OPoI tag on every mining.submit. Missing or wrong tags are rejected with error 26 β€” your share is unpaid.
  • Restart the hashing loop on every new mining.notify; there is no "clean" bool β€” every notify is fresh.
  • Re-target whenever a new mining.set_difficulty arrives; do not drop in-flight shares β€” the 60 s grace period covers one retarget overlap.
  • Reconnect with exponential backoff on any error other than authorize-time error 24.
  • Parse the headerSeed array elements as u64 BigInts (they exceed JS safe-int range).

15. Changelog#

VersionDateNotes
1.22026-07-06Documented the H3 hardfork (DAA 43,450,000, 2026-07-05): Β§12.3 gains the H3 update note β€” the PoM proof-of-PoW-history fold is salted and the block header commits the proof's final_state; the wire envelope is unchanged (same 6th pomProofHex param / Borsh PomProof). Added POM_H3_ACTIVATION_DAA; keryx-miner-supr v0.6.7+ required post-H3 (older builds fail verification with error 27 and are refused at submit post-gate). Algorithmic details (incl. the H3 salt) remain canonical in pom.rs, not reproduced here.
1.12026-06-25Added Β§12 Proof-of-Model (PoM) hardfork extension. Documents the 6th mining.submit param (pomProofHex), the 5th optional ipfsCID, error code 27 (PoM-related rejects), the three pool-verifier modes (disabled / passthrough / verify), POM_TIERS, the v0.6.1+ reference miner, and how to test against the production pool before the Friday 2026-06-26 18:00 UTC fork (DAA 37,780,000).
1.02026-06-08Initial public specification (kaspad-dialect, OPoI tag, SALT v4).

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