musig2

A pure Haskell implementation of BIP-0327 MuSig2 multi-signature
scheme on secp256k1. The library implements partial
signatures with tweak support following best practices and guidelines
from BIP-0327.
Usage
Two signers produce one BIP-340 signature for their aggregate key. Public
values travel between signers as bytes (encode/decode, in the BIP-0327
formats); decoding validates them.
{-# LANGUAGE OverloadedStrings #-}
import Control.Exception (Exception, throwIO)
import qualified Crypto.Curve.Secp256k1.MuSig2 as MuSig2
import qualified Data.ByteString as BS
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
-- | Throws the error of a 'Left'.
orThrow :: (Exception e) => Either e a -> IO a
orThrow = either throwIO pure
-- | Two signers sign a message; returns whether the signature verifies.
example :: IO Bool
example = do
-- Each signer has a secret key and publishes its public key.
-- (Fixed keys for the example only: never hard-code real keys.)
sk1 <- orThrow (MuSig2.secKeyFromBytes (BS.replicate 32 0x11))
sk2 <- orThrow (MuSig2.secKeyFromBytes (BS.replicate 32 0x22))
let pk1 = MuSig2.secKeyPubKey sk1
pk2 = MuSig2.secKeyPubKey sk2
-- Everyone aggregates the same list of public keys, here sorted.
keyAggCtx <- orThrow (MuSig2.keyAgg (MuSig2.keySort (pk1 :| [pk2])))
let aggKey = MuSig2.aggXOnlyPubKey keyAggCtx
msg = "i approve of this message"
-- Each signer generates a fresh nonce and publishes the public part.
(secNonce1, pubNonce1) <- MuSig2.nonceGen pk1 MuSig2.noNonceInputs >>= orThrow
(secNonce2, pubNonce2) <- MuSig2.nonceGen pk2 MuSig2.noNonceInputs >>= orThrow
-- Values travel as bytes; decoding validates them.
received <- orThrow (MuSig2.decode (MuSig2.encode pubNonce2))
-- Everyone aggregates the public nonces and opens the same session.
let session = MuSig2.newSession keyAggCtx (MuSig2.nonceAgg (pubNonce1 :| [received])) msg
-- Each signer signs once with its nonce and publishes its partial signature.
-- 'sign' consumes the secret nonce: signing again with it fails.
psig1 <- MuSig2.sign secNonce1 sk1 session >>= orThrow
psig2 <- MuSig2.sign secNonce2 sk2 session >>= orThrow
-- The aggregator checks each partial signature, then aggregates them.
let valid =
MuSig2.partialSigVerify session pk1 pubNonce1 psig1
&& MuSig2.partialSigVerify session pk2 pubNonce2 psig2
signature = MuSig2.partialSigAgg session (psig1 :| [psig2])
-- The result is an ordinary BIP-340 signature for the aggregate key.
pure (valid && MuSig2.verify aggKey msg signature)
A Taproot output key is the aggregate key tweaked with applyTaprootTweak;
tweaks live in the key aggregation context only, and sessions are opened on
the tweaked context:
-- | Three signers spend a Taproot output by key path.
taprootExample :: IO Bool
taprootExample = do
sks <- traverse (orThrow . MuSig2.secKeyFromBytes . BS.replicate 32) (0x33 :| [0x44, 0x55])
let pks = fmap MuSig2.secKeyPubKey sks
-- The internal key is the aggregate key; the output key commits to the
-- script tree's merkle root ('Nothing' for a key-path-only output, BIP-86).
internal <- orThrow (MuSig2.keyAgg (MuSig2.keySort pks))
output <- orThrow (MuSig2.applyTaprootTweak (Just (BS.replicate 32 0xAB)) internal)
let outputKey = MuSig2.aggXOnlyPubKey output
sighash = BS.replicate 32 0x01
-- Binding the nonces to the key and message adds protection against bad randomness.
let inputs sk =
MuSig2.noNonceInputs
{ MuSig2.nonceSecKey = Just sk
, MuSig2.nonceAggPubKey = Just outputKey
, MuSig2.nonceMessage = Just sighash
}
nonces <- traverse (\sk -> MuSig2.nonceGen (MuSig2.secKeyPubKey sk) (inputs sk) >>= orThrow) sks
-- Sessions are opened on the tweaked context: the tweak lives in the context only.
let session = MuSig2.newSession output (MuSig2.nonceAgg (fmap snd nonces)) sighash
psigs <- traverse (\(sk, (secNonce, _)) -> MuSig2.sign secNonce sk session >>= orThrow) (NE.zip sks nonces)
let signature = MuSig2.partialSigAgg session psigs
pure (MuSig2.verify outputKey sighash signature)
Coordinator
The Crypto.Curve.Secp256k1.MuSig2.Coordinator module collects the signers'
contributions as bytes, by signer index (the position of the signer's key in
the key aggregation context). It decodes and verifies each nonce and partial
signature as it arrives and blames the signer of an invalid, conflicting or
missing one (Blame). The coordinator can be one of the signers or a
separate, untrusted party: a wrong aggregate nonce can only make the session
fail. With Control.Monad (foldM), Data.Bifunctor (first) and the module
imported qualified as Coordinator:
-- | A coordinator collects three signers' nonces and partial signatures as bytes.
coordinatorExample :: IO Bool
coordinatorExample = do
sks <- traverse (orThrow . MuSig2.secKeyFromBytes . BS.replicate 32) (0x66 :| [0x77, 0x88])
-- Signer i is the one with the i-th public key of the context.
keyAggCtx <- orThrow (MuSig2.keyAgg (fmap MuSig2.secKeyPubKey sks))
let msg = "pay Carol 1 BTC"
-- Round one: each signer sends its encoded public nonce, in any order.
nonces <- traverse (\sk -> MuSig2.nonceGen (MuSig2.secKeyPubKey sk) MuSig2.noNonceInputs >>= orThrow) (NE.toList sks)
nonceRound <-
orThrow $
foldM
(\r (i, (_, pubNonce)) -> Coordinator.receivePubNonce i (MuSig2.encode pubNonce) r)
(Coordinator.newNonceRound keyAggCtx)
(zip [0 ..] nonces)
-- Fails if a nonce is missing (here reporting only the first such signer).
collected <- orThrow (first NE.head (Coordinator.finishNonceRound nonceRound))
-- The coordinator sends the aggregate nonce to every signer, who decodes it
-- and opens the session.
aggNonce <- orThrow (MuSig2.decode (MuSig2.encode (Coordinator.collectedAggNonce collected)))
let session = MuSig2.newSession keyAggCtx aggNonce msg
-- Round two: each signer sends its encoded partial signature. The
-- coordinator verifies each one and blames the signer of an invalid one.
psigs <- traverse (\(sk, (secNonce, _)) -> MuSig2.sign secNonce sk session >>= orThrow) (zip (NE.toList sks) nonces)
sigRound <-
orThrow $
foldM
(\r (i, psig) -> Coordinator.receivePartialSig i (MuSig2.encode psig) r)
(Coordinator.newSigRound collected msg)
(zip [0 ..] psigs)
-- Aggregates the partial signatures and verifies the result.
signature <- orThrow (Coordinator.finishSigRound sigRound)
pure (MuSig2.verify (MuSig2.aggXOnlyPubKey keyAggCtx) msg signature)
All three examples are compiled and run by the test suite (test/Example.hs),
which also checks that this README shows them verbatim.
Upgrading from 0.2? The CHANGELOG lists every renamed and
removed function.
Documentation
Haddocks (API documentation, etc.) are hosted at hackage.
Security
Read the security considerations in the
module documentation before using the library. Secret nonces are
one-shot: sign atomically consumes the nonce (and zero-fills its buffer)
before anything else, so a second sign with the same nonce, from any copy
or thread, fails with SecNonceAlreadyUsed. The
Crypto.Curve.Secp256k1.MuSig2.Unsafe module holds the escape hatches this
cannot cover (caller-supplied randomness, counters, exporting secret nonces).
Arithmetic on secret keys and nonces is constant-time (fixed-width
Montgomery arithmetic from ppad-fixed); the module documentation lists the
remaining caveats.
The API is hard to misuse by construction: every value of an exported type is
valid (decoders and smart constructors check points, scalar ranges and
lengths), secret keys and nonces have no Show, Eq, Ord or encoding,
and errors carry no secrets.
The test suite runs the official BIP-0327 test vectors, loaded
from the upstream JSON files, with exact-output assertions; invalid-encoding
vectors check that decoding fails for exactly the contribution the vector
blames, including the DeterministicSign vectors. A committed corpus of 300
sessions generated by the BIP-0327 reference implementation
(scripts/gen-diff-corpus.py) is replayed byte for byte, covering 1 to 8
signers, duplicate keys, tweak chains, long messages, every combination of
NonceGen inputs, DeterministicSign and aggregate nonces at infinity.
QuickCheck properties run complete
multi-signer sessions with random tweaks (plain, x-only and Taproot) and check
the final signature with BIP-340 verification.
Elliptic curve operations are done by the
ppad-secp256k1 package.
Development
You'll require Nix with flake support enabled. Enter a
development shell with:
$ nix develop
Then do e.g.:
$ cabal repl musig2
to get a REPL for the main library.