Qanum User Guide

Running a node, qanumd and qanum-cli, wallets and everyday flows.

This page reproduces docs/USER-GUIDE.md from qanum-core v0.9.0 (release tag). Links to files that are not published on this site are shown as plain text until the source repository is public.

Contents

A practical guide to running a Qanum node, configuring it, and using the command-line tools. This is the how to use it document; for the full reference of every RPC method see RPC.md, and for every configuration option see CONFIGURATION.md.


Table of contents


1. Quick start

# Build (release). The post-quantum OQS backend is mandatory: `crypto-oqs`
# is a default feature and the node does not build or start without it
cargo build --release --features crypto-oqs

# Start a regtest node that mines blocks continuously
./target/release/qanumd -chain=regtest -gen=1 --reward-address=<your_address>

# In another terminal, talk to it
./target/release/qanum-cli -chain=regtest getblockchaininfo

The qanum-cli commands below assume -chain=regtest (and RPC auth flags if your node requires them). See the cheatsheet at the bottom.

Tip: regtest is the recommended network for experimenting. Blocks are generated with proof-of-work at minimum difficulty, so they are fast, and you can mint blocks instantly with generatetoaddress (see Everyday flows).

First sync on testnet

./target/release/qanumd -testnet

# In another terminal: how many peers am I connected to?
./target/release/qanum-cli -testnet getconnectioncount

net_v2 note: the whole P2P stack (peers, header sync, IBD and the QUSN snapshot bootstrap of AIP-0001) is on by default. Opt out with -nonetv2 (CLI) or netv2=0 (qanum.conf); if the log shows WARNING net_v2=disabled, the node is RPC-only (no peers, no sync).


2. The two binaries

Binary Purpose
qanumd The node daemon. Starts the node, syncs over the P2P network, mines, serves RPC. Also has one-shot maintenance commands (init, chainstate-info, reindex, …).
qanum-cli The client. Talks to a running node over RPC (--addr). Also has offline commands that read the datadir directly (listkeys, signrequest, verifyrelease, reindex…).

Ecosystem compatibility: binary roles, flag spelling (-key=value), config keys and RPC method names follow the de-facto conventions of the Bitcoin node ecosystem, so existing operator habits, scripts, explorers and configuration files transfer directly. Qanum-specific commands are marked as such throughout this guide.


3. Networks and default ports

Network -chain= RPC (loopback) P2P
Mainnet main / mainnet 127.0.0.1:8338 8445
Testnet test / testnet 127.0.0.1:18338 18445
Regtest regtest 127.0.0.1:28338 28445

All P2P traffic, on every network, is encrypted and authenticated (Noise hybridXX with ML-KEM-1024 and an ML-DSA-87 node identity pinned trust-on-first-use, TOFU). There is a single P2P port. Every network requires the OQS verifier (see Security notes).

Selecting the network (both binaries, same flags)

Both qanumd and qanum-cli accept the same network flags, in single- and double-dash spelling:

-chain=<net> | --chain=<net> | -chain=<net>   # with = or as separate arg
-testnet | -regtest | -mainnet                    # boolean shorthands

Allowed values: main | mainnet, test | testnet, regtest. Examples that are all equivalent:

qanumd -chain=regtest            qanum-cli -chain=regtest getblockcount
qanumd --network=test            qanum-cli -testnet getblockcount
qanumd -regtest                  qanum-cli --chain=regtest getblockcount

4. Configuration in one minute

Configuration lives in ~/.qanum/qanum.conf (loaded automatically by both binaries). A commented template ships with the repo:

cp qanum.conf ~/.qanum/qanum.conf
# edit to taste

Precedence: command-line flags > qanum.conf > built-in defaults.

The most important keys:

# Which network: main / test / regtest
chain=regtest

# RPC
rpc=1
rpc_bind=127.0.0.1:28338
#rpcuser=user
#rpcpassword=change-me

# Validation (mainnet requires verifier=oqs)
verifier=oqs

# Mine on startup (CPU mining, demo/testing)
#gen=1
#reward_address=tqn1...

# Optional indexes for the explorer RPC
#txindex=1
#addressindex=1

# In-memory block-store index (default 1). 0 = directory-scan fallback.
#blockstore_cache=1
#blockstore_index=1
#checkblockindex=0

See CONFIGURATION.md for the complete list of keys, flags and their defaults.


5. Running the node (qanumd)

Daemon mode (default)

qanumd -chain=regtest                       # start, stay in the foreground
qanumd -chain=regtest -gen=1                # start and mine with CPU
qanumd -chain=regtest -gen=1 --reward-address=<addr>   # rewards to an address
qanumd -chain=testnet -addnode=peer:18445   # join a specific peer

Useful daemon flags (full list in CONFIGURATION.md):

Flag Meaning
-listen=0 Don’t accept inbound P2P connections
-port=<p> / -port <p> Override the P2P port (the only one). Space and = forms are equivalent. -plaintextport was removed: it stops the node
-nonetv2 Disable the whole P2P network stack (net-v2). Any later net-v2 flag (-port, -addnode, …) re-enables it
-addnode=<host[:port]> Also connect to this peer (repeatable). IP or hostname; a hostname is re-resolved every 60 s (dynamic DNS)
-connect=<ip:port> Connect only to these peers (repeatable)
-seednode=<host\|ip>[:port] / -dnsseed=<host> Bootstrap from a static seed (hostname or IP) / DNS seed
-maxconnections=<n> Cap on P2P connections (default 64). Inbound cannot fill the last min(8, n/2) slots, which stay reserved for outbound
-rpcbind=ip:port RPC listen address (default depends on network). --rpc-bind is also accepted.
--rpcuser=u --rpcpassword=p Basic auth for RPC. Prefer qanum.conf (chmod 600) or the QANUM_RPCPASSWORD environment variable over argv — see Secrets and the command line below
--no-rpc Disable the internal RPC server
-rpcthreads=<n> Number of worker threads serving RPC calls (default 16)
-rpcworkqueue=<n> Depth of the RPC work queue (default 64). When full, excess requests are rejected with HTTP 503 “Work queue depth exceeded”
-gen=1, --mine Enable CPU mining
--reward-address=<addr> Address that receives the block reward
--verifier=oqs Post-quantum signature verification
--strict-full Require full validation (PoW + signatures)
-minimumchainwork=<hex> [CVE-2019-25220] Replaces the compiled per-network minimum chain work (0 on every network today). The effective anti-DoS threshold is max(best header tip work − 1 day of work, this value); header chains below it go through the header pre-synchronization before being stored. A node whose tip is below this value stays in IBD. 0 = no static floor
-assumevalid=<hex\|0> Signature-validity assumption: skip post-quantum signature checks only for buried ancestors of this block on the best header chain (PoW and UTXO always verified; never chooses the chain). 0 disables it. Default: the compiled hash of the network (see CONFIGURATION.md)

One-shot maintenance commands

Run once and exit — they don’t need the daemon running:

Command What it does
qanumd init Initialize the chainstate for the selected network
qanumd chainstate-info Show current chainstate info (height, tip, utxos…)
qanumd mempool-state Show mempool statistics (offline)
qanumd list-mempool List transaction ids in the mempool (offline)
qanumd list-blocks List all stored blocks (height, hash, file)
qanumd show-block <h> Show details of one stored block
qanumd reset Reset the chainstate back to the network genesis
qanumd reindex Rebuild the chainstate from the blocks on disk (integrity check + reconnection from genesis with full consensus validation — see “Offline / maintenance”)
qanumd utxo-snapshot-export <path> Export the tip UTXO set as a trustless QUSN v2 snapshot (manifest + full header chain 0..=H + UTXO set) — see “Snapshot sync (QUSN v2)”
qanumd utxo-snapshot-verify <path> Re-link the snapshot’s embedded header chain, re-check PoW where the network enables it, recompute utxo_root and pin it against the local chain
qanumd utxo-snapshot-install <path> [--force] Install a verified v2 snapshot as the sync base of a fresh datadir; refuses to overwrite an existing chain unless --force. Without --force it also refuses a snapshot whose chain has a block marked invalid on this node, or that forks from a known header chain with more work
qanumd mine-block / mine-blocks <n> Mine and import one / n blocks immediately (same template builder as the daemon: max(MTP+1, now), package selection, block budgets)
qanumd start Same as running with no command

Example:

qanumd -chain=regtest --datadir=/tmp/node init
qanumd -chain=regtest --datadir=/tmp/node mine-blocks 5
qanumd -chain=regtest --datadir=/tmp/node chainstate-info

Note: these one-shot commands work on the datadir directly and take the datadir lock, so they refuse to run while qanumd is running on that datadir (stop the daemon first, or use qanum-cli over RPC). To submit a transaction or a block, use qanum-cli sendrawtransaction / submitblock.


6. Querying the node (qanum-cli)

The client follows the ecosystem-standard naming. Options:

Option Meaning
-rpcconnect=<ip> Host of the node to send commands to (default 127.0.0.1)
-rpcport=<port> Port of the node RPC (default from the selected network)
--addr=<ip:port> Host and port in one argument — Qanum shorthand for the two above
-chain=<net> / -chain=<net> main|mainnet, test|testnet, regtest (also -testnet/-regtest/-mainnet)
--rpcuser / --rpcpassword Basic auth credentials (argv risk — see below)
-stdinrpcpass Read the RPC password from standard input (first line) instead of argv
-stdinwalletpassphrase Read the wallet passphrase from stdin (only for walletpassphrase/walletpassphrasechange; the passphrase becomes the first RPC argument)
-stdin Read extra RPC arguments from stdin, one per line until EOF/Ctrl-D — the way to keep ALL secrets (e.g. both passphrases of walletpassphrasechange, or encryptwallet) out of argv
--rpccookiefile=<path> Cookie file (default datadir/.cookie)
--datadir=<path> Local datadir for offline commands and the default cookie
--compact Compact JSON output
-h <cmd> / help <cmd> Per-command help: usage, arguments, result and examples. Also --help <cmd> and <cmd> --help. qanum-cli help prints the general usage
-version / --version Print the client version and exit — also accepted by qanumd

Blockchain

getblockchaininfo           overall chain state (extended with Qanum fields)
getblockcount               height of the best tip
uptime                      seconds since the node started
stop                        request a graceful shutdown of the node
estimatesmartfee [n]        estimated fee rate for ~n blocks (default 6)
getbestblockhash            hash of the best tip
getblockhash <h>            hash at height h
getblock <hash-or-h> [verbose]   block details; verbose=0 → raw hex
getblockstats <hash-or-h>       block statistics (weight, fees, subsidy, percentiles)
getblockfilter <hash> [type]    BIP158 compact block filter of a block (type: basic)
                             (with -blockfilterindex both filter and header come from the persistent index, O(1))
getblockheader <hash-or-h> [verbose]
gettxout <txid> <vout>      UTXO lookup (null if spent/nonexistent)
gettxoutsetinfo             UTXO set statistics
getindexinfo [index_name]   optional-index sync status (txindex / addressindex / blockfilterindex)
getblockstoreinfo           block-store cache + index KPIs (listings = real blocks/ scans; index_source)
getchaintips                known chain tips (active / valid-fork / headers-only / invalid)
getaddressbalance <addr>    balance of an address (requires -addressindex)
getaddresstxids <addr>      history of an address (requires -addressindex)
getaddressutxos <addr>      unspent outputs of an address (requires -addressindex)

Generating

generatetoaddress <n> <addr>   regtest only: mint n blocks instantly to addr

Mining

getmininginfo               mining state (gen, blocks mined, LOCAL hashrate, NETWORK hashrate, difficulty, miner threads)
getnetworkhashps [nblocks] [height]  estimated NETWORK hash rate (chain work/time; -1 = whole chain)

Mempool / raw transactions

getmempoolinfo              mempool statistics (`size`, `bytes`, `maxmempool` = 32e6, fees; plus datadir fields)
getrawmempool [verbose]     list of txids (verbose=1 → detail per tx)
getmempoolentry <txid>      detail of one mempool tx (fee, size, depends…)
getrawtransaction <txid> [verbose]   tx as hex (0) or JSON (1)
sendrawtransaction <hex> [verif=oqs] submit a signed raw tx

Network

getnetworkinfo              node/network info (version, services, fees…)
getpeerinfo                 connected peers
getsnapshotinfo             UTXO snapshot status/progress (AIP-0001)
addnode <ip:port> [add|remove|onetry]   manage the addnode list
getaddednodeinfo [node]     show the addnode list
disconnectnode <ip:port>    remove from addnode list and close the connection
setban <ip> <add|remove> [bantime] [absolute]   ban/unban an IP (default 24 h)
listbanned                  list banned IPs (manual and automatic)
clearbanned                 lift every ban and forget misbehavior scores

Bans and manual peers. A peer that breaks the protocol accumulates a per-IP misbehavior score; at 100 its IP is banned for 24 h (inbound refused, no automatic outbound). Connections you opened with -addnode / -connect (or addnode add) are never scored or banned: the node only logs WARNING: not punishing manually connected peer …, and keeps dialling them even if their IP is banned (e.g. because of an inbound connection from the same host). To stop talking to such a peer, use addnode <node> remove, not setban. setban remove / clearbanned also reset the misbehavior score.

With -blockfilterindex=1, an up-to-date net_v2 node advertises NODE_COMPACT_FILTERS and serves BIP157 getcfilters, getcfheaders, and getcfcheckpt requests from the persistent index. P2P requests never trigger on-demand filter construction; invalid ranges/types/hashes receive no response.

Wallet

getnewaddress [label] [type] new address (`mldsa44` default, `mldsa87` level 5) or recovery key (`slhdsa`, `slhdsa256s`)
getwalletinfo               wallet state (encrypted, unlocked_until, keys, balance)
encryptwallet <pw>          encrypt the wallet (Argon2id + AES-256-GCM); locks it
walletpassphrase <pw> [t]   unlock the wallet in RAM for t s (default 300, 0 = forever)
walletlock                  lock the wallet (drop decrypted keys from RAM)
walletpassphrasechange <o> <n>   change the wallet passphrase
createwallet <name>          create and load a named wallet
loadwallet <name>            load an existing named wallet
unloadwallet <name>          unload a named wallet
listwallets                  list loaded wallets (restored across node restarts)
backupwallet <destination>    atomically back up the wallet file
# WARNING: dumpwallet writes every secret key as plaintext JSON
dumpwallet <filename>         export keys (encrypted wallet must be unlocked)
importwallet <filename>       validate and merge keys (must be unlocked if encrypted; rejects oversized dumps, wrong suite lengths, and — with OQS — secrets that do not match the public key)
--wallet=<name>               select the wallet for the RPC (multi-wallet)
getaddressinfo <addr>         address info (template, ismine, label, nist_level)
getreceivedbyaddress <addr> [minconf]   total received by an address
listtransactions [count] [skip]   P2PQK/P2RKPQ/Vault/HTLC history (confirmed + mempool)
gettransaction <txid>          wallet tx details, including custody sends/receives
listsinceblock [hash]          wallet history since a block
signmessage <addr> <msg>       sign a message with a wallet key
verifymessage <addr> <sig> <msg>   verify a message signature
sendmany <addr:amt,...> <fee>  send to multiple addresses at once
keypoolrefill [size]           pre-generate wallet keys of the -addresstype type (OQS required)
migratesuites [level5] [execute] [options]   move funds to level-5 keys (dry run by default)
abandontransaction <txid>      mark a wallet tx as abandoned

> New encrypted-wallet blobs keep only ownership metadata (`suite_id`,
> `key_id`, label and key count) outside the ciphertext. Balance, UTXO,
> history and `ismine` reads therefore work while locked. Signing, key
> generation, `listkeys`, `showkey`, `dumpwallet` and `importwallet` still
> require `walletpassphrase`. Older blobs migrate on passphrase change or the
> next authenticated wallet rewrite.
getbalance                  live P2PQK/custody balances (spendable/delayed/total)
listunspent [minconf]       wallet-controlled UTXOs
listreceivedbyaddress [minconf] [include_empty]
listkeys                    public keys in your keyring (alias: list-keys)
showkey <key_id>            details of one key (alias: show-key)
sendtoaddress <addr> <value> <fee>     send (value/fee in QAN or jomas)
sendfrom <from> <to> <value> <fee>     send from a specific address

Unconfirmed change and spendable balance. Coin selection spends confirmed UTXOs only: after a send, your change sits in the mempool and is not selectable until the transaction mines. A back-to-back send that needs that change therefore fails with insufficient spendable funds; the error itself reports any unconfirmed wallet-owned outputs pending in the mempool (amount and an example txid) so automated clients can tell the transient state apart from a genuinely empty wallet. getbalance counts those pending outputs in spendable/delayed (confirmed UTXOs already spent in the mempool are excluded), so the balance and the selection policy differ by design: the balance shows what you own, the selector spends what is confirmed.

Advanced addresses and custody

addr-htlc <sender> <recv> <hash_lock> <expiry>     HTLC address
addr-p2rkpq <main_key> <rec_key> <delay>           custodial recovery address
addr-vault <hot_key> <cold_key> <delay>            time-locked vault address
createmultisig <m> <key1> [key2..key16]                m-of-n multisig address
p2rkpq-main <txid> <vout> <dest_key> <value>       spend P2RKPQ main branch
p2rkpq-recovery <txid> <vout> <dest_key> <value>   spend P2RKPQ recovery branch
htlc-claim <txid> <vout> <preimage_hex> <dest_key> <value>  claim before timeout
htlc-refund <txid> <vout> <dest_key> <value>        refund at/after timeout
vault-init <txid> <vout> <pending> [return_val]    initialize a vault
vault-recover <txid> <vout> <dest_key> <value>     recover a vault after delay
vault-finalize <txid> <vout> <dest_key> <value>    finalize a vault
vault-cancel <txid> <vout> <dest_key> <value>      cancel a vault

All P2RKPQ/Vault/HTLC builders honor --wallet=<name>. The selected wallet must own the key for the requested branch: main/recovery for P2RKPQ; hot for vault init/finalize; cold for vault recover/cancel; recipient for HTLC claim; refund for HTLC refund. A wrong wallet fails early with KeyNotOwnedByWallet. Ownership can be checked while a new-format encrypted wallet is locked, but signing still requires walletpassphrase.

The P2RKPQ recovery key must be an SLH-DSA-128s key id — generate it with getnewaddress <label> slhdsa, which returns {key_id, suite, note} instead of an address (SLH-DSA-128s has no standalone payment address; the key_id is what addr-p2rkpq takes as <rec_key>). The requirement is enforced at build time with an actionable error. Consensus also accepts the level-5 pair, an ML-DSA-87 main key with an SLH-DSA-SHAKE-256s recovery key (from the suites5 activation height), but never a pair that mixes levels: create it with getnewaddress <label> mldsa87 and getnewaddress <label> slhdsa256s.

NIST level 5. From the suites5 activation height the wallet creates level-5 keys: getnewaddress <label> mldsa87 (an ML-DSA-87 address) and slhdsa256s (a recovery key). Before that height they are refused with error -4, which names the height. Without -addresstype the default type is ML-DSA-87 once level 5 is active and ML-DSA-44 before; qanumd -addresstype=<type> fixes it; -changetype chooses the change type (if unset, change follows a level-5 recipient, as in Bitcoin Core). getaddressinfo, validateaddress and listkeys report nist_level (ML-DSA-44 = 2, SLH-DSA-128s = 1, ML-DSA-87 and SLH-DSA-256s = 5). A level-5 input costs more: about 7.2 kB of signature against 3.7 kB, so roughly twice the fee.

Retirement of level 1/2. From a second height (retire12: testnet from its genesis, height 0; regtest only with -testactivationheight=retire12@<h>) new outputs may no longer use ML-DSA-44 or SLH-DSA-128s. Old coins stay spendable. getnewaddress mldsa44 and a send to an ML-DSA-44 address fail with -4 (ML-DSA-44 is retired: … use a level-5 suite (mldsa87)); new keys, change and mining rewards use ML-DSA-87. A vault funded before the height can still be unvaulted and finalized. getblockchaininfo shows both heights under softforks.

Moving existing funds to level 5: migratesuites. Migration is optional; old outputs stay spendable.

qanum-cli migratesuites                     # dry run: plan, exact fees, what stays
qanum-cli migratesuites level5 true         # execute (wallet unlocked if encrypted)
qanum-cli migratesuites level5 true '{"fee_rate":2,"consolidate":true}'

The remainder is the fee. These one-shot custody spends take exactly one input (<txid> <vout>) and create only the output(s) you ask for — there is no change output and no fee argument: whatever is left of the input value after <value> (for vault-init, after pending_value and return_value) goes to the miner as fee. Choose <value> accordingly; a <value> equal to the input value leaves a zero fee and the submit fails with FeeTooLow. The payment commands (sendtoaddress, sendfrom, sendmany) and buildp2multispend work the other way around: they take an explicit <fee> and return change.

createmultisig <m> <key1> [key2..keyN] builds an m-of-n multisig (P2MULTI) address. Consensus accepts 1 to 16 keys with 1 <= m <= n, ML-DSA-44 or, from the suites5 activation height, ML-DSA-87, mixed if wanted; qanum-cli help createmultisig prints the current limit, which is read from consensus rather than written into the help text. Unlike the builders above it does not require the wallet to own any of the keys: you can derive the address of a multisig you only cosign, or do not sign at all. Key ids are sorted canonically before encoding, so every cosigner who lists the same set of keys gets the same address regardless of the order they type them in. Duplicate keys are rejected: they would quietly lower the real threshold. Paying such an address works from sendtoaddress, sendmany and sendfrom like any other template. Spending from a P2MULTI output is covered below under Threshold spending.

addr-htlcer> <payment_hash> <timeout> now emits the canonical 109-byte HTLC payload. receiver claims before timeout with the matching preimage; sender refunds at or after timeout. The selected wallet must contain both address keys. htlc-claim validates SHA3-256(preimage) and height before building; htlc-refund validates the timeout height.

listtransactions, gettransaction, listsinceblock, and getwalletinfo.txcount include P2RKPQ, Vault, VaultPending, and canonical HTLC receives/sends. This works through the address index, the no-index canonical fallback, and the mempool (including chained unconfirmed spends). A custody output appears once even if the wallet owns both branch keys.

getbalance and getwalletinfo.balance also include unconfirmed custody outputs. P2RKPQ delays, Vault hot/cold rules, VaultPending unlock heights, and HTLC refund timeouts are reflected in spendable vs delayed totals. HTLC recipient outputs stay delayed/locked until a separate preimage-aware spend is built, because the wallet does not store preimages. Chained mempool outputs that are already spent are not counted.

Threshold spending (P2MULTI)

buildp2multispend <anchor_txid> <anchor_vout> <anchor_value> <anchor_address> \
                  <dest_address> <value> <fee> [txid:vout:value ...]
signp2multi <unsigned_tx_hex> <anchor_address> <anchor_value> <key_id> [input_index]
combinep2multi <tx_hex_or_build_json> <anchor_address> <anchor_values|-> <seat_json_or_file>...

Spending a P2MULTI output takes m cosigners, so it cannot be a single command: no one machine holds m keys, and that is the point of a threshold address. The work splits in four steps, and only the last one needs a node.

  1. Build. buildp2multispend produces the unsigned transaction and the sighash each seat must sign. Change returns to the anchor address — without a change output the remainder of the spent coin would go to the miner as fee.
  2. Sign. Each cosigner runs signp2multi on their own machine with their own wallet (--wallet=<name>), and sends back a small JSON with key_index, input_index, suite_id, pubkey_hex and sig_hex. This step is offline: no node, no network.
  3. Assemble. combinep2multi merges the seat JSONs (inline or as file paths; - takes the anchor values from the buildp2multispend JSON) into the final transaction: it sorts the seats into strictly increasing key_index order, attaches one auth_data blob per input, and refuses to emit below threshold m (listing the missing seats). Every seat is validated against the anchor payload (range, duplicates, pinned suite, key id, lengths) and against the sighash recomputed from the tx and the anchor values, and each signature is verified cryptographically (liboqs) before emitting. The output carries the signed_tx_hex and its txid_hex.
  4. Broadcast. Send the signed_tx_hex with sendrawtransaction. Consensus rejects a transaction whose auth count differs from its input count.

Fees are not an afterthought here: each seat contributes an ML-DSA-44 public key plus signature — 3 735 bytes per signature (7 222 for an ML-DSA-87 seat) — so the cost scales with m, not with n. A 2-of-3 spend is roughly 7.7 kB; a 9-of-16 is about 33 kB per input. A fee guessed too low is rejected at broadcast, after the cosigners have already signed.

That per-signature cost also meets a byte ceiling. All m signatures for one input share that input’s auth entry, capped at MAX_AUTH_BYTES_PER_INPUT (131 072 bytes). The binding rule is the key cap of 16: a 16-of-16 ML-DSA-44 spend uses 59 761 of those bytes, and the budget is sized so that a 16-of-16 with ML-DSA-87 keys (115 553 bytes) still fits.

Spending several outputs at once

A withdrawal larger than any single P2MULTI output needs to spend more than one. List the extra outputs after the fixed arguments as txid:vout:value (value in jomas):

qanum-cli buildp2multispend <txid> 0 40000j <anchor> <dest> 30000j 30470j \
          <txid2>:0:40000

The response then carries a sighashes array with one entry per input (sighash_hex still holds input 0, for single-input callers). Every input is authorised separately, so the signing work multiplies: a two-input spend under a 2-of-3 set needs four signatures, not two. Each cosigner signs each input, passing the input index as the last argument:

qanum-cli --wallet=guardian-a signp2multi <unsigned_hex> <anchor> 40000j <key_id> 0
qanum-cli --wallet=guardian-a signp2multi <unsigned_hex> <anchor> 40000j <key_id> 1

A signature is bound to its input: the sighash preimage commits to the input index and to the value of the output that input spends, so the same seat’s signature cannot be reused on another input. The fee grows roughly linearly with the input count for the same reason — each input carries its own witness.

Because each input pays for its own m signatures, a high threshold also bounds how many outputs one transaction may consolidate: MAX_TX_AUTH_BYTES is 1 000 000, so a 16-of-16 spend fits 16 inputs, while a 2-of-3 fits over a hundred. A withdrawal that needs more must be split across transactions.

signp2multi honours --wallet=<name>. A guardian seat normally lives in its own wallet rather than the datadir root; without the flag the command looks in the root keyring and reports the key as missing.

Peer identities (TOFU) and the node identity

listtofupeers               pinned peer identities (address, peer_id, last_seen)
removetofuentry <ip[:port]> forget a pin (IP clears every port; IP:port clears one)
resettofu                   forget ALL pins (LAN testing)
rotatep2pidentity [true]    new node identity; peers that pinned the old one follow it

These are RPCs of the running node. With the node stopped, delete <datadir>/peer_ids.json to forget every pin. See Security notes before removing pins or rotating.

Offline / maintenance

reindex                     rebuild chainstate from disk (node must be stopped)
reindexfilters              rebuild the BIP157/158 filter index from disk (node must be stopped)
signrequest <request.json> [out.json]   cold-sign a transaction request
verifyrelease <dir>         check a downloaded release: post-quantum signatures of SHA512SUMS + SHA-512 of every file (docs/SECURITY.md)
build-payment <src> <addr> <value> <fee>       build unsigned tx (debug)
build-payment-auto <addr> <value> <fee>        build unsigned tx, auto UTXO
build-p2rkpq-main <txid> <vout> <dest> <value> dry-run P2RKPQ main builder (prints the signing request; remainder is fee)
datadir-info                datadir diagnostics (paths, files existence)
list-blocks                 all stored blocks (height/hash/path + index fields)
reorg-plan                  fork realignment plan (null when on the best tip)

Values in build-payment* accept QAN decimals (1.0) or jomas integers (100000000), like sendtoaddress.

reindex and reindexfilters are full offline rebuilds (the node must be stopped) and print progress while they run. reindex verifies every block file (header re-hash against the index), puts the chainstate back at genesis (the block files are kept) and reconnects the best stored chain with full consensus validation, as Bitcoin Core -reindex-chainstate does: proof of work, subsidy, scripts and signatures with the --verifier of the command (signatures are skipped only for blocks covered by the compiled assumevalid, as in a normal sync). A stored block that fails is discarded and the best valid chain stays; the log ends with reindex: chainstate rebuilt with full validation: height=… tip=… (connected=… discarded=…). Its cost is that of validating the stored chain again (not yet measured on the full testnet chain). If chainstate.bin is missing or corrupt, the node does the same by itself at start (log chainstate reset to genesis: …). On a pruned datadir reindex is refused: the pruned blocks keep no signatures, so the chain cannot be validated again from disk (as in Bitcoin Core, where pruning is incompatible with -reindex-chainstate); sync a new datadir instead. Missing undo data of recent blocks is rebuilt by the node itself at start, pruned or not. reindexfilters rebuilds the BIP157/158 index linearly and is much cheaper. Plan a maintenance window.

When the block data itself is suspect (disk errors, blocks/ copied from another machine), do a fresh initial block download instead: stop the node and start it with a new datadir — keeping only the irreplaceable parts of the old one (wallets/, p2p_identity.json, peer_ids.json, addrman.json) — and let it sync from the network (~25 min for ~24.5k testnet blocks on Pi-class hardware).


7. Everyday flows

generatetoaddress is synchronous: the CLI prints the block hashes when all requested blocks are mined. Progress can be followed with getblockcount from another terminal (it advances during generation).

Mint regtest coins instantly (no CPU wait)

ADDR=$(qanum-cli -chain=regtest getnewaddress | python3 -c "import json,sys;print(json.load(sys.stdin)['address'])")
qanum-cli -chain=regtest generatetoaddress 101 "$ADDR"
qanum-cli -chain=regtest getbalance

generatetoaddress skips the proof-of-work search (regtest does not check the header hash against the target; other regtest nodes accept the blocks), validates each block like a received one and announces it to connected peers, so you reach coinbase maturity (1152 blocks) in about half a minute instead of ~20 minutes of CPU mining.

Mine with the daemon and collect rewards

ADDR=$(qanum-cli -chain=regtest getnewaddress)
qanumd -chain=regtest -gen=1 --reward-address="$ADDR" --mining-interval-ms=500
# in another terminal:
qanum-cli -chain=regtest getbalance

Send a payment

qanum-cli -chain=regtest sendtoaddress <destination> 1.0 0.0001
# 1.0 QAN amount, 0.0001 QAN fee — values accept QAN decimals or jomas

sendtoaddress builds the transaction, signs it locally with your keyring (requires the OQS build for signature generation) and submits it.

Node fee rate (settxfee). Pass 0 as the fee and the send charges the node’s configured rate instead: settxfee <rate> persists a fee rate in jomas per byte (integer — not a QAN/kvB decimal), and every send built with fee=0 (sendtoaddress, sendfrom, sendmany and the build-payment* dry-run builders) then charges rate × estimated signed size. settxfee 0 clears the rate and returns to explicit-fee-only sends. An explicit non-zero fee always wins over the configured rate, so settxfee never silently alters a send that carries its own fee.

Sign a transaction offline (cold signing)

# 1) build an unsigned payment request (the response embeds a signing_request)
qanum-cli -chain=regtest build-payment-auto <dest> 1.0 0.0001 > response.json
python3 -c "import json;json.dump(json.load(open('response.json'))['signing_request'],open('request.json','w'))"
# 2) sign it on an offline machine (needs the OQS build + the wallet datadir)
qanum-cli --datadir=/path/to/offline/wallet signrequest request.json signed.json
# 3) broadcast the signed hex from the node
qanum-cli -chain=regtest sendrawtransaction \
    $(python3 -c "import json;print(json.load(open('signed.json'))['signed_tx_hex'])")

Synchronize two nodes

# Node A (miner)
qanumd -chain=regtest --datadir=/tmp/a -gen=1 --reward-address=<addr> &
# Node B (joins A)
qanumd -chain=regtest --datadir=/tmp/b -addnode=127.0.0.1:28445 &
# both should converge to the same height:
qanum-cli -chain=regtest --addr=127.0.0.1:28338 getblockcount
qanum-cli -chain=regtest --addr=127.0.0.1:28348 getblockcount

Peer admin:

qanum-cli -chain=regtest addnode 127.0.0.1:28445 add
qanum-cli -chain=regtest getaddednodeinfo
qanum-cli -chain=regtest disconnectnode 127.0.0.1:28445

Snapshot sync (QUSN v2)

QUSN snapshots let a fresh node start from a recent UTXO set instead of replaying the whole history — without trusting anyone. Every header commits the UTXO-set root (utxo_root), so the artifact carries its own proof: the downloader re-links the embedded header chain (hash/prev linkage + PoW where the network enables it) and recomputes the root, which must equal the tip header’s own commitment. Compare with assumeutxo-style bootstrap, where the snapshot hash is anchored socially.

Two ways to get the artifact:

1. Out-of-band (operator copies a file). An archive node exports it:

# On the source node, with the daemon STOPPED (one-shot commands take the
# datadir lock and refuse to run while qanumd is running)
qanumd --datadir=/data/qanum utxo-snapshot-export /tmp/snapshot.qusn

# On the receiving machine: verify first (prints the re-linked header
# count, the committed vs recomputed root, and the pin against the local
# chain if one exists)
qanumd --datadir=/data/qanum-fresh utxo-snapshot-verify /tmp/snapshot.qusn

# Install as the sync base (fresh datadir only; --force overrides)
qanumd --datadir=/data/qanum-fresh utxo-snapshot-install /tmp/snapshot.qusn

Then start the daemon normally: it forward-syncs only blocks H+1..tip.

The export always follows the best chain: it walks back from the chainstate tip through prev_hash links rather than scanning the block directory by height, so blocks left on disk by past reorgs (stale fork blocks) can never leak into the artifact — even on a node whose block store contains abandoned branches.

2. Over the network (AIP-0001). Archive nodes advertise service bit NODE_UTXO_SNAPSHOTS and serve the artifact in rate-limited 2 MiB chunks. A fresh node opts in at start:

# Public network: servers and corroborators are found through the DNS seeds
qanumd -testnet --datadir=/data/qanum-fresh -snapshot-ibd=1

# Naming the archive node: it is tried first; corroborators still come from
# the DNS seeds (or from more -connect/-addnode peers, see below)
qanumd -testnet --datadir=/data/qanum-fresh -snapshot-ibd=1 -connect=<archive-host>:<p2p-port>

A single peer is never enough: the snapshot is installed only after an independent peer confirms its tip (see “Corroboration” below).

The bootstrap runs before the P2P runtime starts: connect (Noise + TOFU), handshake, download to <datadir>/.qusn-download/, verify through the same trustless path as a local file, install, then the regular sync loop finishes IBD. The whole bootstrap runs under a global time budget of 15 minutes across all candidates, so fallen or malicious peers can never delay startup indefinitely — when the budget runs out the node logs it and falls back to normal IBD like any other bootstrap failure. On any failure (unreachable peer, peer without the service bit, verification failure, budget exhausted…) it falls back to normal genesis IBD — the flag can never make a node unable to sync.

Resumable, chunk-checked downloads. Every 2 MiB chunk is verified on arrival against a Merkle root announced up front (a corrupt or malicious chunk is rejected immediately, and the bootstrap moves on to the next candidate), and progress is saved after each verified chunk. If the node restarts — or switches to a different server offering the same snapshot — the download resumes from the last verified chunks instead of starting over (gaps included); a different snapshot restarts it. A partial .qusn-download/ is only resumed while the datadir is still fresh (for example after the node was stopped mid-download). Once the node has fallen back to normal IBD and has a chain, a leftover partial download is deleted at the next start, and -snapshot-ibd=0 always deletes it. Size policy: the artifact install cap is 512 MiB and it is enforced up front (offers above it are rejected before any chunk is downloaded, and the staged file is size-checked before it is read into memory), so an oversized or malicious artifact wastes neither bandwidth nor RAM — the node simply falls back to normal IBD.

Parallel download. Multi-chunk snapshots are fetched from up to 4 servers at once: any extra peer offering the same artifact (checked field by field from QusnMeta) gets a disjoint slice of the missing chunks. Per-chunk Merkle proofs keep this trustless — a server feeding corrupt chunks only loses its slice, and the missing chunks are re-dealt to the remaining peers. With a single archive node the download simply runs over one connection. Connections that survive a round are RE-USED in the next one (they keep their registered server stream), and the downloader honors each server’s rate-limit window locally before re-asking — servers answer a too-early GetQusn with an empty meta instead of silence, so a re-acquisition never stalls the round.

Peer discovery. You don’t need to know an archive node up front. Explicit -connect/-addnode candidates are tried first; any reachable peer WITHOUT the snapshot bit is asked for its address table (GetAddr), and the entries that announce the bit join the candidate list. When the list runs dry, the bootstrap refills it from (1) the local addrman — peers recorded with NODE_UTXO_SNAPSHOTS in previous runs — and (2) the network’s DNS seeds (or -dnsseed=<host> overrides). With nothing to try at all it reports no candidate peers and falls back to normal IBD. (qanumd asks the DNS seeds for snapshot candidates even under -connect; the regular P2P runtime that starts afterwards honours connect-only mode.)

Corroboration and minimum work. Verification proves the artifact is internally consistent; it cannot prove that its chain is the one the honest network follows. Two checks tie it to the network before anything is installed:

In practice: on the public testnet the two DNS seeds sit in different networks and serve this role automatically. On a private network name at least one more peer that does not serve the same artifact and is in a different /16 (-connect/-addnode). A private testnet whose nodes all share one LAN (one /16) cannot corroborate, so -snapshot-ibd falls back to normal IBD there — use the out-of-band install (way 1) instead. Regtest groups peers by full socket address, so local multi-node tests work.

After install the node cannot reorganize below the snapshot height (it has no block bodies or undo data there). If the network ever follows a chain that forks below it, the node logs a WARN line saying so and asking to wipe the datadir and resync; the peers are not punished.

Operational notes:

Background IBD: filling in the bodies below the snapshot (iteration 11). A snapshot node starts with headers 0..=H but block bodies only above H. With -backfill-bodies=1 (requires net_v2) the daemon downloads the missing bodies 1..=H from connected peers in the background, through the normal protocol, without ever touching the chainstate. Each body is verified against its PoW-committed header before being stored — header hash, prev link AND the transaction-tree commitments of that header (merkle_root over txids, auth_root over wtxids, duplicate txids rejected), so a peer cannot serve the real header with arbitrary transactions; progress persists in datadir/.body-backfill/resume.json after EVERY stored block (atomic write), so restarts resume exactly where they stopped. The honest guarantee: the stored bodies are exactly those committed by the header chain verified at snapshot install — the transactions inside cannot be re-validated against the historical UTXO set (which this node does not hold), so if full historical verification matters to you, run a full IBD from genesis instead. Progress and state are reported by getsnapshotinfo (body_backfill). One operational note: the worker opens its own connection to each candidate peer. There is no per-IP connection cap (as in Bitcoin Core), so several nodes behind one NAT can backfill from the same serving peer. On the live testnet deployment the full range (~31k bodies) backfilled in ~13 minutes.

Official testnet snapshots (published artifact). Besides the P2P bootstrap above, the project publishes an official testnet artifact so a fresh node does not depend on reaching a particular live peer:


8. Units: QAN and jomas


9. Security notes


10. Troubleshooting

Symptom Likely cause / fix
Connection refused from qanum-cli Node not running, wrong --addr/--network, or RPC disabled
verifier accept-all is not available in this build… Signature verification is mandatory; drop the verifier option (the default is oqs)
post-quantum crypto self-test failed… The liboqs backend (ML-DSA / SLH-DSA signatures or the ML-KEM-1024 key exchange of the P2P transport) gave a wrong answer at startup; the node refuses to start. Rebuild from a clean tree and report it
No such mempool transaction. Enable -txindex… The tx may already be mined; enable -txindex to query confirmed txs
NonceExhausted on mine-blocks An explicit --max-nonce cap was too low; raise it or drop it (default: no cap)
Peers don’t connect Wrong network magic, TOFU mismatch (removetofuentry <ip:port> or resettofu), or onlynet restriction
Blocks not imported / sync stuck Check the log for [chain.fork] / [net.sync] lines; qanumd reindex if chainstate is corrupt
[chain] GENESIS MISMATCH / ParentUnknown(<hash>) on sync The peer’s chain starts from a different genesis than yours — usually an old binary or an old datadir with a previous network genesis. Compare qanum-cli getblockhash 0 on both nodes; upgrade and re-init the outdated node (qanumd -chain=<net> reset keeps wallet_keys.json) or use a fresh --datadir
datadir-info shows missing files The node hasn’t started on that datadir yet (init first)

Command cheatsheet

# Node lifecycle (same flags on both binaries)
qanumd -chain=regtest init
qanumd -chain=<main|test|regtest> [options]   # run daemon
qanumd -regtest chainstate-info               # offline info
qanumd -testnet reindex                       # offline rebuild
qanumd -chain=test reset                      # back to genesis

# Chain state
qanum-cli -chain=regtest getblockchaininfo
qanum-cli -chain=<net> getblockcount
qanum-cli -chain=<net> getblock <hash-or-height>
qanum-cli -chain=<net> getblockheader <hash-or-height>
qanum-cli -chain=<net> gettxout <txid> <vout>

# Mempool / txs
qanum-cli -chain=<net> getmempoolinfo
qanum-cli -chain=<net> getrawmempool
qanum-cli -chain=<net> getrawtransaction <txid> 1
qanum-cli -chain=<net> sendrawtransaction <hex>

# Network / peers
qanum-cli -chain=<net> getnetworkinfo
qanum-cli -chain=<net> getpeerinfo
qanum-cli -chain=<net> addnode <ip:port> add|remove|onetry
qanum-cli -chain=<net> generatetoaddress <n> <addr>   # regtest

# Wallet
qanum-cli -chain=<net> getnewaddress
qanum-cli -chain=<net> getbalance
qanum-cli -chain=<net> listunspent
qanum-cli -chain=<net> sendtoaddress <addr> <qan> <fee_qan>

# Threshold spending (P2MULTI): build, sign with m seats, assemble, broadcast
qanum-cli -chain=<net> buildp2multispend <txid> <vout> <value>j <anchor> <dest> <value>j <fee>j
qanum-cli -chain=<net> --wallet=<seat> signp2multi <unsigned_hex> <anchor> <value>j <key_id> [input_index]

# Native Qanum diagnostics
qanum-cli -chain=<net> list-blocks
qanum-cli -chain=<net> datadir-info
qanum-cli -chain=<net> reorg-plan

reorg-plan prints the fork-realignment plan (detach/attach hashes, common ancestor) only when the local chain is not the best one; otherwise it prints null — including right after invalidateblock, because the invalidation performs the detach itself.