Skip to main content
How to fetch work from a KRNX node, hand it to kHeavyHash miners, take a share back and submit a solved block. Self-contained. This is about the KRNX main chain. Its proof of work is kHeavyHash, the same one Kaspa uses: identical pre-image, the same two cSHAKE256 passes, the same 64×64 matrix, the same little-endian comparison. Any kaspa miner (GMiner, lolMiner, BzMiner, IceRiver, Goldshell) mines KRNX with no firmware change — only the source of work differs. There are two differences from Kaspa, and both break an integration if you do not know them:
  1. The timestamp is in seconds, not milliseconds. Section 4.
  2. Share difficulty and consensus difficulty are different scales, exactly 2^32 apart. Section 5.

1. What the node must provide

Your own KRNX node with HTTP-RPC and template production enabled:
The reward goes to the node’s etherbase, not to the stratum login. The login is only an accounting label; payouts are the pool’s own business. Three RPC methods, nothing else: All three are mirrored in the eth_ namespace (eth_getWork, eth_submitWork) — the names match stock krnxd, so existing ethash pool code only needs its hashing swapped out.

2. krnx_getWork

Eleven strings. A pool needs four of them: Work is new when [0] changes. Everything else in the header is already folded into that hash. If the node is not mining or is still syncing, the method returns an error. That is normal: wait, and do not close the stratum port.

Push instead of polling

With --miner.notify http://pool:1234/work the node POSTs the work array as JSON whenever the template changes. With --miner.notify.full it posts the whole header as JSON instead. Polling krnx_getWork every 200–500 ms is an equally fine alternative; with --miner.recommit 3s the added latency does not matter.

3. The kHeavyHash pre-image

Byte for byte as in kaspad:
Then:
  1. first = cSHAKE256(data, customization="ProofOfWorkHash") → 32 bytes.
  2. matrix = generate(PRE_POW_HASH) — a 64×64 matrix of 4-bit values from xoshiro256++ seeded with the seal hash, regenerated until its rank (float64 Gaussian elimination, eps = 1e-9) reaches 64.
  3. mixed[i] = first[i] XOR (product[2i]<<4 | product[2i+1]), where product[i] = (Σ matrix[i][j] * vector[j]) >> 10 and vector holds the nibbles of first.
  4. final = cSHAKE256(mixed, customization="HeavyHash") → 32 bytes.
  5. The PoW value is final read little-endian. A block is solved when that value is <= the target.
The matrix depends on the seal hash alone: compute it once per work package, then roll only the nonce.

Golden vector

Every implementation must reproduce exactly this:
The node pins the same vector in TestKHeavyHashPowValueGolden (consensus/kheavyhash/kheavyhash_test.go). Every example in section 10 is checked against it.

4. The timestamp: seconds, not milliseconds

This is the one trap when porting code from Kaspa. Kaspa stores block time in milliseconds, and kaspa pools send something like 1787647789790 in mining.notify. KRNX is a krnxd fork: header.Time is in seconds, e.g. 1787647640. The node verifies the seal with that same field:
A miner drops the number it is given straight into the pre-image. Multiply it by 1000 and the miner hashes a different pre-image, so the node rejects every solution it finds:
The same nonce that seals a block with seconds seals nothing with milliseconds. The rule is simple: send exactly the number the header carries.

Where to get it

From work[10] — hex, seconds, nothing to parse:
The same number sits inside the RLP header in [9], which is how it gets verified (below) and which is also the field per-connection work needs in section 8. header.Time is at index 11 there:
A full RLP library is unnecessary: skip eleven fields and read the twelfth. Implementations in four languages are in section 10. One more source is --miner.notify.full: the node POSTs the header as JSON with timestamp as its own field.

Proving the time is right

The seal hash covers Time, so the header from [9] can be re-hashed and compared with [0], and its header.Time compared with [10]. Both matching proves the number is the one the node will verify against. One subtlety: before RLP-encoding, the node appends four zero bytes to Extra (room for an ethash-style extra nonce that kHeavyHash never uses). To get the seal hash, strip those four bytes and take keccak256(rlp(fields)). A ready implementation is verifyWorkSealHash in cmd/krnx-kheavyhash-stratum-bridge/node.go. A mismatch is expected in exactly one case: the header carries a KRNX AuxPoW root, which [9] does not report.

Do not roll the time

Time is part of the seal hash. Change it and PRE_POW_HASH changes, and with it the matrix: the result is a different work package, one the node does not have. The kHeavyHash stratum dialect has no ntime rolling — miners never touch the timestamp.

5. Difficulty: two scales

Stratum difficulty 1 is worth about 2^32 hashes. Hence:
Confusing the two is a factor of 2^32 in the books: either miner hashrate reads four billion times too low, or shares arrive in a flood the pool never sized for. For example, a network difficulty of 4.3·10^12 is 1000 on the miner scale, and a difficulty-125 share costs roughly 5.4·10^11 hashes. Rules that hold wherever the chain’s difficulty happens to sit:
  • Do not hard-code the starting difficulty — derive the band from the current network difficulty: start at netdiff/8, floor at netdiff/4096, ceiling at netdiff.
  • The ceiling is netdiff for a reason: above it every share is a block anyway, and the pool goes blind to a miner that produces nothing.
  • A share that solves a block is always accepted, even when it misses the current vardiff bound.
Miner hashrate is estimated as usual: Σ (share_difficulty * 2^32) / elapsed.

6. Stratum: the kaspa dialect

The full exchange (captured from a working bridge):

Two job formats

Ordinary: the seal hash read as four little-endian uint64s:
The miner reassembles the same 32 bytes from them. The third parameter is the timestamp (section 4). Big job (BzMiner, IceRiver, Goldshell and several ASIC stacks): instead of the array and the time, a single 80-character hex string — the first 40 bytes of the pre-image:
For example 353bd663…7b003b + 9d578d6a00000000 (= 1787647901). Which format to use is decided by a regex on the mining.subscribe user agent.

Extra nonce

The prefix occupies the high bytes of the 64-bit nonce; the miner rolls the rest. Two prefix bytes give 65536 connections and 48 bits of search space each. Never hand out more than three: an ASIC exhausts what is left in a fraction of a second. On submit a miner sends either all eight bytes (0x0001000000000527) or only its own part (000000000527). When fewer than 16 - len(prefix) hex characters arrive, the pool prepends the prefix itself.

7. Submitting a block

The answer is true (block accepted) or false. false means one of:
  • the node no longer holds work with that powHash — a stale;
  • the PoW misses the target — the pool and the node disagree on the pre-image, most often the timestamp;
  • the block is too old relative to the current height.
The method takes an optional fourth parameter, the extra nonce. Pass it only if the pool built the work package itself following section 8; otherwise the node appends bytes to Extra, the seal hash changes and the PoW no longer matches. Do not tie the submission to the miner’s socket: a miner may drop the connection right after solving, and the block still has to go out. Ask for fresh work immediately after a submit — the template is spent either way.

8. Per-connection work and extradata

By default every miner is handed the same PRE_POW_HASH and separated only by its extra-nonce prefix inside the nonce. A pool can go further and give each connection a work package of its own, through the same mechanism ethash pools on krnxd use. work[9] reports the header with four zero bytes appended to Extra — the extra-nonce slot. The pool writes its own value there, hashes the header itself and serves that hash to miners:
On submit the pool names the node’s seal hash (work[0] is the key the node looks work up by) and passes the same four bytes as the fourth parameter of krnx_submitWork. The node appends them to Extra, recomputes the seal hash and arrives at exactly the one the miner hashed:
What it buys:
  • work that is unique per connection or per worker — nonce spaces never overlap even without an extra nonce, and no two miners can find the same share;
  • the pool’s four bytes end up inside the KRNX block (header.Extra): a pool tag, a shift id, a server number.
The constraints are all hard: The code (Go; the other languages are in the examples, section 10):
The scheme is pinned by TestKHeavyHashRemoteWorkWithPoolExtraNonce in consensus/kheavyhash: it patches the four bytes, mines kHeavyHash against the resulting seal hash, submits through krnx_submitWork and checks that the node accepted the block, that the sealed block’s Extra ends with the pool’s tag, and that the header hashes back to the pool’s seal hash. Any of the examples reproduces it:
All four implementations produce the same pool seal hash on the same template — e47d91b45cfbf3b878af24b59ad3376ac4f8b0b5bbda032dde8428fefa761bd6 for extraNonce = 5030304c.

9. What you may and may not change

Free: Not allowed:

10. Code examples

Complete working clients (fetch work → decode the timestamp → compute difficulty → scan nonces → submit a block) live in docs/examples/kheavyhash-pool/. All of them are checked against the golden vector from section 3 and run against a live node. Python and Node.js are fast enough to validate an integration, not to hash for real. Below is the core of each implementation: building the pre-image and both cSHAKE256 passes.

10.1 Go

Targets and scales:
Inside the node the algorithm is already exported, so a Go pool living in this repository does not reimplement it:

10.2 Node.js

The verification path: the same timestamp out of work[9], without an RLP library:

10.3 Rust

Own implementation on the sha3 crate:

10.4 Reusing the rusty-kaspa hasher

Yes — the hasher from Kaspa’s own node plugs in directly, because the pre-image and the domains are identical. Two crates from rusty-kaspa are enough: kaspa-hashes (both cSHAKE256 passes, implemented as pre-computed Keccak states and therefore faster than a generic cSHAKE) and kaspa-pow (the matrix).
Verified: this reproduces the golden vector from section 3 bit for bit. Three notes:
  • features = ["no-asm"] is required off x86_64 (Apple Silicon, for instance): by default kaspa-hashes expects the assembly KeccakF1600 built for x86_64.
  • Do not use kaspa_pow::State::new(header) — it takes a Kaspa header and derives pre_pow_hash and the target from bits itself. KRNX already has the seal hash, and the target comes from getWork()[2]. Use PowHash and Matrix directly.
  • PowHash::new(hash, timestamp) receives milliseconds in Kaspa and seconds in KRNX. The function only places the number into the pre-image; no conversion.

10.5 Python

Checking a whole work package:

11. The ready-made bridge

The repository ships a working implementation of everything above: cmd/krnx-kheavyhash-stratum-bridge. It polls krnx_getWork, serves both job formats, scores shares with the node’s own hasher, runs vardiff and pushes blocks through krnx_submitWork:
The scheme from section 8 is one flag away — every connection gets work of its own and the pool’s tag ends up in the block:
It works both as a finished solo-mining front end and as a reference while writing your own: the bridge README documents every flag.

12. Integration checklist

  1. krnx_getWork answers, and [0] changes when the template does.
  2. Your hasher reproduces the golden vector from section 3.
  3. The timestamp comes from [10]; the header from [9], with the four zero Extra bytes stripped, re-hashes to [0] and reports the same header.Time.
  4. mining.notify ships the seal hash such that a miner rebuilds exactly [0] from the four words.
  5. Difficulty is set on the (2^224−1)/diff scale and below the block difficulty.
  6. A share that solves a block is accepted even if it misses the vardiff bound.
  7. krnx_submitWork returns true and the chain height moves.
  8. The node’s rejection rate is near zero. If it is not, see section 13.

13. Diagnostics