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