Mining pool integration

This guide covers running a pool for NULL: getting templates from a node, what to hash, how to submit, and how to pay miners. The proof of work is Equihash (144, 5) on mainnet, the same family Zcash pools run, so most of an existing Zcash pool applies. The differences are called out below. Method details are in Mining methods.

What is different

  • The node builds the coinbase. The coinbase is a shielded transaction with a zero-knowledge proof, so a pool cannot assemble it. The node builds and proves the coinbase for your payout address and hands you a finished header to grind.
  • You send back only what changed: the timestamp, the nonce and the solution.
  • Payouts are shielded sends through the wallet daemon, up to fifteen recipients per transaction.
  • Rewards mature after 100 blocks. A reward mined at height h enters the note tree at h + 99, the wallet sees it then, and it is spendable from h + 100. getblockchaininfo reports this as coinbase_maturity.

1. Run a synced node with JSON-RPC

nulld run --network main --datadir /var/lib/null \
    --rpc 127.0.0.1:18444 --rpc-http 127.0.0.1:18446 \
    --blocknotify 'curl -s http://127.0.0.1:9000/newtip/%s'

TOKEN=$(cat /var/lib/null/rpc.token)
rpc() {
  curl -s -H "Authorization: Bearer $TOKEN" \
    -d "{\"jsonrpc\":\"2.0\",\"method\":\"$1\",\"params\":$2,\"id\":1}" \
    http://127.0.0.1:18446/
}

rpc getmininginfo '[]'

See Run a node for the rest of the node setup.

--blocknotify is optional. It runs a shell command on every new main-chain tip, with %s replaced by the block hash, for pools that prefer a hook to long polling.

getmininginfo reports height, syncing, next_height, next_target, subsidy, equihash and work_per_second, a coarse network-rate estimate from block timestamps, like Bitcoin's networkhashps.

When the node hands out work. getblocktemplate fails with code -2 and a message starting not ready to mine: while the node is not ready:

MessageCause
not ready to mine: no peers yetNo peer has finished its handshake. Mainnet only.
not ready to mine: catching up with the networkBlocks are downloading, or a peer reports a longer chain.

Retry until it answers. This stops a pool from mining on a stale tip. With --metrics, the node also exports the gauge null_mining_ready (1 when it hands out work, else 0). See Mining.

2. A payout address

Create the pool's wallet and take its main address. The same wallet pays miners later.

export NULL_WALLET_PASSPHRASE='choose a long one'
nulld create --wallet /srv/pool/pool.redb --network main
# seed_phrase: ...
# address: null1...

3. Get a template

rpc getblocktemplate '["null1..."]'

The answer is the block minus its proof of work:

FieldMeaning
template_idWhat you submit against.
expires_atUnix time after which the template is no longer accepted (ten minutes after it was built).
height, prev_hash, branchThe block's position and the rule set in force.
version, timestamp, commitment_root, tx_root, targetHeader fields. target is the compact form, as hex.
min_timestamp, max_timestampThe range a rolled timestamp must stay in.
target_hexThe 256-bit target the block hash must not exceed.
pow_inputThe exact bytes to hash, see below.
equihashn, k, personalization (hex of the eight-byte prefix), solution_length in bytes.
transaction_count, coinbase_value, feesBlock contents. coinbase_value is subsidy plus fees, in units.
longpollidPass it back to wait for the next tip.

The first call builds and proves the coinbase, about a second. The same template is returned for five seconds after that, then rebuilt. Ask again whenever the tip changes.

To be told when it changes, pass the longpollid you were given. The call waits up to a minute for a new tip, then answers with a template:

rpc getblocktemplate '["null1...", "<longpollid>"]'

4. What to hash

pow_input is the header serialized without its proof-of-work fields, 113 bytes in this order:

FieldBytesEncoding
version1
previous block hash32bytes as given in prev_hash
height4little-endian
timestamp8little-endian seconds
commitment root32bytes as given
transaction root32bytes as given
target4little-endian compact form

The full header follows it with a 32-byte nonce and a 100-byte solution field, 245 bytes in all. Rebuild pow_input from the fields yourself and check it equals the template's pow_input before grinding. That catches serialization slips before they cost you a block.

Equihash. It runs as in Zcash, with one change: the BLAKE2b personalization is the eight bytes nullPoW_ followed by n and k as little-endian 32-bit integers, where Zcash uses ZcashPoW. The hash input is pow_input || nonce.

Network(n, k)solution_length
main(144, 5)100 bytes
test(48, 5)36 bytes

The solution uses Zcash's minimal encoding. The header field is always 100 bytes; a shorter solution is zero-padded. On mainnet a solver needs about 1.8 GB of memory per thread.

Block hash. BLAKE2b-256 with personalization null_BlockHash__ over the full 245-byte header (pow_input || nonce || solution, solution padded to 100 bytes). Read the hash as a big-endian 256-bit integer; it must be at or below target_hex.

If you roll the timestamp, keep it between min_timestamp and max_timestamp. The timestamp is part of pow_input, so a share is valid only for the timestamp it was hashed with.

5. Submit

rpc submitblock '["<template_id>", <timestamp>, "<nonce hex>", "<solution hex>"]'

Response

{ "status": "accepted", "hash": "..." }
StatusMeaning
acceptedThe block is on the main chain.
duplicateThe node already had it.
staleValid, but a competing block won. You get this when the tip moved under the template.

An invalid block is error code -2 with the validator's reason, such as hash does not meet the target; the template stays usable for another try. An unknown or expired template_id is also -2.

Pools that assemble their own blocks can submit the full serialized block instead: submitblock '["<block hex>"]'. Submitting by template id is simpler.

6. Shares and Stratum

Stratum is your side. Give miners the template's header fields and a share target easier than the block target. Validate shares against pow_input with your own Equihash verifier, using the personalization above, and submit any share that also meets the block target.

The 32-byte nonce is large enough to partition among miners however you like. Nothing in the header is per-miner, so one template serves every connected miner until the tip moves.

7. Paying miners

Run the wallet daemon on the pool wallet and batch payouts:

null-wallet-rpc --wallet /srv/pool/pool.redb --network main \
    --node 127.0.0.1:18444 --node-token-file /var/lib/null/rpc.token \
    --listen 127.0.0.1:18447

W=$(cat /srv/pool/wallet-rpc.token)
wrpc() {
  curl -s -H "Authorization: Bearer $W" \
    -d "{\"jsonrpc\":\"2.0\",\"method\":\"$1\",\"params\":$2,\"id\":1}" \
    http://127.0.0.1:18447/
}

wrpc sendmany '[[{"address":"null1...","amount":"120000000"}], 100]'
wrpc getoperationstatus '[<operation_id>]'

sendmany takes up to fifteen recipients. Its second parameter is the minimum confirmations a note needs before it is spent. Block rewards are notes like any other, so this is your payout maturity policy. The network refuses reorganizations deeper than 200 blocks, so 200 is safe against anything it accepts; pick your own smaller number.

Batching. The fee is fixed by action class, 10,000 units per action in classes of 2, 4, 8 or 16. Fifteen recipients plus change is sixteen actions, 160,000 units, the same as for nine recipients. Batching is where the savings are.

Consolidation. A pool wallet collects one note per block found, and a transaction can spend at most 16 notes. Merge reward notes into larger ones before large payout runs with quoteconsolidation and consolidate on the wallet daemon, or nulld consolidate --dry-run from the command line. The exchange guide describes both.

Operations persist in the wallet file and are rebuilt if a transaction is not mined. See the exchange guide for the operation lifecycle and Wallet methods for the reference.

8. Test drive on one machine

On the test network Equihash is (48, 5) and the target is trivial, so a naive solver finds a solution in milliseconds, and a node with no peers still hands out templates.

mkdir -p /tmp/null-pool
export NULL_WALLET_PASSPHRASE=test
nulld create --wallet /tmp/null-pool/pool.redb --network test
# address: tnull1...

nulld run --network test --datadir /tmp/null-pool/chain \
    --rpc 127.0.0.1:18444 --rpc-http 127.0.0.1:18446
TOKEN=$(cat /tmp/null-pool/chain/rpc.token)

Then run the four steps in order: getmininginfo, getblocktemplate with the tnull1 address, grind, submitblock. Check your header rebuild against pow_input, submit by template id, and try a full-block submit, a duplicate and a long poll. Switch to --network main and a null1 address for production.