musig2 0.1.2 → 0.1.3
raw patch · 5 files changed
+47/−36 lines, 5 files
Files
- CHANGELOG +10/−0
- bench/Main.hs +6/−4
- lib/Crypto/Curve/Secp256k1/MuSig2.hs +29/−30
- lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs +1/−1
- musig2.cabal +1/−1
CHANGELOG view
@@ -1,5 +1,15 @@ # Changelog +## 0.1.3 (2026-01-04)++* Performance improvements:+ - Cache `KeyAggContext` in `SessionContext` to avoid redundant EC operations+ - Replace lazy `foldl` with strict `foldl'` to prevent space leaks+ - Add strictness annotations to `SessionContext` fields+ - Replace O(n) list indexing with O(log n) `Seq.lookup` in `partialSigVerify`+ - Optimize `integerToBytes32` to eliminate intermediate list allocation+ - Fix `NFData` instances in benchmarks for accurate measurements+ ## 0.1.2 (2025-12-30) * Fix Hackage build failure by implementing `modQ` locally to support `ppad-secp256k1` 0.5.0+.
bench/Main.hs view
@@ -14,8 +14,8 @@ PubNonce (..), SecKey (..), SecNonce (..),- SecNonceGenParams,- SessionContext,+ SecNonceGenParams (..),+ SessionContext (..), Tweak (..), ) import qualified Crypto.Curve.Secp256k1.MuSig2 as M@@ -45,10 +45,12 @@ rnf ctx = rnf (M.aggregatedPubkey ctx) instance NFData SessionContext where- rnf _ = () -- SessionContext contains multiple fields, but we'll keep it simple+ rnf (SessionContext an ps ts m kc) =+ rnf an `seq` rnf ps `seq` rnf ts `seq` rnf m `seq` rnf kc instance NFData SecNonceGenParams where- rnf _ = () -- SecNonceGenParams contains multiple fields, but we'll keep it simple+ rnf (SecNonceGenParams pk sk apk mg ei) =+ rnf pk `seq` rnf sk `seq` rnf apk `seq` rnf mg `seq` rnf ei -- | Main benchmark function. main :: IO ()
lib/Crypto/Curve/Secp256k1/MuSig2.hs view
@@ -82,7 +82,7 @@ partialSigVerify, aggPartials, -- MuSig2 Session- SessionContext,+ SessionContext (..), mkSessionContext, -- Key aggregation KeyAggContext,@@ -115,7 +115,7 @@ import Data.ByteString (ByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS-import Data.Foldable (toList)+import Data.Foldable (foldl', toList) import Data.List (isPrefixOf) import Data.Maybe (fromJust, fromMaybe) import Data.Sequence (Seq)@@ -136,11 +136,9 @@ ByteString aggPartials partials ctx = let- publicKeys = pks ctx- tweaks' = tweaks ctx nonce = getSigningNonce ctx e = bytesToInteger $ getSigningHash ctx- keyCtx = if Seq.null tweaks' then mkKeyAggContext publicKeys Nothing else foldl applyTweak (mkKeyAggContext publicKeys Nothing) tweaks'+ keyCtx = cachedKeyAggCtx ctx aggPk = q keyCtx taccVal = maybe 0 getTweak $ tacc keyCtx gaccVal = gacc keyCtx@@ -174,10 +172,9 @@ sign secnonce sk ctx = let publicKeys = pks ctx- tweaks' = tweaks ctx nonce = getSigningNonce ctx e = bytesToInteger $ getSigningHash ctx- keyCtx = if Seq.null tweaks' then mkKeyAggContext publicKeys Nothing else foldl applyTweak (mkKeyAggContext publicKeys Nothing) tweaks'+ keyCtx = cachedKeyAggCtx ctx aggPk = q keyCtx oddAggPk = not $ isEvenPub aggPk gaccVal = gacc keyCtx@@ -229,9 +226,14 @@ partialSigVerify partial nonces pks tweaks msg idx = let aggNonce = fromJust $ aggNonces nonces ctx = mkSessionContext aggNonce pks tweaks msg- noncesList = toList nonces- pk = if idx < length pks then toList pks !! idx else error "musig2 (partialSigVerify): signer index out of range of the list of public keys"- pubnonce = if idx < length noncesList then noncesList !! idx else error "musig2 (partialSigVerify): signer index out of range of the list of public nonces"+ noncesSeq = Seq.fromList (toList nonces)+ pksSeq = Seq.fromList (toList pks)+ pk = case Seq.lookup idx pksSeq of+ Just p -> p+ Nothing -> error "musig2 (partialSigVerify): signer index out of range of the list of public keys"+ pubnonce = case Seq.lookup idx noncesSeq of+ Just n -> n+ Nothing -> error "musig2 (partialSigVerify): signer index out of range of the list of public nonces" in partialSigVerifyInternal partial pubnonce pk ctx {- | Verifies a 'PartialSignature'.@@ -254,8 +256,7 @@ partialSigVerifyInternal partial pubnonce pk ctx = let publicKeys = pks ctx- tweaks' = tweaks ctx- keyCtx = if Seq.null tweaks' then mkKeyAggContext publicKeys Nothing else foldl applyTweak (mkKeyAggContext publicKeys Nothing) tweaks'+ keyCtx = cachedKeyAggCtx ctx aggPk = q keyCtx oddAggPk = not $ isEvenPub aggPk gaccVal = gacc keyCtx@@ -342,14 +343,16 @@ -- | Session aggregation context that holds the relevant context for a MuSig2 signing session. data SessionContext = SessionContext- { aggNonce :: PubNonce+ { aggNonce :: !PubNonce -- ^ Aggregated 'PubNonce'.- , pks :: Seq Pub+ , pks :: !(Seq Pub) -- ^ Ordered 'Seq' of 'Pub'keys.- , tweaks :: Seq Tweak+ , tweaks :: !(Seq Tweak) -- ^ 'Seq' of 'Tweak's.- , msg :: ByteString+ , msg :: !ByteString -- ^ Message to be signed.+ , cachedKeyAggCtx :: !KeyAggContext+ -- ^ Cached 'KeyAggContext' to avoid recomputation. } {- | Creates a 'SessionContext'.@@ -390,12 +393,16 @@ Nothing -> error "musig2 (mkSessionContext): failed to aggregate public keys" Just aggPk | aggPk == _CURVE_ZERO -> error "musig2 (mkSessionContext): aggregated public key is point at infinity"- | otherwise -> SessionContext aggNonce pks' tweaks' msg+ | otherwise -> SessionContext aggNonce pks' tweaks' msg keyCtx where pks' = Seq.fromList (toList pks) tweaks' = Seq.fromList (toList tweaks) checkNeg = (< 0) . getTweak checkOrder = (>= curveOrder) . getTweak+ -- Compute and cache the KeyAggContext once+ keyCtx =+ let baseCtx = mkKeyAggContext pks' Nothing+ in if Seq.null tweaks' then baseCtx else foldl' applyTweak baseCtx tweaks' {- | Gets the signing nonce as a 'Projective' following [BIP-0327 algorithm and recommendations](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#dealing-with-infinity-in-nonce-aggregation).@@ -419,12 +426,7 @@ let aggNonce = ctx.aggNonce aggNonce' = if aggNonce.r1 == _CURVE_ZERO then PubNonce _CURVE_G aggNonce.r2 else aggNonce- -- Apply tweaks to get the correct aggregate public key- keyCtx =- if Seq.null (tweaks ctx)- then mkKeyAggContext (pks ctx) Nothing- else foldl applyTweak (mkKeyAggContext (pks ctx) Nothing) (tweaks ctx)- aggPubKey = q keyCtx+ aggPubKey = q (cachedKeyAggCtx ctx) msg = ctx.msg nonceBytes = serialize_point aggNonce'.r1 <> serialize_point aggNonce'.r2 qBytes = xBytes aggPubKey@@ -442,12 +444,7 @@ getSigningHash :: SessionContext -> ByteString getSigningHash ctx = let- -- Apply tweaks to get the correct aggregate public key- keyCtx =- if Seq.null (tweaks ctx)- then mkKeyAggContext (pks ctx) Nothing- else foldl applyTweak (mkKeyAggContext (pks ctx) Nothing) (tweaks ctx)- aggPubKey = q keyCtx+ aggPubKey = q (cachedKeyAggCtx ctx) qBytes = xBytes aggPubKey msg = ctx.msg nonce = getSigningNonce ctx@@ -708,6 +705,8 @@ aggNonces :: (Traversable t) => t PubNonce -> Maybe PubNonce aggNonces nonces | Seq.null noncesSeq = Nothing- | otherwise = Just $ foldl1 (<>) noncesSeq+ | otherwise = case Seq.viewl noncesSeq of+ Seq.EmptyL -> Nothing+ x Seq.:< xs -> Just $! foldl' (<>) x xs where noncesSeq = Seq.fromList (toList nonces)
lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs view
@@ -134,7 +134,7 @@ -- | Converts an 'Integer' to a 32-byte big-endian 'ByteString'. integerToBytes32 :: Integer -> ByteString-integerToBytes32 i = BS.pack $ reverse [fromInteger (i `shiftR` (8 * j)) .&. 0xff | j <- [0 .. 31]]+integerToBytes32 i = BS.pack [fromInteger ((i `shiftR` (8 * (31 - j))) .&. 0xff) | j <- [0 .. 31]] -- | @XOR@s two 'ByteString's of same length. xorByteStrings :: ByteString -> ByteString -> ByteString
musig2.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: musig2-version: 0.1.2+version: 0.1.3 synopsis: MuSig2 library license: MIT license-file: LICENSE