Skip to main content

Post-Quantum Multisig

Tutorial for creating and spending from PQ multisig wallets using ML-DSA-44 keys and P2MR Merkle trees.

Choose a Path

Start from the workflow that matches why you are here.

The docs are broad. These two paths jump straight into the operator and builder material behind the new microsites.

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 address
  • redeemScript — the P2MR leaf script
  • descriptor — 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 value
  • pq_multisig.keys — list of public keys
  • pq_multisig.algorithms — algorithm per key (ml-dsa-44 or slh-dsa-shake-128s)

Operational Security

  • Use sortedmulti_pq (or sort=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

ErrorCauseFix
Only address type 'p2mr' is supported for PQ multisigWrong address typeUse {"address_type":"p2mr"}
nrequired cannot exceed number of keysThreshold/key count mismatchEnsure M <= N
Unable to build PQ multisig leaf scriptInvalid key sizes or too many keysVerify key sizes and stay under 8 keys per leaf
Relay fee rejection after finalizeFee too low for large PQ witnessIncrease fee_rate in walletcreatefundedpsbt