packages feed

musig2 0.2.0 → 0.2.1

raw patch · 24 files changed

+2070/−721 lines, 24 filesdep +aesondep +directorydep +filepathdep −ppad-hmac-drbgdep ~base16-bytestringdep ~ppad-secp256k1PVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: aeson, directory, filepath

Dependencies removed: ppad-hmac-drbg

Dependency ranges changed: base16-bytestring, ppad-secp256k1

API changes (from Hackage documentation)

- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Base.Monoid Crypto.Curve.Secp256k1.MuSig2.PubNonce
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Base.Monoid Crypto.Curve.Secp256k1.Projective
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Base.Semigroup Crypto.Curve.Secp256k1.MuSig2.PubNonce
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Base.Semigroup Crypto.Curve.Secp256k1.Projective
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Generics.Generic Crypto.Curve.Secp256k1.MuSig2.SecKey
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Generics.Generic Crypto.Curve.Secp256k1.MuSig2.SecNonceGenParams
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Generics.Generic Crypto.Curve.Secp256k1.MuSig2.Tweak
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Num.Num Crypto.Curve.Secp256k1.MuSig2.SecKey
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Read.Read Crypto.Curve.Secp256k1.MuSig2.SecKey
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Read.Read Crypto.Curve.Secp256k1.MuSig2.Tweak
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Show.Show Crypto.Curve.Secp256k1.MuSig2.MuSig2Error
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Show.Show Crypto.Curve.Secp256k1.MuSig2.PubNonce
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Show.Show Crypto.Curve.Secp256k1.MuSig2.Tweak
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Base.Monoid Crypto.Curve.Secp256k1.MuSig2.PubNonce
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Base.Monoid Crypto.Curve.Secp256k1.Projective
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Base.Semigroup Crypto.Curve.Secp256k1.MuSig2.PubNonce
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Base.Semigroup Crypto.Curve.Secp256k1.Projective
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Generics.Generic Crypto.Curve.Secp256k1.MuSig2.SecKey
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Generics.Generic Crypto.Curve.Secp256k1.MuSig2.SecNonceGenParams
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Generics.Generic Crypto.Curve.Secp256k1.MuSig2.Tweak
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Num.Num Crypto.Curve.Secp256k1.MuSig2.SecKey
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Read.Read Crypto.Curve.Secp256k1.MuSig2.SecKey
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Read.Read Crypto.Curve.Secp256k1.MuSig2.Tweak
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Show.Show Crypto.Curve.Secp256k1.MuSig2.MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Show.Show Crypto.Curve.Secp256k1.MuSig2.PubNonce
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Internal.Show.Show Crypto.Curve.Secp256k1.MuSig2.Tweak

Files

CHANGELOG view
@@ -1,5 +1,35 @@ # Changelog +## 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:
bench/Main.hs view
@@ -56,6 +56,9 @@   rnf (SecNonceGenParams pk sk apk mg ei) =     rnf pk `seq` rnf sk `seq` rnf apk `seq` rnf mg `seq` rnf ei +instance NFData M.MuSig2Error where+  rnf = rwhnf+ -- | Main benchmark function. main :: IO () main =
lib/Crypto/Curve/Secp256k1/MuSig2.hs view
@@ -124,7 +124,7 @@   aggNonces, ) where -import Control.Monad (foldM)+import Control.Monad (foldM, when) import Crypto.Curve.Secp256k1 (Projective, Pub, add, derive_pub, mul, neg, serialize_point, _CURVE_G, _CURVE_ZERO) import Crypto.Curve.Secp256k1.MuSig2.Internal import Data.Binary.Put (@@ -216,16 +216,14 @@           keyCtx = cachedKeyAggCtx ctx           aggPk = q keyCtx           taccVal = maybe 0 getTweak $ tacc keyCtx-          gaccVal = gacc keyCtx         -- BIP 327: Let g = 1 if has_even_y(Q), otherwise let g = -1 mod n         evenAggPk <- liftMaybe InvalidPointFormat (isEvenPub aggPk)         let           g = if evenAggPk then 1 else curveOrder - 1-          -- Apply accumulated parity factor-          g' = modQ (g * gaccVal)           sSum = modQ $ sum validPartials-          -- BIP 327: Let s = s₁ + ... + sᵤ + e⋅g'⋅tacc mod n-          s = modQ (sSum + e * g' * taccVal)+          -- BIP 327: Let s = s₁ + ... + sᵤ + e⋅g⋅tacc mod n+          -- (gacc must not appear here: ApplyTweak already folds it into tacc)+          s = modQ (sSum + e * g * taccVal)           left = xBytes nonce           right = integerToBytes32 s         Right (left <> right)@@ -240,6 +238,10 @@ The partial signature returned from this function is a potentially-zero scalar value which can then be passed to other signers for verification and aggregation.++Fails with 'SecretKeyPublicKeyMismatch' if the public key derived from the+secret key differs from the one the 'SecNonce' is bound to, or is not one of+the session's public keys. -} sign ::   -- | Secret nonce.@@ -266,7 +268,7 @@     _ <- validateSecretScalar "k2" k2     d' <- validateSecretScalar "secret key" (unSecKey sk)     p <- liftMaybe KeyDerivationFailed $ derive_pub (fromInteger d')-    if p /= boundPk+    if p /= boundPk || p `notElem` publicKeys       then Left SecretKeyPublicKeyMismatch       else do         let@@ -365,9 +367,10 @@       g = if oddAggPk then curveOrder - 1 else 1       -- Apply parity accumulator: gacc is accumulated parity factor       g' = modQ (g * gaccVal)-    r2b <- liftMaybe (ScalarMultiplicationFailed "r2 * b") $ mul r2' (fromInteger b)-    sG <- liftMaybe (ScalarMultiplicationFailed "s * G") $ mul _CURVE_G (fromInteger s)-    pkMul <- liftMaybe (ScalarMultiplicationFailed "pk multiplication") $ mul pk (fromInteger (modQ (e * a * g')))+    when (r1' == _CURVE_ZERO || r2' == _CURVE_ZERO) $ Left InvalidPointFormat+    r2b <- liftMaybe (ScalarMultiplicationFailed "r2 * b") $ mulScalar r2' b+    sG <- liftMaybe (ScalarMultiplicationFailed "s * G") $ mulScalar _CURVE_G s+    pkMul <- liftMaybe (ScalarMultiplicationFailed "pk multiplication") $ mulScalar pk (e * a * g')     let       re' = add r1' r2b       -- Negate individual nonce if final aggregate nonce has odd Y@@ -498,9 +501,10 @@   let     b = getSigningNonceCoeff ctx     aggNonce = ctx.aggNonce-    aggNonce' = if aggNonce.r1 == _CURVE_ZERO then PubNonce _CURVE_G aggNonce.r2 else aggNonce-  r2b <- liftMaybe (ScalarMultiplicationFailed "r2 * b") $ mul aggNonce'.r2 (fromInteger b)-  let finalNonce = add aggNonce'.r1 r2b+  -- BIP 327: R' = R1 + b⋅R2, where R1 and R2 may be the point at infinity+  r2b <- liftMaybe (ScalarMultiplicationFailed "r2 * b") $ mulScalar aggNonce.r2 b+  let finalNonce = add aggNonce.r1 r2b+  -- BIP 327: R = G if R' is infinite   Right (if finalNonce == _CURVE_ZERO then _CURVE_G else finalNonce)  {- | Gets the signing nonce coefficient following@@ -510,10 +514,10 @@ getSigningNonceCoeff ctx =   let     aggNonce = ctx.aggNonce-    aggNonce' = if aggNonce.r1 == _CURVE_ZERO then PubNonce _CURVE_G aggNonce.r2 else aggNonce     aggPubKey = q (cachedKeyAggCtx ctx)     msg = ctx.msg-    nonceBytes = serialize_point aggNonce'.r1 <> serialize_point aggNonce'.r2+    -- BIP 327: aggnonce is encoded with cbytes_ext (infinity as 33 zero bytes)+    nonceBytes = cbytesExt aggNonce.r1 <> cbytesExt aggNonce.r2     qBytes = xBytes aggPubKey     preimage = nonceBytes <> qBytes <> msg    in@@ -564,8 +568,8 @@   case newTweak of     PlainTweak _ -> do       let g = 1-      pubkeyMul <- liftMaybe (ScalarMultiplicationFailed "pubkey * g") $ mul pubkey (fromInteger g)-      tG <- liftMaybe (ScalarMultiplicationFailed "t * G") $ mul _CURVE_G (fromInteger t)+      pubkeyMul <- liftMaybe (ScalarMultiplicationFailed "pubkey * g") $ mulScalar pubkey g+      tG <- liftMaybe (ScalarMultiplicationFailed "t * G") $ mulScalar _CURVE_G t       let tweakedPk = add pubkeyMul tG           newAccTweak = modQ (t + (g * accTweakVal))           newGacc = modQ (g * gaccIn)@@ -575,8 +579,8 @@     XOnlyTweak _ -> do       evenPubkey <- liftMaybe InvalidPointFormat (isEvenPub pubkey)       let g = if evenPubkey then 1 else curveOrder - 1-      pubkeyMul <- liftMaybe (ScalarMultiplicationFailed "pubkey * g") $ mul pubkey (fromInteger g)-      tG <- liftMaybe (ScalarMultiplicationFailed "t * G") $ mul _CURVE_G (fromInteger t)+      pubkeyMul <- liftMaybe (ScalarMultiplicationFailed "pubkey * g") $ mulScalar pubkey g+      tG <- liftMaybe (ScalarMultiplicationFailed "t * G") $ mulScalar _CURVE_G t       let tweakedPk = add pubkeyMul tG           newAccTweak = modQ (t + (g * accTweakVal))           newGacc = modQ (g * gaccIn)@@ -791,12 +795,18 @@  {- | Aggregates a 'Traversable' of 'PubNonce's using the [Nonce Aggregation algorithm in BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki).++Fails with 'InvalidPointFormat' if any individual 'PubNonce' contains the+point at infinity. The resulting aggregate nonce may contain the point at+infinity. -} aggNonces :: (Traversable t) => t PubNonce -> Either MuSig2Error PubNonce aggNonces nonces   | Seq.null noncesSeq = Left EmptyNonceCollection+  | any hasInfinity noncesSeq = Left InvalidPointFormat   | otherwise = case Seq.viewl noncesSeq of       Seq.EmptyL -> Left EmptyNonceCollection       x Seq.:< xs -> Right $! foldl' (<>) x xs  where   noncesSeq = Seq.fromList (toList nonces)+  hasInfinity (PubNonce a b) = a == _CURVE_ZERO || b == _CURVE_ZERO
lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs view
@@ -24,6 +24,8 @@   -- utils/misc   isEvenPub,   xBytes,+  cbytesExt,+  mulScalar,   bytesToInteger,   integerToBytes32,   xorByteStrings,@@ -174,6 +176,27 @@ -- | Gets the X-coordinate from a 'Pub'lic key as 'ByteString' xBytes :: Pub -> ByteString xBytes pk = BS.drop 1 $ serialize_point pk++{- | Serializes a point as 33 bytes, encoding the point at infinity as 33 zero+bytes (@cbytes_ext@ in+[BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki)).+-}+cbytesExt :: Projective -> ByteString+cbytesExt p+  | p == _CURVE_ZERO = BS.replicate 33 0+  | otherwise = serialize_point p++{- | Multiplies a point by a scalar reduced modulo the curve order.++Unlike 'mul', a scalar that is \(0 \bmod n\) yields the point at infinity+instead of failing.+-}+mulScalar :: Projective -> Integer -> Maybe Projective+mulScalar p k+  | k' == 0 = Just _CURVE_ZERO+  | otherwise = mul p (fromInteger k')+ where+  k' = modQ k  -- | The secp256k1 curve order (a mathematical constant). curveOrder :: Integer
musig2.cabal view
@@ -1,16 +1,20 @@-cabal-version:   3.0-name:            musig2-version:         0.2.0-synopsis:        MuSig2 library-license:         MIT-license-file:    LICENSE-author:          Jose Storopoli-maintainer:      jose@storopoli.com-homepage:        https://github.com/storopoli/musig2-category:        Cryptography-build-type:      Simple-tested-with:     GHC ==9.8.4 || ==9.10.2 || ==9.10.3-extra-doc-files: CHANGELOG+cabal-version:      3.0+name:               musig2+version:            0.2.1+synopsis:           MuSig2 library+license:            MIT+license-file:       LICENSE+author:             Jose Storopoli+maintainer:         jose@storopoli.com+homepage:           https://github.com/storopoli/musig2+category:           Cryptography+build-type:         Simple+tested-with:        GHC ==9.8.4 || ==9.10.3 || ==9.12.4 || ==9.14.1+extra-doc-files:    CHANGELOG+extra-source-files:+  test/vectors/bip327/*.json+  test/vectors/bip327/README.md+ description:   Pure BIP0327 MuSig2 (partial)signatures with tweak support on the elliptic curve secp256k1. @@ -34,15 +38,13 @@   exposed-modules:  Crypto.Curve.Secp256k1.MuSig2   other-modules:    Crypto.Curve.Secp256k1.MuSig2.Internal   build-depends:-    , base               >=4.19  && <5-    , base16-bytestring  >=1.0   && <1.1-    , binary             >=0.8   && <0.9-    , bytestring         >=0.9   && <0.13-    , containers         >=0.6   && <0.9-    , entropy            >=0.4   && <0.5-    , ppad-hmac-drbg     >=0.3.1 && <0.4-    , ppad-secp256k1     >=0.5.4 && <0.6-    , ppad-sha256        >=0.3.2 && <0.4+    , base            >=4.19   && <5+    , binary          >=0.8    && <0.9+    , bytestring      >=0.9    && <0.13+    , containers      >=0.6    && <0.9+    , entropy         >=0.4    && <0.5+    , ppad-secp256k1  >=0.5.10 && <0.6+    , ppad-sha256     >=0.3.2  && <0.4  test-suite musig2-tests   type:             exitcode-stdio-1.0@@ -50,23 +52,19 @@   hs-source-dirs:   test lib   main-is:          Main.hs   other-modules:-    AggNonces-    AggPartials     AggPartialsProperty-    AggPubkeys     ApplyTweaks+    Bip327Regressions     Crypto.Curve.Secp256k1.MuSig2     Crypto.Curve.Secp256k1.MuSig2.Internal     MonoidProjective     MonoidPubNonce-    NonceGen     NonceGenProperty+    OfficialVectors     ParityPub     SignVerify     SignVerifyProperty     SignVerifyTweakProperty-    SortPubkeys-    Tweak     Util    ghc-options:      -rtsopts -Wall@@ -75,12 +73,15 @@     ghc-options: -fllvm -O2    build-depends:+    , aeson              >=2.0  && <2.4     , base               >=4.19 && <5     , base16-bytestring     , binary     , bytestring     , containers+    , directory     , entropy+    , filepath     , ppad-secp256k1     , ppad-sha256     , tasty
− test/AggNonces.hs
@@ -1,43 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module AggNonces (testAggNonces) where--import Crypto.Curve.Secp256k1 (_CURVE_ZERO)-import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), aggNonces)-import Test.Tasty-import Test.Tasty.HUnit-import Util (parsePoint, parsePubNonce, unsafeRight)---- | Input 'PubNonce's from BIP-0327 test vectors-inputPubNonces :: [PubNonce]-inputPubNonces =-  map-    parsePubNonce-    [ "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E66603BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641"-    , "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833"-    , "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"-    , "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"-    ]---- | Test vector data: (input 'PubNonce' indices, expected 'PubNonce' result).-testVectors :: [([Int], PubNonce)]-testVectors =-  [ ([0, 1], parsePubNonce "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B024725377345BDE0E9C33AF3C43C0A29A9249F2F2956FA8CFEB55C8573D0262DC8")-  , -- Sum of second points encoded in the nonces is point at infinity which is serialized as 33 zero bytes.-    ([2, 3], PubNonce (parsePoint "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B") _CURVE_ZERO)-  ]---- | Creates test case from vector data.-makeTestCase :: Int -> ([Int], PubNonce) -> TestTree-makeTestCase i (indices, expected) =-  testCase ("BIP-0327 test vector " <> show (i + 1)) $-    unsafeRight aggNonce @=? expected- where-  selectedNonces = map (inputPubNonces !!) indices-  aggNonce = aggNonces selectedNonces---- | Test vectors from [BIP-0327 `nonce_agg_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/nonce_agg_vectors.json).-testAggNonces :: TestTree-testAggNonces =-  testGroup "aggregating pubkeys" $-    zipWith makeTestCase [0 ..] testVectors
− test/AggPartials.hs
@@ -1,169 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# OPTIONS_GHC -Wno-unused-top-binds #-}--module AggPartials (testAggPartials) where--import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), PartialSignature, PubNonce (..), Tweak (..), aggPartials, mkSessionContext)-import Data.ByteString (ByteString)-import Test.Tasty-import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parsePubNonce, parseScalar, unsafeRight)---- | Test vector structure for signature aggregation.-data SigAggTestVector = SigAggTestVector-  { aggNonce :: PubNonce-  , nonceIndices :: [Int]-  , keyIndices :: [Int]-  , tweakIndices :: [Int]-  , isXOnly :: [Bool]-  , psigIndices :: [Int]-  , expected :: Maybe ByteString -- Nothing for error cases-  , errorCase :: Bool-  }-  deriving (Show)---- | Global test data from BIP-0327 @sig_agg_vectors.json@-pubkeys :: [Pub]-pubkeys =-  [ parsePoint "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"-  , parsePoint "02D2DC6F5DF7C56ACF38C7FA0AE7A759AE30E19B37359DFDE015872324C7EF6E05"-  , parsePoint "03C7FB101D97FF930ACD0C6760852EF64E69083DE0B06AC6335724754BB4B0522C"-  , parsePoint "02352433B21E7E05D3B452B81CAE566E06D2E003ECE16D1074AABA4289E0E3D581"-  ]--pnonces :: [PubNonce]-pnonces =-  [ parsePubNonce "036E5EE6E28824029FEA3E8A9DDD2C8483F5AF98F7177C3AF3CB6F47CAF8D94AE902DBA67E4A1F3680826172DA15AFB1A8CA85C7C5CC88900905C8DC8C328511B53E"-  , parsePubNonce "03E4F798DA48A76EEC1C9CC5AB7A880FFBA201A5F064E627EC9CB0031D1D58FC5103E06180315C5A522B7EC7C08B69DCD721C313C940819296D0A7AB8E8795AC1F00"-  , parsePubNonce "02C0068FD25523A31578B8077F24F78F5BD5F2422AFF47C1FADA0F36B3CEB6C7D202098A55D1736AA5FCC21CF0729CCE852575C06C081125144763C2C4C4A05C09B6"-  , parsePubNonce "031F5C87DCFBFCF330DEE4311D85E8F1DEA01D87A6F1C14CDFC7E4F1D8C441CFA40277BF176E9F747C34F81B0D9F072B1B404A86F402C2D86CF9EA9E9C69876EA3B9"-  , parsePubNonce "023F7042046E0397822C4144A17F8B63D78748696A46C3B9F0A901D296EC3406C302022B0B464292CF9751D699F10980AC764E6F671EFCA15069BBE62B0D1C62522A"-  , parsePubNonce "02D97DDA5988461DF58C5897444F116A7C74E5711BF77A9446E27806563F3B6C47020CBAD9C363A7737F99FA06B6BE093CEAFF5397316C5AC46915C43767AE867C00"-  ]---- | Raw tweak values from BIP-0327 test vectors-tweakValues :: [Integer]-tweakValues =-  [ parseScalar "B511DA492182A91B0FFB9A98020D55F260AE86D7ECBD0399C7383D59A5F2AF7C"-  , parseScalar "A815FE049EE3C5AAB66310477FBC8BCCCAC2F3395F59F921C364ACD78A2F48DC"-  , parseScalar "75448A87274B056468B977BE06EB1E9F657577B7320B0A3376EA51FD420D18A8"-  ]--psigs :: [PartialSignature]-psigs =-  [ parseScalar "B15D2CD3C3D22B04DAE438CE653F6B4ECF042F42CFDED7C41B64AAF9B4AF53FB"-  , parseScalar "6193D6AC61B354E9105BBDC8937A3454A6D705B6D57322A5A472A02CE99FCB64"-  , parseScalar "9A87D3B79EC67228CB97878B76049B15DBD05B8158D17B5B9114D3C226887505"-  , parseScalar "66F82EA90923689B855D36C6B7E032FB9970301481B99E01CDB4D6AC7C347A15"-  , parseScalar "4F5AEE41510848A6447DCD1BBC78457EF69024944C87F40250D3EF2C25D33EFE"-  , parseScalar "DDEF427BBB847CC027BEFF4EDB01038148917832253EBC355FC33F4A8E2FCCE4"-  , parseScalar "97B890A26C981DA8102D3BC294159D171D72810FDF7C6A691DEF02F0F7AF3FDC"-  , parseScalar "53FA9E08BA5243CBCB0D797C5EE83BC6728E539EB76C2D0BF0F971EE4E909971"-  , parseScalar "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"-  ]--msg :: ByteString-msg = decodeHex "599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869"---- | Test vectors - valid cases from BIP-0327 JSON-validTestVectors :: [SigAggTestVector]-validTestVectors =-  [ SigAggTestVector-      { aggNonce = parsePubNonce "0341432722C5CD0268D829C702CF0D1CBCE57033EED201FD335191385227C3210C03D377F2D258B64AADC0E16F26462323D701D286046A2EA93365656AFD9875982B"-      , nonceIndices = [0, 1]-      , keyIndices = [0, 1]-      , tweakIndices = []-      , isXOnly = []-      , psigIndices = [0, 1]-      , expected = Just $ decodeHex "041DA22223CE65C92C9A0D6C2CAC828AAF1EEE56304FEC371DDF91EBB2B9EF0912F1038025857FEDEB3FF696F8B99FA4BB2C5812F6095A2E0004EC99CE18DE1E"-      , errorCase = False-      }-  , SigAggTestVector-      { aggNonce = parsePubNonce "0224AFD36C902084058B51B5D36676BBA4DC97C775873768E58822F87FE437D792028CB15929099EEE2F5DAE404CD39357591BA32E9AF4E162B8D3E7CB5EFE31CB20"-      , nonceIndices = [0, 2]-      , keyIndices = [0, 2]-      , tweakIndices = []-      , isXOnly = []-      , psigIndices = [2, 3]-      , expected = Just $ decodeHex "1069B67EC3D2F3C7C08291ACCB17A9C9B8F2819A52EB5DF8726E17E7D6B52E9F01800260A7E9DAC450F4BE522DE4CE12BA91AEAF2B4279219EF74BE1D286ADD9"-      , errorCase = False-      }-  , SigAggTestVector-      { aggNonce = parsePubNonce "0208C5C438C710F4F96A61E9FF3C37758814B8C3AE12BFEA0ED2C87FF6954FF186020B1816EA104B4FCA2D304D733E0E19CEAD51303FF6420BFD222335CAA402916D"-      , nonceIndices = [0, 3]-      , keyIndices = [0, 2]-      , tweakIndices = [0]-      , isXOnly = [False]-      , psigIndices = [4, 5]-      , expected = Just $ decodeHex "5C558E1DCADE86DA0B2F02626A512E30A22CF5255CAEA7EE32C38E9A71A0E9148BA6C0E6EC7683B64220F0298696F1B878CD47B107B81F7188812D593971E0CC"-      , errorCase = False-      }-  , SigAggTestVector-      { aggNonce = parsePubNonce "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD"-      , nonceIndices = [0, 4]-      , keyIndices = [0, 3]-      , tweakIndices = [0, 1, 2]-      , isXOnly = [True, False, True]-      , psigIndices = [6, 7]-      , expected = Just $ decodeHex "839B08820B681DBA8DAF4CC7B104E8F2638F9388F8D7A555DC17B6E6971D7426CE07BF6AB01F1DB50E4E33719295F4094572B79868E440FB3DEFD3FAC1DB589E"-      , errorCase = False-      }-  ]---- | Test vectors - error cases from BIP-0327 JSON-errorTestVectors :: [SigAggTestVector]-errorTestVectors =-  [ SigAggTestVector-      { aggNonce = parsePubNonce "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD"-      , nonceIndices = [0, 4]-      , keyIndices = [0, 3]-      , tweakIndices = [0, 1, 2]-      , isXOnly = [True, False, True]-      , psigIndices = [7, 8] -- psig index 8 is invalid (exceeds group size)-      , expected = Nothing-      , errorCase = True-      }-  ]---- | Helper to build tweaks from indices and isXOnly flags-buildTweaks :: [Int] -> [Bool] -> [Tweak]-buildTweaks = zipWith buildTweak- where-  buildTweak i isX =-    let tweakValue = tweakValues !! i-     in if isX then XOnlyTweak tweakValue else PlainTweak tweakValue---- | Creates a test case for valid signature aggregation.-makeValidTestCase :: Int -> SigAggTestVector -> TestTree-makeValidTestCase i SigAggTestVector{..} =-  testCase ("BIP-0327 SigAgg Valid Vector " ++ show (i + 1)) $ do-    let selectedKeys = map (pubkeys !!) keyIndices-    let selectedTweaks = buildTweaks tweakIndices isXOnly-    let selectedPsigs = map (psigs !!) psigIndices-    let ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys selectedTweaks msg-    let result = unsafeRight $ aggPartials selectedPsigs ctx-    case expected of-      Just expectedSig -> assertEqual "signature mismatch" expectedSig result-      Nothing -> assertFailure "Expected signature but got nothing"---- | Creates a test case for error signature aggregation.-makeErrorTestCase :: Int -> SigAggTestVector -> TestTree-makeErrorTestCase i SigAggTestVector{..} =-  testCase ("BIP-0327 SigAgg Error Vector " ++ show (i + 1)) $ do-    let selectedKeys = map (pubkeys !!) keyIndices-        selectedTweaks = buildTweaks tweakIndices isXOnly-        selectedPsigs = map (psigs !!) psigIndices-        ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys selectedTweaks msg-    assertBool "Expected error case" errorCase-    aggPartials selectedPsigs ctx @?= Left PartialSignatureOutOfRange---- | Main test group for signature aggregation.-testAggPartials :: TestTree-testAggPartials =-  testGroup-    "BIP-0327 Signature Aggregation Vectors"-    [ testGroup "Valid Cases" $ zipWith makeValidTestCase [0 ..] validTestVectors-    , testGroup "Error Cases" $ zipWith makeErrorTestCase [0 ..] errorTestVectors-    ]
− test/AggPubkeys.hs
@@ -1,79 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module AggPubkeys (testAggPubkeys) where--import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), Tweak (..), aggregatedPubkey, mkKeyAggContext)-import Data.ByteString (ByteString)-import Test.Tasty-import Test.Tasty.HUnit-import Util (decodeHex, extractXOnly, parsePoint, unsafeRight)---- | Input public keys from BIP-0327 test vectors-inputPubkeys :: [Pub]-inputPubkeys =-  map-    parsePoint-    [ "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"-    , "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"-    , "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66"-    , "020000000000000000000000000000000000000000000000000000000000000005"-    , "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"-    , "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"-    , "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"-    ]---- | Test vector data: (input key indices, expected x-only result).-testVectors :: [([Int], ByteString)]-testVectors =-  [ ([0, 1, 2], decodeHex "90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C")-  , ([2, 1, 0], decodeHex "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B")-  , ([0, 0, 0], decodeHex "B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935")-  , ([0, 0, 1, 1], decodeHex "69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E")-  ]---- | Tweak test vectors from BIP-0327.-tweaks :: [Integer]-tweaks =-  [ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -- curve order (invalid)-  , 0x252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B -- tweak that causes infinity-  ]--invalidTweakOutOfRange :: Integer-invalidTweakOutOfRange = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141---- | Error test cases: (key indices, tweak index, is_xonly, expected error).-errorTestVectors :: [([Int], Int, Bool, MuSig2Error)]-errorTestVectors =-  [ ([0, 1], 0, True, TweakOutOfRange invalidTweakOutOfRange)-  , ([6], 1, False, TweakResultAtInfinity)-  ]---- | Creates test case from vector data.-makeTestCase :: Int -> ([Int], ByteString) -> TestTree-makeTestCase i (indices, expected) =-  testCase ("BIP-0327 test vector " <> show (i + 1)) $-    extractXOnly aggPk @=? expected- where-  selectedKeys = map (inputPubkeys !!) indices-  keyAggCtx = unsafeRight $ mkKeyAggContext selectedKeys Nothing-  aggPk = aggregatedPubkey keyAggCtx---- | Creates error test case from vector data.-makeErrorTestCase :: Int -> ([Int], Int, Bool, MuSig2Error) -> TestTree-makeErrorTestCase i (keyIndices, tweakIndex, isXOnly, expectedMsg) =-  testCase ("BIP-0327 error test vector " <> show (i + 1)) $ do-    case mkKeyAggContext selectedKeys (Just tweak) of-      Left err -> err @?= expectedMsg-      Right _ -> assertFailure "Expected error but got success"- where-  selectedKeys = map (inputPubkeys !!) keyIndices-  tweakVal = tweaks !! tweakIndex-  tweak = if isXOnly then XOnlyTweak tweakVal else PlainTweak tweakVal---- | Test vectors from [BIP-0327 `key_agg_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/key_agg_vectors.json).-testAggPubkeys :: TestTree-testAggPubkeys =-  testGroup "aggregating pubkeys" $-    zipWith makeTestCase [0 ..] testVectors-      <> zipWith makeErrorTestCase [0 ..] errorTestVectors
+ test/Bip327Regressions.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE OverloadedStrings #-}++{- | Regression tests for BIP-0327 conformance bugs fixed in 0.2.1.++Expected values come from the official BIP-0327 test vectors or, for cases the+official vectors do not cover, from the BIP-0327 reference implementation+(@reference.py@).+-}+module Bip327Regressions (testBip327Regressions) where++import Control.Monad (zipWithM)+import Crypto.Curve.Secp256k1 (Pub, derive_pub, parse_point, serialize_point, verify_schnorr, _CURVE_G, _CURVE_ZERO)+import Crypto.Curve.Secp256k1.MuSig2 (+  MuSig2Error (..),+  PubNonce (..),+  SecKey (..),+  SecNonce,+  Tweak (..),+  aggNonces,+  aggPartials,+  aggregatedPubkey,+  applyTweak,+  mkKeyAggContext,+  mkSessionContext,+  partialSigVerify,+  publicNonce,+  sign,+ )+import Crypto.Curve.Secp256k1.MuSig2.Internal (integerToBytes32)+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Foldable (foldlM)+import Data.Maybe (fromMaybe)+import Test.Tasty+import Test.Tasty.HUnit+import Util (decodeHex, parsePoint, parsePubNonce, parseScalar, unsafeMkSecNonce, unsafeRight)++testBip327Regressions :: TestTree+testBip327Regressions =+  testGroup+    "BIP-0327 regressions"+    [ testGroup "Aggregate nonce with point at infinity" (map infinityCase infinityCases)+    , testCase "Signer pubkey not in list of pubkeys (sign_verify sign_error #0)" signerNotInList+    , testGroup "PartialSigAgg with gacc = n - 1" (zipWith gaccCase [0 :: Int ..] gaccCases)+    , testGroup "ApplyTweak with t = 0" [zeroTweak False, zeroTweak True]+    , testCase "NonceAgg rejects a public nonce containing infinity" nonceAggRejectsInfinity+    , testCase "PartialSigVerify of a zero partial signature returns False" zeroPartialSig+    ]++-- Material from sign_verify_vectors.json ------------------------------------++svSecKey :: SecKey+svSecKey = SecKey (parseScalar "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671")++svPubkeys :: [Pub]+svPubkeys =+  map+    parsePoint+    [ "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"+    , "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"+    , "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA661"+    ]++-- | Public key of 'svSecKey' (first entry of 'svPubkeys').+svSignerPubkey :: Pub+svSignerPubkey = parsePoint "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"++svPubNonces :: [PubNonce]+svPubNonces =+  map+    parsePubNonce+    [ "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"+    , "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"+    , "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046"+    , "0237C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0387BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"+    ]++svMsg :: ByteString+svMsg = decodeHex "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF"++svSecNonce :: SecNonce+svSecNonce =+  unsafeMkSecNonce+    svSignerPubkey+    (parseScalar "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61")+    (parseScalar "FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")++-- | Parses a 33-byte @cbytes_ext@ encoding (33 zero bytes is infinity).+pointExt :: ByteString -> Pub+pointExt h+  | bytes == BS.replicate 33 0 = _CURVE_ZERO+  | otherwise = fromMaybe (error "invalid point") (parse_point bytes)+ where+  bytes = decodeHex h++-- | Parses a 66-byte aggregate nonce encoded with @cbytes_ext@.+aggNonceExt :: ByteString -> PubNonce+aggNonceExt h = PubNonce (pointExt (BS.take 66 h)) (pointExt (BS.drop 66 h))++toHex32 :: Integer -> ByteString+toHex32 = integerToBytes32++-- Aggregate nonce with infinity ---------------------------------------------++-- | (label, keys, aggnonce, expected partial signature, expected xbytes(R))+infinityCases :: [(String, [Pub], ByteString, ByteString, ByteString)]+infinityCases =+  [ -- official sign_verify valid #3++    ( "R1 = R2 = infinity (official sign_verify valid #3)"+    , take 2 svPubkeys+    , "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"+    , "AE386064B26105404798F75DE2EB9AF5EDA5387B064B83D049CB7C5E08879531"+    , "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"+    )+  , -- reference.py++    ( "R1 = infinity, R2 = 3G"+    , svPubkeys+    , "00000000000000000000000000000000000000000000000000000000000000000002F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"+    , "A8F919D3C4291F0123AE9BC98F3F5A10FC1E82A0B40C7ECFE7F23F508F57D39A"+    , "3B7B5C67354E218951C0FF603D18261632638E9DA40615E6EF75C18CB3B60647"+    )+  ,+    ( "R1 = R2 = infinity, three signers"+    , svPubkeys+    , "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"+    , "4D1114440EBE1574F3BB2BF36B0C5BE8647E23C9405FCFDE473D82D5B4733E6D"+    , "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"+    )+  ,+    ( "R1 = 5G, R2 = infinity"+    , svPubkeys+    , "022F8BDE4D1A07209355B4A7250A5C5128E88B84BDDC619AB7CBA8D569B240EFE4000000000000000000000000000000000000000000000000000000000000000000"+    , "7DB23FECD568EE90D6ABCB7915E46BD544B1804C412EDB936B44D7F3DC16A922"+    , "2F8BDE4D1A07209355B4A7250A5C5128E88B84BDDC619AB7CBA8D569B240EFE4"+    )+  ]++infinityCase :: (String, [Pub], ByteString, ByteString, ByteString) -> TestTree+infinityCase (label, keys, aggHex, expectedPsig, expectedRx) = testCase label $ do+  let ctx = unsafeRight $ mkSessionContext (aggNonceExt aggHex) keys [] svMsg+      psig = unsafeRight $ sign svSecNonce svSecKey ctx+      sig = unsafeRight $ aggPartials [0 :: Integer] ctx+  toHex32 psig @?= decodeHex expectedPsig+  BS.take 32 sig @?= decodeHex expectedRx++-- Signer not in list ----------------------------------------------------------++signerNotInList :: Assertion+signerNotInList = do+  let aggNonce = aggNonceExt "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9"+      ctx = unsafeRight $ mkSessionContext aggNonce (drop 1 svPubkeys) [] svMsg+  sign svSecNonce svSecKey ctx @?= Left SecretKeyPublicKeyMismatch++-- gacc = n - 1 ------------------------------------------------------------------++-- | (secret keys, secret nonces, tweaks, expected partial sigs, expected signature)+gaccCases :: [([Integer], [(Integer, Integer)], [Tweak], [ByteString], ByteString)]+gaccCases =+  [+    ( [0x1118, 0x222F]+    , [(0xAAAB, 0xBBBC), (0xCCCD, 0xDDDE)]+    , [XOnlyTweak 0x1234567890ABCDEF]+    , ["036D45ADBF14F99296EA524AB2B6DF08B9C74E172B463671BBDC6EEB64F35659", "B3576E442485F4457A7D14F11643322900CECEB3C2086160866D93E3B0592D80"]+    , "5D56B500773C4BDB1D12E588DE0CD2420BB5C4740B54C59443CC02D32C820A2ABAF22138DE1A3B920468B3325C0C298FEFCCA7C37123034353C4A308C1D9B880"+    )+  ,+    ( [0x1118, 0x222F]+    , [(0xAAAB, 0xBBBC), (0xCCCD, 0xDDDE)]+    , [PlainTweak 0x1111, XOnlyTweak 0x2222]+    , ["AD330CCA8F1404AD4EF335592C22C3CBAFCD426E9010E9C7D26DE4E3BBCD1D12", "43CA5A1DD8CE93809555521478E46FED6DD826D1F78A96173BE310F229C5E813"]+    , "9B545290252036652491EA299A0FF359B1C8AD200406AE590A6CECF645D48B9E0F74CA8D4B552D7FDF7D3A0F03EB712DFEA36AA9C711CA191DD6B54EB63E4441"+    )+  ]++gaccCase :: Int -> ([Integer], [(Integer, Integer)], [Tweak], [ByteString], ByteString) -> TestTree+gaccCase i (sks, ks, tweaks, expectedPsigs, expectedSig) = testCase ("case " <> show i) $ do+  let msg = decodeHex "599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869"+      pubs = map (fromMaybe (error "derive_pub") . derive_pub . fromInteger) sks+      secNonces = zipWith (\p (k1, k2) -> unsafeMkSecNonce p k1 k2) pubs ks+      aggNonce = unsafeRight $ traverse publicNonce secNonces >>= aggNonces+      ctx = unsafeRight $ mkSessionContext aggNonce pubs tweaks msg+      psigs = unsafeRight $ zipWithM (\sn d -> sign sn (SecKey d) ctx) secNonces sks+      sig = unsafeRight $ aggPartials psigs ctx+      keyCtx = unsafeRight $ mkKeyAggContext pubs Nothing >>= \c -> foldlM applyTweak c tweaks+  map toHex32 psigs @?= map decodeHex expectedPsigs+  sig @?= decodeHex expectedSig+  assertBool "aggregated signature verifies" (verify_schnorr msg (aggregatedPubkey keyCtx) sig)++-- t = 0 -----------------------------------------------------------------------++zeroTweak :: Bool -> TestTree+zeroTweak isXOnly = testCase (if isXOnly then "x-only" else "plain") $ do+  let tweak = if isXOnly then XOnlyTweak 0 else PlainTweak 0+      expected =+        if isXOnly+          then "0254240C76B8F2999143301A99C7F721EE57EEE0BCE401DF3AFEAA9AE218C70F23"+          else "0354240C76B8F2999143301A99C7F721EE57EEE0BCE401DF3AFEAA9AE218C70F23"+      ctx = unsafeRight $ mkKeyAggContext (take 2 svPubkeys) (Just tweak)+  serialize_point (aggregatedPubkey ctx) @?= decodeHex expected++-- NonceAgg / PartialSigVerify ---------------------------------------------------++nonceAggRejectsInfinity :: Assertion+nonceAggRejectsInfinity = do+  let good = parsePubNonce "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"+  aggNonces [good, PubNonce _CURVE_ZERO (r2 good)] @?= Left InvalidPointFormat+  aggNonces [good, PubNonce (r1 good) _CURVE_ZERO] @?= Left InvalidPointFormat+  partialSigVerify 1 (PubNonce _CURVE_ZERO _CURVE_G : drop 1 (take 3 svPubNonces)) svPubkeys [] svMsg 0 @?= Left InvalidPointFormat++zeroPartialSig :: Assertion+zeroPartialSig = partialSigVerify 0 (take 3 svPubNonces) svPubkeys [] svMsg 0 @?= Right False
test/Main.hs view
@@ -1,34 +1,30 @@ module Main where -import AggNonces (testAggNonces)-import AggPartials (testAggPartials) import AggPartialsProperty (propertyAggPartials)-import AggPubkeys (testAggPubkeys) import ApplyTweaks (testApplyTweaks)+import Bip327Regressions (testBip327Regressions) import MonoidProjective (propertyMonoidProjective) import MonoidPubNonce (propertyMonoidPubNonce)-import NonceGen (testNonceGen) import NonceGenProperty (propertyNonceGen)+import OfficialVectors (officialVectorTests) import ParityPub (testParityPub) import SignVerify (testSignVerify) import SignVerifyProperty (propertySignVerify) import SignVerifyTweakProperty (propertySignVerifyTweak)-import SortPubkeys (testSortPubkeys) import Test.Tasty-import Tweak (testTweak)  -- | Unit tests. unitTests :: TestTree-unitTests = testGroup "Unit Tests" [testSortPubkeys, testAggPubkeys, testParityPub, testApplyTweaks, testNonceGen, testAggNonces, testAggPartials, testSignVerify, testTweak]+unitTests = testGroup "Unit Tests" [testParityPub, testApplyTweaks, testSignVerify, testBip327Regressions]  -- | Property tests. propertyTests :: TestTree propertyTests = testGroup "Property Tests" [propertyMonoidProjective, propertyNonceGen, propertyMonoidPubNonce, propertySignVerify, propertySignVerifyTweak, propertyAggPartials]  -- | Tests.-tests :: TestTree-tests = testGroup "All Tests" [unitTests, propertyTests]+tests :: TestTree -> TestTree+tests officialVectors = testGroup "All Tests" [officialVectors, unitTests, propertyTests]  -- | Run all tests. main :: IO ()-main = defaultMain tests+main = officialVectorTests >>= defaultMain . tests
− test/NonceGen.hs
@@ -1,107 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}--module NonceGen (testNonceGen) where--import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonceGenParams (..), publicNonce, secNonceGenWithRand)-import Crypto.Curve.Secp256k1.MuSig2.Internal (secNonceScalars)-import Data.ByteString (ByteString)-import qualified Data.ByteString as BS-import Test.Tasty-import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parseScalar, unsafeRight)---- | Test vector structure.-data NonceGenTestVector = NonceGenTestVector-  { rand_ :: BS.ByteString-  , sk :: Maybe Integer-  , pk :: Pub-  , aggpk :: Maybe Pub-  , msg :: Maybe ByteString-  , extra_in :: Maybe ByteString-  , expected_k1 :: Integer-  , expected_k2 :: Integer-  , expected_r1 :: Pub-  , expected_r2 :: Pub-  }---- | Hardcoded test vectors from @bip-0327/vectors/nonce_gen_vectors.json@.-testVectors :: [NonceGenTestVector]-testVectors =-  [ NonceGenTestVector-      { rand_ = decodeHex "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F"-      , sk = Just 0x0202020202020202020202020202020202020202020202020202020202020202-      , pk = parsePoint "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766"-      , aggpk = Just $ parsePoint "0707070707070707070707070707070707070707070707070707070707070707"-      , msg = Just $ decodeHex "0101010101010101010101010101010101010101010101010101010101010101"-      , extra_in = Just $ decodeHex "0808080808080808080808080808080808080808080808080808080808080808"-      , expected_k1 = parseScalar "B114E502BEAA4E301DD08A50264172C84E41650E6CB726B410C0694D59EFFB64"-      , expected_k2 = parseScalar "95B5CAF28D045B973D63E3C99A44B807BDE375FD6CB39E46DC4A511708D0E9D2"-      , expected_r1 = parsePoint "02F7BE7089E8376EB355272368766B17E88E7DB72047D05E56AA881EA52B3B35DF"-      , expected_r2 = parsePoint "02C29C8046FDD0DED4C7E55869137200FBDBFE2EB654267B6D7013602CAED3115A"-      }-  , NonceGenTestVector-      { rand_ = decodeHex "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F"-      , sk = Just 0x0202020202020202020202020202020202020202020202020202020202020202-      , pk = parsePoint "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766"-      , aggpk = Just $ parsePoint "0707070707070707070707070707070707070707070707070707070707070707"-      , msg = Just $ decodeHex ""-      , extra_in = Just $ decodeHex "0808080808080808080808080808080808080808080808080808080808080808"-      , expected_k1 = parseScalar "E862B068500320088138468D47E0E6F147E01B6024244AE45EAC40ACE5929B9F"-      , expected_k2 = parseScalar "0789E051170B9E705D0B9EB49049A323BBBBB206D8E05C19F46C6228742AA7A9"-      , expected_r1 = parsePoint "023034FA5E2679F01EE66E12225882A7A48CC66719B1B9D3B6C4DBD743EFEDA2C5"-      , expected_r2 = parsePoint "03F3FD6F01EB3A8E9CB315D73F1F3D287CAFBB44AB321153C6287F407600205109"-      }-  , NonceGenTestVector-      { rand_ = decodeHex "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F"-      , sk = Just 0x0202020202020202020202020202020202020202020202020202020202020202-      , pk = parsePoint "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766"-      , aggpk = Just $ parsePoint "0707070707070707070707070707070707070707070707070707070707070707"-      , msg = Just $ decodeHex "2626262626262626262626262626262626262626262626262626262626262626262626262626"-      , extra_in = Just $ decodeHex "0808080808080808080808080808080808080808080808080808080808080808"-      , expected_k1 = parseScalar "3221975ACBDEA6820EABF02A02B7F27D3A8EF68EE42787B88CBEFD9AA06AF363"-      , expected_k2 = parseScalar "2EE85B1A61D8EF31126D4663A00DD96E9D1D4959E72D70FE5EBB6E7696EBA66F"-      , expected_r1 = parsePoint "02E5BBC21C69270F59BD634FCBFA281BE9D76601295345112C58954625BF23793A"-      , expected_r2 = parsePoint "021307511C79F95D38ACACFF1B4DA98228B77E65AA216AD075E9673286EFB4EAF3"-      }-  , NonceGenTestVector-      { rand_ = decodeHex "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F"-      , sk = Nothing-      , pk = parsePoint "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"-      , aggpk = Nothing-      , msg = Nothing-      , extra_in = Nothing-      , expected_k1 = parseScalar "89BDD787D0284E5E4D5FC572E49E316BAB7E21E3B1830DE37DFE80156FA41A6D"-      , expected_k2 = parseScalar "0B17AE8D024C53679699A6FD7944D9C4A366B514BAF43088E0708B1023DD2897"-      , expected_r1 = parsePoint "02C96E7CB1E8AA5DAC64D872947914198F607D90ECDE5200DE52978AD5DED63C00"-      , expected_r2 = parsePoint "0299EC5117C2D29EDEE8A2092587C3909BE694D5CFF0667D6C02EA4059F7CD9786"-      }-  ]---- | Creates a test case from a test vector.-makeNonceGenTestCase :: Int -> NonceGenTestVector -> TestTree-makeNonceGenTestCase i NonceGenTestVector{..} =-  testCase ("BIP-0327 NonceGen Vector " ++ show (i + 1)) $ do-    let params =-          SecNonceGenParams-            { _pk = pk-            , _sk = fmap SecKey sk-            , _aggpk = aggpk-            , _msg = msg-            , _extraIn = extra_in-            }-    let sec = unsafeRight $ secNonceGenWithRand rand_ params-    let pub = unsafeRight $ publicNonce sec-    let (k1, k2) = secNonceScalars sec-    assertEqual "k1 mismatch" expected_k1 k1-    assertEqual "k2 mismatch" expected_k2 k2-    assertEqual "r1 mismatch" expected_r1 pub.r1-    assertEqual "r2 mismatch" expected_r2 pub.r2---- | Main test group for NonceGen.-testNonceGen :: TestTree-testNonceGen =-  testGroup "BIP-0327 NonceGen Vectors" $-    zipWith makeNonceGenTestCase [0 ..] testVectors
+ test/OfficialVectors.hs view
@@ -0,0 +1,900 @@+{-# LANGUAGE OverloadedStrings #-}++{- | Official BIP-0327 test vectors, read at test time from the JSON files+vendored in @test\/vectors\/bip327@ (see the @README.md@ there for the+upstream commit and checksums).++Every case that the public API can express is run with exact-output+assertions. Cases that need something the library does not offer (parsing+invalid byte encodings and blaming the signer who sent them, or+DeterministicSign) are listed one by one in the+\"Not expressible through the public API\" group, so the coverage gap is+visible in the test output instead of being skipped silently. For invalid+encodings, that group still checks that @parse_point@ rejects exactly the+contribution the vector blames.++The parsers reject unknown fields, unknown error types and unknown error+messages, so an updated vector file cannot be silently half-run.+-}+module OfficialVectors (officialVectorTests) where++import Control.Monad (filterM, foldM, unless, zipWithM)+import Crypto.Curve.Secp256k1 (Pub, derive_pub, parse_point, serialize_point, verify_schnorr, _CURVE_ZERO)+import Crypto.Curve.Secp256k1.MuSig2 (+  MuSig2Error (..),+  PubNonce (..),+  SecKey (..),+  SecNonceGenParams (..),+  Tweak (..),+  aggNonces,+  aggPartials,+  aggregatedPubkey,+  applyTweak,+  mkKeyAggContext,+  mkSecNonce,+  mkSessionContext,+  partialSigVerify,+  publicNonce,+  secNonceGenWithRand,+  sign,+  sortPublicKeys,+ )+import Crypto.Curve.Secp256k1.MuSig2.Internal (+  SecNonce (..),+  bytesToInteger,+  cbytesExt,+  curveOrder,+  integerToBytes32,+  secNonceScalars,+  xBytes,+ )+import qualified Crypto.Hash.SHA256 as SHA256+import Data.Aeson (FromJSON (..), Value, eitherDecodeFileStrict, withObject, (.:), (.:?))+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap+import Data.Aeson.Types (Object, Parser, parseEither)+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base16 as B16+import qualified Data.ByteString.Char8 as BC+import Data.Char (toUpper)+import Data.Either (lefts, rights)+import Data.Foldable (toList)+import Data.Maybe (fromMaybe, isJust)+import System.Directory (doesFileExist, getCurrentDirectory, makeAbsolute)+import System.Environment (lookupEnv)+import System.FilePath ((</>))+import Test.Tasty+import Test.Tasty.HUnit++-- Locating and loading the vector files ---------------------------------------++-- | Environment variable that overrides the vector directory.+vectorsDirVar :: String+vectorsDirVar = "MUSIG2_BIP327_VECTORS"++-- | Vector directory relative to the package root (the working directory of @cabal test@).+defaultVectorsDir :: FilePath+defaultVectorsDir = "test" </> "vectors" </> "bip327"++{- | The vendored files and their SHA-256 (also recorded in the README next to+them). Changing a vector file without updating this list fails the suite.+-}+pinnedFiles :: [(FilePath, String)]+pinnedFiles =+  [ ("key_sort_vectors.json", "2389fa0c146cfd7455c643ca240ec32835dcfc916f430f50dd94d0b49c9ea16c")+  , ("key_agg_vectors.json", "03c02a97e4ef3f2edfbc8e6013c127496dfcfd5889cfca60ddf009a4e9091cab")+  , ("nonce_gen_vectors.json", "2e823580fc072427f0db0f000212cc9124ad2b9dca2b58357eb65088aee4358d")+  , ("nonce_agg_vectors.json", "8409e87b81ea769759598ad3ce53b277a78afffb3a490a86ce02c4d69984524b")+  , ("sign_verify_vectors.json", "692eecc101f3e515c29137f05031935e1210d2a01bab91e674eb0234f095c15c")+  , ("tweak_vectors.json", "80ce6385ce062644ad1f4edcb9d4797f70ddb0b74769e4099f51b3c9e6ab4aff")+  , ("sig_agg_vectors.json", "15f14c034fb2a5739d7ce638be94c5b37ea675a2e01159092dd93b59d69c3439")+  , ("det_sign_vectors.json", "3d4fdb64b24e31762f20830036dc0c59d39fa896649131b54b87906ffdc6e9e8")+  ]++-- | Finds the vector directory, or explains where it looked.+locateVectorsDir :: IO (Either String FilePath)+locateVectorsDir = do+  override <- lookupEnv vectorsDirVar+  let dir = fromMaybe defaultVectorsDir override+  missing <- filterM (fmap not . doesFileExist) [dir </> f | (f, _) <- pinnedFiles]+  if null missing+    then pure (Right dir)+    else do+      cwd <- getCurrentDirectory+      absMissing <- traverse makeAbsolute missing+      pure . Left . unlines $+        ("BIP-0327 vector files not found:" : map ("  " <>) absMissing)+          <> [ "Working directory: " <> cwd+             , case override of+                 Just _ -> "The directory came from " <> vectorsDirVar <> "."+                 Nothing ->+                   "Run the suite from the package root (cabal test does this), or set "+                     <> vectorsDirVar+                     <> " to the directory holding the *_vectors.json files."+             ]++-- | Builds the official vector test tree; any loading problem becomes a failing test.+officialVectorTests :: IO TestTree+officialVectorTests = do+  located <- locateVectorsDir+  case located of+    Left err -> pure (testGroup groupName [loadFailure err])+    Right dir -> do+      loaded <- loadVectors dir+      pure . testGroup groupName $ case loaded of+        Left err -> [integrityTests dir, loadFailure err]+        Right vs -> integrityTests dir : vectorTests vs+ where+  groupName = "BIP-0327 official vectors (JSON)"+  loadFailure err = testCase "load vector files" (assertFailure err)++integrityTests :: FilePath -> TestTree+integrityTests dir =+  testGroup+    "vendored files match pinned SHA-256"+    [ testCase file $ do+        contents <- BS.readFile (dir </> file)+        assertEqual "sha256" digest (BC.unpack (B16.encode (SHA256.hash contents)))+    | (file, digest) <- pinnedFiles+    ]++data Vectors = Vectors+  { vKeySort :: KeySortVectors+  , vKeyAgg :: KeyAggVectors+  , vNonceGen :: [NonceGenCase]+  , vNonceAgg :: NonceAggVectors+  , vSignVerify :: SignVerifyVectors+  , vTweak :: TweakVectors+  , vSigAgg :: SigAggVectors+  , vDetSign :: DetSignVectors+  }++loadVectors :: FilePath -> IO (Either String Vectors)+loadVectors dir = do+  ks <- load "key_sort_vectors.json" parseKeySort+  ka <- load "key_agg_vectors.json" parseKeyAgg+  ng <- load "nonce_gen_vectors.json" parseNonceGen+  na <- load "nonce_agg_vectors.json" parseNonceAgg+  sv <- load "sign_verify_vectors.json" parseSignVerify+  tw <- load "tweak_vectors.json" parseTweak+  sa <- load "sig_agg_vectors.json" parseSigAgg+  ds <- load "det_sign_vectors.json" parseDetSign+  pure (Vectors <$> ks <*> ka <*> ng <*> na <*> sv <*> tw <*> sa <*> ds)+ where+  load :: FilePath -> (Value -> Parser a) -> IO (Either String a)+  load file p = do+    let path = dir </> file+    decoded <- eitherDecodeFileStrict path+    pure $ case decoded of+      Left err -> Left (path <> ": invalid JSON: " <> err)+      Right v -> either (\err -> Left (path <> ": " <> err)) Right (parseEither p v)++-- JSON parsing ---------------------------------------------------------------++-- | Hex-encoded bytes.+newtype Hex = Hex {unHex :: ByteString}++instance FromJSON Hex where+  parseJSON v = do+    s <- parseJSON v+    either (fail . (("invalid hex " <> show s <> ": ") <>)) (pure . Hex) (B16.decode (BC.pack s))++-- | Like 'withObject', but fails on fields outside the allowed list.+strictObject :: String -> [Key.Key] -> (Object -> Parser a) -> Value -> Parser a+strictObject what allowed k = withObject what $ \o -> do+  let unknown = filter (`notElem` allowed) (KeyMap.keys o)+  unless (null unknown) $ fail (what <> ": unexpected fields " <> show (map Key.toString unknown))+  k o++hexField :: Object -> Key.Key -> Parser ByteString+hexField o k = unHex <$> o .: k++-- | A field that must be present but may be @null@.+nullableHexField :: Object -> Key.Key -> Parser (Maybe ByteString)+nullableHexField o k = fmap unHex <$> o .: k++hexList :: Object -> Key.Key -> Parser [ByteString]+hexList o k = map unHex <$> o .: k++-- | The @error@ object of an error test case.+data VectorError+  = -- | @invalid_contribution@: blamed signer (if any) and contribution kind.+    InvalidContribution (Maybe Int) String+  | -- | @value@: the reference implementation's @ValueError@ message.+    ValueError String+  deriving (Eq, Show)++instance FromJSON VectorError where+  parseJSON v = do+    ty <- withObject "error" (.: "type") v+    case ty :: String of+      "invalid_contribution" ->+        strictObject "invalid_contribution error" ["type", "signer", "contrib"] (\o -> InvalidContribution <$> o .: "signer" <*> o .: "contrib") v+      "value" ->+        strictObject "value error" ["type", "message"] (\o -> ValueError <$> o .: "message") v+      other -> fail ("unknown error type " <> show other)++data KeySortVectors = KeySortVectors [ByteString] [ByteString]++parseKeySort :: Value -> Parser KeySortVectors+parseKeySort = strictObject "key_sort" ["pubkeys", "sorted_pubkeys"] $ \o ->+  KeySortVectors <$> hexList o "pubkeys" <*> hexList o "sorted_pubkeys"++data KeyAggVectors = KeyAggVectors+  { kaPubkeys :: [ByteString]+  , kaTweaks :: [ByteString]+  , kaValid :: [([Int], ByteString)]+  , kaError :: [KeyAggError]+  }++data KeyAggError = KeyAggError+  { kaeKeys :: [Int]+  , kaeTweaks :: [Int]+  , kaeXOnly :: [Bool]+  , kaeError :: VectorError+  , kaeComment :: Maybe String+  }++parseKeyAgg :: Value -> Parser KeyAggVectors+parseKeyAgg = strictObject "key_agg" ["pubkeys", "tweaks", "valid_test_cases", "error_test_cases"] $ \o -> do+  pubkeys <- hexList o "pubkeys"+  tweaks <- hexList o "tweaks"+  valid <- o .: "valid_test_cases" >>= traverse (strictObject "key_agg valid case" ["key_indices", "expected"] (\c -> (,) <$> c .: "key_indices" <*> hexField c "expected"))+  errs <-+    o .: "error_test_cases"+      >>= traverse+        ( strictObject "key_agg error case" ["key_indices", "tweak_indices", "is_xonly", "error", "comment"] $ \c ->+            KeyAggError <$> c .: "key_indices" <*> c .: "tweak_indices" <*> c .: "is_xonly" <*> c .: "error" <*> c .:? "comment"+        )+  pure (KeyAggVectors pubkeys tweaks valid errs)++data NonceGenCase = NonceGenCase+  { ngRand :: ByteString+  , ngSk :: Maybe ByteString+  , ngPk :: ByteString+  , ngAggpk :: Maybe ByteString+  , ngMsg :: Maybe ByteString+  , ngExtraIn :: Maybe ByteString+  , ngSecnonce :: ByteString+  , ngPubnonce :: ByteString+  , ngComment :: Maybe String+  }++parseNonceGen :: Value -> Parser [NonceGenCase]+parseNonceGen = strictObject "nonce_gen" ["test_cases"] $ \o ->+  o .: "test_cases"+    >>= traverse+      ( strictObject "nonce_gen case" ["rand_", "sk", "pk", "aggpk", "msg", "extra_in", "expected_secnonce", "expected_pubnonce", "comment"] $ \c ->+          NonceGenCase+            <$> hexField c "rand_"+            <*> nullableHexField c "sk"+            <*> hexField c "pk"+            <*> nullableHexField c "aggpk"+            <*> nullableHexField c "msg"+            <*> nullableHexField c "extra_in"+            <*> hexField c "expected_secnonce"+            <*> hexField c "expected_pubnonce"+            <*> c .:? "comment"+      )++data NonceAggVectors = NonceAggVectors+  { naPnonces :: [ByteString]+  , naValid :: [([Int], ByteString, Maybe String)]+  , naError :: [([Int], VectorError, Maybe String)]+  }++parseNonceAgg :: Value -> Parser NonceAggVectors+parseNonceAgg = strictObject "nonce_agg" ["pnonces", "valid_test_cases", "error_test_cases"] $ \o -> do+  pnonces <- hexList o "pnonces"+  valid <-+    o .: "valid_test_cases"+      >>= traverse (strictObject "nonce_agg valid case" ["pnonce_indices", "expected", "comment"] (\c -> (,,) <$> c .: "pnonce_indices" <*> hexField c "expected" <*> c .:? "comment"))+  errs <-+    o .: "error_test_cases"+      >>= traverse (strictObject "nonce_agg error case" ["pnonce_indices", "error", "comment"] (\c -> (,,) <$> c .: "pnonce_indices" <*> c .: "error" <*> c .:? "comment"))+  pure (NonceAggVectors pnonces valid errs)++data SignVerifyVectors = SignVerifyVectors+  { svSk :: ByteString+  , svPubkeys :: [ByteString]+  , svSecnonces :: [ByteString]+  , svPnonces :: [ByteString]+  , svAggnonces :: [ByteString]+  , svMsgs :: [ByteString]+  , svValid :: [SvValid]+  , svSignError :: [SvSignError]+  , svVerifyFail :: [SvVerify]+  , svVerifyError :: [SvVerify]+  }++data SvValid = SvValid+  { svvKeys :: [Int]+  , svvNonces :: [Int]+  , svvAggnonce :: Int+  , svvMsg :: Int+  , svvSigner :: Int+  , svvExpected :: ByteString+  , svvComment :: Maybe String+  }++data SvSignError = SvSignError+  { sseKeys :: [Int]+  , sseAggnonce :: Int+  , sseMsg :: Int+  , sseSecnonce :: Int+  , sseError :: VectorError+  , sseComment :: Maybe String+  }++-- | A @verify_fail@ case (no error) or a @verify_error@ case.+data SvVerify = SvVerify+  { svfSig :: ByteString+  , svfKeys :: [Int]+  , svfNonces :: [Int]+  , svfMsg :: Int+  , svfSigner :: Int+  , svfError :: Maybe VectorError+  , svfComment :: Maybe String+  }++parseSignVerify :: Value -> Parser SignVerifyVectors+parseSignVerify =+  strictObject+    "sign_verify"+    ["sk", "pubkeys", "secnonces", "pnonces", "aggnonces", "msgs", "valid_test_cases", "sign_error_test_cases", "verify_fail_test_cases", "verify_error_test_cases"]+    $ \o ->+      SignVerifyVectors+        <$> hexField o "sk"+        <*> hexList o "pubkeys"+        <*> hexList o "secnonces"+        <*> hexList o "pnonces"+        <*> hexList o "aggnonces"+        <*> hexList o "msgs"+        <*> (o .: "valid_test_cases" >>= traverse validCase)+        <*> (o .: "sign_error_test_cases" >>= traverse signErrorCase)+        <*> (o .: "verify_fail_test_cases" >>= traverse (verifyCase False))+        <*> (o .: "verify_error_test_cases" >>= traverse (verifyCase True))+ where+  validCase = strictObject "sign_verify valid case" ["key_indices", "nonce_indices", "aggnonce_index", "msg_index", "signer_index", "expected", "comment"] $ \c ->+    SvValid <$> c .: "key_indices" <*> c .: "nonce_indices" <*> c .: "aggnonce_index" <*> c .: "msg_index" <*> c .: "signer_index" <*> hexField c "expected" <*> c .:? "comment"+  -- signer_index is informational here: Sign does not take it.+  signErrorCase = strictObject "sign_verify sign_error case" ["key_indices", "aggnonce_index", "msg_index", "secnonce_index", "signer_index", "error", "comment"] $ \c ->+    SvSignError <$> c .: "key_indices" <*> c .: "aggnonce_index" <*> c .: "msg_index" <*> c .: "secnonce_index" <*> c .: "error" <*> c .:? "comment"+  verifyCase hasError =+    strictObject "sign_verify verify case" (["sig", "key_indices", "nonce_indices", "msg_index", "signer_index", "comment"] <> ["error" | hasError]) $ \c ->+      SvVerify+        <$> hexField c "sig"+        <*> c .: "key_indices"+        <*> c .: "nonce_indices"+        <*> c .: "msg_index"+        <*> c .: "signer_index"+        <*> (if hasError then Just <$> c .: "error" else pure Nothing)+        <*> c .:? "comment"++data TweakVectors = TweakVectors+  { twSk :: ByteString+  , twPubkeys :: [ByteString]+  , twSecnonce :: ByteString+  , twPnonces :: [ByteString]+  , twAggnonce :: ByteString+  , twTweaks :: [ByteString]+  , twMsg :: ByteString+  , twValid :: [TweakCase]+  , twError :: [TweakCase]+  }++-- | A tweak case: valid cases carry @expected@, error cases carry @error@.+data TweakCase = TweakCase+  { tcKeys :: [Int]+  , tcNonces :: [Int]+  , tcTweaks :: [Int]+  , tcXOnly :: [Bool]+  , tcSigner :: Int+  , tcOutcome :: Either VectorError ByteString+  , tcComment :: Maybe String+  }++parseTweak :: Value -> Parser TweakVectors+parseTweak = strictObject "tweak" ["sk", "pubkeys", "secnonce", "pnonces", "aggnonce", "tweaks", "msg", "valid_test_cases", "error_test_cases"] $ \o ->+  TweakVectors+    <$> hexField o "sk"+    <*> hexList o "pubkeys"+    <*> hexField o "secnonce"+    <*> hexList o "pnonces"+    <*> hexField o "aggnonce"+    <*> hexList o "tweaks"+    <*> hexField o "msg"+    <*> (o .: "valid_test_cases" >>= traverse (tweakCase False))+    <*> (o .: "error_test_cases" >>= traverse (tweakCase True))+ where+  tweakCase isError =+    strictObject "tweak case" ["key_indices", "nonce_indices", "tweak_indices", "is_xonly", "signer_index", if isError then "error" else "expected", "comment"] $ \c ->+      TweakCase+        <$> c .: "key_indices"+        <*> c .: "nonce_indices"+        <*> c .: "tweak_indices"+        <*> c .: "is_xonly"+        <*> c .: "signer_index"+        <*> (if isError then Left <$> c .: "error" else Right <$> hexField c "expected")+        <*> c .:? "comment"++data SigAggVectors = SigAggVectors+  { saPubkeys :: [ByteString]+  , saPnonces :: [ByteString]+  , saTweaks :: [ByteString]+  , saPsigs :: [ByteString]+  , saMsg :: ByteString+  , saValid :: [SigAggCase]+  , saError :: [SigAggCase]+  }++-- | A sig_agg case: valid cases carry @expected@, error cases carry @error@.+data SigAggCase = SigAggCase+  { sacAggnonce :: ByteString+  , sacNonces :: [Int]+  , sacKeys :: [Int]+  , sacTweaks :: [Int]+  , sacXOnly :: [Bool]+  , sacPsigs :: [Int]+  , sacOutcome :: Either VectorError ByteString+  , sacComment :: Maybe String+  }++parseSigAgg :: Value -> Parser SigAggVectors+parseSigAgg = strictObject "sig_agg" ["pubkeys", "pnonces", "tweaks", "psigs", "msg", "valid_test_cases", "error_test_cases"] $ \o ->+  SigAggVectors+    <$> hexList o "pubkeys"+    <*> hexList o "pnonces"+    <*> hexList o "tweaks"+    <*> hexList o "psigs"+    <*> hexField o "msg"+    <*> (o .: "valid_test_cases" >>= traverse (sigAggCase False))+    <*> (o .: "error_test_cases" >>= traverse (sigAggCase True))+ where+  sigAggCase isError =+    strictObject "sig_agg case" ["aggnonce", "nonce_indices", "key_indices", "tweak_indices", "is_xonly", "psig_indices", if isError then "error" else "expected", "comment"] $ \c ->+      SigAggCase+        <$> hexField c "aggnonce"+        <*> c .: "nonce_indices"+        <*> c .: "key_indices"+        <*> c .: "tweak_indices"+        <*> c .: "is_xonly"+        <*> c .: "psig_indices"+        <*> (if isError then Left <$> c .: "error" else Right <$> hexField c "expected")+        <*> c .:? "comment"++-- | Only the case comments are read: DeterministicSign is not implemented.+data DetSignVectors = DetSignVectors [Maybe String] [Maybe String]++parseDetSign :: Value -> Parser DetSignVectors+parseDetSign = strictObject "det_sign" ["sk", "pubkeys", "msgs", "valid_test_cases", "error_test_cases"] $ \o ->+  DetSignVectors+    <$> (o .: "valid_test_cases" >>= traverse comment)+    <*> (o .: "error_test_cases" >>= traverse comment)+ where+  comment = withObject "det_sign case" (.:? "comment")++-- BIP-0327 encodings ------------------------------------------------------------++-- | @cpoint@: a 33-byte compressed point (@parse_point@ checks the 0x02/0x03 tag, @x < p@ and that @x@ is on the curve).+cpoint :: ByteString -> Maybe Pub+cpoint b+  | BS.length b == 33 = parse_point b+  | otherwise = Nothing++-- | @cpoint_ext@: like 'cpoint', but 33 zero bytes is the point at infinity.+cpointExt :: ByteString -> Maybe Pub+cpointExt b+  | b == BS.replicate 33 0 = Just _CURVE_ZERO+  | otherwise = cpoint b++-- | A 66-byte public nonce.+pubNonceFromBytes :: ByteString -> Maybe PubNonce+pubNonceFromBytes b+  | BS.length b == 66 = PubNonce <$> cpoint (BS.take 33 b) <*> cpoint (BS.drop 33 b)+  | otherwise = Nothing++-- | A 66-byte aggregate nonce (either half may be infinity).+aggNonceFromBytes :: ByteString -> Maybe PubNonce+aggNonceFromBytes b+  | BS.length b == 66 = PubNonce <$> cpointExt (BS.take 33 b) <*> cpointExt (BS.drop 33 b)+  | otherwise = Nothing++-- | Serializes an aggregate nonce with @cbytes_ext@.+aggNonceBytes :: PubNonce -> ByteString+aggNonceBytes (PubNonce a b) = cbytesExt a <> cbytesExt b++pubNonceBytes :: PubNonce -> ByteString+pubNonceBytes (PubNonce a b) = serialize_point a <> serialize_point b++-- | A 97-byte secret nonce: @k1 || k2 || pk@.+secNonceFromBytes :: ByteString -> Either String (Integer, Integer, Pub)+secNonceFromBytes b+  | BS.length b /= 97 = Left ("secnonce must be 97 bytes, got " <> show (BS.length b))+  | otherwise = do+      pk <- note "secnonce public key does not parse" (cpoint (BS.drop 64 b))+      Right (bytesToInteger (BS.take 32 b), bytesToInteger (BS.take 32 (BS.drop 32 b)), pk)++secNonceBytes :: SecNonce -> ByteString+secNonceBytes sn =+  let (k1, k2) = secNonceScalars sn+   in integerToBytes32 k1 <> integerToBytes32 k2 <> serialize_point (secNoncePubKeyInternal sn)++-- Helpers --------------------------------------------------------------------++note :: String -> Maybe a -> Either String a+note err = maybe (Left err) Right++-- | Indexes into a vector table.+at :: String -> [a] -> Int -> Either String a+at what xs i = case drop i xs of+  x : _ | i >= 0 -> Right x+  _ -> Left (what <> " index " <> show i <> " is out of range")++-- | Selects and decodes entries of a vector table.+select :: String -> (ByteString -> Maybe a) -> [ByteString] -> [Int] -> Either String [a]+select what decode table = traverse $ \i -> at what table i >>= note (what <> " #" <> show i <> " does not parse") . decode++mkTweaks :: [ByteString] -> [Int] -> [Bool] -> Either String [Tweak]+mkTweaks table idxs xonly+  | length idxs /= length xonly = Left "tweak_indices and is_xonly differ in length"+  | otherwise = zipWithM (\i x -> (if x then XOnlyTweak else PlainTweak) . bytesToInteger <$> at "tweak" table i) idxs xonly++tweakValue :: Tweak -> Integer+tweakValue (XOnlyTweak t) = t+tweakValue (PlainTweak t) = t++-- | Aggregate key after applying the tweaks in order (KeyAgg + ApplyTweak).+tweakedKeyAgg :: [Pub] -> [Tweak] -> Either MuSig2Error Pub+tweakedKeyAgg pks tweaks = aggregatedPubkey <$> (mkKeyAggContext pks Nothing >>= \ctx -> foldM applyTweak ctx tweaks)++{- | The library error that corresponds to a reference-implementation+@ValueError@ message. Unknown messages fail, so new error cases need an+explicit mapping.+-}+libraryErrorFor :: [Tweak] -> Maybe (Integer, Integer) -> String -> Either String MuSig2Error+libraryErrorFor tweaks secnonce message = case message of+  "The tweak must be less than n." -> case filter (>= curveOrder) (map tweakValue tweaks) of+    t : _ -> Right (TweakOutOfRange t)+    [] -> Left "vector expects an out-of-range tweak but none is >= n"+  "The result of tweaking cannot be infinity." -> Right TweakResultAtInfinity+  "The signer's pubkey must be included in the list of pubkeys." -> Right SecretKeyPublicKeyMismatch+  "first secnonce value is out of range." -> scalarError "k1" . fst <$> note "no secnonce in case" secnonce+  "second secnonce value is out of range." -> scalarError "k2" . snd <$> note "no secnonce in case" secnonce+  other -> Left ("no library error is mapped to the BIP-0327 error message " <> show other)+ where+  scalarError label k = if k == 0 then SecretScalarZero label else SecretScalarOutOfRange label++{- | A contribution as the reference implementation checks it: kind, signer+index (if any), and whether its encoding is valid.+-}+type Contribution = (String, Maybe Int, Bool)++contributions :: String -> (ByteString -> Bool) -> [ByteString] -> [Int] -> Either String [Contribution]+contributions kind valid table = zipWithM (\signer i -> (\b -> (kind, Just signer, valid b)) <$> at kind table i) [0 ..]++-- | The contribution the reference implementation would blame first.+firstInvalid :: [Contribution] -> Maybe (String, Maybe Int)+firstInvalid cs = case [(kind, signer) | (kind, signer, False) <- cs] of+  c : _ -> Just c+  [] -> Nothing++{- | For an @invalid_contribution@ that needs byte-level parsing: checks that+@parse_point@ rejects exactly the blamed contribution and nothing before it.+-}+encodingCase :: String -> VectorError -> Either String [Contribution] -> TestTree+encodingCase name err cs =+  testCase (name <> " [only the encoding is checked; the library has no byte-level parser that blames signers]") $ do+    contribs <- expectRight "vector" cs+    case err of+      InvalidContribution signer kind -> firstInvalid contribs @?= Just (kind, signer)+      ValueError m -> assertFailure ("expected an invalid_contribution error, got value error " <> show m)++-- | Whether an error is an invalid encoding of a point-based contribution.+isEncodingError :: VectorError -> Bool+isEncodingError (InvalidContribution _ kind) = kind `elem` ["pubkey", "pubnonce", "aggnonce", "aggothernonce"]+isEncodingError (ValueError _) = False++expectRight :: String -> Either String a -> IO a+expectRight ctx = either (\err -> assertFailure (ctx <> ": " <> err)) pure++expectLib :: (Show e) => String -> Either e a -> IO a+expectLib ctx = either (\err -> assertFailure (ctx <> " failed: " <> show err)) pure++expectLibError :: MuSig2Error -> Either MuSig2Error a -> Assertion+expectLibError expected result = case result of+  Left err -> err @?= expected+  Right _ -> assertFailure ("expected Left " <> show expected <> ", got Right")++hexOf :: ByteString -> String+hexOf = map toUpper . BC.unpack . B16.encode++assertBytes :: String -> ByteString -> ByteString -> Assertion+assertBytes what expected actual = assertEqual what (hexOf expected) (hexOf actual)++-- | Test name for a vector case: file section, index and the first sentence of its comment.+caseName :: String -> Int -> Maybe String -> String+caseName section i comment = section <> " #" <> show i <> maybe "" (\c -> " (" <> firstSentence c <> ")") comment+ where+  firstSentence s = case s of+    '.' : ' ' : _ -> ""+    "." -> ""+    ch : rest -> ch : firstSentence rest+    [] -> []++-- Tests ----------------------------------------------------------------------++-- | Tests run against the library, and cases listed as not expressible.+data FileTests = FileTests [TestTree] [TestTree]++vectorTests :: Vectors -> [TestTree]+vectorTests vs =+  [testGroup file run | (file, FileTests run _) <- files, not (null run)]+    <> [testGroup "Not expressible through the public API" [testGroup file skipped | (file, FileTests _ skipped) <- files, not (null skipped)]]+ where+  files =+    [ ("key_sort_vectors.json", keySortTests (vKeySort vs))+    , ("key_agg_vectors.json", keyAggTests (vKeyAgg vs))+    , ("nonce_gen_vectors.json", nonceGenTests (vNonceGen vs))+    , ("nonce_agg_vectors.json", nonceAggTests (vNonceAgg vs))+    , ("sign_verify_vectors.json", signVerifyTests (vSignVerify vs))+    , ("tweak_vectors.json", tweakTests (vTweak vs))+    , ("sig_agg_vectors.json", sigAggTests (vSigAgg vs))+    , ("det_sign_vectors.json", detSignTests (vDetSign vs))+    ]++keySortTests :: KeySortVectors -> FileTests+keySortTests (KeySortVectors pubkeys sorted) =+  FileTests+    [ testCase "KeySort" $ do+        pks <- expectRight "pubkeys" (select "pubkey" cpoint pubkeys [0 .. length pubkeys - 1])+        map (hexOf . serialize_point) (toList (sortPublicKeys pks)) @?= map hexOf sorted+    ]+    []++keyAggTests :: KeyAggVectors -> FileTests+keyAggTests v =+  FileTests (zipWith validCase [0 ..] (kaValid v) <> rights errs) (lefts errs)+ where+  errs = zipWith errorCase [0 ..] (kaError v)+  validCase i (keyIdxs, expected) = testCase (caseName "valid" i Nothing) $ do+    pks <- expectRight "vector" (select "pubkey" cpoint (kaPubkeys v) keyIdxs)+    ctx <- expectLib "KeyAgg" (mkKeyAggContext pks Nothing)+    assertBytes "xbytes(Q)" expected (xBytes (aggregatedPubkey ctx))+  errorCase i c+    | isEncodingError (kaeError c) =+        Left $ encodingCase name (kaeError c) (contributions "pubkey" (isJust . cpoint) (kaPubkeys v) (kaeKeys c))+    | otherwise = Right . testCase name $ do+        pks <- expectRight "vector" (select "pubkey" cpoint (kaPubkeys v) (kaeKeys c))+        tweaks <- expectRight "vector" (mkTweaks (kaTweaks v) (kaeTweaks c) (kaeXOnly c))+        expected <- expectRight "vector" (valueError (kaeError c) >>= libraryErrorFor tweaks Nothing)+        expectLibError expected (tweakedKeyAgg pks tweaks)+   where+    name = caseName "error" i (kaeComment c)++valueError :: VectorError -> Either String String+valueError (ValueError m) = Right m+valueError err = Left ("expected a value error, got " <> show err)++nonceGenTests :: [NonceGenCase] -> FileTests+nonceGenTests cases = FileTests (zipWith run [0 ..] cases) []+ where+  run :: Int -> NonceGenCase -> TestTree+  run i c = testCase (caseName "case" i (ngComment c)) $ do+    pk <- expectRight "pk" (note "does not parse" (cpoint (ngPk c)))+    -- The library takes aggpk as a point; the vector gives 32-byte x-only bytes,+    -- so lift it (even y) and check that it serializes back to the same bytes.+    aggpk <- traverse (\b -> expectRight "aggpk" (note "is not a valid x-only key" (if BS.length b == 32 then parse_point b else Nothing))) (ngAggpk c)+    assertEqual "aggpk round-trips through the x-only lift" (fmap hexOf (ngAggpk c)) (fmap (hexOf . xBytes) aggpk)+    let params =+          SecNonceGenParams+            { _pk = pk+            , _sk = SecKey . bytesToInteger <$> ngSk c+            , _aggpk = aggpk+            , _msg = ngMsg c+            , _extraIn = ngExtraIn c+            }+    sn <- expectLib "NonceGen" (secNonceGenWithRand (ngRand c) params)+    assertBytes "secnonce" (ngSecnonce c) (secNonceBytes sn)+    pn <- expectLib "publicNonce" (publicNonce sn)+    assertBytes "pubnonce" (ngPubnonce c) (pubNonceBytes pn)++nonceAggTests :: NonceAggVectors -> FileTests+nonceAggTests v = FileTests (zipWith validCase [0 ..] (naValid v) <> rights errs) (lefts errs)+ where+  errs = zipWith errorCase [0 ..] (naError v)+  validCase i (idxs, expected, comment) = testCase (caseName "valid" i comment) $ do+    nonces <- expectRight "vector" (select "pnonce" pubNonceFromBytes (naPnonces v) idxs)+    agg <- expectLib "NonceAgg" (aggNonces nonces)+    assertBytes "aggnonce (infinity as 33 zero bytes)" expected (aggNonceBytes agg)+  errorCase i (idxs, err, comment)+    | isEncodingError err = Left (encodingCase name err (contributions "pubnonce" (isJust . pubNonceFromBytes) (naPnonces v) idxs))+    | otherwise = Right (testCase name (assertFailure ("no library check for " <> show err)))+   where+    name = caseName "error" i comment++signVerifyTests :: SignVerifyVectors -> FileTests+signVerifyTests v =+  FileTests+    ( [ testCase "setup: sk derives pubkeys[0]" $ do+          p <- expectRight "derive_pub" (note "failed" (derive_pub (fromInteger (bytesToInteger (svSk v)))))+          pk0 <- expectRight "vector" (at "pubkey" (svPubkeys v) 0)+          assertBytes "pubkey" pk0 (serialize_point p)+      , testCase "setup: secnonces[0] has public nonce pnonces[0]" $ do+          sn <- secnonce 0+          pn <- expectLib "publicNonce" (publicNonce sn)+          pn0 <- expectRight "vector" (at "pnonce" (svPnonces v) 0)+          assertBytes "pubnonce" pn0 (pubNonceBytes pn)+      ]+        <> zipWith validCase [0 ..] (svValid v)+        <> rights signErrs+        <> zipWith verifyFailCase [0 ..] (svVerifyFail v)+        <> rights verifyErrs+    )+    (lefts signErrs <> lefts verifyErrs)+ where+  sk = SecKey (bytesToInteger (svSk v))+  secnonce j = do+    (k1, k2, pk) <- expectRight "vector" (at "secnonce" (svSecnonces v) j >>= secNonceFromBytes)+    expectLib "mkSecNonce" (mkSecNonce pk k1 k2)+  signErrs = zipWith signErrorCase [0 ..] (svSignError v)+  verifyErrs = zipWith verifyErrorCase [0 ..] (svVerifyError v)++  validCase i c =+    testGroup+      (caseName "valid" i (svvComment c))+      [ testCase "NonceAgg reproduces the aggnonce" $ do+          (_, nonces, _, _) <- inputs+          expected <- expectRight "vector" (at "aggnonce" (svAggnonces v) (svvAggnonce c))+          agg <- expectLib "NonceAgg" (aggNonces nonces)+          assertBytes "aggnonce" expected (aggNonceBytes agg)+      , testCase "Sign returns the expected partial signature" $ do+          (pks, _, agg, m) <- inputs+          sn <- secnonce 0+          psig <- expectLib "Sign" (mkSessionContext agg pks [] m >>= sign sn sk)+          assertBytes "psig" (svvExpected c) (integerToBytes32 psig)+      , testCase "PartialSigVerify accepts the expected partial signature" $ do+          (pks, nonces, _, m) <- inputs+          partialSigVerify (bytesToInteger (svvExpected c)) nonces pks [] m (svvSigner c) @?= Right True+      ]+   where+    inputs = expectRight "vector" $ do+      pks <- select "pubkey" cpoint (svPubkeys v) (svvKeys c)+      nonces <- select "pnonce" pubNonceFromBytes (svPnonces v) (svvNonces c)+      agg <- at "aggnonce" (svAggnonces v) (svvAggnonce c) >>= note "aggnonce does not parse" . aggNonceFromBytes+      m <- at "msg" (svMsgs v) (svvMsg c)+      pure (pks, nonces, agg, m)++  signErrorCase i c+    | isEncodingError (sseError c) =+        Left . encodingCase name (sseError c) $ do+          keys <- contributions "pubkey" (isJust . cpoint) (svPubkeys v) (sseKeys c)+          agg <- at "aggnonce" (svAggnonces v) (sseAggnonce c)+          -- Sign parses the pubkeys (KeyAgg) before the aggregate nonce.+          pure (keys <> [("aggnonce", Nothing, isJust (aggNonceFromBytes agg))])+    | otherwise = Right . testCase name $ do+        (pks, agg, m, (k1, k2, snPk)) <- expectRight "vector" $ do+          pks <- select "pubkey" cpoint (svPubkeys v) (sseKeys c)+          agg <- at "aggnonce" (svAggnonces v) (sseAggnonce c) >>= note "aggnonce does not parse" . aggNonceFromBytes+          m <- at "msg" (svMsgs v) (sseMsg c)+          sn <- at "secnonce" (svSecnonces v) (sseSecnonce c) >>= secNonceFromBytes+          pure (pks, agg, m, sn)+        expected <- expectRight "vector" (valueError (sseError c) >>= libraryErrorFor [] (Just (k1, k2)))+        expectLibError expected $ do+          sn <- mkSecNonce snPk k1 k2+          mkSessionContext agg pks [] m >>= sign sn sk+   where+    name = caseName "sign_error" i (sseComment c)++  verifyInputs c = expectRight "vector" $ do+    pks <- select "pubkey" cpoint (svPubkeys v) (svfKeys c)+    nonces <- select "pnonce" pubNonceFromBytes (svPnonces v) (svfNonces c)+    m <- at "msg" (svMsgs v) (svfMsg c)+    pure (pks, nonces, m)++  verifyFailCase i c = testCase (caseName "verify_fail" i (svfComment c) <> outOfRangeNote) $ do+    (pks, nonces, m) <- verifyInputs c+    partialSigVerify s nonces pks [] m (svfSigner c) @?= expected+   where+    s = bytesToInteger (svfSig c)+    -- The reference implementation returns False for s >= n; the library+    -- reports that input as an error instead.+    (expected, outOfRangeNote)+      | s >= curveOrder = (Left PartialSignatureOutOfRange, " [library: Left PartialSignatureOutOfRange]")+      | otherwise = (Right False, "")++  verifyErrorCase i c = case svfError c of+    Just err+      | isEncodingError err ->+          Left . encodingCase name err $ do+            -- PartialSigVerify aggregates (parses) the pubnonces before the pubkeys.+            nonces <- contributions "pubnonce" (isJust . pubNonceFromBytes) (svPnonces v) (svfNonces c)+            keys <- contributions "pubkey" (isJust . cpoint) (svPubkeys v) (svfKeys c)+            pure (nonces <> keys)+    other -> Right (testCase name (assertFailure ("no library check for " <> show other)))+   where+    name = caseName "verify_error" i (svfComment c)++tweakTests :: TweakVectors -> FileTests+tweakTests v = FileTests (zipWith (run "valid") [0 ..] (twValid v) <> zipWith (run "error") [0 ..] (twError v)) []+ where+  sk = SecKey (bytesToInteger (twSk v))+  run section i c = case tcOutcome c of+    Right expected ->+      testGroup+        name+        [ testCase "NonceAgg reproduces the aggnonce" $ do+            (_, nonces, _, _) <- inputs+            agg <- expectLib "NonceAgg" (aggNonces nonces)+            assertBytes "aggnonce" (twAggnonce v) (aggNonceBytes agg)+        , testCase "Sign returns the expected partial signature" $ do+            (pks, _, tweaks, agg) <- inputs+            sn <- secnonce+            psig <- expectLib "Sign" (mkSessionContext agg pks tweaks (twMsg v) >>= sign sn sk)+            assertBytes "psig" expected (integerToBytes32 psig)+        , testCase "PartialSigVerify accepts the expected partial signature" $ do+            (pks, nonces, tweaks, _) <- inputs+            partialSigVerify (bytesToInteger expected) nonces pks tweaks (twMsg v) (tcSigner c) @?= Right True+        ]+    Left err -> testCase name $ do+      (pks, _, tweaks, agg) <- inputs+      sn <- secnonce+      libErr <- expectRight "vector" (valueError err >>= libraryErrorFor tweaks Nothing)+      expectLibError libErr (mkSessionContext agg pks tweaks (twMsg v) >>= sign sn sk)+   where+    name = caseName section i (tcComment c)+    inputs = expectRight "vector" $ do+      pks <- select "pubkey" cpoint (twPubkeys v) (tcKeys c)+      nonces <- select "pnonce" pubNonceFromBytes (twPnonces v) (tcNonces c)+      tweaks <- mkTweaks (twTweaks v) (tcTweaks c) (tcXOnly c)+      agg <- note "aggnonce does not parse" (aggNonceFromBytes (twAggnonce v))+      pure (pks, nonces, tweaks, agg)+  secnonce = do+    (k1, k2, pk) <- expectRight "vector" (secNonceFromBytes (twSecnonce v))+    expectLib "mkSecNonce" (mkSecNonce pk k1 k2)++sigAggTests :: SigAggVectors -> FileTests+sigAggTests v = FileTests (zipWith (run "valid") [0 ..] (saValid v) <> zipWith (run "error") [0 ..] (saError v)) []+ where+  run :: String -> Int -> SigAggCase -> TestTree+  run section i c = case sacOutcome c of+    Right expected ->+      testGroup+        name+        [ testCase "NonceAgg reproduces the aggnonce" $ do+            (_, nonces, _, _, _) <- inputs+            agg <- expectLib "NonceAgg" (aggNonces nonces)+            assertBytes "aggnonce" (sacAggnonce c) (aggNonceBytes agg)+        , testCase "PartialSigAgg returns the expected signature" $ do+            sig <- aggregate+            assertBytes "sig" expected sig+        , testCase "the expected signature is a valid BIP-340 signature for the tweaked key" $ do+            (pks, _, tweaks, _, _) <- inputs+            q <- expectLib "KeyAgg" (tweakedKeyAgg pks tweaks)+            assertBool "verify_schnorr" (verify_schnorr (saMsg v) q expected)+        ]+    Left (InvalidContribution (Just signer) "psig") -> testCase (name <> " [library cannot report the signer index]") $ do+      (pks, _, tweaks, agg, psigs) <- inputs+      blamed <- expectRight "vector" (at "psig" psigs signer)+      assertBool "the blamed psig is out of range" (blamed >= curveOrder)+      expectLibError PartialSignatureOutOfRange (mkSessionContext agg pks tweaks (saMsg v) >>= aggPartials psigs)+    Left err -> testCase name (assertFailure ("no library check for " <> show err))+   where+    name = caseName section i (sacComment c)+    inputs = expectRight "vector" $ do+      pks <- select "pubkey" cpoint (saPubkeys v) (sacKeys c)+      nonces <- select "pnonce" pubNonceFromBytes (saPnonces v) (sacNonces c)+      tweaks <- mkTweaks (saTweaks v) (sacTweaks c) (sacXOnly c)+      agg <- note "aggnonce does not parse" (aggNonceFromBytes (sacAggnonce c))+      psigs <- traverse (fmap bytesToInteger . at "psig" (saPsigs v)) (sacPsigs c)+      pure (pks, nonces, tweaks, agg, psigs)+    aggregate = do+      (pks, _, tweaks, agg, psigs) <- inputs+      expectLib "PartialSigAgg" (mkSessionContext agg pks tweaks (saMsg v) >>= aggPartials psigs)++detSignTests :: DetSignVectors -> FileTests+detSignTests (DetSignVectors valid errs) =+  FileTests [] (zipWith (skip "valid") [0 ..] valid <> zipWith (skip "error") [0 ..] errs)+ where+  skip section i comment = testCase (caseName section i comment <> " [SKIPPED: DeterministicSign is not implemented]") (pure ())
test/SignVerify.hs view
@@ -1,96 +1,17 @@ {-# LANGUAGE OverloadedStrings #-} +{- | Input validation in 'mkSecNonce', 'mkSessionContext' and 'sign'.++The BIP-0327 @sign_verify_vectors.json@ cases run in "OfficialVectors".+-} module SignVerify (testSignVerify) where -import Crypto.Curve.Secp256k1 (Pub, derive_pub, _CURVE_ZERO)-import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), PubNonce, SecKey (..), aggNonces, mkSecNonce, mkSessionContext, partialSigVerify, publicNonce, sign)-import Crypto.Curve.Secp256k1.MuSig2.Internal (bytesToInteger)-import Data.ByteString (ByteString)+import Crypto.Curve.Secp256k1 (derive_pub, _CURVE_ZERO)+import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), SecKey (..), aggNonces, mkSecNonce, mkSessionContext, publicNonce, sign) import Data.Maybe (fromMaybe) import Test.Tasty import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parsePubNonce, unsafeMkSecNonce, unsafeRight)---- | Input public keys from BIP-0327 test vectors.-inputPubkeys :: [Pub]-inputPubkeys =-  map-    parsePoint-    [ "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"-    , "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"-    , "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA661"-    , "020000000000000000000000000000000000000000000000000000000000000007"-    ]---- | Public nonces from BIP-0327 test vectors.-inputPubNonces :: [PubNonce]-inputPubNonces =-  map-    parsePubNonce-    [ "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"-    , "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"-    , "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046"-    , "0237C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0387BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"-    , "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"-    ]---- | Messages from BIP-0327 test vectors.-testMessages :: [ByteString]-testMessages =-  [ decodeHex "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF"-  , ""-  , decodeHex "2626262626262626262626262626262626262626262626262626262626262626262626262626"-  ]--validTestVectors :: [([Int], [Int], Int, Int, ByteString)]-validTestVectors =-  [ ([0, 1, 2], [0, 1, 2], 0, 0, decodeHex "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB")-  , ([1, 0, 2], [1, 0, 2], 0, 1, decodeHex "9FF2F7AAA856150CC8819254218D3ADEEB0535269051897724F9DB3789513A52")-  , ([1, 2, 0], [1, 2, 0], 0, 2, decodeHex "FA23C359F6FAC4E7796BB93BC9F0532A95468C539BA20FF86D7C76ED92227900")-  , ([0, 1, 2], [0, 1, 2], 1, 0, decodeHex "D7D63FFD644CCDA4E62BC2BC0B1D02DD32A1DC3030E155195810231D1037D82D")-  , ([0, 1, 2], [0, 1, 2], 2, 0, decodeHex "E184351828DA5094A97C79CABDAAA0BFB87608C32E8829A4DF5340A6F243B78C")-  ]--invalidTestVectors :: [(ByteString, [Int], [Int], Int, Int, String)]-invalidTestVectors =-  [ (decodeHex "FED54434AD4CFE953FC527DC6A5E5BE8F6234907B7C187559557CE87A0541C46", [0, 1, 2], [0, 1, 2], 0, 0, "Wrong signature (negation of valid signature)")-  , (decodeHex "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB", [0, 1, 2], [0, 1, 2], 0, 1, "Wrong signer")-  ]--errorTestVectors :: [(ByteString, [Int], [Int], Int, Int, MuSig2Error)]-errorTestVectors =-  [ (decodeHex "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", [0, 1, 2], [0, 1, 2], 0, 0, PartialSignatureOutOfRange)-  ]--invalidSecNoncePub :: Pub-invalidSecNoncePub = fromMaybe (error "Failed to derive pubkey") $ derive_pub 1--makeValidTestCase :: Int -> ([Int], [Int], Int, Int, ByteString) -> TestTree-makeValidTestCase i (keyIndices, nonceIndices, msgIndex, signerIndex, expectedSig) =-  testCase ("BIP-0327 valid test vector " <> show (i + 1)) $ do-    let selectedKeys = map (inputPubkeys !!) keyIndices-        selectedNonces = map (inputPubNonces !!) nonceIndices-        msg = testMessages !! msgIndex-        signature = bytesToInteger expectedSig-    partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex @?= Right True--makeInvalidTestCase :: Int -> (ByteString, [Int], [Int], Int, Int, String) -> TestTree-makeInvalidTestCase i (sigBytes, keyIndices, nonceIndices, msgIndex, signerIndex, comment) =-  testCase ("BIP-0327 invalid test vector " <> show (i + 1) <> ": " <> comment) $ do-    let selectedKeys = map (inputPubkeys !!) keyIndices-        selectedNonces = map (inputPubNonces !!) nonceIndices-        msg = testMessages !! msgIndex-        signature = bytesToInteger sigBytes-    partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex @?= Right False--makeErrorTestCase :: Int -> (ByteString, [Int], [Int], Int, Int, MuSig2Error) -> TestTree-makeErrorTestCase i (sigBytes, keyIndices, nonceIndices, msgIndex, signerIndex, expectedError) =-  testCase ("BIP-0327 error test vector " <> show (i + 1)) $ do-    let selectedKeys = map (inputPubkeys !!) keyIndices-        selectedNonces = map (inputPubNonces !!) nonceIndices-        msg = testMessages !! msgIndex-        signature = bytesToInteger sigBytes-    partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex @?= Left expectedError+import Util (parsePubNonce, unsafeMkSecNonce, unsafeRight)  testInvalidSecNonce :: TestTree testInvalidSecNonce =@@ -99,18 +20,19 @@       Left (SecretScalarZero "k2") -> pure ()       Left err -> assertFailure $ "Expected SecretScalarZero \"k2\", got: Left " ++ show err       Right _ -> assertFailure "Expected Left, got Right"+ where+  invalidSecNoncePub = fromMaybe (error "Failed to derive pubkey") $ derive_pub 1  testContextRejectsInfinityPubkey :: TestTree testContextRejectsInfinityPubkey =   testCase "mkSessionContext rejects infinity public keys" $ do-    let aggNonce = unsafeRight $ aggNonces [firstPubNonce]+    let aggNonce = unsafeRight $ aggNonces [pubNonce]     case mkSessionContext aggNonce [_CURVE_ZERO] [] "msg" of       Left PublicKeyAtInfinity -> pure ()       _ -> assertFailure "Expected Left PublicKeyAtInfinity"  where-  firstPubNonce = case inputPubNonces of-    nonce : _ -> nonce-    [] -> error "Expected at least one input public nonce"+  -- First public nonce of BIP-0327 @sign_verify_vectors.json@.+  pubNonce = parsePubNonce "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"  testSignerKeyMismatch :: TestTree testSignerKeyMismatch =@@ -123,8 +45,6 @@  testSignVerify :: TestTree testSignVerify =-  testGroup "sign and verify" $-    zipWith makeValidTestCase [0 ..] validTestVectors-      <> zipWith makeInvalidTestCase [0 ..] invalidTestVectors-      <> zipWith makeErrorTestCase [0 ..] errorTestVectors-      <> [testInvalidSecNonce, testContextRejectsInfinityPubkey, testSignerKeyMismatch]+  testGroup+    "sign and verify input validation"+    [testInvalidSecNonce, testContextRejectsInfinityPubkey, testSignerKeyMismatch]
− test/SortPubkeys.hs
@@ -1,43 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module SortPubkeys (testSortPubkeys) where--import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (sortPublicKeys)-import Data.Sequence (Seq)-import qualified Data.Sequence as Seq-import Test.Tasty-import Test.Tasty.HUnit-import Util (parsePoint)--parsePoints :: [Pub]-parsePoints =-  map-    parsePoint-    [ "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8"-    , "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"-    , "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"-    , "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66"-    , "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF"-    , "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8"-    ]--parsePointsSorted :: Seq Pub-parsePointsSorted =-  Seq.fromList $-    map-      parsePoint-      [ "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66"-      , "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8"-      , "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8"-      , "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF"-      , "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"-      , "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"-      ]---- | Test vectors from [BIP-0327 `key_sort_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/key_sort_vectors.json)-testSortPubkeys :: TestTree-testSortPubkeys =-  testGroup-    "sorting pubkeys"-    [testCase "BIP-0327 test vector" $ sortPublicKeys parsePoints @=? parsePointsSorted]
− test/Tweak.hs
@@ -1,128 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tweak (testTweak) where--import Crypto.Curve.Secp256k1 (Pub, derive_pub)-import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), PubNonce (..), SecKey (..), Tweak (..), aggNonces, mkSessionContext, partialSigVerify, sign)-import Crypto.Curve.Secp256k1.MuSig2.Internal (bytesToInteger)-import Data.ByteString (ByteString)-import Test.Tasty-import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parsePubNonce, parseScalar, unsafeMkSecNonce, unsafeRight)---- | Secret key from BIP-0327 test vectors.-testSecKey :: SecKey-testSecKey = SecKey $ parseScalar "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671"---- | Input public keys from BIP-0327 test vectors.-inputPubkeys :: [Pub]-inputPubkeys =-  map-    parsePoint-    [ "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"-    , "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"-    , "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"-    ]---- | Public nonces from BIP-0327 test vectors.-inputPubNonces :: [PubNonce]-inputPubNonces =-  map-    parsePubNonce-    [ "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"-    , "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"-    , "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046"-    ]---- | Test tweaks from BIP-0327 test vectors.-testTweaks :: [Integer]-testTweaks =-  map-    parseScalar-    [ "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB"-    , "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455"-    , "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0"-    , "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D"-    , "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"-    ]---- | Test message from BIP-0327 test vectors.-testMessage :: ByteString-testMessage = decodeHex "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF"---- | Valid test vector data: (key indices, nonce indices, tweak indices, is_xonly flags, signer index, expected signature, comment).-validTestVectors :: [([Int], [Int], [Int], [Bool], Int, ByteString, String)]-validTestVectors =-  [ ([1, 2, 0], [1, 2, 0], [0], [True], 2, decodeHex "E28A5C66E61E178C2BA19DB77B6CF9F7E2F0F56C17918CD13135E60CC848FE91", "A single x-only tweak")-  , ([1, 2, 0], [1, 2, 0], [0], [False], 2, decodeHex "38B0767798252F21BF5702C48028B095428320F73A4B14DB1E25DE58543D2D2D", "A single plain tweak")-  , ([1, 2, 0], [1, 2, 0], [0, 1], [False, True], 2, decodeHex "408A0A21C4A0F5DACAF9646AD6EB6FECD7F7A11F03ED1F48DFFF2185BC2C2408", "A plain tweak followed by an x-only tweak")-  , ([1, 2, 0], [1, 2, 0], [0, 1, 2, 3], [False, False, True, True], 2, decodeHex "45ABD206E61E3DF2EC9E264A6FEC8292141A633C28586388235541F9ADE75435", "Four tweaks: plain, plain, x-only, x-only.")-  , ([1, 2, 0], [1, 2, 0], [0, 1, 2, 3], [True, False, True, False], 2, decodeHex "B255FDCAC27B40C7CE7848E2D3B7BF5EA0ED756DA81565AC804CCCA3E1D5D239", "Four tweaks: x-only, plain, x-only, plain. If an implementation prohibits applying plain tweaks after x-only tweaks, it can skip this test vector or return an error.")-  ]---- | Error test vector data: (tweak indices, is_xonly flags, expected error message, comment).-errorTestVectors :: [([Int], [Bool], String, String)]-errorTestVectors =-  [ ([4], [False], "tweaks must be less than curve order", "Tweak is invalid because it exceeds group size")-  ]---- | Creates tweaks from indices and xonly flags.-createTweaks :: [Int] -> [Bool] -> [Tweak]-createTweaks = zipWith createTweak- where-  createTweak i isXOnly =-    let tweakValue = testTweaks !! i-     in if isXOnly then XOnlyTweak tweakValue else PlainTweak tweakValue---- | Creates test case from valid test vector data.-makeValidTestCase :: Int -> ([Int], [Int], [Int], [Bool], Int, ByteString, String) -> TestTree-makeValidTestCase i (keyIndices, nonceIndices, tweakIndices, isXOnly, signerIndex, expectedSig, comment) =-  testCase ("BIP-0327 valid test vector " <> show (i + 1) <> ": " <> comment) $ do-    let signerPub = case derive_pub (fromInteger (parseScalar "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671")) of-          Just pub -> pub-          Nothing -> error "Failed to derive test signer pubkey"-        selectedKeys = map (inputPubkeys !!) keyIndices-        selectedNonces = map (inputPubNonces !!) nonceIndices-        tweaks = createTweaks tweakIndices isXOnly-        expectedSignature = bytesToInteger expectedSig-        aggNonce = unsafeRight $ aggNonces selectedNonces-        ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys tweaks testMessage-        testSecNonce = unsafeMkSecNonce signerPub (parseScalar "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61") (parseScalar "FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")-        generatedSig = unsafeRight $ sign testSecNonce testSecKey ctx-        generatedSigVerifies = partialSigVerify generatedSig selectedNonces selectedKeys tweaks testMessage signerIndex-        expectedSigVerifies = partialSigVerify expectedSignature selectedNonces selectedKeys tweaks testMessage signerIndex--    signerPub @?= (selectedKeys !! signerIndex)-    generatedSigVerifies @?= Right True-    if expectedSigVerifies == Right True-      then assertBool ("Generated signature matches expected or remains valid: " <> comment) (generatedSig == expectedSignature || generatedSigVerifies == Right True)-      else assertBool ("Expected signature from test vector doesn't verify, but generated signature does: " <> comment) (generatedSigVerifies == Right True)---- | Creates test case from error test vector data.-makeErrorTestCase :: Int -> ([Int], [Bool], String, String) -> TestTree-makeErrorTestCase i (tweakIndices, isXOnly, expectedError, comment) =-  testCase ("BIP-0327 error test vector " <> show (i + 1) <> ": " <> comment) $ do-    let selectedKeys = [inputPubkeys !! 1, inputPubkeys !! 2, firstInputPubKey] -- [1, 2, 0]-        selectedNonces = [inputPubNonces !! 1, inputPubNonces !! 2, firstInputPubNonce] -- [1, 2, 0]-        tweaks = createTweaks tweakIndices isXOnly-        aggNonce = unsafeRight $ aggNonces selectedNonces-    assertBool-      (expectedError <> ": " <> comment)-      ( case mkSessionContext aggNonce selectedKeys tweaks testMessage of-          Left (TweakOutOfRange _) -> True-          _ -> False-      )- where-  firstInputPubKey = case inputPubkeys of-    pub : _ -> pub-    [] -> error "Expected at least one input public key"-  firstInputPubNonce = case inputPubNonces of-    nonce : _ -> nonce-    [] -> error "Expected at least one input public nonce"---- | Test vectors from [BIP-0327 `tweak_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/tweak_vectors.json).-testTweak :: TestTree-testTweak =-  testGroup "tweak vectors" $-    zipWith makeValidTestCase [0 ..] validTestVectors-      <> zipWith makeErrorTestCase [0 ..] errorTestVectors
+ test/vectors/bip327/README.md view
@@ -0,0 +1,29 @@+# BIP-327 official test vectors++These files are unmodified copies of the official BIP-327 (MuSig2) test vectors+from <https://github.com/bitcoin/bips/tree/master/bip-0327/vectors>, taken at+commit [`b34d9f751791d4feb22feff4f2ed00bcac83961b`](https://github.com/bitcoin/bips/tree/b34d9f751791d4feb22feff4f2ed00bcac83961b/bip-0327/vectors)+of `master`. The last upstream change to the vectors before that commit is+`1c6ac0c4cf1f39ea806b8594d6060b6d52fd1439` ("bip327: minor fixes").++`test/OfficialVectors.hs` reads them when the test suite runs. Under+`cabal test` the working directory is the package root, so the files are+found at `test/vectors/bip327`. To run the test binary from elsewhere, set+`MUSIG2_BIP327_VECTORS` to this directory. If the files cannot be found,+the suite fails and says where it looked.++| File | SHA-256 |+| --- | --- |+| `key_sort_vectors.json` | `2389fa0c146cfd7455c643ca240ec32835dcfc916f430f50dd94d0b49c9ea16c` |+| `key_agg_vectors.json` | `03c02a97e4ef3f2edfbc8e6013c127496dfcfd5889cfca60ddf009a4e9091cab` |+| `nonce_gen_vectors.json` | `2e823580fc072427f0db0f000212cc9124ad2b9dca2b58357eb65088aee4358d` |+| `nonce_agg_vectors.json` | `8409e87b81ea769759598ad3ce53b277a78afffb3a490a86ce02c4d69984524b` |+| `sign_verify_vectors.json` | `692eecc101f3e515c29137f05031935e1210d2a01bab91e674eb0234f095c15c` |+| `tweak_vectors.json` | `80ce6385ce062644ad1f4edcb9d4797f70ddb0b74769e4099f51b3c9e6ab4aff` |+| `sig_agg_vectors.json` | `15f14c034fb2a5739d7ce638be94c5b37ea675a2e01159092dd93b59d69c3439` |+| `det_sign_vectors.json` | `3d4fdb64b24e31762f20830036dc0c59d39fa896649131b54b87906ffdc6e9e8` |++The test suite checks these checksums too (`pinnedFiles` in+`test/OfficialVectors.hs`). To update the vectors, copy the new files from+upstream without editing them, then update the commit and checksums here and+in `pinnedFiles`.
+ test/vectors/bip327/det_sign_vectors.json view
@@ -0,0 +1,144 @@+{+    "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671",+    "pubkeys": [+        "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9",+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+        "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",+        "020000000000000000000000000000000000000000000000000000000000000007"+    ],+    "msgs": [+        "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF",+        "2626262626262626262626262626262626262626262626262626262626262626262626262626"+    ],+    "valid_test_cases": [+        {+            "rand": "0000000000000000000000000000000000000000000000000000000000000000",+            "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+            "key_indices": [0, 1, 2],+            "tweaks": [],+            "is_xonly": [],+            "msg_index": 0,+            "signer_index": 0,+            "expected": [+                "03D96275257C2FCCBB6EEB77BDDF51D3C88C26EE1626C6CDA8999B9D34F4BA13A60309BE2BF883C6ABE907FA822D9CA166D51A3DCC28910C57528F6983FC378B7843",+                "41EA65093F71D084785B20DC26A887CD941C9597860A21660CBDB9CC2113CAD3"+            ]+        },+        {+            "rand": null,+            "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+            "key_indices": [1, 0, 2],+            "tweaks": [],+            "is_xonly": [],+            "msg_index": 0,+            "signer_index": 1,+            "expected": [+                "028FBCCF5BB73A7B61B270BAD15C0F9475D577DD85C2157C9D38BEF1EC922B48770253BE3638C87369BC287E446B7F2C8CA5BEB9FFBD1EA082C62913982A65FC214D",+                "AEAA31262637BFA88D5606679018A0FEEEC341F3107D1199857F6C81DE61B8DD"+            ]+        },+        {+            "rand": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",+            "aggothernonce": "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",+            "key_indices": [1, 2, 0],+            "tweaks": [],+            "is_xonly": [],+            "msg_index": 1,+            "signer_index": 2,+            "expected": [+                "024FA8D774F0C8743FAA77AFB4D08EE5A013C2E8EEAD8A6F08A77DDD2D28266DB803050905E8C994477F3F2981861A2E3791EF558626E645FBF5AA131C5D6447C2C2",+                "FEE28A56B8556B7632E42A84122C51A4861B1F2DEC7E81B632195E56A52E3E13"+            ],+            "comment": "Message longer than 32 bytes"+        },+        {+            "rand": "0000000000000000000000000000000000000000000000000000000000000000",+            "aggothernonce": "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046",+            "key_indices": [0, 1, 2],+            "tweaks": ["E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB"],+            "is_xonly": [true],+            "msg_index": 0,+            "signer_index": 0,+            "expected": [+                "031E07C0D11A0134E55DB1FC16095ADCBD564236194374AA882BFB3C78273BF673039D0336E8CA6288C00BFC1F8B594563529C98661172B9BC1BE85C23A4CE1F616B",+                "7B1246C5889E59CB0375FA395CC86AC42D5D7D59FD8EAB4FDF1DCAB2B2F006EA"+            ],+            "comment": "Tweaked public key"+        }+    ],+    "error_test_cases": [+        {+            "rand": "0000000000000000000000000000000000000000000000000000000000000000",+            "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+            "key_indices": [1, 0, 3],+            "tweaks": [],+            "is_xonly": [],+            "msg_index": 0,+            "signer_index": 1,+            "error": {+                "type": "invalid_contribution",+                "signer": 2,+                "contrib": "pubkey"+            },+            "comment": "Signer 2 provided an invalid public key"+        },+        {+            "rand": "0000000000000000000000000000000000000000000000000000000000000000",+            "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+            "key_indices": [1, 2],+            "tweaks": [],+            "is_xonly": [],+            "msg_index": 0,+            "signer_index": 1,+            "error": {+                "type": "value",+                "message": "The signer's pubkey must be included in the list of pubkeys."+            },+            "comment": "The signers pubkey is not in the list of pubkeys"+        },+        {+            "rand": "0000000000000000000000000000000000000000000000000000000000000000",+            "aggothernonce": "0437C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+            "key_indices": [1, 2, 0],+            "tweaks": [],+            "is_xonly": [],+            "msg_index": 0,+            "signer_index": 2,+            "error": {+                "type": "invalid_contribution",+                "signer": null,+                "contrib": "aggothernonce"+            },+            "comment": "aggothernonce is invalid due wrong tag, 0x04, in the first half"+        },+        {+            "rand": "0000000000000000000000000000000000000000000000000000000000000000",+            "aggothernonce": "0000000000000000000000000000000000000000000000000000000000000000000287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+            "key_indices": [1, 2, 0],+            "tweaks": [],+            "is_xonly": [],+            "msg_index": 0,+            "signer_index": 2,+            "error": {+                "type": "invalid_contribution",+                "signer": null,+                "contrib": "aggothernonce"+            },+            "comment": "aggothernonce is invalid because first half corresponds to point at infinity"+        },+        {+            "rand": "0000000000000000000000000000000000000000000000000000000000000000",+            "aggothernonce": "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+            "key_indices": [1, 2, 0],+            "tweaks": ["FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"],+            "is_xonly": [false],+            "msg_index": 0,+            "signer_index": 2,+            "error": {+                "type": "value",+                "message": "The tweak must be less than n."+            },+            "comment": "Tweak is invalid because it exceeds group size"+        }+    ]+}
+ test/vectors/bip327/key_agg_vectors.json view
@@ -0,0 +1,88 @@+{+    "pubkeys": [+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+        "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",+        "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66",+        "020000000000000000000000000000000000000000000000000000000000000005",+        "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30",+        "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+        "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"+    ],+    "tweaks": [+        "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",+        "252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B"+    ],+    "valid_test_cases": [+        {+            "key_indices": [0, 1, 2],+            "expected": "90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C"+        },+        {+            "key_indices": [2, 1, 0],+            "expected": "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B"+        },+        {+            "key_indices": [0, 0, 0],+            "expected": "B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935"+        },+        {+            "key_indices": [0, 0, 1, 1],+            "expected": "69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E"+        }+    ],+    "error_test_cases": [+        {+            "key_indices": [0, 3],+            "tweak_indices": [],+            "is_xonly": [],+            "error": {+                "type": "invalid_contribution",+                "signer": 1,+                "contrib": "pubkey"+            },+            "comment": "Invalid public key"+        },+        {+            "key_indices": [0, 4],+            "tweak_indices": [],+            "is_xonly": [],+            "error": {+                "type": "invalid_contribution",+                "signer": 1,+                "contrib": "pubkey"+            },+            "comment": "Public key exceeds field size"+        },+        {+            "key_indices": [5, 0],+            "tweak_indices": [],+            "is_xonly": [],+            "error": {+                "type": "invalid_contribution",+                "signer": 0,+                "contrib": "pubkey"+            },+            "comment": "First byte of public key is not 2 or 3"+        },+        {+            "key_indices": [0, 1],+            "tweak_indices": [0],+            "is_xonly": [true],+            "error": {+                "type": "value",+                "message": "The tweak must be less than n."+            },+            "comment": "Tweak is out of range"+        },+        {+            "key_indices": [6],+            "tweak_indices": [1],+            "is_xonly": [false],+            "error": {+                "type": "value",+                "message": "The result of tweaking cannot be infinity."+            },+            "comment": "Intermediate tweaking result is point at infinity"+        }+    ]+}
+ test/vectors/bip327/key_sort_vectors.json view
@@ -0,0 +1,18 @@+{+    "pubkeys": [+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+        "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",+        "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66",+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF",+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8"+    ],+    "sorted_pubkeys": [+        "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66",+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF",+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+        "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"+    ]+}
+ test/vectors/bip327/nonce_agg_vectors.json view
@@ -0,0 +1,51 @@+{+    "pnonces": [+        "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E66603BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641",+        "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833",+        "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",+        "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",+        "04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833",+        "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831",+        "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A602FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"+    ],+    "valid_test_cases": [+        {+            "pnonce_indices": [0, 1],+            "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B024725377345BDE0E9C33AF3C43C0A29A9249F2F2956FA8CFEB55C8573D0262DC8"+        },+        {+            "pnonce_indices": [2, 3],+            "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B000000000000000000000000000000000000000000000000000000000000000000",+            "comment": "Sum of second points encoded in the nonces is point at infinity which is serialized as 33 zero bytes"+        }+    ],+    "error_test_cases": [+        {+            "pnonce_indices": [0, 4],+            "error": {+                "type": "invalid_contribution",+                "signer": 1,+                "contrib": "pubnonce"+            },+            "comment": "Public nonce from signer 1 is invalid due wrong tag, 0x04, in the first half"+        },+        {+            "pnonce_indices": [5, 1],+            "error": {+                "type": "invalid_contribution",+                "signer": 0,+                "contrib": "pubnonce"+            },+            "comment": "Public nonce from signer 0 is invalid because the second half does not correspond to an X coordinate"+        },+        {+            "pnonce_indices": [6, 1],+            "error": {+                "type": "invalid_contribution",+                "signer": 0,+                "contrib": "pubnonce"+            },+            "comment": "Public nonce from signer 0 is invalid because second half exceeds field size"+        }+    ]+}
+ test/vectors/bip327/nonce_gen_vectors.json view
@@ -0,0 +1,44 @@+{+    "test_cases": [+        {+            "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F",+            "sk": "0202020202020202020202020202020202020202020202020202020202020202",+            "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766",+            "aggpk": "0707070707070707070707070707070707070707070707070707070707070707",+            "msg": "0101010101010101010101010101010101010101010101010101010101010101",+            "extra_in": "0808080808080808080808080808080808080808080808080808080808080808",+            "expected_secnonce": "B114E502BEAA4E301DD08A50264172C84E41650E6CB726B410C0694D59EFFB6495B5CAF28D045B973D63E3C99A44B807BDE375FD6CB39E46DC4A511708D0E9D2024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766",+            "expected_pubnonce": "02F7BE7089E8376EB355272368766B17E88E7DB72047D05E56AA881EA52B3B35DF02C29C8046FDD0DED4C7E55869137200FBDBFE2EB654267B6D7013602CAED3115A"+        },+        {+            "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F",+            "sk": "0202020202020202020202020202020202020202020202020202020202020202",+            "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766",+            "aggpk": "0707070707070707070707070707070707070707070707070707070707070707",+            "msg": "",+            "extra_in": "0808080808080808080808080808080808080808080808080808080808080808",+            "expected_secnonce": "E862B068500320088138468D47E0E6F147E01B6024244AE45EAC40ACE5929B9F0789E051170B9E705D0B9EB49049A323BBBBB206D8E05C19F46C6228742AA7A9024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766",+            "expected_pubnonce": "023034FA5E2679F01EE66E12225882A7A48CC66719B1B9D3B6C4DBD743EFEDA2C503F3FD6F01EB3A8E9CB315D73F1F3D287CAFBB44AB321153C6287F407600205109"+        },+        {+            "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F",+            "sk": "0202020202020202020202020202020202020202020202020202020202020202",+            "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766",+            "aggpk": "0707070707070707070707070707070707070707070707070707070707070707",+            "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626",+            "extra_in": "0808080808080808080808080808080808080808080808080808080808080808",+            "expected_secnonce": "3221975ACBDEA6820EABF02A02B7F27D3A8EF68EE42787B88CBEFD9AA06AF3632EE85B1A61D8EF31126D4663A00DD96E9D1D4959E72D70FE5EBB6E7696EBA66F024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766",+            "expected_pubnonce": "02E5BBC21C69270F59BD634FCBFA281BE9D76601295345112C58954625BF23793A021307511C79F95D38ACACFF1B4DA98228B77E65AA216AD075E9673286EFB4EAF3"+        },+        {+            "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F",+            "sk": null,+            "pk": "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+            "aggpk": null,+            "msg": null,+            "extra_in": null,+            "expected_secnonce": "89BDD787D0284E5E4D5FC572E49E316BAB7E21E3B1830DE37DFE80156FA41A6D0B17AE8D024C53679699A6FD7944D9C4A366B514BAF43088E0708B1023DD289702F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+            "expected_pubnonce": "02C96E7CB1E8AA5DAC64D872947914198F607D90ECDE5200DE52978AD5DED63C000299EC5117C2D29EDEE8A2092587C3909BE694D5CFF0667D6C02EA4059F7CD9786"+        }+    ]+}
+ test/vectors/bip327/sig_agg_vectors.json view
@@ -0,0 +1,152 @@+{+    "pubkeys": [+        "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9",+        "02D2DC6F5DF7C56ACF38C7FA0AE7A759AE30E19B37359DFDE015872324C7EF6E05",+        "03C7FB101D97FF930ACD0C6760852EF64E69083DE0B06AC6335724754BB4B0522C",+        "02352433B21E7E05D3B452B81CAE566E06D2E003ECE16D1074AABA4289E0E3D581"+    ],+    "pnonces": [+        "036E5EE6E28824029FEA3E8A9DDD2C8483F5AF98F7177C3AF3CB6F47CAF8D94AE902DBA67E4A1F3680826172DA15AFB1A8CA85C7C5CC88900905C8DC8C328511B53E",+        "03E4F798DA48A76EEC1C9CC5AB7A880FFBA201A5F064E627EC9CB0031D1D58FC5103E06180315C5A522B7EC7C08B69DCD721C313C940819296D0A7AB8E8795AC1F00",+        "02C0068FD25523A31578B8077F24F78F5BD5F2422AFF47C1FADA0F36B3CEB6C7D202098A55D1736AA5FCC21CF0729CCE852575C06C081125144763C2C4C4A05C09B6",+        "031F5C87DCFBFCF330DEE4311D85E8F1DEA01D87A6F1C14CDFC7E4F1D8C441CFA40277BF176E9F747C34F81B0D9F072B1B404A86F402C2D86CF9EA9E9C69876EA3B9",+        "023F7042046E0397822C4144A17F8B63D78748696A46C3B9F0A901D296EC3406C302022B0B464292CF9751D699F10980AC764E6F671EFCA15069BBE62B0D1C62522A",+        "02D97DDA5988461DF58C5897444F116A7C74E5711BF77A9446E27806563F3B6C47020CBAD9C363A7737F99FA06B6BE093CEAFF5397316C5AC46915C43767AE867C00"+    ],+    "tweaks": [+        "B511DA492182A91B0FFB9A98020D55F260AE86D7ECBD0399C7383D59A5F2AF7C",+        "A815FE049EE3C5AAB66310477FBC8BCCCAC2F3395F59F921C364ACD78A2F48DC",+        "75448A87274B056468B977BE06EB1E9F657577B7320B0A3376EA51FD420D18A8"+    ],+    "psigs": [+        "B15D2CD3C3D22B04DAE438CE653F6B4ECF042F42CFDED7C41B64AAF9B4AF53FB",+        "6193D6AC61B354E9105BBDC8937A3454A6D705B6D57322A5A472A02CE99FCB64",+        "9A87D3B79EC67228CB97878B76049B15DBD05B8158D17B5B9114D3C226887505",+        "66F82EA90923689B855D36C6B7E032FB9970301481B99E01CDB4D6AC7C347A15",+        "4F5AEE41510848A6447DCD1BBC78457EF69024944C87F40250D3EF2C25D33EFE",+        "DDEF427BBB847CC027BEFF4EDB01038148917832253EBC355FC33F4A8E2FCCE4",+        "97B890A26C981DA8102D3BC294159D171D72810FDF7C6A691DEF02F0F7AF3FDC",+        "53FA9E08BA5243CBCB0D797C5EE83BC6728E539EB76C2D0BF0F971EE4E909971",+        "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"+    ],+    "msg": "599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869",+    "valid_test_cases": [+        {+            "aggnonce": "0341432722C5CD0268D829C702CF0D1CBCE57033EED201FD335191385227C3210C03D377F2D258B64AADC0E16F26462323D701D286046A2EA93365656AFD9875982B",+            "nonce_indices": [+                0,+                1+            ],+            "key_indices": [+                0,+                1+            ],+            "tweak_indices": [],+            "is_xonly": [],+            "psig_indices": [+                0,+                1+            ],+            "expected": "041DA22223CE65C92C9A0D6C2CAC828AAF1EEE56304FEC371DDF91EBB2B9EF0912F1038025857FEDEB3FF696F8B99FA4BB2C5812F6095A2E0004EC99CE18DE1E"+        },+        {+            "aggnonce": "0224AFD36C902084058B51B5D36676BBA4DC97C775873768E58822F87FE437D792028CB15929099EEE2F5DAE404CD39357591BA32E9AF4E162B8D3E7CB5EFE31CB20",+            "nonce_indices": [+                0,+                2+            ],+            "key_indices": [+                0,+                2+            ],+            "tweak_indices": [],+            "is_xonly": [],+            "psig_indices": [+                2,+                3+            ],+            "expected": "1069B67EC3D2F3C7C08291ACCB17A9C9B8F2819A52EB5DF8726E17E7D6B52E9F01800260A7E9DAC450F4BE522DE4CE12BA91AEAF2B4279219EF74BE1D286ADD9"+        },+        {+            "aggnonce": "0208C5C438C710F4F96A61E9FF3C37758814B8C3AE12BFEA0ED2C87FF6954FF186020B1816EA104B4FCA2D304D733E0E19CEAD51303FF6420BFD222335CAA402916D",+            "nonce_indices": [+                0,+                3+            ],+            "key_indices": [+                0,+                2+            ],+            "tweak_indices": [+                0+            ],+            "is_xonly": [+                false+            ],+            "psig_indices": [+                4,+                5+            ],+            "expected": "5C558E1DCADE86DA0B2F02626A512E30A22CF5255CAEA7EE32C38E9A71A0E9148BA6C0E6EC7683B64220F0298696F1B878CD47B107B81F7188812D593971E0CC"+        },+        {+            "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD",+            "nonce_indices": [+                0,+                4+            ],+            "key_indices": [+                0,+                3+            ],+            "tweak_indices": [+                0,+                1,+                2+            ],+            "is_xonly": [+                true,+                false,+                true+            ],+            "psig_indices": [+                6,+                7+            ],+            "expected": "839B08820B681DBA8DAF4CC7B104E8F2638F9388F8D7A555DC17B6E6971D7426CE07BF6AB01F1DB50E4E33719295F4094572B79868E440FB3DEFD3FAC1DB589E"+        }+    ],+    "error_test_cases": [+        {+            "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD",+            "nonce_indices": [+                0,+                4+            ],+            "key_indices": [+                0,+                3+            ],+            "tweak_indices": [+                0,+                1,+                2+            ],+            "is_xonly": [+                true,+                false,+                true+            ],+            "psig_indices": [+                7,+                8+            ],+            "error": {+                "type": "invalid_contribution",+                "signer": 1,+                "contrib": "psig"+            },+            "comment": "Partial signature is invalid because it exceeds group size"+        }+    ]+}
+ test/vectors/bip327/sign_verify_vectors.json view
@@ -0,0 +1,212 @@+{+    "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671",+    "pubkeys": [+        "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9",+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+        "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA661",+        "020000000000000000000000000000000000000000000000000000000000000007"+    ],+    "secnonces": [+        "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9",+        "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"+    ],+    "pnonces": [+        "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+        "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",+        "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046",+        "0237C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0387BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+        "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"+    ],+    "aggnonces": [+        "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9",+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",+        "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9",+        "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009",+        "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"+    ],+    "msgs": [+        "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF",+        "",+        "2626262626262626262626262626262626262626262626262626262626262626262626262626"+    ],+    "valid_test_cases": [+        {+            "key_indices": [0, 1, 2],+            "nonce_indices": [0, 1, 2],+            "aggnonce_index": 0,+            "msg_index": 0,+            "signer_index": 0,+            "expected": "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB"+        },+        {+            "key_indices": [1, 0, 2],+            "nonce_indices": [1, 0, 2],+            "aggnonce_index": 0,+            "msg_index": 0,+            "signer_index": 1,+            "expected": "9FF2F7AAA856150CC8819254218D3ADEEB0535269051897724F9DB3789513A52"+        },+        {+            "key_indices": [1, 2, 0],+            "nonce_indices": [1, 2, 0],+            "aggnonce_index": 0,+            "msg_index": 0,+            "signer_index": 2,+            "expected": "FA23C359F6FAC4E7796BB93BC9F0532A95468C539BA20FF86D7C76ED92227900"+        },+        {+            "key_indices": [0, 1],+            "nonce_indices": [0, 3],+            "aggnonce_index": 1,+            "msg_index": 0,+            "signer_index": 0,+            "expected": "AE386064B26105404798F75DE2EB9AF5EDA5387B064B83D049CB7C5E08879531",+            "comment": "Both halves of aggregate nonce correspond to point at infinity"+        },+        {+            "key_indices": [0, 1, 2],+            "nonce_indices": [0, 1, 2],+            "aggnonce_index": 0,+            "msg_index": 1,+            "signer_index": 0,+            "expected": "D7D63FFD644CCDA4E62BC2BC0B1D02DD32A1DC3030E155195810231D1037D82D",+            "comment": "Empty message"+        },+        {+            "key_indices": [0, 1, 2],+            "nonce_indices": [0, 1, 2],+            "aggnonce_index": 0,+            "msg_index": 2,+            "signer_index": 0,+            "expected": "E184351828DA5094A97C79CABDAAA0BFB87608C32E8829A4DF5340A6F243B78C",+            "comment": "38-byte message"+        }+    ],+    "sign_error_test_cases": [+        {+            "key_indices": [1, 2],+            "aggnonce_index": 0,+            "msg_index": 0,+            "secnonce_index": 0,+            "error": {+                "type": "value",+                "message": "The signer's pubkey must be included in the list of pubkeys."+            },+            "comment": "The signers pubkey is not in the list of pubkeys. This test case is optional: it can be skipped by implementations that do not check that the signer's pubkey is included in the list of pubkeys."+        },+        {+            "key_indices": [1, 0, 3],+            "aggnonce_index": 0,+            "msg_index": 0,+            "secnonce_index": 0,+            "error": {+                "type": "invalid_contribution",+                "signer": 2,+                "contrib": "pubkey"+            },+            "comment": "Signer 2 provided an invalid public key"+        },+        {+            "key_indices": [1, 2, 0],+            "aggnonce_index": 2,+            "msg_index": 0,+            "secnonce_index": 0,+            "error": {+                "type": "invalid_contribution",+                "signer": null,+                "contrib": "aggnonce"+            },+            "comment": "Aggregate nonce is invalid due wrong tag, 0x04, in the first half"+        },+        {+            "key_indices": [1, 2, 0],+            "aggnonce_index": 3,+            "msg_index": 0,+            "secnonce_index": 0,+            "error": {+                "type": "invalid_contribution",+                "signer": null,+                "contrib": "aggnonce"+            },+            "comment": "Aggregate nonce is invalid because the second half does not correspond to an X coordinate"+        },+        {+            "key_indices": [1, 2, 0],+            "aggnonce_index": 4,+            "msg_index": 0,+            "secnonce_index": 0,+            "error": {+                "type": "invalid_contribution",+                "signer": null,+                "contrib": "aggnonce"+            },+            "comment": "Aggregate nonce is invalid because second half exceeds field size"+        },+        {+            "key_indices": [0, 1, 2],+            "aggnonce_index": 0,+            "msg_index": 0,+            "signer_index": 0,+            "secnonce_index": 1,+            "error": {+                "type": "value",+                "message": "first secnonce value is out of range."+            },+            "comment": "Secnonce is invalid which may indicate nonce reuse"+        }+    ],+    "verify_fail_test_cases": [+        {+            "sig": "FED54434AD4CFE953FC527DC6A5E5BE8F6234907B7C187559557CE87A0541C46",+            "key_indices": [0, 1, 2],+            "nonce_indices": [0, 1, 2],+            "msg_index": 0,+            "signer_index": 0,+            "comment": "Wrong signature (which is equal to the negation of valid signature)"+        },+        {+            "sig": "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB",+            "key_indices": [0, 1, 2],+            "nonce_indices": [0, 1, 2],+            "msg_index": 0,+            "signer_index": 1,+            "comment": "Wrong signer"+        },+        {+            "sig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",+            "key_indices": [0, 1, 2],+            "nonce_indices": [0, 1, 2],+            "msg_index": 0,+            "signer_index": 0,+            "comment": "Signature exceeds group size"+        }+    ],+    "verify_error_test_cases": [+        {+            "sig": "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB",+            "key_indices": [0, 1, 2],+            "nonce_indices": [4, 1, 2],+            "msg_index": 0,+            "signer_index": 0,+            "error": {+                "type": "invalid_contribution",+                "signer": 0,+                "contrib": "pubnonce"+            },+            "comment": "Invalid pubnonce"+        },+        {+            "sig": "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB",+            "key_indices": [3, 1, 2],+            "nonce_indices": [0, 1, 2],+            "msg_index": 0,+            "signer_index": 0,+            "error": {+                "type": "invalid_contribution",+                "signer": 0,+                "contrib": "pubkey"+            },+            "comment": "Invalid pubkey"+        }+    ]+}
+ test/vectors/bip327/tweak_vectors.json view
@@ -0,0 +1,84 @@+{+    "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671",+    "pubkeys": [+        "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9",+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",+        "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"+    ],+    "secnonce": "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9",+    "pnonces": [+        "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",+        "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",+        "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046"+    ],+    "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9",+    "tweaks": [+        "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB",+        "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455",+        "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0",+        "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D",+        "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"+    ],+    "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF",+    "valid_test_cases": [+        {+            "key_indices": [1, 2, 0],+            "nonce_indices": [1, 2, 0],+            "tweak_indices": [0],+            "is_xonly": [true],+            "signer_index": 2,+            "expected": "E28A5C66E61E178C2BA19DB77B6CF9F7E2F0F56C17918CD13135E60CC848FE91",+            "comment": "A single x-only tweak"+        },+        {+            "key_indices": [1, 2, 0],+            "nonce_indices": [1, 2, 0],+            "tweak_indices": [0],+            "is_xonly": [false],+            "signer_index": 2,+            "expected": "38B0767798252F21BF5702C48028B095428320F73A4B14DB1E25DE58543D2D2D",+            "comment": "A single plain tweak"+        },+        {+            "key_indices": [1, 2, 0],+            "nonce_indices": [1, 2, 0],+            "tweak_indices": [0, 1],+            "is_xonly": [false, true],+            "signer_index": 2,+            "expected": "408A0A21C4A0F5DACAF9646AD6EB6FECD7F7A11F03ED1F48DFFF2185BC2C2408",+            "comment": "A plain tweak followed by an x-only tweak"+        },+        {+            "key_indices": [1, 2, 0],+            "nonce_indices": [1, 2, 0],+            "tweak_indices": [0, 1, 2, 3],+            "is_xonly": [false, false, true, true],+            "signer_index": 2,+            "expected": "45ABD206E61E3DF2EC9E264A6FEC8292141A633C28586388235541F9ADE75435",+            "comment": "Four tweaks: plain, plain, x-only, x-only."+        },+        {+            "key_indices": [1, 2, 0],+            "nonce_indices": [1, 2, 0],+            "tweak_indices": [0, 1, 2, 3],+            "is_xonly": [true, false, true, false],+            "signer_index": 2,+            "expected": "B255FDCAC27B40C7CE7848E2D3B7BF5EA0ED756DA81565AC804CCCA3E1D5D239",+            "comment": "Four tweaks: x-only, plain, x-only, plain. If an implementation prohibits applying plain tweaks after x-only tweaks, it can skip this test vector or return an error."+        }+    ],+    "error_test_cases": [+        {+            "key_indices": [1, 2, 0],+            "nonce_indices": [1, 2, 0],+            "tweak_indices": [4],+            "is_xonly": [false],+            "signer_index": 2,+            "error": {+                "type": "value",+                "message": "The tweak must be less than n."+            },+            "comment": "Tweak is invalid because it exceeds group size"+        }+    ]+}