packages feed

ppad-bolt3-0.0.1: lib/Lightning/Protocol/BOLT3/Keys.hs

{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}

-- |
-- Module: Lightning.Protocol.BOLT3.Keys
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Per-commitment key derivation per BOLT #3.
--
-- Implements key derivation formulas:
--
-- @
-- pubkey = basepoint + SHA256(per_commitment_point || basepoint) * G
-- revocationpubkey = revocation_basepoint * SHA256(revocation_basepoint
--                      || per_commitment_point)
--                  + per_commitment_point * SHA256(per_commitment_point
--                      || revocation_basepoint)
-- @

module Lightning.Protocol.BOLT3.Keys (
    -- * Per-commitment point derivation
    derive_per_commitment_point

    -- * Key derivation
  , derive_pubkey
  , derive_localpubkey
  , derive_local_htlcpubkey
  , derive_remote_htlcpubkey
  , derive_local_delayedpubkey
  , derive_remote_delayedpubkey

    -- * Revocation key derivation
  , derive_revocationpubkey

    -- * Per-commitment secret generation
  , generate_from_seed
  , derive_secret

    -- * Per-commitment secret storage
  , SecretStore
  , empty_store
  , insert_secret
  , derive_old_secret

    -- * Commitment number obscuring
  , obscured_commitment_number
  ) where

import Data.Bits ((.&.), xor, shiftL, testBit, complementBit)
import qualified Data.ByteString as BS
import Data.Word (Word64)
import GHC.Generics (Generic)
import qualified Crypto.Curve.Secp256k1 as S
import qualified Crypto.Hash.SHA256 as SHA256
import Lightning.Protocol.BOLT3.Types

-- Per-commitment point derivation ----------------------------------------

-- | Derive the per-commitment point from a per-commitment secret.
--
-- @per_commitment_point = per_commitment_secret * G@
--
-- >>> let secret = PerCommitmentSecret (BS.replicate 32 0x01)
-- >>> derive_per_commitment_point secret
-- Just (PerCommitmentPoint ...)
derive_per_commitment_point
  :: PerCommitmentSecret
  -> Maybe PerCommitmentPoint
derive_per_commitment_point (PerCommitmentSecret sec) = do
  sk <- S.parse_int256 sec
  pk <- S.derive_pub sk
  let !bs = S.serialize_point pk
  pure $! PerCommitmentPoint (Point bs)
{-# INLINE derive_per_commitment_point #-}

-- Key derivation ---------------------------------------------------------

-- | Derive a pubkey from a basepoint and per-commitment point.
--
-- @pubkey = basepoint + SHA256(per_commitment_point || basepoint) * G@
--
-- This is the general derivation formula used for localpubkey,
-- local_htlcpubkey, remote_htlcpubkey, local_delayedpubkey, and
-- remote_delayedpubkey.
--
-- >>> derive_pubkey basepoint per_commitment_point
-- Just (Pubkey ...)
derive_pubkey
  :: Point             -- ^ basepoint
  -> PerCommitmentPoint -- ^ per_commitment_point
  -> Maybe Pubkey
derive_pubkey (Point basepointBs) (PerCommitmentPoint (Point pcpBs)) = do
  basepoint <- S.parse_point basepointBs
  -- SHA256(per_commitment_point || basepoint)
  let !h = SHA256.hash (pcpBs <> basepointBs)
  -- Treat hash as scalar and multiply by G
  tweak <- S.parse_int256 h
  tweakPoint <- S.derive_pub tweak
  -- Add basepoint + tweak*G
  let !result = S.add basepoint tweakPoint
      !bs = S.serialize_point result
  pure $! Pubkey bs
{-# INLINE derive_pubkey #-}

-- | Derive localpubkey from payment_basepoint and per_commitment_point.
--
-- >>> derive_localpubkey payment_basepoint per_commitment_point
-- Just (LocalPubkey ...)
derive_localpubkey
  :: PaymentBasepoint
  -> PerCommitmentPoint
  -> Maybe LocalPubkey
derive_localpubkey (PaymentBasepoint pt) pcp =
  LocalPubkey <$> derive_pubkey pt pcp
{-# INLINE derive_localpubkey #-}

-- | Derive local_htlcpubkey from htlc_basepoint and per_commitment_point.
--
-- >>> derive_local_htlcpubkey htlc_basepoint per_commitment_point
-- Just (LocalHtlcPubkey ...)
derive_local_htlcpubkey
  :: HtlcBasepoint
  -> PerCommitmentPoint
  -> Maybe LocalHtlcPubkey
derive_local_htlcpubkey (HtlcBasepoint pt) pcp =
  LocalHtlcPubkey <$> derive_pubkey pt pcp
{-# INLINE derive_local_htlcpubkey #-}

-- | Derive remote_htlcpubkey from htlc_basepoint and per_commitment_point.
--
-- >>> derive_remote_htlcpubkey htlc_basepoint per_commitment_point
-- Just (RemoteHtlcPubkey ...)
derive_remote_htlcpubkey
  :: HtlcBasepoint
  -> PerCommitmentPoint
  -> Maybe RemoteHtlcPubkey
derive_remote_htlcpubkey (HtlcBasepoint pt) pcp =
  RemoteHtlcPubkey <$> derive_pubkey pt pcp
{-# INLINE derive_remote_htlcpubkey #-}

-- | Derive local_delayedpubkey from delayed_payment_basepoint and
-- per_commitment_point.
--
-- >>> derive_local_delayedpubkey delayed_payment_basepoint per_commitment_point
-- Just (LocalDelayedPubkey ...)
derive_local_delayedpubkey
  :: DelayedPaymentBasepoint
  -> PerCommitmentPoint
  -> Maybe LocalDelayedPubkey
derive_local_delayedpubkey (DelayedPaymentBasepoint pt) pcp =
  LocalDelayedPubkey <$> derive_pubkey pt pcp
{-# INLINE derive_local_delayedpubkey #-}

-- | Derive remote_delayedpubkey from delayed_payment_basepoint and
-- per_commitment_point.
--
-- >>> derive_remote_delayedpubkey delayed_payment_basepoint pcp
-- Just (RemoteDelayedPubkey ...)
derive_remote_delayedpubkey
  :: DelayedPaymentBasepoint
  -> PerCommitmentPoint
  -> Maybe RemoteDelayedPubkey
derive_remote_delayedpubkey (DelayedPaymentBasepoint pt) pcp =
  RemoteDelayedPubkey <$> derive_pubkey pt pcp
{-# INLINE derive_remote_delayedpubkey #-}

-- Revocation key derivation ----------------------------------------------

-- | Derive revocationpubkey from revocation_basepoint and
-- per_commitment_point.
--
-- @
-- revocationpubkey = revocation_basepoint
--                      * SHA256(revocation_basepoint || per_commitment_point)
--                  + per_commitment_point
--                      * SHA256(per_commitment_point || revocation_basepoint)
-- @
--
-- >>> derive_revocationpubkey revocation_basepoint per_commitment_point
-- Just (RevocationPubkey ...)
derive_revocationpubkey
  :: RevocationBasepoint
  -> PerCommitmentPoint
  -> Maybe RevocationPubkey
derive_revocationpubkey
  (RevocationBasepoint (Point rbpBs))
  (PerCommitmentPoint (Point pcpBs)) = do
    rbp <- S.parse_point rbpBs
    pcp <- S.parse_point pcpBs
    -- SHA256(revocation_basepoint || per_commitment_point)
    let !h1 = SHA256.hash (rbpBs <> pcpBs)
    -- SHA256(per_commitment_point || revocation_basepoint)
    let !h2 = SHA256.hash (pcpBs <> rbpBs)
    -- Multiply points by their respective scalars
    s1 <- S.parse_int256 h1
    s2 <- S.parse_int256 h2
    p1 <- S.mul rbp s1  -- revocation_basepoint * h1
    p2 <- S.mul pcp s2  -- per_commitment_point * h2
    -- Add the two points
    let !result = S.add p1 p2
        !bs = S.serialize_point result
    pure $! RevocationPubkey (Pubkey bs)
{-# INLINE derive_revocationpubkey #-}

-- Per-commitment secret generation ---------------------------------------

-- | Generate the I'th per-commitment secret from a seed.
--
-- Implements the generate_from_seed algorithm from BOLT #3:
--
-- @
-- generate_from_seed(seed, I):
--     P = seed
--     for B in 47 down to 0:
--         if B set in I:
--             flip(B) in P
--             P = SHA256(P)
--     return P
-- @
--
-- >>> generate_from_seed seed 281474976710655
-- <32-byte secret>
generate_from_seed
  :: BS.ByteString  -- ^ seed (32 bytes)
  -> Word64         -- ^ index I (max 2^48 - 1)
  -> BS.ByteString  -- ^ per-commitment secret (32 bytes)
generate_from_seed seed idx = go 47 seed where
  go :: Int -> BS.ByteString -> BS.ByteString
  go !b !p
    | b < 0 = p
    | testBit idx b =
        let !p' = flip_bit b p
            !p'' = SHA256.hash p'
        in  go (b - 1) p''
    | otherwise = go (b - 1) p
{-# INLINE generate_from_seed #-}

-- | Derive a secret from a base secret.
--
-- This is a generalization of generate_from_seed used for efficient
-- secret storage. Given a base secret whose index has bits..47 the same
-- as target index I, derive the I'th secret.
--
-- @
-- derive_secret(base, bits, I):
--     P = base
--     for B in bits - 1 down to 0:
--         if B set in I:
--             flip(B) in P
--             P = SHA256(P)
--     return P
-- @
derive_secret
  :: BS.ByteString  -- ^ base secret
  -> Int            -- ^ bits (number of trailing bits to process)
  -> Word64         -- ^ target index I
  -> BS.ByteString  -- ^ derived secret
derive_secret base bits idx = go (bits - 1) base where
  go :: Int -> BS.ByteString -> BS.ByteString
  go !b !p
    | b < 0 = p
    | testBit idx b =
        let !p' = flip_bit b p
            !p'' = SHA256.hash p'
        in  go (b - 1) p''
    | otherwise = go (b - 1) p
{-# INLINE derive_secret #-}

-- | Flip bit B in a 32-byte bytestring.
--
-- "flip(B)" alternates the (B mod 8) bit of the (B div 8) byte.
flip_bit :: Int -> BS.ByteString -> BS.ByteString
flip_bit b bs =
  let !byteIdx = b `div` 8
      !bitIdx = b `mod` 8
      !len = BS.length bs
  in  if byteIdx >= len
      then bs
      else
        let !prefix = BS.take byteIdx bs
            !byte = BS.index bs byteIdx
            !byte' = complementBit byte bitIdx
            !suffix = BS.drop (byteIdx + 1) bs
        in  prefix <> BS.singleton byte' <> suffix
{-# INLINE flip_bit #-}

-- Per-commitment secret storage ------------------------------------------

-- | Entry in the secret store: (bucket, index, secret).
data SecretEntry = SecretEntry
  { se_bucket :: {-# UNPACK #-} !Int
  , se_index  :: {-# UNPACK #-} !Word64
  , se_secret :: !BS.ByteString
  } deriving (Eq, Show, Generic)

-- | Compact storage for per-commitment secrets.
--
-- Stores up to 49 (value, index) pairs, allowing efficient derivation
-- of any previously-received secret. This is possible because for a
-- given secret on a 2^X boundary, all secrets up to the next 2^X
-- boundary can be derived from it.
newtype SecretStore = SecretStore { unSecretStore :: [SecretEntry] }
  deriving (Eq, Show, Generic)

-- | Empty secret store.
empty_store :: SecretStore
empty_store = SecretStore []
{-# INLINE empty_store #-}

-- | Determine which bucket to store a secret in based on its index.
--
-- Counts trailing zeros in the index. Returns 0-47 for normal indices,
-- or 48 if index is 0 (the seed).
where_to_put_secret :: Word64 -> Int
where_to_put_secret idx = go 0 where
  go !b
    | b > 47 = 48  -- index 0, this is the seed
    | testBit idx b = b
    | otherwise = go (b + 1)
{-# INLINE where_to_put_secret #-}

-- | Insert a secret into the store, validating against existing secrets.
--
-- Returns Nothing if the secret doesn't derive correctly from known
-- secrets (indicating the secrets weren't generated from the same seed).
--
-- >>> insert_secret secret 281474976710655 empty_store
-- Just (SecretStore ...)
insert_secret
  :: BS.ByteString  -- ^ secret (32 bytes)
  -> Word64         -- ^ index
  -> SecretStore    -- ^ current store
  -> Maybe SecretStore
insert_secret secret idx (SecretStore known) = do
  let !bucket = where_to_put_secret idx
  -- Validate: for each bucket < this bucket, check we can derive
  validated <- validateBuckets bucket known
  if validated
    then
      -- Remove entries at bucket >= this bucket, then insert
      let !known' = filter (\e -> se_bucket e < bucket) known
          !entry = SecretEntry bucket idx secret
      in  pure $! SecretStore (known' ++ [entry])
    else Nothing
  where
    validateBuckets :: Int -> [SecretEntry] -> Maybe Bool
    validateBuckets b entries = go entries where
      go [] = Just True
      go (SecretEntry entryBucket knownIdx knownSecret : rest)
        | entryBucket >= b = go rest  -- skip entries at higher buckets
        | otherwise =
            -- Check if we can derive the known secret from the new one
            let !derived = derive_secret secret b knownIdx
            in  if derived == knownSecret
                then go rest
                else Nothing
{-# INLINE insert_secret #-}

-- | Derive a previously-received secret from the store.
--
-- Iterates over known secrets to find one whose index is a prefix of
-- the target index, then derives the target secret from it.
--
-- >>> derive_old_secret 281474976710654 store
-- Just <32-byte secret>
derive_old_secret
  :: Word64       -- ^ target index
  -> SecretStore  -- ^ store
  -> Maybe BS.ByteString
derive_old_secret targetIdx (SecretStore known) = go known where
  go :: [SecretEntry] -> Maybe BS.ByteString
  go [] = Nothing
  go (SecretEntry bucket knownIdx knownSecret : rest) =
    -- Mask off the non-zero prefix of the index using the entry's bucket
    let !mask = complement ((1 `shiftL` bucket) - 1)
    in  if (targetIdx .&. mask) == knownIdx
        then Just $! derive_secret knownSecret bucket targetIdx
        else go rest

  complement :: Word64 -> Word64
  complement x = x `xor` 0xFFFFFFFFFFFFFFFF
{-# INLINE derive_old_secret #-}

-- Commitment number obscuring --------------------------------------------

-- | Calculate the obscured commitment number.
--
-- The 48-bit commitment number is obscured by XOR with the lower 48 bits
-- of SHA256(payment_basepoint from open_channel
--         || payment_basepoint from accept_channel).
--
-- >>> obscured_commitment_number local_payment_bp remote_payment_bp cn
-- <obscured value>
obscured_commitment_number
  :: PaymentBasepoint   -- ^ opener's payment_basepoint
  -> PaymentBasepoint   -- ^ accepter's payment_basepoint
  -> CommitmentNumber   -- ^ commitment number (48-bit)
  -> Word64             -- ^ obscured commitment number
obscured_commitment_number
  (PaymentBasepoint (Point openerBs))
  (PaymentBasepoint (Point accepterBs))
  (CommitmentNumber cn) =
    let !h = SHA256.hash (openerBs <> accepterBs)
        -- Extract lower 48 bits (6 bytes) from the hash
        !lower48 = extractLower48 h
        -- Mask commitment number to 48 bits
        !cn48 = cn .&. 0xFFFFFFFFFFFF
    in  cn48 `xor` lower48
{-# INLINE obscured_commitment_number #-}

-- | Extract lower 48 bits from a 32-byte hash.
--
-- Takes bytes 26-31 (last 6 bytes) and interprets as big-endian Word64.
extractLower48 :: BS.ByteString -> Word64
extractLower48 h =
  let !b0 = fromIntegral (BS.index h 26) `shiftL` 40
      !b1 = fromIntegral (BS.index h 27) `shiftL` 32
      !b2 = fromIntegral (BS.index h 28) `shiftL` 24
      !b3 = fromIntegral (BS.index h 29) `shiftL` 16
      !b4 = fromIntegral (BS.index h 30) `shiftL` 8
      !b5 = fromIntegral (BS.index h 31)
  in  b0 + b1 + b2 + b3 + b4 + b5
{-# INLINE extractLower48 #-}