packages feed

musig2-0.1.0: test/AggPartialsProperty.hs

{-# LANGUAGE OverloadedStrings #-}

module AggPartialsProperty (propertyAggPartials) where

import Crypto.Curve.Secp256k1 (derive_pub)
import Crypto.Curve.Secp256k1.MuSig2 (SecKey (..), SecNonce (..), aggNonces, aggPartials, mkSessionContext, partialSigVerify, publicNonce, sign)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Maybe (fromJust)
import Test.Tasty
import Test.Tasty.QuickCheck as QC
import Util ()

propertyAggPartials :: TestTree
propertyAggPartials =
  testGroup
    "aggPartials Properties"
    [ testProperty "Aggregated partials from valid signatures" prop_aggValidPartials
    , testProperty "Aggregation is deterministic" prop_aggDeterministic
    , testProperty "All partial sigs verify before aggregation" prop_partialsVerifyBeforeAgg
    , testProperty "Aggregation with single signer equals partial sig" prop_singleSignerAgg
    ]

-- | Property: Aggregating valid partial signatures produces a consistent result
prop_aggValidPartials :: SecNonce -> SecKey -> SecNonce -> SecKey -> ByteString -> Property
prop_aggValidPartials secNonce1 secKey1 secNonce2 secKey2 msg =
  let pubkey1 = derive_pub (case secKey1 of SecKey sk -> sk)
      pubkey2 = derive_pub (case secKey2 of SecKey sk -> sk)
      pubkeys = [pubkey1, pubkey2]
      pubNonces = [publicNonce secNonce1, publicNonce secNonce2]
      aggNonce = fromJust $ aggNonces pubNonces
      ctx = mkSessionContext aggNonce pubkeys [] msg
      partialSig1 = sign secNonce1 secKey1 ctx
      partialSig2 = sign secNonce2 secKey2 ctx
      aggregated = aggPartials [partialSig1, partialSig2] ctx
   in BS.length aggregated === 64 -- Schnorr signature is 64 bytes

-- | Property: Aggregation is deterministic - same inputs produce same output
prop_aggDeterministic :: SecNonce -> SecKey -> SecNonce -> SecKey -> ByteString -> Property
prop_aggDeterministic secNonce1 secKey1 secNonce2 secKey2 msg =
  let pubkey1 = derive_pub (case secKey1 of SecKey sk -> sk)
      pubkey2 = derive_pub (case secKey2 of SecKey sk -> sk)
      pubkeys = [pubkey1, pubkey2]
      pubNonces = [publicNonce secNonce1, publicNonce secNonce2]
      aggNonce = fromJust $ aggNonces pubNonces
      ctx = mkSessionContext aggNonce pubkeys [] msg
      partialSig1 = sign secNonce1 secKey1 ctx
      partialSig2 = sign secNonce2 secKey2 ctx
      partialSigs = [partialSig1, partialSig2]
      aggregated1 = aggPartials partialSigs ctx
      aggregated2 = aggPartials partialSigs ctx
   in aggregated1 === aggregated2

-- | Property: All partial signatures should verify individually before aggregation
prop_partialsVerifyBeforeAgg :: SecNonce -> SecKey -> SecNonce -> SecKey -> ByteString -> Property
prop_partialsVerifyBeforeAgg secNonce1 secKey1 secNonce2 secKey2 msg =
  let pubkey1 = derive_pub (case secKey1 of SecKey sk -> sk)
      pubkey2 = derive_pub (case secKey2 of SecKey sk -> sk)
      pubkeys = [pubkey1, pubkey2]
      pubNonces = [publicNonce secNonce1, publicNonce secNonce2]
      aggNonce = fromJust $ aggNonces pubNonces
      ctx = mkSessionContext aggNonce pubkeys [] msg
      partialSig1 = sign secNonce1 secKey1 ctx
      partialSig2 = sign secNonce2 secKey2 ctx
      verify1 = partialSigVerify partialSig1 pubNonces pubkeys [] msg 0
      verify2 = partialSigVerify partialSig2 pubNonces pubkeys [] msg 1
   in verify1 === True .&&. verify2 === True

-- | Property: For a single signer, aggregation should work correctly
prop_singleSignerAgg :: SecNonce -> SecKey -> ByteString -> Property
prop_singleSignerAgg secNonce secKey@(SecKey sk) msg =
  let pubkey = derive_pub sk
      pubNonce = publicNonce secNonce
      pubNonces = [pubNonce]
      pubkeys = [pubkey]
      aggNonce = fromJust $ aggNonces pubNonces
      ctx = mkSessionContext aggNonce pubkeys [] msg
      partialSig = sign secNonce secKey ctx
      aggregated = aggPartials [partialSig] ctx
      verifyResult = partialSigVerify partialSig pubNonces pubkeys [] msg 0
   in verifyResult === True .&&. BS.length aggregated === 64