packages feed

musig2-0.2.0: test/AggPartials.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-unused-top-binds #-}

module AggPartials (testAggPartials) where

import Crypto.Curve.Secp256k1 (Pub)
import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), PartialSignature, PubNonce (..), Tweak (..), aggPartials, mkSessionContext)
import Data.ByteString (ByteString)
import Test.Tasty
import Test.Tasty.HUnit
import Util (decodeHex, parsePoint, parsePubNonce, parseScalar, unsafeRight)

-- | Test vector structure for signature aggregation.
data SigAggTestVector = SigAggTestVector
  { aggNonce :: PubNonce
  , nonceIndices :: [Int]
  , keyIndices :: [Int]
  , tweakIndices :: [Int]
  , isXOnly :: [Bool]
  , psigIndices :: [Int]
  , expected :: Maybe ByteString -- Nothing for error cases
  , errorCase :: Bool
  }
  deriving (Show)

-- | Global test data from BIP-0327 @sig_agg_vectors.json@
pubkeys :: [Pub]
pubkeys =
  [ parsePoint "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9"
  , parsePoint "02D2DC6F5DF7C56ACF38C7FA0AE7A759AE30E19B37359DFDE015872324C7EF6E05"
  , parsePoint "03C7FB101D97FF930ACD0C6760852EF64E69083DE0B06AC6335724754BB4B0522C"
  , parsePoint "02352433B21E7E05D3B452B81CAE566E06D2E003ECE16D1074AABA4289E0E3D581"
  ]

pnonces :: [PubNonce]
pnonces =
  [ parsePubNonce "036E5EE6E28824029FEA3E8A9DDD2C8483F5AF98F7177C3AF3CB6F47CAF8D94AE902DBA67E4A1F3680826172DA15AFB1A8CA85C7C5CC88900905C8DC8C328511B53E"
  , parsePubNonce "03E4F798DA48A76EEC1C9CC5AB7A880FFBA201A5F064E627EC9CB0031D1D58FC5103E06180315C5A522B7EC7C08B69DCD721C313C940819296D0A7AB8E8795AC1F00"
  , parsePubNonce "02C0068FD25523A31578B8077F24F78F5BD5F2422AFF47C1FADA0F36B3CEB6C7D202098A55D1736AA5FCC21CF0729CCE852575C06C081125144763C2C4C4A05C09B6"
  , parsePubNonce "031F5C87DCFBFCF330DEE4311D85E8F1DEA01D87A6F1C14CDFC7E4F1D8C441CFA40277BF176E9F747C34F81B0D9F072B1B404A86F402C2D86CF9EA9E9C69876EA3B9"
  , parsePubNonce "023F7042046E0397822C4144A17F8B63D78748696A46C3B9F0A901D296EC3406C302022B0B464292CF9751D699F10980AC764E6F671EFCA15069BBE62B0D1C62522A"
  , parsePubNonce "02D97DDA5988461DF58C5897444F116A7C74E5711BF77A9446E27806563F3B6C47020CBAD9C363A7737F99FA06B6BE093CEAFF5397316C5AC46915C43767AE867C00"
  ]

-- | Raw tweak values from BIP-0327 test vectors
tweakValues :: [Integer]
tweakValues =
  [ parseScalar "B511DA492182A91B0FFB9A98020D55F260AE86D7ECBD0399C7383D59A5F2AF7C"
  , parseScalar "A815FE049EE3C5AAB66310477FBC8BCCCAC2F3395F59F921C364ACD78A2F48DC"
  , parseScalar "75448A87274B056468B977BE06EB1E9F657577B7320B0A3376EA51FD420D18A8"
  ]

psigs :: [PartialSignature]
psigs =
  [ parseScalar "B15D2CD3C3D22B04DAE438CE653F6B4ECF042F42CFDED7C41B64AAF9B4AF53FB"
  , parseScalar "6193D6AC61B354E9105BBDC8937A3454A6D705B6D57322A5A472A02CE99FCB64"
  , parseScalar "9A87D3B79EC67228CB97878B76049B15DBD05B8158D17B5B9114D3C226887505"
  , parseScalar "66F82EA90923689B855D36C6B7E032FB9970301481B99E01CDB4D6AC7C347A15"
  , parseScalar "4F5AEE41510848A6447DCD1BBC78457EF69024944C87F40250D3EF2C25D33EFE"
  , parseScalar "DDEF427BBB847CC027BEFF4EDB01038148917832253EBC355FC33F4A8E2FCCE4"
  , parseScalar "97B890A26C981DA8102D3BC294159D171D72810FDF7C6A691DEF02F0F7AF3FDC"
  , parseScalar "53FA9E08BA5243CBCB0D797C5EE83BC6728E539EB76C2D0BF0F971EE4E909971"
  , parseScalar "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
  ]

msg :: ByteString
msg = decodeHex "599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869"

-- | Test vectors - valid cases from BIP-0327 JSON
validTestVectors :: [SigAggTestVector]
validTestVectors =
  [ SigAggTestVector
      { aggNonce = parsePubNonce "0341432722C5CD0268D829C702CF0D1CBCE57033EED201FD335191385227C3210C03D377F2D258B64AADC0E16F26462323D701D286046A2EA93365656AFD9875982B"
      , nonceIndices = [0, 1]
      , keyIndices = [0, 1]
      , tweakIndices = []
      , isXOnly = []
      , psigIndices = [0, 1]
      , expected = Just $ decodeHex "041DA22223CE65C92C9A0D6C2CAC828AAF1EEE56304FEC371DDF91EBB2B9EF0912F1038025857FEDEB3FF696F8B99FA4BB2C5812F6095A2E0004EC99CE18DE1E"
      , errorCase = False
      }
  , SigAggTestVector
      { aggNonce = parsePubNonce "0224AFD36C902084058B51B5D36676BBA4DC97C775873768E58822F87FE437D792028CB15929099EEE2F5DAE404CD39357591BA32E9AF4E162B8D3E7CB5EFE31CB20"
      , nonceIndices = [0, 2]
      , keyIndices = [0, 2]
      , tweakIndices = []
      , isXOnly = []
      , psigIndices = [2, 3]
      , expected = Just $ decodeHex "1069B67EC3D2F3C7C08291ACCB17A9C9B8F2819A52EB5DF8726E17E7D6B52E9F01800260A7E9DAC450F4BE522DE4CE12BA91AEAF2B4279219EF74BE1D286ADD9"
      , errorCase = False
      }
  , SigAggTestVector
      { aggNonce = parsePubNonce "0208C5C438C710F4F96A61E9FF3C37758814B8C3AE12BFEA0ED2C87FF6954FF186020B1816EA104B4FCA2D304D733E0E19CEAD51303FF6420BFD222335CAA402916D"
      , nonceIndices = [0, 3]
      , keyIndices = [0, 2]
      , tweakIndices = [0]
      , isXOnly = [False]
      , psigIndices = [4, 5]
      , expected = Just $ decodeHex "5C558E1DCADE86DA0B2F02626A512E30A22CF5255CAEA7EE32C38E9A71A0E9148BA6C0E6EC7683B64220F0298696F1B878CD47B107B81F7188812D593971E0CC"
      , errorCase = False
      }
  , SigAggTestVector
      { aggNonce = parsePubNonce "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD"
      , nonceIndices = [0, 4]
      , keyIndices = [0, 3]
      , tweakIndices = [0, 1, 2]
      , isXOnly = [True, False, True]
      , psigIndices = [6, 7]
      , expected = Just $ decodeHex "839B08820B681DBA8DAF4CC7B104E8F2638F9388F8D7A555DC17B6E6971D7426CE07BF6AB01F1DB50E4E33719295F4094572B79868E440FB3DEFD3FAC1DB589E"
      , errorCase = False
      }
  ]

-- | Test vectors - error cases from BIP-0327 JSON
errorTestVectors :: [SigAggTestVector]
errorTestVectors =
  [ SigAggTestVector
      { aggNonce = parsePubNonce "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD"
      , nonceIndices = [0, 4]
      , keyIndices = [0, 3]
      , tweakIndices = [0, 1, 2]
      , isXOnly = [True, False, True]
      , psigIndices = [7, 8] -- psig index 8 is invalid (exceeds group size)
      , expected = Nothing
      , errorCase = True
      }
  ]

-- | Helper to build tweaks from indices and isXOnly flags
buildTweaks :: [Int] -> [Bool] -> [Tweak]
buildTweaks = zipWith buildTweak
 where
  buildTweak i isX =
    let tweakValue = tweakValues !! i
     in if isX then XOnlyTweak tweakValue else PlainTweak tweakValue

-- | Creates a test case for valid signature aggregation.
makeValidTestCase :: Int -> SigAggTestVector -> TestTree
makeValidTestCase i SigAggTestVector{..} =
  testCase ("BIP-0327 SigAgg Valid Vector " ++ show (i + 1)) $ do
    let selectedKeys = map (pubkeys !!) keyIndices
    let selectedTweaks = buildTweaks tweakIndices isXOnly
    let selectedPsigs = map (psigs !!) psigIndices
    let ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys selectedTweaks msg
    let result = unsafeRight $ aggPartials selectedPsigs ctx
    case expected of
      Just expectedSig -> assertEqual "signature mismatch" expectedSig result
      Nothing -> assertFailure "Expected signature but got nothing"

-- | Creates a test case for error signature aggregation.
makeErrorTestCase :: Int -> SigAggTestVector -> TestTree
makeErrorTestCase i SigAggTestVector{..} =
  testCase ("BIP-0327 SigAgg Error Vector " ++ show (i + 1)) $ do
    let selectedKeys = map (pubkeys !!) keyIndices
        selectedTweaks = buildTweaks tweakIndices isXOnly
        selectedPsigs = map (psigs !!) psigIndices
        ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys selectedTweaks msg
    assertBool "Expected error case" errorCase
    aggPartials selectedPsigs ctx @?= Left PartialSignatureOutOfRange

-- | Main test group for signature aggregation.
testAggPartials :: TestTree
testAggPartials =
  testGroup
    "BIP-0327 Signature Aggregation Vectors"
    [ testGroup "Valid Cases" $ zipWith makeValidTestCase [0 ..] validTestVectors
    , testGroup "Error Cases" $ zipWith makeErrorTestCase [0 ..] errorTestVectors
    ]