> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kronex.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Pool operator reference

> The complete node-to-stratum contract for kHeavyHash pools: work, pre-image, difficulty scales, submission.

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:

```bash theme={null}
krnxd --http --http.api eth,krnx,net,web3 --http.addr 127.0.0.1 --http.port 8545 \
      --mine --miner.threads 0 --miner.etherbase 0xYOUR_ADDRESS --miner.recommit 3s
```

| Flag                 | Why                                                                 |
| -------------------- | ------------------------------------------------------------------- |
| `--mine`             | without it the node builds no pending block and `getWork` errors    |
| `--miner.threads 0`  | disables the node's own CPU mining while templates keep being built |
| `--miner.etherbase`  | the address the block reward is credited to                         |
| `--miner.recommit`   | how often the template is rebuilt (3s by default)                   |
| `--miner.notify URL` | optional: the node POSTs new work to the pool itself                |

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:

| Method                                       | Returns                                 |
| -------------------------------------------- | --------------------------------------- |
| `krnx_getWork`                               | an array of 11 strings — the work       |
| `krnx_submitWork(nonce, powHash, mixDigest)` | `true` / `false`                        |
| `krnx_submitHashrate(rate, id)`              | `true` (optional, for the node's stats) |

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`

```bash theme={null}
curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"krnx_getWork","params":[]}' \
  http://127.0.0.1:8545 | jq -r '.result[0,2,3]'
```

```
0x7bbec5a103d39fcd618e0233e6b872f8b1824f4e4d326ae5a4ef35f7736b3f9f
0x000005f67467c79dc2c3a72af48c650090096beaedfae8c63e25a35f67d59763
0xe193
```

Eleven strings. A pool needs four of them:

| Index       | What it is                                                            | Needed                                                                |
| ----------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `[0]`       | the header's **seal hash** — this is the `PRE_POW_HASH` of kHeavyHash | yes                                                                   |
| `[1]`       | DAG seed hash — always zero, kHeavyHash has no DAG                    | no                                                                    |
| `[2]`       | block target = 2^256/difficulty                                       | yes                                                                   |
| `[3]`       | block number                                                          | yes (logs, staleness)                                                 |
| `[4]`       | parent hash                                                           | no                                                                    |
| `[5]`–`[8]` | gas limit, gas used, transaction count, uncle count                   | no                                                                    |
| `[9]`       | RLP of the header being mined                                         | yes — for per-connection work (section 8) and to verify the timestamp |
| `[10]`      | **the header timestamp in seconds** — the `TIME` of the pre-image     | yes                                                                   |

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:

```
                 32 bytes              8 bytes       32 bytes        8 bytes
       ┌──────────────────────┬───────────────┬──────────────┬───────────────┐
       │   PRE_POW_HASH       │  TIME u64 LE  │  zero bytes  │ NONCE u64 LE  │
       └──────────────────────┴───────────────┴──────────────┴───────────────┘
        = krnx_getWork()[0]     = getWork()[10]  fixed 32       the miner's
                                  (SECONDS)      zero bytes     search space
```

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:

```
PRE_POW_HASH  0x37a01e19f6d0f7bd2b95bb45f4d1a5da5cbccdca4c1652deb3a4116ad4d0a4f1
TIME          1784505600
NONCE         0x123456789abcdef0
pow value     fb08e9df4e0cfa2e2f27a317413e7033856fd20a0e880cc4a936d92848e316c3
```

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:

```go theme={null}
// consensus/kheavyhash/consensus.go
powValue := kHeavyHashPowValue(matrix, sealHash, header.Time, header.Nonce.Uint64())
```

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:

```
pow(seconds)       48a81dc862655be0467039c2f0b1b5f98c8a4a13f656676633aa4c587b3e4a18
pow(milliseconds)  83ecb0f6068f5c8e60093716fbbd7b0cddedb581ed01617672c539e9fffb910f
```

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:

```
"0x6a8d5549"  ->  1787647305
```

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:

```
0 ParentHash   3 Root        6 Bloom        9  GasLimit   12 Extra
1 UncleHash    4 TxHash      7 Difficulty   10 GasUsed    13 BaseFee (if present)
2 Coinbase     5 ReceiptHash 8 Number       11 Time
```

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

| Scale           | Target                   | Used by                         |
| --------------- | ------------------------ | ------------------------------- |
| KRNX consensus  | 2^256 / difficulty       | the node, `getWork()[2]`        |
| Stratum (kaspa) | (2^224 − 1) / difficulty | miners, `mining.set_difficulty` |

Stratum difficulty 1 is worth about 2^32 hashes. Hence:

```
difficulty_stratum = difficulty_consensus / 2^32
share_target       = (2^224 - 1) / share_difficulty
hashes_per_share   = share_difficulty * 2^32
```

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):

```
miner -> {"id":1,"method":"mining.subscribe","params":["GMiner/3.28"]}
pool  -> {"id":1,"result":[true,"EthereumStratum/1.0.0"],"error":null}
miner -> {"id":2,"method":"mining.authorize","params":["krnx.rig1","x"]}
pool  -> {"id":2,"result":true,"error":null}
pool  -> {"id":null,"jsonrpc":"2.0","method":"mining.set_extranonce","params":["0001",6]}
pool  -> {"id":null,"jsonrpc":"2.0","method":"mining.set_difficulty","params":[0.0000816]}
pool  -> {"id":1,"jsonrpc":"2.0","method":"mining.notify",
          "params":["1",[4581632339856567309,5783595065747121291,
                         6077351350858350511,17714875238603159769],1787647640]}
miner -> {"id":3,"method":"mining.submit","params":["krnx.rig1","1","0001000000000527"]}
pool  -> {"id":3,"result":true,"error":null}
```

| Message                 | Rule                                                                                                     |
| ----------------------- | -------------------------------------------------------------------------------------------------------- |
| `mining.subscribe`      | reply exactly `[true,"EthereumStratum/1.0.0"]`; `params[0]` is the user agent and decides the job format |
| `mining.authorize`      | reply `true`; the `user.worker` login is an accounting label                                             |
| `mining.set_extranonce` | `[hex prefix, how many nonce bytes the miner keeps]`                                                     |
| `mining.set_difficulty` | `[float]` on the scale from section 5                                                                    |
| `mining.notify`         | `[jobId, [4×uint64], timestamp]`                                                                         |
| `mining.submit`         | `[worker, jobId, nonce hex]`, answered with `true` or a stratum error                                    |

### Two job formats

**Ordinary:** the seal hash read as four little-endian uint64s:

```
words[i] = LE_uint64(seal_hash[i*8 : i*8+8])
```

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:

```
hex(seal_hash) + hex(timestamp as u64 LE)
```

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

```bash theme={null}
curl -sS -H 'Content-Type: application/json' --data '{
  "jsonrpc":"2.0","id":2,"method":"krnx_submitWork","params":[
    "0x0001000000000527",
    "0x7bbec5a103d39fcd618e0233e6b872f8b1824f4e4d326ae5a4ef35f7736b3f9f",
    "0x0000000000000000000000000000000000000000000000000000000000000000"
  ]}' http://127.0.0.1:8545
```

| Parameter   | Rule                                                                                     |
| ----------- | ---------------------------------------------------------------------------------------- |
| `nonce`     | 8 bytes, big-endian hex — precisely the 64-bit number the miner hashed                   |
| `powHash`   | `getWork()[0]` of the work the nonce was found against, byte for byte                    |
| `mixDigest` | **always 32 zero bytes.** kHeavyHash has no mix; a non-zero one is rejected by consensus |

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:

```
pool_seal_hash = keccak256( work[9] with the four trailing bytes of the Extra
                             field replaced by the pool's value )
```

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:

```json theme={null}
{"method":"krnx_submitWork","params":[
  "0x0001000000000527",
  "0x7bbec5a103d39fcd618e0233e6b872f8b1824f4e4d326ae5a4ef35f7736b3f9f",
  "0x0000000000000000000000000000000000000000000000000000000000000000",
  "0x5030304c"
]}
```

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:

| Rule                                                         | Why                                                                                                      |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| exactly four bytes                                           | an in-place replacement keeps every RLP length prefix valid; another length means re-encoding the header |
| patch the `Extra` field (index 12), not the tail of the blob | `BaseFee` is encoded after `Extra`, so patching "the last four bytes" corrupts it                        |
| the node's `--miner.extradata` must be ≤ 28 bytes            | `Extra` plus four bytes has to stay inside `params.MaximumExtraDataSize` = 32                            |
| keep the value next to the work package                      | on submit it must match the bytes the seal hash was computed with                                        |
| never substitute `work[0]` on submit                         | it is the node's lookup key, not what the miner hashed                                                   |

The code (Go; the other languages are in the examples, section 10):

```go theme={null}
// field 12 is Extra; end is the offset just past its payload
extra, end, err := field(headerRLP, 12)
patched := append([]byte(nil), headerRLP...)
copy(patched[end-len(extraNonce):end], extraNonce) // the pool's four bytes

hasher := sha3.NewLegacyKeccak256()
hasher.Write(patched)
poolSealHash := hasher.Sum(nil) // the PRE_POW_HASH for this connection
```

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:

```bash theme={null}
go run . --extranonce P00L --scan 100000
```

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:

| What                       | Bounds                                                |
| -------------------------- | ----------------------------------------------------- |
| nonce                      | all 64 bits, minus the extra-nonce prefix bytes       |
| share difficulty           | your own vardiff, see section 5                       |
| job format                 | ordinary or big job, per user agent                   |
| the four extra-nonce bytes | per-connection work and a tag in the block, section 8 |

Not allowed:

| What                                                                   | Result                                                 |
| ---------------------------------------------------------------------- | ------------------------------------------------------ |
| multiplying the timestamp by 1000                                      | every solution rejected                                |
| rolling the timestamp                                                  | a different seal hash, i.e. work the node never issued |
| a non-zero `mixDigest`                                                 | `errInvalidMixDigest`                                  |
| passing an extra nonce without hashing the header yourself (section 8) | the seal hash changes, the PoW stops matching          |
| altering `powHash` on submit                                           | "Work submitted but none pending"                      |
| share difficulty above the block difficulty                            | every share is a block, miner stats go blind           |

***

## 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.

| Language             | Directory            | Scan rate (M1, single thread) |
| -------------------- | -------------------- | ----------------------------- |
| Go                   | `go/`                | ≈ 265 kH/s                    |
| Node.js              | `node/`              | ≈ 52 kH/s                     |
| Rust                 | `rust/`              | ≈ 580 kH/s (release)          |
| Rust on kaspa crates | `rust-kaspa-crates/` | —                             |
| Python               | `python/`            | ≈ 4 kH/s                      |

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

```go theme={null}
// PowValue hashes PRE_POW_HASH || TIME || 32 zero bytes || NONCE and reads the
// result little-endian, exactly like the node.
func PowValue(matrix *Matrix, prePowHash [32]byte, timestamp, nonce uint64) *big.Int {
	var pre [80]byte
	copy(pre[:32], prePowHash[:])
	binary.LittleEndian.PutUint64(pre[32:40], timestamp)
	binary.LittleEndian.PutUint64(pre[72:80], nonce)

	first := cshake256("ProofOfWorkHash", pre[:])
	final := matrix.heavyHash(first)
	for i, j := 0, len(final)-1; i < j; i, j = i+1, j-1 {
		final[i], final[j] = final[j], final[i]
	}
	return new(big.Int).SetBytes(final[:])
}

func cshake256(domain string, data []byte) [32]byte {
	hasher := sha3.NewCShake256(nil, []byte(domain)) // golang.org/x/crypto/sha3
	hasher.Write(data)
	var out [32]byte
	hasher.Read(out[:])
	return out
}
```

Targets and scales:

```go theme={null}
var stratumDiffOneTarget = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 224), big.NewInt(1))

// the share target for a stratum difficulty
func difficultyToTarget(difficulty float64) *big.Int {
	target, _ := new(big.Float).Quo(new(big.Float).SetInt(stratumDiffOneTarget), big.NewFloat(difficulty)).Int(nil)
	return target
}

// network difficulty on the miner scale: 2^256/D against (2^224-1) is D/2^32
func stratumDifficulty(blockTarget *big.Int) float64 {
	value, _ := new(big.Float).Quo(new(big.Float).SetInt(stratumDiffOneTarget), new(big.Float).SetInt(blockTarget)).Float64()
	return value
}
```

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

```go theme={null}
job := kheavyhash.NewKHeavyHashJob(sealHash)        // the matrix, computed once
value := job.PowValue(header.Time, nonce)       // pre-image plus both passes
target := kheavyhash.KHeavyHashTarget(difficulty)   // 2^256/difficulty
```

### 10.2 Node.js

```js theme={null}
const { cshake256 } = require('js-sha3');

// PRE_POW_HASH || TIME || 32 zero bytes || NONCE -> pow value as a BigInt
function powValue(matrix, prePowHash, timestamp, nonce) {
  const pre = new Uint8Array(80);
  pre.set(prePowHash, 0);
  new DataView(pre.buffer).setBigUint64(32, BigInt(timestamp), true); // LE
  new DataView(pre.buffer).setBigUint64(72, BigInt(nonce), true);     // LE

  const first = cshake('ProofOfWorkHash', pre);
  const final = heavyHash(matrix, first);

  let value = 0n; // little-endian reading
  for (let i = 31; i >= 0; i--) value = (value << 8n) | BigInt(final[i]);
  return value;
}

function cshake(domain, data) {
  return Uint8Array.from(Buffer.from(cshake256(data, 256, '', domain), 'hex'));
}
```

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

```js theme={null}
function headerTimestamp(headerRlpHex) {
  const data = Buffer.from(headerRlpHex.replace(/^0x/, ''), 'hex');
  let offset = listPayloadOffset(data);
  for (let i = 0; i < 11; i++) offset = nextField(data, offset).next; // Time is field 11
  return BigInt('0x' + nextField(data, offset).payload.toString('hex'));
}
```

### 10.3 Rust

Own implementation on the `sha3` crate:

```rust theme={null}
use sha3::digest::{ExtendableOutput, Update, XofReader};
use sha3::{CShake256, CShake256Core};

/// PRE_POW_HASH || TIME || 32 zero bytes || NONCE, returned big-endian.
pub fn pow_value(matrix: &Matrix, pre_pow_hash: &[u8; 32], timestamp: u64, nonce: u64) -> [u8; 32] {
    let mut pre = [0u8; 80];
    pre[..32].copy_from_slice(pre_pow_hash);
    pre[32..40].copy_from_slice(&timestamp.to_le_bytes());
    pre[72..80].copy_from_slice(&nonce.to_le_bytes());

    let first = cshake256(b"ProofOfWorkHash", &pre);
    let mut value = matrix.heavy_hash(first);
    value.reverse(); // the node reads little-endian; reversed, it compares as a number
    value
}

fn cshake256(domain: &[u8], data: &[u8]) -> [u8; 32] {
    let mut hasher = CShake256::from_core(CShake256Core::new(domain));
    hasher.update(data);
    let mut out = [0u8; 32];
    hasher.finalize_xof().read(&mut out);
    out
}
```

### 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](https://github.com/kaspanet/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).

```toml theme={null}
[dependencies]
kaspa-hashes = { git = "https://github.com/kaspanet/rusty-kaspa", branch = "master", features = ["no-asm"] }
kaspa-pow    = { git = "https://github.com/kaspanet/rusty-kaspa", branch = "master" }
```

```rust theme={null}
use kaspa_hashes::{Hash, PowHash};
use kaspa_pow::matrix::Matrix;

let seal = Hash::from_bytes(seal_hash_bytes);   // krnx_getWork()[0]
let matrix = Matrix::generate(seal);            // once per work package
let first = PowHash::new(seal, timestamp)       // timestamp is header.Time, SECONDS
    .finalize_with_nonce(nonce);
let mut value = matrix.heavy_hash(first).as_bytes();
value.reverse();                                // little-endian -> comparable number
```

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

```python theme={null}
from Crypto.Hash import cSHAKE256   # pip install pycryptodome

def cshake256(domain: bytes, data: bytes) -> bytes:
    return cSHAKE256.new(data=data, custom=domain).read(32)

def pow_value(mat, pre_pow_hash: bytes, timestamp: int, nonce: int) -> int:
    """PRE_POW_HASH || TIME || 32 zero bytes || NONCE, read little-endian."""
    pre = pre_pow_hash + timestamp.to_bytes(8, "little") + bytes(32) + nonce.to_bytes(8, "little")
    first = cshake256(b"ProofOfWorkHash", pre)
    final = heavy_hash(mat, first)
    return int.from_bytes(final, "little")
```

Checking a whole work package:

```python theme={null}
work = call(rpc, "krnx_getWork")
seal_hash = bytes.fromhex(work[0][2:])
block_target = int(work[2], 16)
timestamp = int(work[10], 16)                   # seconds; work[9] is the cross-check
network_diff = ((1 << 224) - 1) / block_target  # block difficulty on the miner scale
matrix = generate_matrix(seal_hash)
value = pow_value(matrix, seal_hash, timestamp, nonce)
solved = value <= block_target
```

***

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

```bash theme={null}
go run ./cmd/krnx-kheavyhash-stratum-bridge \
  --krnx.rpc http://127.0.0.1:8545 --stratum.listen 0.0.0.0:5555
```

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:

```bash theme={null}
go run ./cmd/krnx-kheavyhash-stratum-bridge --work.unique --work.tag KR
```

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

| Symptom                                                      | Cause                                                                                                                                    |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| The node rejects **every** solution while shares keep coming | the pre-image diverged: timestamp in milliseconds, taken from the wrong work package, or an implementation that misses the golden vector |
| `krnx_getWork` returns an error                              | the node runs without `--mine`, without an etherbase, or is still syncing                                                                |
| A miner connects and stays silent                            | share difficulty above the block difficulty (section 5) — every find leaves as a block                                                   |
| Many `false` answers from `krnx_submitWork` on valid shares  | the template moved on: slow polling, or the miner works on stale jobs                                                                    |
| "Work submitted but none pending" in the node log            | the `powHash` on submit is not the one from the work package                                                                             |
| The node logs `errInvalidMixDigest`                          | a non-zero `mixDigest` was submitted                                                                                                     |
| Reported hashrate is orders of magnitude off                 | shares counted on the consensus scale instead of the stratum one (the gap is exactly 2^32)                                               |
