ppad-bolt4-0.0.1: lib/Lightning/Protocol/BOLT4/Process.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module: Lightning.Protocol.BOLT4.Process
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Onion packet processing for BOLT4.
module Lightning.Protocol.BOLT4.Process (
-- * Processing
process
-- * Rejection reasons
, RejectReason(..)
) where
import Data.Bits (xor)
import qualified Crypto.Curve.Secp256k1 as Secp256k1
import qualified Data.ByteString as BS
import Data.Word (Word8)
import GHC.Generics (Generic)
import Lightning.Protocol.BOLT4.Codec
import Lightning.Protocol.BOLT4.Prim
import Lightning.Protocol.BOLT4.Types
-- | Reasons for rejecting a packet.
data RejectReason
= InvalidVersion !Word8 -- ^ Version byte is not 0x00
| InvalidEphemeralKey -- ^ Malformed public key
| HmacMismatch -- ^ HMAC verification failed
| InvalidPayload !String -- ^ Malformed hop payload
deriving (Eq, Show, Generic)
-- | Process an incoming onion packet.
--
-- Takes the receiving node's private key, the incoming packet, and
-- associated data (typically the payment hash).
--
-- Returns either a rejection reason or the processing result
-- (forward to next hop or receive at final destination).
process
:: BS.ByteString -- ^ 32-byte secret key of this node
-> OnionPacket -- ^ incoming onion packet
-> BS.ByteString -- ^ associated data (payment hash)
-> Either RejectReason ProcessResult
process !secKey !packet !assocData = do
-- Step 1: Validate version
validateVersion packet
-- Step 2: Parse ephemeral public key
ephemeral <- parseEphemeralKey packet
-- Step 3: Compute shared secret
ss <- case computeSharedSecret secKey ephemeral of
Nothing -> Left InvalidEphemeralKey
Just s -> Right s
-- Step 4: Derive keys
let !muKey = deriveMu ss
!rhoKey = deriveRho ss
-- Step 5: Verify HMAC
if not (verifyPacketHmac muKey packet assocData)
then Left HmacMismatch
else pure ()
-- Step 6: Decrypt hop payloads
let !decrypted = decryptPayloads rhoKey (opHopPayloads packet)
-- Step 7: Extract payload
(payloadBytes, nextHmac, remaining) <- extractPayload decrypted
-- Step 8: Parse payload TLV
hopPayload <- case decodeHopPayload payloadBytes of
Nothing -> Left (InvalidPayload "failed to decode TLV")
Just hp -> Right hp
-- Step 9: Check if final hop
let SharedSecret ssBytes = ss
if isFinalHop nextHmac
then Right $! Receive $! ReceiveInfo
{ riPayload = hopPayload
, riSharedSecret = ssBytes
}
else do
-- Step 10: Prepare forward packet
nextPacket <- case prepareForward ephemeral ss remaining nextHmac of
Nothing -> Left InvalidEphemeralKey
Just np -> Right np
Right $! Forward $! ForwardInfo
{ fiNextPacket = nextPacket
, fiPayload = hopPayload
, fiSharedSecret = ssBytes
}
-- | Validate packet version is 0x00.
validateVersion :: OnionPacket -> Either RejectReason ()
validateVersion !packet
| opVersion packet == versionByte = Right ()
| otherwise = Left (InvalidVersion (opVersion packet))
{-# INLINE validateVersion #-}
-- | Parse and validate ephemeral public key from packet.
parseEphemeralKey :: OnionPacket -> Either RejectReason Secp256k1.Projective
parseEphemeralKey !packet =
case Secp256k1.parse_point (opEphemeralKey packet) of
Nothing -> Left InvalidEphemeralKey
Just pub -> Right pub
{-# INLINE parseEphemeralKey #-}
-- | Decrypt hop payloads by XORing with rho stream.
--
-- Generates a stream of 2*1300 bytes and XORs with hop_payloads
-- extended with 1300 zero bytes.
decryptPayloads
:: DerivedKey -- ^ rho key
-> BS.ByteString -- ^ hop_payloads (1300 bytes)
-> BS.ByteString -- ^ decrypted (2600 bytes, first 1300 useful)
decryptPayloads !rhoKey !payloads =
let !streamLen = 2 * hopPayloadsSize -- 2600 bytes
!stream = generateStream rhoKey streamLen
-- Extend payloads with zeros for the shift operation
!extended = payloads <> BS.replicate hopPayloadsSize 0
in xorBytes stream extended
{-# INLINE decryptPayloads #-}
-- | XOR two bytestrings of equal length.
xorBytes :: BS.ByteString -> BS.ByteString -> BS.ByteString
xorBytes !a !b = BS.pack (BS.zipWith xor a b)
{-# INLINE xorBytes #-}
-- | Extract payload from decrypted buffer.
--
-- Parses BigSize length prefix, extracts payload bytes and next HMAC.
extractPayload
:: BS.ByteString
-> Either RejectReason (BS.ByteString, BS.ByteString, BS.ByteString)
-- ^ (payload_bytes, next_hmac, remaining_hop_payloads)
extractPayload !decrypted = do
-- Parse length prefix
(len, afterLen) <- case decodeBigSize decrypted of
Nothing -> Left (InvalidPayload "invalid length prefix")
Just (l, r) -> Right (fromIntegral l :: Int, r)
-- Validate length
if len > BS.length afterLen
then Left (InvalidPayload "payload length exceeds buffer")
else if len == 0
then Left (InvalidPayload "zero-length payload")
else pure ()
-- Extract payload bytes
let !payloadBytes = BS.take len afterLen
!afterPayload = BS.drop len afterLen
-- Extract next HMAC (32 bytes)
if BS.length afterPayload < hmacSize
then Left (InvalidPayload "insufficient bytes for HMAC")
else do
let !nextHmac = BS.take hmacSize afterPayload
-- Remaining payloads: skip the HMAC, take first 1300 bytes
-- This is already "shifted" by the payload extraction
!remaining = BS.drop hmacSize afterPayload
Right (payloadBytes, nextHmac, remaining)
-- | Verify packet HMAC.
--
-- Computes HMAC over (hop_payloads || associated_data) using mu key
-- and compares with packet's HMAC using constant-time comparison.
verifyPacketHmac
:: DerivedKey -- ^ mu key
-> OnionPacket -- ^ packet with HMAC to verify
-> BS.ByteString -- ^ associated data
-> Bool
verifyPacketHmac !muKey !packet !assocData =
let !computed = computeHmac muKey (opHopPayloads packet) assocData
in verifyHmac (opHmac packet) computed
{-# INLINE verifyPacketHmac #-}
-- | Prepare packet for forwarding to next hop.
--
-- Computes blinded ephemeral key and constructs next OnionPacket.
prepareForward
:: Secp256k1.Projective -- ^ current ephemeral key
-> SharedSecret -- ^ shared secret (for blinding)
-> BS.ByteString -- ^ remaining hop_payloads (after shift)
-> BS.ByteString -- ^ next HMAC
-> Maybe OnionPacket
prepareForward !ephemeral !ss !remaining !nextHmac = do
-- Compute blinding factor and blind ephemeral key
let !bf = computeBlindingFactor ephemeral ss
newEphemeral <- blindPubKey ephemeral bf
-- Serialize new ephemeral key
let !newEphBytes = Secp256k1.serialize_point newEphemeral
-- Truncate remaining to exactly 1300 bytes
let !newPayloads = BS.take hopPayloadsSize remaining
-- Construct next packet
pure $! OnionPacket
{ opVersion = versionByte
, opEphemeralKey = newEphBytes
, opHopPayloads = newPayloads
, opHmac = nextHmac
}
-- | Check if this is the final hop.
--
-- Final hop is indicated by next_hmac being all zeros.
isFinalHop :: BS.ByteString -> Bool
isFinalHop !hmac = hmac == BS.replicate hmacSize 0
{-# INLINE isFinalHop #-}