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