musig2-0.3.0: test/OfficialVectors.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{- | 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 runs with exact-output assertions. Invalid encodings are decoded
with 'decode', contribution by contribution in the order the reference
implementation parses them, and the first failure must be exactly the
contribution (and signer) the vector blames. The @NonceAgg@ and
@PartialSigAgg@ cases also run through
"Crypto.Curve.Secp256k1.MuSig2.Coordinator": it must produce the expected
aggregate nonce and signature, and blame exactly the vector's signer.
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, void, zipWithM, (>=>))
import Crypto.Curve.Secp256k1.MuSig2
import Crypto.Curve.Secp256k1.MuSig2.Coordinator (
Blame (..),
BlameReason (..),
CollectedNonces,
collectedAggNonce,
finishNonceRound,
finishSigRound,
newNonceRound,
newSigRound,
receivePartialSig,
receivePubNonce,
)
import Crypto.Curve.Secp256k1.MuSig2.Internal.Nonce (secNoncePubNonce)
import Crypto.Curve.Secp256k1.MuSig2.Internal.Scalar (bytesToInteger, curveOrder)
import Crypto.Curve.Secp256k1.MuSig2.Unsafe (nonceGenWithRand, secNonceFromBytes, secNonceToBytes)
import qualified Crypto.Hash.SHA256 as SHA256
import Data.Aeson (FromJSON (..), Value, eitherDecodeFileStrict, withObject, (.:), (.:?))
import qualified Data.Aeson.Key as Key
import Data.Aeson.Types (Object, Parser, parseEither)
import Data.Bifunctor (first)
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.Either (lefts)
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Maybe (fromMaybe)
import System.Directory (doesFileExist, getCurrentDirectory, makeAbsolute)
import System.Environment (lookupEnv)
import System.FilePath ((</>))
import Test.Tasty
import Test.Tasty.HUnit
import Util (Hex (..), hexOf, secKeyFromInt, strictObject)
-- 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 ---------------------------------------------------------------
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 :: [KeyAggErrorCase]
}
data KeyAggErrorCase = KeyAggErrorCase
{ 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 ->
KeyAggErrorCase <$> 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"
data DetSignVectors = DetSignVectors
{ dsSk :: ByteString
, dsPubkeys :: [ByteString]
, dsMsgs :: [ByteString]
, dsValid :: [DetSignCase]
, dsError :: [DetSignCase]
}
-- | A det_sign case: valid cases carry the expected (pubnonce, psig), error cases carry @error@.
data DetSignCase = DetSignCase
{ dscRand :: Maybe ByteString
, dscAggOtherNonce :: ByteString
, dscKeys :: [Int]
, dscTweaks :: [ByteString]
, dscXOnly :: [Bool]
, dscMsg :: Int
, dscSigner :: Int
, dscOutcome :: Either VectorError (ByteString, ByteString)
, dscComment :: Maybe String
}
parseDetSign :: Value -> Parser DetSignVectors
parseDetSign = strictObject "det_sign" ["sk", "pubkeys", "msgs", "valid_test_cases", "error_test_cases"] $ \o ->
DetSignVectors
<$> hexField o "sk"
<*> hexList o "pubkeys"
<*> hexList o "msgs"
<*> (o .: "valid_test_cases" >>= traverse (detSignCase False))
<*> (o .: "error_test_cases" >>= traverse (detSignCase True))
where
detSignCase isError =
strictObject "det_sign case" ["rand", "aggothernonce", "key_indices", "tweaks", "is_xonly", "msg_index", "signer_index", if isError then "error" else "expected", "comment"] $ \c ->
DetSignCase
<$> nullableHexField c "rand"
<*> hexField c "aggothernonce"
<*> c .: "key_indices"
<*> hexList c "tweaks"
<*> c .: "is_xonly"
<*> c .: "msg_index"
<*> c .: "signer_index"
<*> (if isError then Left <$> c .: "error" else Right <$> (hexList c "expected" >>= pair))
<*> c .:? "comment"
pair [pn, psig] = pure (pn, psig)
pair other = fail ("det_sign: expected [pubnonce, psig], got " <> show (length other) <> " values")
-- Library steps ------------------------------------------------------------------
-- | A failure of any library step (or a malformed vector).
data Failure
= FDecode DecodeError
| FKeyAgg KeyAggError
| FSign SignError
| FVector String
deriving (Eq, Show)
decodeF :: (Encode a) => ByteString -> Either Failure a
decodeF = first FDecode . decode
nonEmptyF :: String -> [a] -> Either Failure (NonEmpty a)
nonEmptyF what = maybe (Left (FVector ("empty " <> what <> " list"))) Right . nonEmpty
-- | Decodes and applies tweaks (raw bytes, is_xonly) in order.
applyTweaks :: [(ByteString, Bool)] -> KeyAggContext -> Either Failure KeyAggContext
applyTweaks ts ctx0 = foldM step ctx0 ts
where
step ctx (b, xonly) = do
t <- decodeF b
first FKeyAgg ((if xonly then applyXOnlyTweak else applyPlainTweak) t ctx)
-- | KeyAgg of encoded keys, then the tweaks.
keyAggBytes :: [ByteString] -> [(ByteString, Bool)] -> Either Failure KeyAggContext
keyAggBytes keys ts = do
pks <- traverse decodeF keys >>= nonEmptyF "pubkey"
first FKeyAgg (keyAgg pks) >>= applyTweaks ts
-- | Opens a session from encoded inputs and signs with an encoded secret nonce.
signBytes :: SecKey -> ByteString -> [ByteString] -> [(ByteString, Bool)] -> ByteString -> ByteString -> IO (Either Failure PartialSignature)
signBytes sk secnonce keys ts aggnonce m = case (,) <$> keyAggBytes keys ts <*> decodeF aggnonce of
Left err -> pure (Left err)
Right (ctx, agg) -> do
imported <- secNonceFromBytes secnonce
case imported of
Left err -> pure (Left (FDecode err))
Right sn -> first FSign <$> sign sn sk (newSession ctx agg m)
{- | DeterministicSign from encoded inputs, in the order the reference
implementation parses them: public keys and tweaks, then aggothernonce.
-}
detSignBytes :: SecKey -> ByteString -> [ByteString] -> [(ByteString, Bool)] -> ByteString -> Maybe ByteString -> Either Failure (PubNonce, PartialSignature)
detSignBytes sk aggOther keys ts m rand = do
ctx <- keyAggBytes keys ts
agg <- decodeF aggOther
first FSign (deterministicSign sk agg ctx m rand)
-- | PartialSigVerify as BIP-327 defines it: aggregates the given nonces itself.
verifyBytes :: [ByteString] -> [ByteString] -> [(ByteString, Bool)] -> ByteString -> Int -> PartialSignature -> Either Failure Bool
verifyBytes nonces keys ts m signer psig = do
pns <- traverse decodeF nonces >>= nonEmptyF "pubnonce"
ctx <- keyAggBytes keys ts
pk <- first FVector (at "pubkey" (NE.toList (keyAggPubKeys ctx)) signer)
pn <- first FVector (at "pubnonce" (NE.toList pns) signer)
pure (partialSigVerify (newSession ctx (nonceAgg pns) m) pk pn psig)
-- 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 entries of a vector table.
select :: String -> [ByteString] -> [Int] -> Either String [ByteString]
select what table = traverse (at what table)
tweakSpecs :: [ByteString] -> [Int] -> [Bool] -> Either String [(ByteString, Bool)]
tweakSpecs table idxs xonly
| length idxs /= length xonly = Left "tweak_indices and is_xonly differ in length"
| otherwise = zipWithM (\i x -> (,x) <$> at "tweak" table i) idxs xonly
{- | The library failure that corresponds to a reference-implementation
@ValueError@ message. Unknown messages fail, so new error cases need an
explicit mapping.
-}
expectedFailure :: [(ByteString, Bool)] -> Maybe ByteString -> String -> Either String Failure
expectedFailure tweaks secnonce message = case message of
-- A tweak >= n cannot be decoded into a 'Tweak'.
"The tweak must be less than n." ->
if any ((>= curveOrder) . bytesToInteger . fst) tweaks
then Right (FDecode ScalarOutOfRange)
else Left "vector expects an out-of-range tweak but none is >= n"
"The result of tweaking cannot be infinity." -> Right (FKeyAgg TweakResultAtInfinity)
"The signer's pubkey must be included in the list of pubkeys." -> Right (FSign SignerNotInSession)
-- An out-of-range secnonce scalar cannot be decoded into a 'SecNonce'.
"first secnonce value is out of range." -> scalarFailure . BS.take 32 <$> note "no secnonce in case" secnonce
"second secnonce value is out of range." -> scalarFailure . BS.take 32 . BS.drop 32 <$> note "no secnonce in case" secnonce
other -> Left ("no library failure is mapped to the BIP-0327 error message " <> show other)
where
scalarFailure b = FDecode (if bytesToInteger b == 0 then ScalarZero else ScalarOutOfRange)
valueError :: VectorError -> Either String String
valueError (ValueError m) = Right m
valueError err = Left ("expected a value error, got " <> show err)
{- | A contribution in the order the reference implementation parses it:
kind, signer index (if any), and the result of decoding it.
-}
type Contribution = (String, Maybe Int, Either DecodeError ())
contributions :: String -> (ByteString -> Either DecodeError ()) -> [ByteString] -> [Int] -> Either String [Contribution]
contributions kind dec table = zipWithM (\signer i -> (\b -> (kind, Just signer, dec b)) <$> at kind table i) [0 ..]
asPubKey, asPubNonce, asAggNonce, asPsig :: ByteString -> Either DecodeError ()
asPubKey = void . (decode :: ByteString -> Either DecodeError PubKey)
asPubNonce = void . (decode :: ByteString -> Either DecodeError PubNonce)
asAggNonce = void . (decode :: ByteString -> Either DecodeError AggNonce)
asPsig = void . (decode :: ByteString -> Either DecodeError PartialSignature)
-- | The first contribution that does not decode.
firstInvalid :: [Contribution] -> Maybe (String, Maybe Int, DecodeError)
firstInvalid cs = case [(kind, signer, err) | (kind, signer, Left err) <- cs] of
c : _ -> Just c
[] -> Nothing
{- | An @invalid_contribution@ case: decoding must fail first at exactly the
blamed contribution, with an invalid point (or, for a partial signature, a
scalar out of range).
-}
encodingCase :: String -> VectorError -> Either String [Contribution] -> TestTree
encodingCase name err cs = testCase name $ do
contribs <- expectRight "vector" cs
case err of
InvalidContribution signer kind ->
firstInvalid contribs @?= Just (kind, signer, if kind == "psig" then ScalarOutOfRange else InvalidPoint)
ValueError m -> assertFailure ("expected an invalid_contribution error, got value error " <> show m)
-- | Whether an error is an invalid encoding of a contribution.
isEncodingError :: VectorError -> Bool
isEncodingError (InvalidContribution _ kind) = kind `elem` ["pubkey", "pubnonce", "aggnonce", "psig"]
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
expectFailure :: Failure -> Either Failure a -> Assertion
expectFailure expected result = case result of
Left err -> err @?= expected
Right _ -> assertFailure ("expected Left " <> show expected <> ", got Right")
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 ----------------------------------------------------------------------
vectorTests :: Vectors -> [TestTree]
vectorTests vs = [testGroup file run | (file, run) <- files]
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 -> [TestTree]
keySortTests (KeySortVectors pubkeys sorted) =
[ testCase "KeySort" $ do
pks <- expectLib "decode" (traverse decodeF pubkeys >>= nonEmptyF "pubkey")
map (hexOf . encode) (NE.toList (keySort (pks :: NonEmpty PubKey))) @?= map hexOf sorted
]
keyAggTests :: KeyAggVectors -> [TestTree]
keyAggTests v = zipWith validCase [0 ..] (kaValid v) <> zipWith errorCase [0 ..] (kaError v)
where
validCase i (keyIdxs, expected) = testCase (caseName "valid" i Nothing) $ do
keys <- expectRight "vector" (select "pubkey" (kaPubkeys v) keyIdxs)
ctx <- expectLib "KeyAgg" (keyAggBytes keys [])
assertBytes "xbytes(Q)" expected (encode (aggXOnlyPubKey ctx))
errorCase i c
| isEncodingError (kaeError c) = encodingCase name (kaeError c) (contributions "pubkey" asPubKey (kaPubkeys v) (kaeKeys c))
| otherwise = testCase name $ do
keys <- expectRight "vector" (select "pubkey" (kaPubkeys v) (kaeKeys c))
tweaks <- expectRight "vector" (tweakSpecs (kaTweaks v) (kaeTweaks c) (kaeXOnly c))
expected <- expectRight "vector" (valueError (kaeError c) >>= expectedFailure tweaks Nothing)
expectFailure expected (keyAggBytes keys tweaks)
where
name = caseName "error" (i :: Int) (kaeComment c)
nonceGenTests :: [NonceGenCase] -> [TestTree]
nonceGenTests = zipWith run [0 ..]
where
run :: Int -> NonceGenCase -> TestTree
run i c = testCase (caseName "case" i (ngComment c)) $ do
(pk, inputs) <- expectLib "decode" $ do
pk <- decodeF (ngPk c)
sk <- traverse (first FDecode . secKeyFromBytes) (ngSk c)
aggpk <- traverse decodeF (ngAggpk c)
pure (pk, NonceInputs{nonceSecKey = sk, nonceAggPubKey = aggpk, nonceMessage = ngMsg c, nonceExtraInput = ngExtraIn c})
(sn, pn) <- nonceGenWithRand (ngRand c) pk inputs >>= expectLib "NonceGen"
exported <- secNonceToBytes sn >>= maybe (assertFailure "a fresh nonce cannot be exported") pure
assertBytes "secnonce" (ngSecnonce c) exported
assertBytes "pubnonce" (ngPubnonce c) (encode pn)
nonceAggTests :: NonceAggVectors -> [TestTree]
nonceAggTests v = zipWith validCase [0 ..] (naValid v) <> zipWith errorCase [0 ..] (naError v)
where
validCase i (idxs, expected, comment) = testCase (caseName "valid" i comment) $ do
nonces <- expectRight "vector" (select "pnonce" (naPnonces v) idxs)
pns <- expectLib "decode" (traverse decodeF nonces >>= nonEmptyF "pubnonce")
assertBytes "aggnonce (infinity as 33 zero bytes)" expected (encode (nonceAgg pns))
ctx <- expectLib "KeyAgg" (placeholderKeyAgg (length nonces))
collected <- expectLib "coordinator" (coordinateNonces ctx nonces)
assertBytes "coordinator aggnonce" expected (encode (collectedAggNonce collected))
errorCase i (idxs, err, comment) = case err of
InvalidContribution (Just signer) "pubnonce" -> testGroup name [encodingCase "decode" err contribs, coordinatorCase signer]
_ -> testCase name (assertFailure ("no library check for " <> show err))
where
name = caseName "error" (i :: Int) comment
contribs = contributions "pubnonce" asPubNonce (naPnonces v) idxs
-- Each nonce is received on its own: the coordinator blames exactly the vector's signer.
coordinatorCase signer = testCase "the coordinator blames the signer" $ do
nonces <- expectRight "vector" (select "pnonce" (naPnonces v) idxs)
ctx <- expectLib "KeyAgg" (placeholderKeyAgg (length nonces))
lefts [receivePubNonce j b (newNonceRound ctx) | (j, b) <- zip [0 ..] nonces]
@?= [Blame signer (InvalidPubNonceEncoding InvalidPoint)]
{- | A key aggregation context for @u@ signers, for nonce aggregation vectors
(which have no keys: nonce aggregation does not depend on them).
-}
placeholderKeyAgg :: Int -> Either Failure KeyAggContext
placeholderKeyAgg u = nonEmptyF "pubkey" [secKeyPubKey (secKeyFromInt k) | k <- [1 .. toInteger u]] >>= first FKeyAgg . keyAgg
-- | Round one of the coordinator, receiving the encoded nonces in signer order.
coordinateNonces :: KeyAggContext -> [ByteString] -> Either String CollectedNonces
coordinateNonces ctx nonces = do
r <- first show (foldM (\r (j, b) -> receivePubNonce j b r) (newNonceRound ctx) (zip [0 ..] nonces))
first show (finishNonceRound r)
signVerifyTests :: SignVerifyVectors -> [TestTree]
signVerifyTests v =
[ testCase "setup: sk derives pubkeys[0]" $ do
pk0 <- expectRight "vector" (at "pubkey" (svPubkeys v) 0)
sk <- secKey
assertBytes "pubkey" pk0 (encode (secKeyPubKey sk))
, testCase "setup: secnonces[0] has public nonce pnonces[0]" $ do
sn <- expectRight "vector" (at "secnonce" (svSecnonces v) 0) >>= (secNonceFromBytes >=> expectLib "secnonce")
pn0 <- expectRight "vector" (at "pnonce" (svPnonces v) 0)
assertBytes "pubnonce" pn0 (encode (secNoncePubNonce sn))
]
<> zipWith validCase [0 ..] (svValid v)
<> zipWith signErrorCase [0 ..] (svSignError v)
<> zipWith verifyFailCase [0 ..] (svVerifyFail v)
<> zipWith verifyErrorCase [0 ..] (svVerifyError v)
where
secKey = expectLib "secKeyFromBytes" (secKeyFromBytes (svSk v))
validCase i c =
testGroup
(caseName "valid" i (svvComment c))
[ testCase "NonceAgg reproduces the aggnonce" $ do
(_, nonces, agg, _) <- inputs
pns <- expectLib "decode" (traverse decodeF nonces >>= nonEmptyF "pubnonce")
assertBytes "aggnonce" agg (encode (nonceAgg pns))
, testCase "Sign returns the expected partial signature" $ do
(keys, _, agg, m) <- inputs
sk <- secKey
sn <- expectRight "vector" (at "secnonce" (svSecnonces v) 0)
psig <- signBytes sk sn keys [] agg m >>= expectLib "Sign"
assertBytes "psig" (svvExpected c) (encode psig)
, testCase "PartialSigVerify accepts the expected partial signature" $ do
(keys, nonces, _, m) <- inputs
psig <- expectLib "decode psig" (decodeF (svvExpected c))
verifyBytes nonces keys [] m (svvSigner c) psig @?= Right True
]
where
inputs = expectRight "vector" $ do
keys <- select "pubkey" (svPubkeys v) (svvKeys c)
nonces <- select "pnonce" (svPnonces v) (svvNonces c)
agg <- at "aggnonce" (svAggnonces v) (svvAggnonce c)
m <- at "msg" (svMsgs v) (svvMsg c)
pure (keys, nonces, agg, m)
signErrorCase i c
| isEncodingError (sseError c) =
encodingCase name (sseError c) $ do
keys <- contributions "pubkey" asPubKey (svPubkeys v) (sseKeys c)
agg <- at "aggnonce" (svAggnonces v) (sseAggnonce c)
-- Sign parses the pubkeys (KeyAgg) before the aggregate nonce.
pure (keys <> [("aggnonce", Nothing, asAggNonce agg)])
| otherwise = testCase name $ do
(keys, agg, m, sn) <- expectRight "vector" $ do
keys <- select "pubkey" (svPubkeys v) (sseKeys c)
agg <- at "aggnonce" (svAggnonces v) (sseAggnonce c)
m <- at "msg" (svMsgs v) (sseMsg c)
sn <- at "secnonce" (svSecnonces v) (sseSecnonce c)
pure (keys, agg, m, sn)
expected <- expectRight "vector" (valueError (sseError c) >>= expectedFailure [] (Just sn))
sk <- secKey
signBytes sk sn keys [] agg m >>= expectFailure expected
where
name = caseName "sign_error" (i :: Int) (sseComment c)
verifyInputs c = expectRight "vector" $ do
keys <- select "pubkey" (svPubkeys v) (svfKeys c)
nonces <- select "pnonce" (svPnonces v) (svfNonces c)
m <- at "msg" (svMsgs v) (svfMsg c)
pure (keys, nonces, m)
-- BIP-327 returns False for s >= n; such a partial signature does not decode.
verifyFailCase i c = testCase (caseName "verify_fail" i (svfComment c)) $ do
(keys, nonces, m) <- verifyInputs c
if bytesToInteger (svfSig c) >= curveOrder
then (decodeF (svfSig c) :: Either Failure PartialSignature) @?= Left (FDecode ScalarOutOfRange)
else do
psig <- expectLib "decode psig" (decodeF (svfSig c))
verifyBytes nonces keys [] m (svfSigner c) psig @?= Right False
verifyErrorCase i c = case svfError c of
Just err
| isEncodingError err ->
encodingCase name err $ do
-- PartialSigVerify aggregates (parses) the pubnonces before the pubkeys.
nonces <- contributions "pubnonce" asPubNonce (svPnonces v) (svfNonces c)
keys <- contributions "pubkey" asPubKey (svPubkeys v) (svfKeys c)
pure (nonces <> keys)
other -> testCase name (assertFailure ("no library check for " <> show other))
where
name = caseName "verify_error" (i :: Int) (svfComment c)
tweakTests :: TweakVectors -> [TestTree]
tweakTests v = zipWith (run "valid") [0 ..] (twValid v) <> zipWith (run "error") [0 ..] (twError v)
where
run :: String -> Int -> TweakCase -> TestTree
run section i c = case tcOutcome c of
Right expected ->
testGroup
name
[ testCase "NonceAgg reproduces the aggnonce" $ do
(_, nonces, _) <- inputs
pns <- expectLib "decode" (traverse decodeF nonces >>= nonEmptyF "pubnonce")
assertBytes "aggnonce" (twAggnonce v) (encode (nonceAgg pns))
, testCase "Sign returns the expected partial signature" $ do
(keys, _, tweaks) <- inputs
sk <- secKey
psig <- signBytes sk (twSecnonce v) keys tweaks (twAggnonce v) (twMsg v) >>= expectLib "Sign"
assertBytes "psig" expected (encode psig)
, testCase "PartialSigVerify accepts the expected partial signature" $ do
(keys, nonces, tweaks) <- inputs
psig <- expectLib "decode psig" (decodeF expected)
verifyBytes nonces keys tweaks (twMsg v) (tcSigner c) psig @?= Right True
]
Left err -> testCase name $ do
(keys, _, tweaks) <- inputs
sk <- secKey
expected <- expectRight "vector" (valueError err >>= expectedFailure tweaks Nothing)
signBytes sk (twSecnonce v) keys tweaks (twAggnonce v) (twMsg v) >>= expectFailure expected
where
name = caseName section i (tcComment c)
inputs = expectRight "vector" $ do
keys <- select "pubkey" (twPubkeys v) (tcKeys c)
nonces <- select "pnonce" (twPnonces v) (tcNonces c)
tweaks <- tweakSpecs (twTweaks v) (tcTweaks c) (tcXOnly c)
pure (keys, nonces, tweaks)
secKey = expectLib "secKeyFromBytes" (secKeyFromBytes (twSk v))
sigAggTests :: SigAggVectors -> [TestTree]
sigAggTests v = 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
pns <- expectLib "decode" (traverse decodeF nonces >>= nonEmptyF "pubnonce")
assertBytes "aggnonce" (sacAggnonce c) (encode (nonceAgg pns))
, testCase "PartialSigAgg returns the expected signature" $ do
(sig, _) <- aggregate
assertBytes "sig" expected (encode sig)
, testCase "the expected signature is a valid BIP-340 signature for aggXOnlyPubKey" $ do
(_, ctx) <- aggregate
sig <- expectLib "decode sig" (decodeF expected)
assertBool "verify" (verify (aggXOnlyPubKey ctx) (saMsg v) sig)
, testCase "the coordinator reproduces the aggnonce, accepts every partial signature and returns the signature" $ do
(keys, nonces, tweaks, psigs) <- inputs
ctx <- expectLib "KeyAgg" (keyAggBytes keys tweaks)
collected <- expectLib "round one" (coordinateNonces ctx nonces)
assertBytes "aggnonce" (sacAggnonce c) (encode (collectedAggNonce collected))
r <- expectLib "round two" (foldM (\r (j, b) -> receivePartialSig j b r) (newSigRound collected (saMsg v)) (zip [0 ..] psigs))
sig <- expectLib "finishSigRound" (finishSigRound r)
assertBytes "sig" expected (encode sig)
]
Left err@(InvalidContribution (Just signer) "psig") ->
testGroup
name
[ encodingCase "decode" err (contributions "psig" asPsig (saPsigs v) (sacPsigs c))
, -- PartialSigAgg only range-checks, so the other partial signatures
-- of the vector need not verify in this session (and do not): only
-- the vector's signer is checked.
testCase "the coordinator blames the signer" $ do
(keys, nonces, tweaks, psigs) <- inputs
ctx <- expectLib "KeyAgg" (keyAggBytes keys tweaks)
collected <- expectLib "round one" (coordinateNonces ctx nonces)
psig <- expectRight "vector" (at "psig" psigs signer)
lefts [receivePartialSig signer psig (newSigRound collected (saMsg v))]
@?= [Blame signer (InvalidPartialSigEncoding ScalarOutOfRange)]
]
Left err -> testCase name (assertFailure ("no library check for " <> show err))
where
name = caseName section i (sacComment c)
inputs = expectRight "vector" $ do
keys <- select "pubkey" (saPubkeys v) (sacKeys c)
nonces <- select "pnonce" (saPnonces v) (sacNonces c)
tweaks <- tweakSpecs (saTweaks v) (sacTweaks c) (sacXOnly c)
psigs <- select "psig" (saPsigs v) (sacPsigs c)
pure (keys, nonces, tweaks, psigs)
aggregate = do
(keys, _, tweaks, psigs) <- inputs
expectLib "PartialSigAgg" $ do
ctx <- keyAggBytes keys tweaks
agg <- decodeF (sacAggnonce c)
ps <- traverse decodeF psigs >>= nonEmptyF "psig"
pure (partialSigAgg (newSession ctx agg (saMsg v)) ps, ctx)
detSignTests :: DetSignVectors -> [TestTree]
detSignTests v =
testCase "setup: sk derives pubkeys[0]" (do pk0 <- expectRight "vector" (at "pubkey" (dsPubkeys v) 0); sk <- secKey; assertBytes "pubkey" pk0 (encode (secKeyPubKey sk)))
: zipWith (run "valid") [0 ..] (dsValid v)
<> zipWith (run "error") [0 ..] (dsError v)
where
secKey = expectLib "secKeyFromBytes" (secKeyFromBytes (dsSk v))
run :: String -> Int -> DetSignCase -> TestTree
run section i c = case dscOutcome c of
Right (expectedNonce, expectedPsig) ->
testGroup
name
[ testCase "DeterministicSign returns the expected public nonce and partial signature" $ do
(pn, psig) <- detSign
assertBytes "pubnonce" expectedNonce (encode pn)
assertBytes "psig" expectedPsig (encode psig)
, testCase "PartialSigVerify accepts it in the session of aggothernonce and the public nonce" $ do
(keys, tweaks, m) <- inputs
(pn, psig) <- detSign
sk <- secKey
-- NonceAgg((aggothernonce, pubnonce)): aggothernonce parses as a public nonce.
other <- expectLib "decode aggothernonce" (decodeF (dscAggOtherNonce c))
ctx <- expectLib "KeyAgg" (keyAggBytes keys tweaks)
pk <- expectRight "vector" (at "pubkey" (NE.toList (keyAggPubKeys ctx)) (dscSigner c))
pk @?= secKeyPubKey sk
assertBool "partialSigVerify" (partialSigVerify (newSession ctx (nonceAgg (other :| [pn])) m) pk pn psig)
]
Left err@(InvalidContribution (Just _) "pubkey") ->
encodingCase name err $ do
keys <- contributions "pubkey" asPubKey (dsPubkeys v) (dscKeys c)
pure (keys <> [("aggothernonce", Nothing, asAggNonce (dscAggOtherNonce c))])
-- BIP-327 blames the nonce aggregator both for an invalid encoding (which
-- does not decode to an 'AggNonce') and for a half at infinity (which
-- 'deterministicSign' rejects).
Left (InvalidContribution Nothing "aggothernonce") -> testCase name $ do
(keys, tweaks, m) <- inputs
sk <- secKey
ctx <- expectLib "KeyAgg" (keyAggBytes keys tweaks)
case decode (dscAggOtherNonce c) of
Left decodeErr -> decodeErr @?= InvalidPoint
Right agg -> either Just (const Nothing) (deterministicSign sk agg ctx m (dscRand c)) @?= Just AggOtherNonceAtInfinity
Left err -> testCase name $ do
(keys, tweaks, m) <- inputs
sk <- secKey
expected <- expectRight "vector" (valueError err >>= expectedFailure tweaks Nothing)
expectFailure expected (detSignBytes sk (dscAggOtherNonce c) keys tweaks m (dscRand c))
where
name = caseName section i (dscComment c)
inputs = expectRight "vector" $ do
keys <- select "pubkey" (dsPubkeys v) (dscKeys c)
tweaks <- tweakSpecs (dscTweaks c) [0 .. length (dscTweaks c) - 1] (dscXOnly c)
m <- at "msg" (dsMsgs v) (dscMsg c)
pure (keys, tweaks, m)
detSign = do
(keys, tweaks, m) <- inputs
sk <- secKey
expectLib "DeterministicSign" (detSignBytes sk (dscAggOtherNonce c) keys tweaks m (dscRand c))