packages feed

musig2-0.1.0: lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}
{-# OPTIONS_GHC -Wno-x-partial #-}

{- |
Module: Crypto.Curve.Secp256k1.MuSig2.Internal
Copyright: (c) 2025 Jose Storopoli
License: MIT
Maintainer: Jose Storopoli <jose@storopoli.com>

Internal MuSig2 functions - not part of the public API.
-}
module Crypto.Curve.Secp256k1.MuSig2.Internal (
  -- Pubkey functions
  aggPublicKeys,
  -- Key aggregation
  computeKeyAggCoef,
  getSecondKey,
  -- utils/misc
  isEvenPub,
  xBytes,
  bytesToInteger,
  integerToBytes32,
  xorByteStrings,
  encodeLen,
  hashTag,
  hashTagModQ,
  hashProjectivesTag,
) where

import Crypto.Curve.Secp256k1 (Projective, Pub, add, modQ, mul, serialize_point, _CURVE_ZERO)
import Crypto.Hash.SHA256 (hash)
import Data.Bits (shiftR, xor, (.&.))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.ByteString.Builder (toLazyByteString, word64BE)
import qualified Data.ByteString.Lazy as BSL
import Data.Foldable (Foldable (fold), find, toList)
import qualified Data.Foldable as F
import Data.Maybe (fromMaybe)
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Traversable ()

{- | Aggregates a 'Traversable' of 'Pub'keys using the
[Key Aggregation algorithm in BIP327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki).

The algorith can be briefly described as

\[
f(pk_1, \dots, pk_u) = a_i \cdot pk_i
\]

where \(pk_i\) is the \(i\)th participant's public key and \(a_i\) is the
respective public key aggregation coefficient.

== WARNING

'aggPublicKeys' do not sort the keys and aggregates public keys according to the
ordering of the 'Traversable' provided.
-}
aggPublicKeys :: (Traversable t) => t Pub -> Maybe Pub
aggPublicKeys pks
  | Seq.null pksSeq = Nothing
  | otherwise = Just $ fold1WithDefault _CURVE_ZERO (Seq.zipWith aggPk coefs pksSeq)
 where
  pksSeq = Seq.fromList (toList pks)
  coefs = fmap (`computeKeyAggCoef` pksSeq) pksSeq
  aggPk i p = mul p i -- mul takes first point then scalar
  -- Safe fold1 that handles empty sequences
  fold1WithDefault def xs = case Seq.viewl xs of
    Seq.EmptyL -> def
    x Seq.:< xs' -> F.foldl' add x xs'

{- | Computes the key aggregation coefficient from:

1. Desired key to compute the key aggregation coefficient
2. 'Seq' of 'Pub'keys
-}
computeKeyAggCoef :: Pub -> Seq Pub -> Integer
computeKeyAggCoef pk pks =
  let pk2 = getSecondKey pks
      hashKeys = hashProjectivesTag "KeyAgg list" pks
      taggedHash = hashTag "KeyAgg coefficient" (hashKeys <> serialize_point pk)
   in if pk == pk2 then 1 else modQ $ bytesToInteger taggedHash

{- | Returns the first second key that is different from the first key in
a 'Seq' of 'Pub'keys.

Returns the point at infinity, i.e. zero'th point of monoidal identity.
-}
getSecondKey :: Seq Pub -> Pub
getSecondKey pks =
  case Seq.viewl pks of
    Seq.EmptyL -> _CURVE_ZERO
    pk1 Seq.:< _ ->
      let pk2 = find (/= pk1) pks
       in fromMaybe _CURVE_ZERO pk2

-- | "Taghashes" a 'Seq' of 'Projective's by concatenating all their 'ByteString' representations together.
hashProjectivesTag :: ByteString -> Seq Projective -> ByteString
hashProjectivesTag tag ps = hashTag tag $ fold byteStrings
 where
  byteStrings = fmap serialize_point ps

{- | Tagged hashes used in [BIP327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki).

Takes a tag and a string.
-}
hashTag :: ByteString -> ByteString -> ByteString
hashTag t s = hash (taggedHash <> taggedHash <> s)
 where
  taggedHash = hash t

{- | Tagged hashes used in [BIP327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki)
modulo the curve order.

Takes a tag and a string.
-}
hashTagModQ :: ByteString -> ByteString -> ByteString
hashTagModQ t s = hashModQ taggedHash
 where
  taggedHash = hashTag t s
  hashModQ h = integerToBytes32 $ modQ $ bytesToInteger h

-- | Converts a SHA-256 'ByteString' to an 'Integer'.
bytesToInteger :: ByteString -> Integer
bytesToInteger = BS.foldl' (\acc b -> acc * 256 + fromIntegral b) 0

-- | 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]]

-- | @XOR@s two 'ByteString's of same length.
xorByteStrings :: ByteString -> ByteString -> ByteString
xorByteStrings = BS.packZipWith xor

-- | Returns the 8-byte big-endian encoding of the length of a 'ByteString'.
encodeLen :: ByteString -> ByteString
encodeLen bs = BSL.toStrict . toLazyByteString . word64BE . fromIntegral $ BS.length bs

-- | Checks if a 'Pub'key is even.
isEvenPub :: Pub -> Bool
isEvenPub pub = case BS.unpack (serialize_point pub) of
  (0x02 : _) -> True -- even y-coordinate
  (0x03 : _) -> False -- odd y-coordinate
  _ -> error "musig2 (isEvenPub): invalid compressed point format"

-- | Gets the X-coordinate from a 'Pub'lic key as 'ByteString'
xBytes :: Pub -> ByteString
xBytes pk = BS.drop 1 $ serialize_point pk