packages feed

musig2-0.1.0: test/AggPubkeys.hs

{-# LANGUAGE OverloadedStrings #-}

module AggPubkeys (testAggPubkeys) where

import Control.Exception (ErrorCall (..), evaluate, try)
import Crypto.Curve.Secp256k1 (Pub)
import Crypto.Curve.Secp256k1.MuSig2 (Tweak (..), aggregatedPubkey, mkKeyAggContext)
import Data.ByteString (ByteString)
import Test.Tasty
import Test.Tasty.HUnit
import Util (decodeHex, extractXOnly, parsePoint)

-- | Input public keys from BIP327 test vectors
inputPubkeys :: [Pub]
inputPubkeys =
  map
    parsePoint
    [ "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"
    , "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"
    , "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66"
    , "020000000000000000000000000000000000000000000000000000000000000005"
    , "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"
    , "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"
    , "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"
    ]

-- | Test vector data: (input key indices, expected x-only result).
testVectors :: [([Int], ByteString)]
testVectors =
  [ ([0, 1, 2], decodeHex "90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C")
  , ([2, 1, 0], decodeHex "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B")
  , ([0, 0, 0], decodeHex "B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935")
  , ([0, 0, 1, 1], decodeHex "69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E")
  ]

-- | Tweak test vectors from BIP327.
tweaks :: [Integer]
tweaks =
  [ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -- curve order (invalid)
  , 0x252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B -- tweak that causes infinity
  ]

-- | Error test cases: (key indices, tweak index, is_xonly, expected error message).
errorTestVectors :: [([Int], Int, Bool, String)]
errorTestVectors =
  [ ([0, 1], 0, True, "tweak must be less than curve order") -- Tweak is out of range
  , ([6], 1, False, "result of tweaking cannot be infinity") -- Intermediate tweaking result is point at infinity
  ]

-- | Creates test case from vector data.
makeTestCase :: Int -> ([Int], ByteString) -> TestTree
makeTestCase i (indices, expected) =
  testCase ("BIP327 test vector " <> show (i + 1)) $
    extractXOnly aggPk @=? expected
 where
  selectedKeys = map (inputPubkeys !!) indices
  keyAggCtx = mkKeyAggContext selectedKeys Nothing
  aggPk = aggregatedPubkey keyAggCtx

-- | Creates error test case from vector data.
makeErrorTestCase :: Int -> ([Int], Int, Bool, String) -> TestTree
makeErrorTestCase i (keyIndices, tweakIndex, isXOnly, expectedMsg) =
  testCase ("BIP327 error test vector " <> show (i + 1)) $ do
    result <- try $ evaluate $ mkKeyAggContext selectedKeys (Just tweak)
    case result of
      Left (ErrorCall msg) -> assertBool ("Expected '" <> expectedMsg <> "' in error message, got: " <> msg) (expectedMsg `isSubsequenceOf` msg)
      Right _ -> assertFailure "Expected error but got success"
 where
  selectedKeys = map (inputPubkeys !!) keyIndices
  tweakVal = tweaks !! tweakIndex
  tweak = if isXOnly then XOnlyTweak tweakVal else PlainTweak tweakVal

  isSubsequenceOf :: String -> String -> Bool
  isSubsequenceOf [] _ = True
  isSubsequenceOf _ [] = False
  isSubsequenceOf (x : xs) (y : ys)
    | x == y = isSubsequenceOf xs ys
    | otherwise = isSubsequenceOf (x : xs) ys

-- | Test vectors from [BIP327 `key_agg_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/key_agg_vectors.json).
testAggPubkeys :: TestTree
testAggPubkeys =
  testGroup "aggregating pubkeys" $
    zipWith makeTestCase [0 ..] testVectors
      <> zipWith makeErrorTestCase [0 ..] errorTestVectors