Build/Mining & pools

Mining & pools

Mining ZKas is mining Kaspa. Same kHeavyHash, same ASICs, same stratum, same difficulty maths. Exactly five things differ, and only three of them are mandatory.

Proof of workkHeavyHashbyte-identical to Kaspa
Mandatory changes3of five differences
Block subsidy60 → 57ZKAS after the 5% dev fee
Stratum changesnonevardiff, shares, ASICs unchanged
Merged miningoptionalnative is never turned off

The five differences

  1. The payout address is shielded, version 9 mandatory You pass a zkas: address to getBlockTemplate exactly like a kaspa: one, but the resulting coinbase output script is a raw 43-byte Orchard address, which is not a standard script class. If your pool checks that a coinbase output "looks like P2PK/P2SH" before accepting a template, that check rejects every ZKas template. Remove it.
    crypto/addresses/src/lib.rs        Version::ShieldedOrchard = 9
    crypto/txscript/src/standard.rs    pay_to_address_script -> pay_to_shielded_address
    crypto/txscript/src/script_class.rs  ShieldedOrchard => ScriptClass::NonStandard
  2. The coinbase carries a mandatory 5% dev-fee output mandatory Submit the node's template coinbase verbatim. If you rebuild or rewrite it, you must reproduce the node's expected transaction byte for byte, dev-fee output included, or the block is rejected.
    consensus/src/processes/coinbase.rs   expected_coinbase_transaction()
  3. The coinbase payload has an extra 32-byte field mandatory ZKas inserts the shielded state root between subsidy and the script-pubkey block:
    Kaspa: blue_score(8) | subsidy(8) |                    spk_version(2) | spk_len(1) | spk | extra
    ZKas:  blue_score(8) | subsidy(8) | shielded_root(32) | spk_version(2) | spk_len(1) | spk | extra
    Any pool that writes its extranonce at a hardcoded offset will corrupt the payload. Shift by 32 bytes, or better, parse the payload rather than index into it.
  4. Merged mining with Kaspa optional One hash earns both KAS and ZKAS. Native mining always remains valid — there is no flag day for miners. Section below.
  5. Custodial payouts optional Only if you mine to your own treasury rather than direct-to-miner. There are no transparent UTXOs to sweep, so you need shielded payout tooling. Section below.
Do (1), (2) and (3) and you are mining ZKas. Because the PoW is byte-identical to Kaspa, your stratum layer, share validation, vardiff, job distribution and difficulty maths need no changes at all.

Standing up native mining

  1. Run a node
    kaspad --appdir=<dir> --rpclisten=127.0.0.1:16810 --utxoindex
    Add --enable-unsynced-mining only when bootstrapping a brand-new network with no peers. Never on a pool mining the live chain.
  2. Point your bridge at the node's gRPCgetBlockTemplate / submitBlock behave exactly as on Kaspa.
    GetBlockTemplateRequest {
      pay_address: "zkas:...",
      extra_data:  <your extranonce>
    }
    The node rejects a pay_address whose HRP does not match its network — invalid prefix … means you pointed a zkas: address at a Kaspa node, or a pre-rebrand node at a zkas: address.
  3. Hand the header to miners over stratum, unchangedkHeavyHash ASICs — Bitmain, IceRiver, Goldshell, GodMiner firmware — work as-is.
  4. Submit with submitBlockNative blocks need no aux data.
  5. Pay miners to zkas: addressesThe simplest correct pool is a direct pool: no coinbase override, so the template pays the coinbase straight to the address the finding miner authorized with, and the chain mints the reward as a shielded note to that miner. The pool never holds funds and there is nothing to sweep, claim or sign.

Solo miners can use the packaged solo gateway instead, which bundles the node, an optional Kaspa parent and the bridge.

Template caching — the one bug that costs you blocks

This silently destroyed ~80% of blocks on the reference pool before it was found.

Kaspa pools commonly cache one template and rewrite the miner's payout into it per worker, and the usual shortcut is "rewrite the last coinbase output". On ZKas the last coinbase output is the dev-fee output, and the second-to-last may be the red-block reward. Rewriting outputs.last() therefore hijacks the dev fee, leaves the red reward stale, and every such block is rejected with:

"coinbase transaction is not built as expected"

Two correct options:

  • Re-point by scanning outputs in reverse and skipping the dev-fee output — it pays the dev recipient, a version-0 script whose first 43 bytes are the dev fund's Orchard address; or
  • simplest and always correct: call getBlockTemplate per miner and cache nothing.

Watch the node log for that message. A non-zero rate means you are losing blocks, not that miners are sending bad shares. The node's own modify_block_template() shows the correct shape: keep blue score, subsidy and the shielded commitment, and re-point only the miner's own outputs.

Merged mining with Kaspa (aux-PoW)

A ZKas block is valid if either holds:

Native
the block's own kHeavyHash header clears the block's ZKas target. Ordinary mining. This is the backbone — chain liveness never depends on Kaspa miners. Accepted at all times
Aux-PoW
the block carries proof that a parent kHeavyHash block did work clearing ZKas's own target and is cryptographically bound to this ZKas block

The aux proof is checked against ZKas's target from its own DAA retarget, not Kaspa's. Kaspa's target is far harder, so hashes that fail Kaspa routinely clear ZKas — merged mining adds ZKas security at essentially zero marginal cost. The verifier tries native first, then aux, and a valid native block can never be invalidated by a malformed or stripped aux proof because aux data is excluded from the block hash.

Binding chain

parent header a real Kaspa block hash_merkle_root leaf 0 = coinbase parent coinbase ZKMM ‖ H_zk ZKas block H_zk kHeavyHash(parent) ≤ ZKas target ← check (c) work check (b) inclusion check (a) binding H_zk excludes aux_pow, so it is stable whichever parent later carries it — and a parent committing to H_a can never be reused for H_b.
Verification runs right to left: the parent's work must clear ZKas's target, its merkle root must contain the coinbase, and that coinbase must commit exactly this block's hash.
  1. Build the ZKas block and compute H_zkH_zk is computed over the explicit header fields and excludes aux_pow, so it is stable no matter which parent later carries it.
    The single most common cause of a rejected merged block. H_zk commits the ZKas coinbase through hash_merkle_root, and the coinbase carries the mandatory dev-fee output. Use the template coinbase verbatim. If you rebuild it, the H_zk you committed will not equal the hash of the block the node validates, the binding check fails, and — since a merged block has no native PoW to fall back on — submitBlock returns block has invalid proof-of-work.
  2. Embed the commitment in the parent's coinbase payloadIn the miner-writable extra_data slot — the only field you can set in a real Kaspa block without modifying Kaspa. Exactly once; duplicate or ambiguous commitments are rejected, erring safe.
    MERGE_MINE_MAGIC (4 bytes, ASCII "ZKMM") || H_zk (32 bytes)
  3. Mine the parent header with kHeavyHash against ZKas's bits
  4. Submit the ZKas block with the aux proof attached
    AuxPow {
        parent_header:          Header,       // the mined parent
        parent_coinbase:        Transaction,  // carries ZKMM || H_zk
        coinbase_merkle_branch: Vec<Hash>,    // coinbase (leaf 0) -> root, max 64
    }

What the verifier checks

CheckAssertion
Bindingcommitted_hash(parent_coinbase) == H_zk
Inclusionfold(coinbase, branch) == parent_header.hash_merkle_root
WorkkHeavyHash(parent_header, nonce) <= ZKas target

The parent need not be a valid Kaspa block — only that real kHeavyHash work, bound to H_zk, cleared ZKas's target. Stealing another block's PoW fails: a parent committing to H_a cannot be reused for H_b.

Dual-chain operation

To earn KAS and ZKAS from one hash: pull the parent template from a real Kaspa node, embed ZKMM‖H_zk in its coinbase extra_data, mine it against ZKas's bits, and submit the solved parent to both chains — to Kaspa as a normal block, to ZKas as an aux block. If the Kaspa node is unreachable or in IBD, fall back to a synthetic parent so ZKas blocks keep flowing; that round earns no KAS but the chain stays live.

In the reference bridge this is entirely environment-driven, no code changes:

ZKAS_MERGED_MINING=1                 # enable aux-PoW mode
ZKAS_KASPA_NODE=<host:port>          # a real Kaspa node gRPC
ZKAS_KASPA_PAY=kaspa:qz9v...         # where KAS rewards are paid

Two tag formats exist in the wild — ZKMM<hash> and /zkas/…. Index both if you are measuring merge-mining participation.

RPC and wire reference

SurfaceDetail
Header fieldHeader.aux_pow: Option<Box<AuxPow>> (borsh; None = native). Excluded from hashing, so H_zk is unchanged by it
P2PBlockHeader.auxPow = field 15, borsh bytes; empty means native
gRPCRpcBlockHeader.auxPow = field 16. RpcRawHeader.aux_pow is a borsh-hex string
ConstantMERGE_MINE_MAGIC = b"ZKMM"; commitment is the 32-byte H_zk
Submit merged blocks via the RpcRawHeader / wRPC path. The plain gRPC RpcBlockHeader → Header conversion sets aux_pow = None and silently drops the proof. Native blocks are unaffected.

Payout: direct or custodial

Direct — recommended

Run with no coinbase override. Every template pays the coinbase to the address the finding miner authorized with, and the chain mints it as a shielded note to that miner's own zkas: address. The pool never holds miner funds, there is nothing to claim, and no signature is ever needed. This is by far the simplest correct ZKas pool, and it is a full solo pool.

Custodial — understand what you are taking on

ZKas has no transparent UTXOs. You cannot sweep with Kaspa payout tooling; you need a wallet that builds Orchard transactions.

  • Reward discovery is easy. Coinbase mints are public — recipient and value are visible in GetShieldedBlocks with no viewing key — so you can watch your own treasury from a plain node stream. Use GetShieldedCoinbaseRewards for the income view, keyed by (coinbaseTxid, outputIndex).
  • Paying out is where the cost is. Each transaction spends at most 38 notes at ~2.4 core-seconds per note. A treasury accumulating one coinbase note per block accumulates 86,400 notes per day. Paying that out one note at a time is hours of CPU.
  • The fix is consolidation, not more cores. Merge many small notes into few large ones in the background so a payout spends 3 notes instead of 300. zkas-walletd --auto-consolidate is on by default with a ceiling of 500.
A coinbase pays its mergeset, not its finder. Matching "blocks I found" against "coins I received" will not reconcile. Attribute income from GetShieldedCoinbaseRewards over the recipients you care about.
Do not set a coinbase override on a live pool until your payout path is complete and end-to-end tested. Overriding redirects every reward to you with no machinery yet to pay it back out.

Full sizing model, measured numbers and the payout playbook: Wallets & walletd.

Error message → cause

coinbase transaction is not built as expected
You rebuilt or rewrote the coinbase and it does not match consensus. Almost always the dev-fee output, or a hardcoded payload offset that ignores the 32-byte shielded commitment. Applies to native blocks
block has invalid proof-of-work on a merged submission
The H_zk you committed into the parent coinbase is not the hash of the block you submitted, so the binding check failed and a merged block has no native fallback. Usually the same root cause: the coinbase you bound differs from the coinbase you submitted
invalid prefix zkas / invalid prefix kaspa
Address HRP mismatch between your pay address and the node's network
Aux proof accepted by your bridge but the node says native-only
Merged mining is gated on that network and the block's DAA score is below activation. Mine natively until then
Template fetch works, submission always fails, shares look fine
You cache and rewrite templates. See template caching
Hashrate looks right but block rate collapsed
Difficulty retargets over ~44 minutes. An abrupt hashrate exit strands the chain at too-high difficulty and stretches block times until it corrects

Source map

Source map: the exact files and symbols6 entries
Emission, dev fee, payload layout
consensus/src/processes/coinbase.rs — SUBSIDY_HALVING_INTERVAL_MONTHS, TAIL_SUBSIDY_*, dev_fee_cut(), expected_coinbase_transaction(), serialize/modify/deserialize_coinbase_payload(), calc_block_subsidy()
Shielded coinbase minting
consensus/src/processes/shielded.rs — build_coinbase_mint()
Merged mining
consensus/core/src/auxpow.rs, consensus/pow/src/auxpow.rs — verify_aux_pow(), check_pow_dual(), check_pow_gated(); consensus/core/src/hashing/header.rs proves aux is excluded
Template building
mining/src/block_template/builder.rs — modify_block_template()
Network parameters
consensus/core/src/config/params.rs, constants.rs, network.rs
Reference bridge
bridge/src/kaspaapi.rs (template fetch, submit, merged parent), merged.rs (aux assembly, dual submit), client_handler.rs (per-vendor stratum handshakes, job watchdog), share_handler.rs (validation, vardiff)

What joining does to difficulty, rewards and everyone else's income: merged-mining economics.

Verified against zkas-rusty at zkas-v1.0.9. Byte layouts, endpoint shapes and parameters are read from source; figures marked measured come from the live mainnet node. If this page contradicts the code, the code is right — report it.