Post-Quantum Multisig
Tutorial for creating and spending from PQ multisig wallets using ML-DSA-44 keys and P2MR Merkle trees.
BTX supports post-quantum M-of-N multisignature spending using ML-DSA-44 (Dilithium) keys in P2MR (Pay-to-Merkle-Root) witness v2 outputs. This guide walks through a complete 2-of-3 multisig workflow using PSBT.
How PQ Multisig Works
BTX PQ multisig uses a Merkle tree of leaf scripts rather than legacy OP_CHECKMULTISIG. Each leaf contains a threshold script with the new OP_CHECKSIGADD_MLDSA or OP_CHECKSIGADD_SLHDSA opcodes.
Key facts:
- Key type: ML-DSA-44 (1312-byte public keys) by default
- Address type: P2MR (witness v2)
- Max keys per leaf: 8 (
MAX_PQ_PUBKEYS_PER_MULTISIG) - Mixed algorithms: ML-DSA and SLH-DSA keys can coexist in a single leaf
- Signing: PSBT-based partial signing with union merge
Prerequisites
- Descriptor wallets enabled (BTX default)
- Three signer wallets:
signerA,signerB,signerC - One coordinator/watch-only wallet:
coordinator
btx-cli createwallet signerA
btx-cli createwallet signerB
btx-cli createwallet signerC
btx-cli createwallet coordinator true # watch-only Step 1: Export PQ Keys
Use deterministic key export from each signer wallet. This avoids manual witness/script parsing.
# Generate addresses and export PQ keys
A_ADDR=$(btx-cli -rpcwallet=signerA getnewaddress)
B_ADDR=$(btx-cli -rpcwallet=signerB getnewaddress)
C_ADDR=$(btx-cli -rpcwallet=signerC getnewaddress)
PK1=$(btx-cli -rpcwallet=signerA exportpqkey "$A_ADDR" | jq -r '.key')
PK2=$(btx-cli -rpcwallet=signerB exportpqkey "$B_ADDR" | jq -r '.key')
PK3=$(btx-cli -rpcwallet=signerC exportpqkey "$C_ADDR" | jq -r '.key') Fresh BTX descriptor wallets export ml-dsa-44 keys by default. For SLH-DSA backup keys, use the pk_slh(<32-byte-hex>) syntax.
Step 2: Create a PQ Multisig Address
Option A: Utility RPC (no wallet import)
Use createmultisig to generate the address without importing into a wallet:
btx-cli createmultisig 2 "[\"$PK1\",\"$PK2\",\"$PK3\"]" \
'{"address_type":"p2mr","sort":true}' Returns:
address— the P2MR multisig addressredeemScript— the P2MR leaf scriptdescriptor— e.g.mr(sortedmulti_pq(2,...))
Option B: Wallet RPC (create + import)
Use addpqmultisigaddress to create and import in one step:
btx-cli -rpcwallet=coordinator addpqmultisigaddress 2 \
"[\"$PK1\",\"$PK2\",\"$PK3\"]" "team-safe" true The true parameter enables sorted key ordering (sortedmulti_pq) for deterministic descriptor construction.
Step 3: Fund the Multisig
btx-cli -rpcwallet=funder sendtoaddress 3.0
btx-cli -rpcwallet=funder generatetoaddress 1 Step 4: Create an Unsigned PSBT
Use a fee rate suitable for large PQ witnesses. PQ signatures are significantly larger than classical signatures.
btx-cli -rpcwallet=coordinator walletcreatefundedpsbt \
'[{"txid":"","vout":}]' \
'[{"":1.0}]' \
0 \
'{"add_inputs":false,"changeAddress":"","fee_rate":25}' Take .psbt from the result.
Step 5: Add Metadata (Updater)
btx-cli -rpcwallet=coordinator walletprocesspsbt false "ALL" true false Use the returned .psbt as input for each signer.
Step 6: Sign on Two Independent Signers
Each signer independently processes the PSBT, adding their partial signature:
# Signer A
btx-cli -rpcwallet=signerA walletprocesspsbt
# Signer B
btx-cli -rpcwallet=signerB walletprocesspsbt Each response contains a partially signed PSBT with that signer's PQ signature included.
Step 7: Combine, Finalize, and Broadcast
# Combine partial signatures
btx-cli combinepsbt '["",""]'
# Finalize (assembles witness stack)
btx-cli finalizepsbt
# Broadcast
btx-cli sendrawtransaction
# Verify
btx-cli gettransaction The finalized witness stack layout is:
[sig_3_or_empty] [sig_2_or_empty] [sig_1_or_empty] [leaf_script] [control_block] Finalization succeeds only when the threshold number of valid non-empty signatures is present.
Mixed ML-DSA + SLH-DSA Keys
You can mix ML-DSA and SLH-DSA keys in a single multisig leaf for defense in depth. Use the pk_slh() prefix for SLH-DSA keys:
btx-cli createmultisig 2 \
'["","","pk_slh()"]' \
'{"address_type":"p2mr","sort":true}' The resulting leaf script uses the appropriate opcode for each key:
OP_CHECKSIG_MLDSA
OP_CHECKSIGADD_MLDSA
OP_CHECKSIGADD_SLHDSA
OP_2 OP_NUMEQUAL Validation weight per signature: 500 for ML-DSA, 5000 for SLH-DSA.
Recovery Paths
SLH-DSA Backup Key
For high-security wallets, include an SLH-DSA (SHAKE-128s) key as one of the signers. SLH-DSA is hash-based and believed to be conservative against quantum attacks even beyond lattice assumptions. A 2-of-3 with two ML-DSA keys and one SLH-DSA backup provides defense in depth.
Descriptor Backup
Always back up the full descriptor string (e.g. mr(sortedmulti_pq(2,...))) and all public keys. The descriptor is sufficient to reconstruct the address and spending conditions on any BTX node.
Large Quorums
For quorums larger than 8 keys, split policy across multiple P2MR leaves instead of a single oversized leaf. Each leaf can hold up to MAX_PQ_PUBKEYS_PER_MULTISIG = 8 keys.
Script Inspection
Decode and inspect a PQ multisig leaf script:
btx-cli decodescript For PQ multisig leaves, the output includes:
pq_multisig.threshold— the M valuepq_multisig.keys— list of public keyspq_multisig.algorithms— algorithm per key (ml-dsa-44 or slh-dsa-shake-128s)
Operational Security
- Use
sortedmulti_pq(orsort=true) for deterministic descriptor construction across all signers - Keep signer wallets on separate machines or hardware — never share private key material
- Transfer PSBTs between signers via secure channels (encrypted file, air-gapped media)
- Test the full sign/combine/finalize cycle on regtest before committing funds on mainnet
- Back up descriptor strings and wallet metadata for disaster recovery
- Consider geographic distribution of signer keys for resilience
Common Errors
| Error | Cause | Fix |
|---|---|---|
Only address type 'p2mr' is supported for PQ multisig | Wrong address type | Use {"address_type":"p2mr"} |
nrequired cannot exceed number of keys | Threshold/key count mismatch | Ensure M <= N |
Unable to build PQ multisig leaf script | Invalid key sizes or too many keys | Verify key sizes and stay under 8 keys per leaf |
| Relay fee rejection after finalize | Fee too low for large PQ witness | Increase fee_rate in walletcreatefundedpsbt |