{-# OPTIONS_HADDOCK prune #-}

{-# LANGUAGE BangPatterns #-}

-- |
-- Module: Lightning.Protocol.BOLT7.Hash
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Signature hash computation for BOLT #7 messages.
--
-- These functions compute the double-SHA256 hash that is signed in each
-- message type. The hash covers the message content excluding the
-- signature field(s).

module Lightning.Protocol.BOLT7.Hash (
  -- * Signature hashes
    channelAnnouncementHash
  , nodeAnnouncementHash
  , channelUpdateHash

  -- * Checksums
  , channelUpdateChecksum
  ) where

import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Word (Word32)
import qualified Crypto.Hash.SHA256 as SHA256
import Lightning.Protocol.BOLT7.CRC32C (crc32c)
import Lightning.Protocol.BOLT7.Types (signatureLen, chainHashLen)

-- | Double SHA-256 hash (used for Lightning message signing).
doubleSha256 :: ByteString -> ByteString
doubleSha256 :: ByteString -> ByteString
doubleSha256 = ByteString -> ByteString
SHA256.hash (ByteString -> ByteString)
-> (ByteString -> ByteString) -> ByteString -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
SHA256.hash
{-# INLINE doubleSha256 #-}

-- | Compute signature hash for channel_announcement.
--
-- The hash covers the message starting at byte offset 256, which is after
-- the four 64-byte signatures (node_sig_1, node_sig_2, bitcoin_sig_1,
-- bitcoin_sig_2).
--
-- Returns the double-SHA256 hash (32 bytes).
channelAnnouncementHash :: ByteString -> ByteString
channelAnnouncementHash :: ByteString -> ByteString
channelAnnouncementHash !ByteString
msg =
  let offset :: Int
offset = Int
4 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
signatureLen
      payload :: ByteString
payload = Int -> ByteString -> ByteString
BS.drop Int
offset ByteString
msg
  in  ByteString -> ByteString
doubleSha256 ByteString
payload
{-# INLINE channelAnnouncementHash #-}

-- | Compute signature hash for node_announcement.
--
-- The hash covers the message starting after the signature field (64 bytes).
--
-- Returns the double-SHA256 hash (32 bytes).
nodeAnnouncementHash :: ByteString -> ByteString
nodeAnnouncementHash :: ByteString -> ByteString
nodeAnnouncementHash !ByteString
msg =
  let payload :: ByteString
payload = Int -> ByteString -> ByteString
BS.drop Int
signatureLen ByteString
msg
  in  ByteString -> ByteString
doubleSha256 ByteString
payload
{-# INLINE nodeAnnouncementHash #-}

-- | Compute signature hash for channel_update.
--
-- The hash covers the message starting after the signature field (64 bytes).
--
-- Returns the double-SHA256 hash (32 bytes).
channelUpdateHash :: ByteString -> ByteString
channelUpdateHash :: ByteString -> ByteString
channelUpdateHash !ByteString
msg =
  let payload :: ByteString
payload = Int -> ByteString -> ByteString
BS.drop Int
signatureLen ByteString
msg
  in  ByteString -> ByteString
doubleSha256 ByteString
payload
{-# INLINE channelUpdateHash #-}

-- | Compute checksum for channel_update.
--
-- This is the CRC-32C of the channel_update message excluding the
-- signature field (bytes 0-63) and timestamp field (bytes 96-99).
--
-- The checksum is used in the checksums_tlv of reply_channel_range.
--
-- Message layout after signature:
--   - chain_hash: 32 bytes (offset 64-95)
--   - short_channel_id: 8 bytes (offset 96-103)
--   - timestamp: 4 bytes (offset 104-107) -- EXCLUDED
--   - message_flags: 1 byte (offset 108)
--   - channel_flags: 1 byte (offset 109)
--   - cltv_expiry_delta: 2 bytes (offset 110-111)
--   - htlc_minimum_msat: 8 bytes (offset 112-119)
--   - fee_base_msat: 4 bytes (offset 120-123)
--   - fee_proportional_millionths: 4 bytes (offset 124-127)
--   - htlc_maximum_msat: 8 bytes (offset 128-135, if present)
channelUpdateChecksum :: ByteString -> Word32
channelUpdateChecksum :: ByteString -> Word32
channelUpdateChecksum !ByteString
msg =
  let -- Offset 64: chain_hash (32 bytes)
      chainHash :: ByteString
chainHash = Int -> ByteString -> ByteString
BS.take Int
chainHashLen (Int -> ByteString -> ByteString
BS.drop Int
signatureLen ByteString
msg)
      -- Offset 96: short_channel_id (8 bytes)
      scid :: ByteString
scid = Int -> ByteString -> ByteString
BS.take Int
8 (Int -> ByteString -> ByteString
BS.drop (Int
signatureLen Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
chainHashLen) ByteString
msg)
      -- Skip timestamp (4 bytes at offset 104)
      -- Offset 108 to end: rest of message
      restOffset :: Int
restOffset = Int
signatureLen Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
chainHashLen Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
8 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
4  -- 64 + 32 + 8 + 4 = 108
      rest :: ByteString
rest = Int -> ByteString -> ByteString
BS.drop Int
restOffset ByteString
msg
      -- Concatenate: chain_hash + scid + (message without sig and timestamp)
      checksumData :: ByteString
checksumData = [ByteString] -> ByteString
BS.concat [ByteString
chainHash, ByteString
scid, ByteString
rest]
  in  ByteString -> Word32
crc32c ByteString
checksumData
{-# INLINE channelUpdateChecksum #-}