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.

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