Key aggregation (off-chain computation)
21. How OP_CHECKSIGADD Enables Taproot Multisig
Overview
Taproot's combination of Schnorr signatures, key aggregation, and OP_CHECKSIGADD enables a spectrum of multisig constructions that are more efficient, more private, and more flexible than legacy multisig.
Three Approaches to Taproot Multisig
Approach 1: MuSig2 Key Aggregation (Most Private)
For N-of-N schemes, all participants can aggregate their public keys into a single key and produce a single Schnorr signature. This appears as a simple key-spend on-chain:
# Key aggregation (off-chain computation)
individual_keys = [pk_alice, pk_bob, pk_carol]
aggregated_key = MuSig2.key_aggregate(individual_keys)
# Taproot output
scriptPubKey = OP_1 <aggregated_key> # P2TR key-spend
# Spending: a single 64-byte Schnorr sig from the MuSig2 protocol
# On-chain: looks identical to a single-key spend
# Privacy: observers can't tell it's 3-of-3 or 1-of-1
Approach 2: Tapscript with OP_CHECKSIGADD (General Threshold)
For M-of-N where M < N, use a Tapscript leaf:
# 3-of-5 Tapscript using OP_CHECKSIGADD
<pk1> OP_CHECKSIG
<pk2> OP_CHECKSIGADD
<pk3> OP_CHECKSIGADD
<pk4> OP_CHECKSIGADD
<pk5> OP_CHECKSIGADD
OP_3
OP_GREATERTHANOREQUAL
Approach 3: FROST for Threshold Schnorr
FROST (Flexible Round-Optimized Schnorr Threshold signatures) allows M-of-N threshold signing where the on-chain result is a single Schnorr signature — the threshold is completely hidden:
# FROST: 3-of-5 threshold where on-chain appears as single-key spend
# Participants: Alice, Bob, Carol, Dave, Eve (any 3 can sign)
# Key generation (off-chain)
(secret_shares, threshold_pubkey) = FROST.keygen(5, 3)
# Signing (any 3 participants)
partial_sigs = [FROST.sign(share_alice, msg),
FROST.sign(share_bob, msg),
FROST.sign(share_carol, msg)]
final_sig = FROST.aggregate(partial_sigs)
# On-chain output: P2TR with threshold_pubkey
# On-chain signature: a single 64-byte Schnorr sig
# Privacy: indistinguishable from any other P2TR spend
Script Tree for Complex Multisig Policies
Taproot's MAST (Merklized Abstract Syntax Trees) allows multiple multisig policies as separate leaves:
# Policy: "2-of-3 normally, or 1-of-3 after 1 year"
internal_key = key_aggregate(pk_alice, pk_bob, pk_carol)
leaf_a = Script("2-of-3 via OP_CHECKSIGADD with alice, bob, carol")
leaf_b = Script("1-of-3 with 52560 blocks CLTV timelock")
taproot_tree = TaprootTree([leaf_a, leaf_b])
taproot_output_key = tweak(internal_key, taproot_tree.root)
# Spending via leaf_a (normal case):
witness = [<sig_alice>, <>, <sig_carol>, <leaf_a_script>, <control_block_for_leaf_a>]
# Spending via leaf_b (after timeout):
witness = [<sig_alice>, <leaf_b_script>, <control_block_for_leaf_b>]
Weight and Fee Efficiency
``` Script Type | vBytes for 2-of-3 input
TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation: