MuSig2 Multi-Signatures
View SourceMuSig2 (BIP-327) is a multi-signature scheme that allows multiple parties to aggregate their public keys into a single public key. A signature created by combining partial signatures from all parties looks exactly like a standard Schnorr signature to a verifier.
Concepts
- Key Aggregation: Combining individual public keys into a single aggregated x-only public key.
- Nonce Generation: Each signer generates a secret and public nonce.
- Nonce Aggregation: Combining all public nonces into an aggregate nonce.
- Partial Signing: Each signer creates a partial signature using their secret key, secret nonce, and the aggregate nonce.
- Signature Aggregation: Combining partial signatures into the final valid Schnorr signature.
Example: 3-of-3 Signing Session
In this example, 3 parties (Alice, Bob, and Carol) want to sign a message together. MuSig2 is an n-of-n scheme relative to the aggregated key, meaning all parties whose keys were aggregated must participate to produce a valid signature.
1. Setup Signers
We generate keys for our 3 participants.
# Generate keys for 3 parties
participants = for i <- 1..3 do
{seckey, pubkey} = Secp256k1.keypair(:compressed)
%{id: i, seckey: seckey, pubkey: pubkey}
end
participant_pubkeys = Enum.map(participants, & &1.pubkey)2. Key Aggregation
The participants aggregate their public keys to create the group public key.
alias Secp256k1.MuSig
# Aggregate public keys
{:ok, agg_xonly_pubkey, keyagg_cache} = MuSig.pubkey_agg(participant_pubkeys)
# agg_xonly_pubkey is the 32-byte public key that verifies the final signature3. Nonce Generation & Exchange
Each participant generates a nonce pair (secret and public). They must exchange public nonces.
message = :crypto.hash(:sha256, "Joint Account Authorization")
# Each participant generates a nonce
participants_with_nonces = Enum.map(participants, fn p ->
{:ok, secnonce, pubnonce} = MuSig.nonce_gen(p.seckey, p.pubkey, message, keyagg_cache, nil)
p
|> Map.put(:secnonce, secnonce) # KEEP SECRET!
|> Map.put(:pubnonce, pubnonce) # Share this
end)
# Collect all public nonces
pubnonces = Enum.map(participants_with_nonces, & &1.pubnonce)4. Nonce Aggregation & Session Setup
Combine the public nonces to create a signing session.
# Aggregate nonces
aggnonce = MuSig.nonce_agg(pubnonces)
# Create the signing session (processes the aggregate nonce)
session = MuSig.nonce_process(aggnonce, message, keyagg_cache)5. Partial Signing
Each participant creates their partial signature.
participants_with_sigs = Enum.map(participants_with_nonces, fn p ->
partial_sig = MuSig.partial_sign(p.secnonce, p.seckey, keyagg_cache, session)
Map.put(p, :partial_sig, partial_sig)
end)
# Collect all partial signatures
partial_sigs = Enum.map(participants_with_sigs, & &1.partial_sig)6. Signature Aggregation & Verification
Finally, aggregate the partial signatures into the final Schnorr signature.
# Combine partial signatures
final_signature = MuSig.partial_sig_agg(session, partial_sigs)
# Verify the signature against the aggregated public key
is_valid = Secp256k1.schnorr_valid?(final_signature, message, agg_xonly_pubkey)
# => trueSecurity Considerations
- Nonce Reuse: NEVER reuse nonces. The
nonce_genfunction uses randomness and the message to protect against this, but you must ensure that a freshnonce_gencall is made for every signature attempt. Reusing a nonce with the same key leaks the secret key. - Round Communication: MuSig2 is a 2-round protocol.
- Round 1: Exchange public nonces.
- Round 2: Exchange partial signatures.
- All public nonces must be received before signing (Round 2) begins.