packages feed

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