- The timestamp is in seconds, not milliseconds. Section 4.
- 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
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:first = cSHAKE256(data, customization="ProofOfWorkHash")→ 32 bytes.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.mixed[i] = first[i] XOR (product[2i]<<4 | product[2i+1]), whereproduct[i] = (Σ matrix[i][j] * vector[j]) >> 10andvectorholds the nibbles offirst.final = cSHAKE256(mixed, customization="HeavyHash")→ 32 bytes.- The PoW value is
finalread little-endian. A block is solved when that value is<=the target.
Golden vector
Every implementation must reproduce exactly this: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 like1787647789790 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:
Where to get it
Fromwork[10] — hex, seconds, nothing to parse:
[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:
--miner.notify.full: the node POSTs the header as JSON with
timestamp as its own field.
Proving the time is right
The seal hash coversTime, 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:
- Do not hard-code the starting difficulty — derive the band from the current
network difficulty: start at
netdiff/8, floor atnetdiff/4096, ceiling atnetdiff. - The ceiling is
netdifffor 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.
Σ (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: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.
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 samePRE_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:
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:
- 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 code (Go; the other languages are in the examples, section 10):
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:
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 indocs/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
10.2 Node.js
work[9], without an RLP library:
10.3 Rust
Own implementation on thesha3 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).
features = ["no-asm"]is required off x86_64 (Apple Silicon, for instance): by defaultkaspa-hashesexpects the assembly KeccakF1600 built for x86_64.- Do not use
kaspa_pow::State::new(header)— it takes a Kaspa header and derivespre_pow_hashand the target frombitsitself. KRNX already has the seal hash, and the target comes fromgetWork()[2]. UsePowHashandMatrixdirectly. 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
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:
12. Integration checklist
krnx_getWorkanswers, and[0]changes when the template does.- Your hasher reproduces the golden vector from section 3.
- The timestamp comes from
[10]; the header from[9], with the four zeroExtrabytes stripped, re-hashes to[0]and reports the sameheader.Time. mining.notifyships the seal hash such that a miner rebuilds exactly[0]from the four words.- Difficulty is set on the (2^224−1)/diff scale and below the block difficulty.
- A share that solves a block is accepted even if it misses the vardiff bound.
krnx_submitWorkreturnstrueand the chain height moves.- The node’s rejection rate is near zero. If it is not, see section 13.

