ppad-bolt3-0.0.1: lib/Lightning/Protocol/BOLT3/Decode.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
-- |
-- Module: Lightning.Protocol.BOLT3.Decode
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Parsing for BOLT #3 transactions and scripts.
--
-- Decodes SegWit Bitcoin transactions from raw bytes.
module Lightning.Protocol.BOLT3.Decode (
-- * Error types
DecodeError(..)
-- * Raw transaction type
, RawTx(..)
, RawInput(..)
, RawOutput(..)
-- * Transaction parsing
, decode_tx
-- * Witness parsing
, decode_witness
-- * Primitive decoding
, decode_varint
, decode_le32
, decode_le64
, decode_outpoint
, decode_output
) where
import Data.Bits ((.|.), shiftL)
import Data.Word (Word8, Word32, Word64)
import qualified Data.ByteString as BS
import GHC.Generics (Generic)
import Lightning.Protocol.BOLT3.Types
-- error types -----------------------------------------------------------------
-- | Errors that can occur during transaction decoding.
data DecodeError
= InsufficientBytes !Int !Int
-- ^ Expected bytes, actual bytes available
| InvalidMarker !Word8
-- ^ Invalid SegWit marker byte (expected 0x00)
| InvalidFlag !Word8
-- ^ Invalid SegWit flag byte (expected 0x01)
| InvalidVarint
-- ^ Malformed varint encoding
| EmptyInput
-- ^ No bytes to decode
deriving (Eq, Show, Generic)
-- raw transaction types -------------------------------------------------------
-- | A raw transaction input as parsed from bytes.
data RawInput = RawInput
{ ri_outpoint :: !Outpoint
, ri_script_sig :: !BS.ByteString
, ri_sequence :: !Sequence
} deriving (Eq, Show, Generic)
-- | A raw transaction output as parsed from bytes.
data RawOutput = RawOutput
{ ro_value :: !Satoshi
, ro_script :: !Script
} deriving (Eq, Show, Generic)
-- | A raw transaction as parsed from bytes.
--
-- Supports both legacy and SegWit transaction formats.
data RawTx = RawTx
{ rtx_version :: {-# UNPACK #-} !Word32
, rtx_inputs :: ![RawInput]
, rtx_outputs :: ![RawOutput]
, rtx_witness :: ![[BS.ByteString]]
-- ^ Witness stack for each input (empty list for legacy tx)
, rtx_locktime :: !Locktime
} deriving (Eq, Show, Generic)
-- primitive decoding ----------------------------------------------------------
-- | Decode a little-endian 32-bit integer.
--
-- >>> decode_le32 (BS.pack [0x01, 0x00, 0x00, 0x00])
-- Right (1, "")
decode_le32 :: BS.ByteString -> Either DecodeError (Word32, BS.ByteString)
decode_le32 !bs
| BS.length bs < 4 = Left (InsufficientBytes 4 (BS.length bs))
| otherwise =
let !b0 = fromIntegral (BS.index bs 0)
!b1 = fromIntegral (BS.index bs 1)
!b2 = fromIntegral (BS.index bs 2)
!b3 = fromIntegral (BS.index bs 3)
!val = b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16)
.|. (b3 `shiftL` 24)
!rest = BS.drop 4 bs
in Right (val, rest)
{-# INLINE decode_le32 #-}
-- | Decode a little-endian 64-bit integer.
--
-- >>> decode_le64 (BS.pack [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
-- Right (1, "")
decode_le64 :: BS.ByteString -> Either DecodeError (Word64, BS.ByteString)
decode_le64 !bs
| BS.length bs < 8 = Left (InsufficientBytes 8 (BS.length bs))
| otherwise =
let !b0 = fromIntegral (BS.index bs 0)
!b1 = fromIntegral (BS.index bs 1)
!b2 = fromIntegral (BS.index bs 2)
!b3 = fromIntegral (BS.index bs 3)
!b4 = fromIntegral (BS.index bs 4)
!b5 = fromIntegral (BS.index bs 5)
!b6 = fromIntegral (BS.index bs 6)
!b7 = fromIntegral (BS.index bs 7)
!val = b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16)
.|. (b3 `shiftL` 24) .|. (b4 `shiftL` 32)
.|. (b5 `shiftL` 40) .|. (b6 `shiftL` 48)
.|. (b7 `shiftL` 56)
!rest = BS.drop 8 bs
in Right (val, rest)
{-# INLINE decode_le64 #-}
-- | Decode a Bitcoin varint (CompactSize).
--
-- Encoding:
-- * 0x00-0xFC: 1 byte
-- * 0xFD: 2 bytes little-endian follow
-- * 0xFE: 4 bytes little-endian follow
-- * 0xFF: 8 bytes little-endian follow
--
-- >>> decode_varint (BS.pack [0x01])
-- Right (1, "")
-- >>> decode_varint (BS.pack [0xfd, 0x00, 0x01])
-- Right (256, "")
decode_varint :: BS.ByteString -> Either DecodeError (Word64, BS.ByteString)
decode_varint !bs
| BS.null bs = Left EmptyInput
| otherwise =
let !first = BS.index bs 0
!rest = BS.drop 1 bs
in case first of
0xFD -> decode_varint_16 rest
0xFE -> decode_varint_32 rest
0xFF -> decode_le64 rest
_ -> Right (fromIntegral first, rest)
{-# INLINE decode_varint #-}
-- | Decode a 16-bit varint payload.
decode_varint_16 :: BS.ByteString -> Either DecodeError (Word64, BS.ByteString)
decode_varint_16 !bs
| BS.length bs < 2 = Left (InsufficientBytes 2 (BS.length bs))
| otherwise =
let !b0 = fromIntegral (BS.index bs 0) :: Word64
!b1 = fromIntegral (BS.index bs 1) :: Word64
!val = b0 .|. (b1 `shiftL` 8)
!rest = BS.drop 2 bs
in Right (val, rest)
{-# INLINE decode_varint_16 #-}
-- | Decode a 32-bit varint payload.
decode_varint_32 :: BS.ByteString -> Either DecodeError (Word64, BS.ByteString)
decode_varint_32 !bs
| BS.length bs < 4 = Left (InsufficientBytes 4 (BS.length bs))
| otherwise =
let !b0 = fromIntegral (BS.index bs 0) :: Word64
!b1 = fromIntegral (BS.index bs 1) :: Word64
!b2 = fromIntegral (BS.index bs 2) :: Word64
!b3 = fromIntegral (BS.index bs 3) :: Word64
!val = b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16)
.|. (b3 `shiftL` 24)
!rest = BS.drop 4 bs
in Right (val, rest)
{-# INLINE decode_varint_32 #-}
-- | Decode a transaction outpoint (txid + output index).
--
-- Format: 32 bytes txid (little-endian) + 4 bytes index (little-endian)
--
-- >>> let txid = BS.replicate 32 0
-- >>> let idx = BS.pack [0x01, 0x00, 0x00, 0x00]
-- >>> decode_outpoint (txid <> idx)
-- Right (Outpoint {outpoint_txid = ..., outpoint_index = 1}, "")
decode_outpoint
:: BS.ByteString
-> Either DecodeError (Outpoint, BS.ByteString)
decode_outpoint !bs
| BS.length bs < 36 = Left (InsufficientBytes 36 (BS.length bs))
| otherwise =
let !txid = TxId (BS.take 32 bs)
!rest1 = BS.drop 32 bs
in case decode_le32 rest1 of
Left err -> Left err
Right (!idx, !rest2) ->
let !outpoint = Outpoint txid idx
in Right (outpoint, rest2)
{-# INLINE decode_outpoint #-}
-- | Decode a transaction output (value + scriptPubKey).
--
-- Format: 8 bytes value (little-endian) + varint script length + script
decode_output :: BS.ByteString -> Either DecodeError (RawOutput, BS.ByteString)
decode_output !bs = do
(!value, !rest1) <- decode_le64 bs
(!scriptLen, !rest2) <- decode_varint rest1
let !len = fromIntegral scriptLen
if BS.length rest2 < len
then Left (InsufficientBytes len (BS.length rest2))
else
let !script = Script (BS.take len rest2)
!rest3 = BS.drop len rest2
!output = RawOutput (Satoshi value) script
in Right (output, rest3)
{-# INLINE decode_output #-}
-- witness parsing -------------------------------------------------------------
-- | Decode a witness stack for one input.
--
-- Format: varint num_items + (varint length + data) for each item
decode_witness
:: BS.ByteString
-> Either DecodeError (Witness, BS.ByteString)
decode_witness !bs = do
(!numItems, !rest1) <- decode_varint bs
(!items, !rest2) <- decode_witness_items (fromIntegral numItems) rest1 []
Right (Witness items, rest2)
{-# INLINE decode_witness #-}
-- | Decode witness items recursively.
decode_witness_items
:: Int
-> BS.ByteString
-> [BS.ByteString]
-> Either DecodeError ([BS.ByteString], BS.ByteString)
decode_witness_items 0 !bs !acc = Right (reverse acc, bs)
decode_witness_items !n !bs !acc = do
(!itemLen, !rest1) <- decode_varint bs
let !len = fromIntegral itemLen
if BS.length rest1 < len
then Left (InsufficientBytes len (BS.length rest1))
else
let !item = BS.take len rest1
!rest2 = BS.drop len rest1
in decode_witness_items (n - 1) rest2 (item : acc)
-- | Decode witness stacks for all inputs (internal, returns list).
decode_witness_stacks
:: Int
-> BS.ByteString
-> [[BS.ByteString]]
-> Either DecodeError ([[BS.ByteString]], BS.ByteString)
decode_witness_stacks 0 !bs !acc = Right (reverse acc, bs)
decode_witness_stacks !n !bs !acc = do
(Witness !items, !rest) <- decode_witness bs
decode_witness_stacks (n - 1) rest (items : acc)
-- transaction parsing ---------------------------------------------------------
-- | Decode a raw Bitcoin transaction from bytes.
--
-- Handles both legacy and SegWit transaction formats.
--
-- SegWit format:
-- * version (4 bytes LE)
-- * marker (0x00) + flag (0x01)
-- * input count (varint)
-- * inputs: outpoint (32+4), scriptSig length (varint), scriptSig, sequence
-- * output count (varint)
-- * outputs: value (8 LE), scriptPubKey length (varint), scriptPubKey
-- * witness data (for each input)
-- * locktime (4 bytes LE)
--
-- >>> decode_tx rawTxBytes
-- Right (RawTx {...})
decode_tx :: BS.ByteString -> Either DecodeError RawTx
decode_tx !bs = do
-- Version (4 bytes LE)
(!version, !rest1) <- decode_le32 bs
-- Check for SegWit marker/flag
let !hasWitness = BS.length rest1 >= 2 &&
BS.index rest1 0 == 0x00 &&
BS.index rest1 1 == 0x01
if hasWitness
then decode_tx_segwit version (BS.drop 2 rest1)
else decode_tx_legacy version rest1
{-# INLINE decode_tx #-}
-- | Decode a SegWit transaction (after marker/flag consumed).
decode_tx_segwit
:: Word32
-> BS.ByteString
-> Either DecodeError RawTx
decode_tx_segwit !version !bs = do
-- Input count and inputs
(!inputCount, !rest1) <- decode_varint bs
(!inputs, !rest2) <- decode_inputs (fromIntegral inputCount) rest1 []
-- Output count and outputs
(!outputCount, !rest3) <- decode_varint rest2
(!outputs, !rest4) <- decode_outputs (fromIntegral outputCount) rest3 []
-- Witness data for each input
(!witnesses, !rest5) <- decode_witness_stacks (length inputs) rest4 []
-- Locktime (4 bytes LE)
(!locktime, !_rest6) <- decode_le32 rest5
Right RawTx
{ rtx_version = version
, rtx_inputs = inputs
, rtx_outputs = outputs
, rtx_witness = witnesses
, rtx_locktime = Locktime locktime
}
-- | Decode a legacy (non-SegWit) transaction.
decode_tx_legacy
:: Word32
-> BS.ByteString
-> Either DecodeError RawTx
decode_tx_legacy !version !bs = do
-- Input count and inputs
(!inputCount, !rest1) <- decode_varint bs
(!inputs, !rest2) <- decode_inputs (fromIntegral inputCount) rest1 []
-- Output count and outputs
(!outputCount, !rest3) <- decode_varint rest2
(!outputs, !rest4) <- decode_outputs (fromIntegral outputCount) rest3 []
-- Locktime (4 bytes LE)
(!locktime, !_rest5) <- decode_le32 rest4
Right RawTx
{ rtx_version = version
, rtx_inputs = inputs
, rtx_outputs = outputs
, rtx_witness = []
, rtx_locktime = Locktime locktime
}
-- | Decode transaction inputs recursively.
decode_inputs
:: Int
-> BS.ByteString
-> [RawInput]
-> Either DecodeError ([RawInput], BS.ByteString)
decode_inputs 0 !bs !acc = Right (reverse acc, bs)
decode_inputs !n !bs !acc = do
(!input, !rest) <- decode_input bs
decode_inputs (n - 1) rest (input : acc)
-- | Decode a single transaction input.
--
-- Format: outpoint (36 bytes) + scriptSig length (varint) + scriptSig +
-- sequence (4 bytes LE)
decode_input :: BS.ByteString -> Either DecodeError (RawInput, BS.ByteString)
decode_input !bs = do
(!outpoint, !rest1) <- decode_outpoint bs
(!scriptLen, !rest2) <- decode_varint rest1
let !len = fromIntegral scriptLen
if BS.length rest2 < len
then Left (InsufficientBytes len (BS.length rest2))
else do
let !scriptSig = BS.take len rest2
!rest3 = BS.drop len rest2
(!seqNum, !rest4) <- decode_le32 rest3
let !input = RawInput outpoint scriptSig (Sequence seqNum)
Right (input, rest4)
-- | Decode transaction outputs recursively.
decode_outputs
:: Int
-> BS.ByteString
-> [RawOutput]
-> Either DecodeError ([RawOutput], BS.ByteString)
decode_outputs 0 !bs !acc = Right (reverse acc, bs)
decode_outputs !n !bs !acc = do
(!output, !rest) <- decode_output bs
decode_outputs (n - 1) rest (output : acc)