MuSig2 Multi-Signatures

View Source

MuSig2 (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 signature

3. 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)
# => true

Security Considerations

  1. Nonce Reuse: NEVER reuse nonces. The nonce_gen function uses randomness and the message to protect against this, but you must ensure that a fresh nonce_gen call is made for every signature attempt. Reusing a nonce with the same key leaks the secret key.
  2. 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.