musig2-0.3.0: CHANGELOG
# Changelog
## 0.3.0 (2026-11-12)
A redesigned, hard-to-misuse API. Every value of an exported type is valid by
construction, secrets cannot be printed, compared or encoded, public values
have BIP-0327 byte encodings, and each kind of failure has its own error type.
This release breaks every 0.2 program; the migration table below maps the old
names to the new ones.
* Breaking API changes (old -> new):
- `SecKey` (a newtype over `Integer` with `Num`, `Read`, `Eq`, `Ord`) ->
opaque `SecKey`: `secKeyFromBytes` (32 bytes, `0 < d < n`),
`secKeyToBytes`, `secKeyPubKey`. No `Show`, `Eq`, `Ord`, `Read`, `Num`
or `Generic`.
- `Pub` (ppad's `Projective`) -> `PubKey` (valid, finite, `Eq`/`Ord`/`Show`
on its 33-byte encoding) and `XOnlyPubKey`; `pubKeyFromPoint` and
`pubKeyPoint` convert from and to `ppad-secp256k1` points
- New `Encode` class (`encode`/`decode`) with the BIP-0327 byte formats for
`PubKey`, `XOnlyPubKey`, `PubNonce`, `AggNonce`, `PartialSignature`,
`Signature` and `Tweak`. Decoders reject non-canonical input: 32- and
65-byte public keys, invalid points, infinity where a finite point is
required, scalars `>= n`.
- `mkKeyAggContext pks mTweak` -> `keyAgg :: NonEmpty PubKey -> Either KeyAggError KeyAggContext`,
then `applyPlainTweak`/`applyXOnlyTweak`/`applyTaprootTweak`
- `sortPublicKeys` -> `keySort :: NonEmpty PubKey -> NonEmpty PubKey`
- `aggregatedPubkey` -> `aggPubKey` (possibly tweaked `Q`) and
`aggXOnlyPubKey` (what the final signature verifies against); new
`keyAggPubKeys` and `signerIndex`
- `Tweak (XOnlyTweak t | PlainTweak t)` and `applyTweak ctx tweak` ->
opaque `Tweak` (`tweakFromBytes`, `0 <= t < n`) with `applyPlainTweak`
and `applyXOnlyTweak`. New `applyTaprootTweak` computes the BIP-0341
TapTweak from the current key and an optional merkle root.
- Tweaks are given once, to the `KeyAggContext`; sessions no longer take a
tweak list
- `SecNonceGenParams`, `defaultSecNonceGenParams`, `secNonceGen`,
`publicNonce` -> `nonceGen :: PubKey -> NonceInputs -> IO (Either NonceGenError (SecNonce, PubNonce))`
with `NonceInputs` (`nonceSecKey`, `nonceAggPubKey` (x-only, as in
BIP-0327), `nonceMessage`, `nonceExtraInput`) and `noNonceInputs`
- `mkSecNonce` and `secNonceGenWithRand` removed from the public API:
secret nonces come from `nonceGen` (or, deliberately, from the new
`Crypto.Curve.Secp256k1.MuSig2.Unsafe` module)
- `SecNonce` is one-shot: its scalars live in a pinned buffer with an
atomic used flag. `sign :: SecNonce -> SecKey -> Session -> IO (Either SignError PartialSignature)`
consumes the nonce before any check (as BIP-0327's reference code and
libsecp256k1 do) and zero-fills the buffer; any later `sign` with it,
through any copy and from any thread, returns `SecNonceAlreadyUsed`.
A failed `sign` consumes the nonce too.
- Nonces generated with `nonceAggPubKey` or `nonceMessage` are bound to
them: `sign` returns `SecNonceSessionMismatch` for a session with
another aggregate key or message
- New `deterministicSign :: SecKey -> AggNonce -> KeyAggContext -> ByteString -> Maybe ByteString -> Either SignError (PubNonce, PartialSignature)`
(BIP-0327 `DeterministicSign`) for a signer without randomness that sends
its nonce last
- New module `Crypto.Curve.Secp256k1.MuSig2.Unsafe` with
`nonceGenWithRand` (caller-supplied randomness), `nonceGenCounter`
(libsecp256k1's `musig_nonce_gen_counter`), `secNonceToBytes` (BIP-0327
97-byte encoding; consumes the nonce), `secNonceFromBytes` and
`signWithoutVerification`
- New module `Crypto.Curve.Secp256k1.MuSig2.Coordinator`: a coordinator
collects the signers' encoded public nonces (`newNonceRound`,
`receivePubNonce`, `finishNonceRound`) and partial signatures
(`newSigRound`, `receivePartialSig`, `finishSigRound`) by signer index,
decodes and verifies each one as it arrives, and blames the signer of an
invalid, conflicting, unknown or missing contribution (`Blame`,
`BlameReason`). `finishSigRound` also checks the final BIP-340 signature
(`SigRoundError`).
- `NonceGenError` gains `InvalidRandLength` and `NonceIsZero` (from the
`Unsafe` generators only); `SignError` gains `SecNonceAlreadyUsed`,
`SecNonceSessionMismatch`, and, for `deterministicSign`,
`InvalidAuxRandLength`, `DeterministicNonceIsZero` and
`AggOtherNonceAtInfinity`
- `aggNonces` -> `nonceAgg :: NonEmpty PubNonce -> AggNonce` (total);
aggregate nonces have their own type, `AggNonce`, whose points may be
infinity
- `SessionContext(..)` and `mkSessionContext aggNonce pks tweaks msg` ->
opaque `Session` from `newSession :: KeyAggContext -> AggNonce -> ByteString -> Session`
(total), which computes the session values once; `sessionKeyAgg`
- `sign` now returns `IO (Either SignError PartialSignature)`;
`PartialSignature` is opaque instead of `Integer`
- `partialSigVerify psig nonces pks tweaks msg i` ->
`partialSigVerify :: Session -> PubKey -> PubNonce -> PartialSignature -> Bool`,
without rebuilding the session per call
- `aggPartials psigs ctx` -> `partialSigAgg :: Session -> NonEmpty PartialSignature -> Signature`
(total)
- New `verify :: XOnlyPubKey -> ByteString -> Signature -> Bool` (BIP-0340)
- `MuSig2Error` -> `DecodeError`, `KeyAggError`, `NonceGenError` and
`SignError`, with no `String` or secret payloads and `Exception`
instances. `sign` reports `SecNonceKeyMismatch`, `SignerNotInSession` or
`SelfVerificationFailed` instead of the misleading
`SecretKeyPublicKeyMismatch`/`PublicNonceGenerationFailed`.
- Removed the orphan `Ord`, `Semigroup` and `Monoid` instances on ppad's
`Projective`, and the `Semigroup`/`Monoid` instances on `PubNonce`
- `NFData` instances for all exported types
* Other changes:
- Key aggregation is `O(n)` instead of `O(n^2)`: the key list is hashed
once and keys with coefficient 1 are not multiplied (200 keys: ~600 ms
-> ~50 ms)
- Public scalar multiplications are variable-time; multiplications of the
generator use ppad's precomputed (timing-safe) wNAF tables. A 2-of-2
session from nonce aggregation to the verified signature is ~5x faster.
- `nonceGen` rejects extra input longer than `2^32 - 1` bytes instead of
silently truncating its length (`NonceExtraInputTooLong`)
- All fields holding points or scalars are strict, and nonce aggregation
folds strictly
- `pubKeyFromPoint` rejects points that are not on the curve
- The implementation lives in a private `internal` library used by the
tests and benchmarks
- The README and module examples are compiled and run by the test suite
- Tests: official BIP-0327 vectors for invalid encodings and
`DeterministicSign` now run, checking that decoding fails for exactly the
contribution the vector blames (the `NonceAgg` and `PartialSigAgg`
vectors also run through the coordinator, which must blame the same
signer); a committed differential corpus of 300 sessions and 48
`NonceGen` cases generated by the BIP-0327 reference implementation
(`scripts/gen-diff-corpus.py`, `test/vectors/diff`) is replayed byte for
byte; the coordinator's blame is tested for every kind of misbehavior
and for any order of arrival; nonce reuse (sequential, through copies
and from many threads at once), session binding and zero-filling are
tested;
property tests run complete multi-signer sessions with random plain,
x-only and Taproot tweaks and check the final signature, and corrupt
inputs to check that verification fails
* Drop support for GHC 9.8 (`base >= 4.20`); tested with GHC 9.10.3, 9.12.4
and 9.14.1
* Security hardening:
- Constant-time arithmetic on secrets: secret keys and nonce scalars are
no longer `Integer`s. They are parsed, range-checked (`secKeyFromBytes`,
`secNonceFromBytes`), reduced from hashes (`NonceGen`,
`DeterministicSign`), combined in `sign`, negated and encoded with
`ppad-fixed`'s constant-time Montgomery arithmetic, and passed to the
generator multiplication as fixed-width words instead of through the
variable-time `fromInteger`. Public values stay `Integer`. New
dependency: `ppad-fixed >= 0.2.2`.
- Require `entropy >= 0.4.1.6`, which waits for the kernel entropy pool
(`getrandom`) instead of reading `/dev/urandom` early at boot
- Require `ppad-sha256 >= 0.3.5`, which no longer crashes on aarch64 CPUs
without SHA2 instructions
- Under the `llvm` flag on x86_64, disable LLVM's cmov-to-branch conversion
(`-optlc=-x86-cmov-converter=false`), as `ppad-secp256k1` does
- Drop the `binary` dependency
- Correct the security documentation: nonce reuse in any session leaks the
secret key, `secNonceGenWithRand` is deterministic, and the remaining
caveats of constant-time code (public values are variable-time by
design, GHC's code generation, best-effort zeroization) are spelled out
* Fix the README and module examples, which imported a module from a package
that is no longer a dependency
## 0.2.1 (2026-11-12)
* BIP-0327 conformance fixes:
- Fix `aggPartials` producing invalid signatures when an x-only tweak is
applied to an aggregated public key with odd y (it used `g * gacc` instead
of `g` in the final `e * g * tacc` term)
- Fix the session nonce coefficient and final nonce when either half of the
aggregated nonce is the point at infinity: no longer substitute `G` for an
infinite `R1`, and encode infinity as 33 zero bytes when hashing
- `sign` rejects a signer whose public key is not in the session's public keys
(`SecretKeyPublicKeyMismatch`)
- `applyTweak` accepts the valid tweak value `0`
- `partialSigVerify` returns `Right False` for a zero partial signature instead
of an error
- `aggNonces` and `partialSigVerify` reject individual public nonces that
contain the point at infinity (`InvalidPointFormat`)
* Dependencies:
- Require `ppad-secp256k1 >= 0.5.10`, which fixes x-only point parsing and
several `ppad-fixed` arithmetic bugs
- Drop the unused `ppad-hmac-drbg` and `base16-bytestring` library dependencies
- Tested with GHC 9.8.4, 9.10.3, 9.12.4 and 9.14.1
* Tests, benchmarks and CI:
- Run the official BIP-0327 test vectors from the upstream JSON files
(pinned and checksummed) instead of hand-copied subsets, with exact-output
assertions; vectors the API cannot express yet are listed explicitly
- Add regression tests for the fixed bugs, using the reference implementation
for cases the official vectors do not cover
- Fix the benchmark suite build and build it in CI
- Run Linux CI jobs on `ubuntu-24.04` so LLVM jobs can install `llvm-15`
## 0.2.0 (2026-04-10)
* Security hardening and API changes:
- Bind `SecNonce` values to the originating signer public key and reject nonce/key mismatches during signing
- Replace panic-based validation in signing, verification, context creation, tweaking, and nonce generation with structured `MuSig2Error` results
- Eliminate all remaining `error` calls from library code (`publicNonce`, `getSigningNonce`, `isEvenPub`)
- Make `SecNonce` opaque and require checked construction via `mkSecNonce` or the nonce generation APIs
- Remove `Eq` and `Ord` instances from `SecNonce` to discourage storing nonces in collections
- Move `secNonceScalars` out of public API into `Internal` module to prevent leaking secret nonce scalars
- Strip secret scalar values from `MuSig2Error` constructors to prevent leaking secrets through error logging
- Reject out-of-range partial signatures during `aggPartials` instead of reducing them modulo the curve order
- Update nonce generation and session construction APIs to return checked `Either` results
- Add security considerations documentation (nonce reuse, non-constant-time arithmetic, no zeroization)
- Refresh tests, benchmarks, and documentation to cover the new checked API and regression cases
## 0.1.3 (2026-01-04)
* Performance improvements:
- Cache `KeyAggContext` in `SessionContext` to avoid redundant EC operations
- Replace lazy `foldl` with strict `foldl'` to prevent space leaks
- Add strictness annotations to `SessionContext` fields
- Replace O(n) list indexing with O(log n) `Seq.lookup` in `partialSigVerify`
- Optimize `integerToBytes32` to eliminate intermediate list allocation
- Fix `NFData` instances in benchmarks for accurate measurements
## 0.1.2 (2025-12-30)
* Fix Hackage build failure by implementing `modQ` locally to support `ppad-secp256k1` 0.5.0+.
## 0.1.1 (2025-12-22)
* Bump `ppad-secp256k1` to 0.5.0. More efficient and faster EC operations.
## 0.1.0 (2025-09-20)
* Initial release, full BIP-0327 support except [Deterministic and Stateless Signing for a Single Signer](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Deterministic_and_Stateless_Signing_for_a_Single_Signer)