Visitar URL original
musig2: MuSig2 library

musig2: MuSig2 library

[ cryptography, library, mit ] [ Propose Tags ] [ Report a vulnerability ]

Pure BIP0327 MuSig2 (partial)signatures with tweak support on the elliptic curve secp256k1.


[Skip to Readme]

Flags

Manual Flags

NameDescriptionDefault
llvm

Use GHC's LLVM backend.

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0, 0.1.1, 0.1.2, 0.1.3, 0.1.4, 0.2.0, 0.2.1, 0.3.0
Change log CHANGELOG
Dependencies base (>=4.20 && <5), bytestring (>=0.11 && <0.13), containers (>=0.6 && <0.9), deepseq (>=1.4 && <1.7), entropy (>=0.4.1.6 && <0.5), ppad-fixed (>=0.2.2 && <0.3), ppad-secp256k1 (>=0.5.10 && <0.6), ppad-sha256 (>=0.3.5 && <0.4) [details]
Tested with ghc ==9.10.3 || ==9.12.4 || ==9.14.1
License MIT
Author Jose Storopoli
Maintainer jose@storopoli.com
Uploaded by storopoli at 2026-11-12T22:43:29Z
Category Cryptography
Home page https://github.com/storopoli/musig2
Source repo head: git clone github.com/storopoli/musig2.git
Distributions
Downloads 230 total (21 in the last 30 days)
Rating 2.0 (votes: 1) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user [build log]
All reported builds failed as of 2026-11-13 [all 2 reports]

Readme for musig2-0.3.0

[back to package description]

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.