musig2-0.2.1: test/OfficialVectors.hs
{-# 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 ())