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.
The five differences
- The payout address is shielded, version 9 mandatory
You pass a
zkas:address togetBlockTemplateexactly like akaspa: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
- 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()
- The coinbase payload has an extra 32-byte field mandatory
ZKas inserts the shielded state root between
subsidyand the script-pubkey block: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.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
- 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.
- 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.
Standing up native mining
- Run a nodeAdd
kaspad --appdir=<dir> --rpclisten=127.0.0.1:16810 --utxoindex
--enable-unsynced-miningonly when bootstrapping a brand-new network with no peers. Never on a pool mining the live chain. - Point your bridge at the node's gRPC
getBlockTemplate/submitBlockbehave exactly as on Kaspa.The node rejects aGetBlockTemplateRequest { pay_address: "zkas:...", extra_data: <your extranonce> }pay_addresswhose HRP does not match its network —invalid prefix …means you pointed azkas:address at a Kaspa node, or a pre-rebrand node at azkas:address. - Hand the header to miners over stratum, unchangedkHeavyHash ASICs — Bitmain, IceRiver, Goldshell, GodMiner firmware — work as-is.
- Submit with
submitBlockNative blocks need no aux data. - 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
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
getBlockTemplateper 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
- Build the ZKas block and compute
H_zkH_zkis computed over the explicit header fields and excludesaux_pow, so it is stable no matter which parent later carries it.The single most common cause of a rejected merged block.H_zkcommits the ZKas coinbase throughhash_merkle_root, and the coinbase carries the mandatory dev-fee output. Use the template coinbase verbatim. If you rebuild it, theH_zkyou 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 —submitBlockreturnsblock has invalid proof-of-work. - Embed the commitment in the parent's coinbase payloadIn the miner-writable
extra_dataslot — 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)
- Mine the parent header with kHeavyHash against ZKas's bits
- 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
| Check | Assertion |
|---|---|
| Binding | committed_hash(parent_coinbase) == H_zk |
| Inclusion | fold(coinbase, branch) == parent_header.hash_merkle_root |
| Work | kHeavyHash(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
| Surface | Detail |
|---|---|
| Header field | Header.aux_pow: Option<Box<AuxPow>> (borsh; None = native). Excluded from hashing, so H_zk is unchanged by it |
| P2P | BlockHeader.auxPow = field 15, borsh bytes; empty means native |
| gRPC | RpcBlockHeader.auxPow = field 16. RpcRawHeader.aux_pow is a borsh-hex string |
| Constant | MERGE_MINE_MAGIC = b"ZKMM"; commitment is the 32-byte H_zk |
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
GetShieldedBlockswith no viewing key — so you can watch your own treasury from a plain node stream. UseGetShieldedCoinbaseRewardsfor 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-consolidateis on by default with a ceiling of 500.
GetShieldedCoinbaseRewards over the recipients you care about.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-workon a merged submission- The
H_zkyou 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.rsproves 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.
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.