diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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:
diff --git a/bench/Main.hs b/bench/Main.hs
--- a/bench/Main.hs
+++ b/bench/Main.hs
@@ -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 =
diff --git a/lib/Crypto/Curve/Secp256k1/MuSig2.hs b/lib/Crypto/Curve/Secp256k1/MuSig2.hs
--- a/lib/Crypto/Curve/Secp256k1/MuSig2.hs
+++ b/lib/Crypto/Curve/Secp256k1/MuSig2.hs
@@ -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
diff --git a/lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs b/lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs
--- a/lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs
+++ b/lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs
@@ -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
diff --git a/musig2.cabal b/musig2.cabal
--- a/musig2.cabal
+++ b/musig2.cabal
@@ -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
diff --git a/test/AggNonces.hs b/test/AggNonces.hs
deleted file mode 100644
--- a/test/AggNonces.hs
+++ /dev/null
@@ -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
diff --git a/test/AggPartials.hs b/test/AggPartials.hs
deleted file mode 100644
--- a/test/AggPartials.hs
+++ /dev/null
@@ -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
-    ]
diff --git a/test/AggPubkeys.hs b/test/AggPubkeys.hs
deleted file mode 100644
--- a/test/AggPubkeys.hs
+++ /dev/null
@@ -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
diff --git a/test/Bip327Regressions.hs b/test/Bip327Regressions.hs
new file mode 100644
--- /dev/null
+++ b/test/Bip327Regressions.hs
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -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
diff --git a/test/NonceGen.hs b/test/NonceGen.hs
deleted file mode 100644
--- a/test/NonceGen.hs
+++ /dev/null
@@ -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
diff --git a/test/OfficialVectors.hs b/test/OfficialVectors.hs
new file mode 100644
--- /dev/null
+++ b/test/OfficialVectors.hs
@@ -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 ())
diff --git a/test/SignVerify.hs b/test/SignVerify.hs
--- a/test/SignVerify.hs
+++ b/test/SignVerify.hs
@@ -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]
diff --git a/test/SortPubkeys.hs b/test/SortPubkeys.hs
deleted file mode 100644
--- a/test/SortPubkeys.hs
+++ /dev/null
@@ -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]
diff --git a/test/Tweak.hs b/test/Tweak.hs
deleted file mode 100644
--- a/test/Tweak.hs
+++ /dev/null
@@ -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
diff --git a/test/vectors/bip327/README.md b/test/vectors/bip327/README.md
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/README.md
@@ -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`.
diff --git a/test/vectors/bip327/det_sign_vectors.json b/test/vectors/bip327/det_sign_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/det_sign_vectors.json
@@ -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"
+        }
+    ]
+}
diff --git a/test/vectors/bip327/key_agg_vectors.json b/test/vectors/bip327/key_agg_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/key_agg_vectors.json
@@ -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"
+        }
+    ]
+}
diff --git a/test/vectors/bip327/key_sort_vectors.json b/test/vectors/bip327/key_sort_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/key_sort_vectors.json
@@ -0,0 +1,18 @@
+{
+    "pubkeys": [
+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
+        "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
+        "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66",
+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF",
+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8"
+    ],
+    "sorted_pubkeys": [
+        "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66",
+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
+        "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF",
+        "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
+        "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"
+    ]
+}
diff --git a/test/vectors/bip327/nonce_agg_vectors.json b/test/vectors/bip327/nonce_agg_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/nonce_agg_vectors.json
@@ -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"
+        }
+    ]
+}
diff --git a/test/vectors/bip327/nonce_gen_vectors.json b/test/vectors/bip327/nonce_gen_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/nonce_gen_vectors.json
@@ -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"
+        }
+    ]
+}
diff --git a/test/vectors/bip327/sig_agg_vectors.json b/test/vectors/bip327/sig_agg_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/sig_agg_vectors.json
@@ -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"
+        }
+    ]
+}
diff --git a/test/vectors/bip327/sign_verify_vectors.json b/test/vectors/bip327/sign_verify_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/sign_verify_vectors.json
@@ -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"
+        }
+    ]
+}
diff --git a/test/vectors/bip327/tweak_vectors.json b/test/vectors/bip327/tweak_vectors.json
new file mode 100644
--- /dev/null
+++ b/test/vectors/bip327/tweak_vectors.json
@@ -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"
+        }
+    ]
+}
