musig2-0.2.0: test/Tweak.hs
{-# 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