commit 81cf235e919f65a07689222d84d5aef283a177d7
parent b10bb01f03011c3759dfbd7d08a92dc6f37a78c5
Author: Jared Tobin <jared@jtobin.io>
Date: Sun, 25 Jan 2026 11:17:15 +0400
Merge branch 'impl/encode'
Diffstat:
1 file changed, 279 insertions(+), 8 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT3/Encode.hs b/lib/Lightning/Protocol/BOLT3/Encode.hs
@@ -8,20 +8,291 @@
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Serialization for BOLT #3 transactions and scripts.
+--
+-- Provides Bitcoin transaction serialization in both standard SegWit
+-- format (with witness data) and the stripped format used for signing.
+--
+-- == Transaction Format (SegWit)
+--
+-- * 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)
module Lightning.Protocol.BOLT3.Encode (
-- * Transaction serialization
- -- encode_tx
- -- , encode_tx_for_signing
+ encode_tx
+ , encode_htlc_tx
+ , encode_closing_tx
+ , encode_tx_for_signing
- -- * Script serialization
- -- , encode_script
- -- , encode_witness
+ -- * Witness serialization
+ , encode_witness
+ , encode_funding_witness
-- * Primitive encoding
- -- , encode_varint
- -- , encode_le32
- -- , encode_le64
+ , encode_varint
+ , encode_le32
+ , encode_le64
+ , encode_outpoint
+ , encode_output
) where
+import Data.Word (Word32, Word64)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Builder as BSB
+import qualified Data.ByteString.Lazy as BSL
import Lightning.Protocol.BOLT3.Types
+import Lightning.Protocol.BOLT3.Tx
+
+-- primitive encoding ----------------------------------------------------------
+
+-- | Encode a 32-bit value in little-endian format.
+--
+-- >>> encode_le32 0x12345678
+-- "\x78\x56\x34\x12"
+encode_le32 :: Word32 -> BS.ByteString
+encode_le32 = BSL.toStrict . BSB.toLazyByteString . BSB.word32LE
+{-# INLINE encode_le32 #-}
+
+-- | Encode a 64-bit value in little-endian format.
+--
+-- >>> encode_le64 0x123456789ABCDEF0
+-- "\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
+encode_le64 :: Word64 -> BS.ByteString
+encode_le64 = BSL.toStrict . BSB.toLazyByteString . BSB.word64LE
+{-# INLINE encode_le64 #-}
+
+-- | Encode a value as a Bitcoin varint (CompactSize).
+--
+-- Encoding scheme:
+--
+-- * 0-252: 1 byte
+-- * 253-65535: 0xFD followed by 2 bytes LE
+-- * 65536-4294967295: 0xFE followed by 4 bytes LE
+-- * larger: 0xFF followed by 8 bytes LE
+--
+-- >>> encode_varint 100
+-- "\x64"
+-- >>> encode_varint 1000
+-- "\xFD\xE8\x03"
+encode_varint :: Word64 -> BS.ByteString
+encode_varint !n
+ | n < 0xFD = BS.singleton (fromIntegral n)
+ | n <= 0xFFFF = BSL.toStrict $ BSB.toLazyByteString $
+ BSB.word8 0xFD <> BSB.word16LE (fromIntegral n)
+ | n <= 0xFFFFFFFF = BSL.toStrict $ BSB.toLazyByteString $
+ BSB.word8 0xFE <> BSB.word32LE (fromIntegral n)
+ | otherwise = BSL.toStrict $ BSB.toLazyByteString $
+ BSB.word8 0xFF <> BSB.word64LE n
+{-# INLINE encode_varint #-}
+
+-- | Encode an outpoint (txid + output index).
+--
+-- Format: 32 bytes txid (already LE in TxId) + 4 bytes output index LE
+--
+-- >>> encode_outpoint (Outpoint txid 0)
+-- <32-byte txid><4-byte index>
+encode_outpoint :: Outpoint -> BS.ByteString
+encode_outpoint !op = BSL.toStrict $ BSB.toLazyByteString $
+ BSB.byteString (unTxId $ outpoint_txid op) <>
+ BSB.word32LE (outpoint_index op)
+{-# INLINE encode_outpoint #-}
+
+-- | Encode a transaction output.
+--
+-- Format: 8 bytes value LE + varint scriptPubKey length + scriptPubKey
+--
+-- >>> encode_output (TxOutput (Satoshi 100000) script OutputToLocal)
+-- <8-byte value><varint length><scriptPubKey>
+encode_output :: TxOutput -> BS.ByteString
+encode_output !out = BSL.toStrict $ BSB.toLazyByteString $
+ let !script = unScript (txout_script out)
+ !scriptLen = fromIntegral (BS.length script) :: Word64
+ in BSB.word64LE (unSatoshi $ txout_value out) <>
+ varint_builder scriptLen <>
+ BSB.byteString script
+{-# INLINE encode_output #-}
+
+-- witness encoding ------------------------------------------------------------
+
+-- | Encode a witness stack.
+--
+-- Format: varint item count + (varint length + data) for each item
+--
+-- >>> encode_witness (Witness [sig, pubkey])
+-- <varint 2><varint sigLen><sig><varint pkLen><pubkey>
+encode_witness :: Witness -> BS.ByteString
+encode_witness (Witness !items) = BSL.toStrict $ BSB.toLazyByteString $
+ let !count = fromIntegral (length items) :: Word64
+ in varint_builder count <> mconcat (map encode_witness_item items)
+{-# INLINE encode_witness #-}
+
+-- | Encode a single witness stack item.
+encode_witness_item :: BS.ByteString -> BSB.Builder
+encode_witness_item !bs =
+ let !len = fromIntegral (BS.length bs) :: Word64
+ in varint_builder len <> BSB.byteString bs
+{-# INLINE encode_witness_item #-}
+
+-- | Encode a funding witness (2-of-2 multisig).
+--
+-- The witness stack is: @0 <sig1> <sig2> <witnessScript>@
+--
+-- Signatures must be ordered to match pubkey order in the funding script.
+--
+-- >>> encode_funding_witness sig1 sig2 fundingScript
+-- <witness with 4 items: empty, sig1, sig2, script>
+encode_funding_witness
+ :: BS.ByteString -- ^ Signature for pubkey1 (lexicographically lesser)
+ -> BS.ByteString -- ^ Signature for pubkey2 (lexicographically greater)
+ -> Script -- ^ The funding witness script
+ -> BS.ByteString
+encode_funding_witness !sig1 !sig2 (Script !witnessScript) =
+ BSL.toStrict $ BSB.toLazyByteString $
+ varint_builder 4 <>
+ encode_witness_item BS.empty <>
+ encode_witness_item sig1 <>
+ encode_witness_item sig2 <>
+ encode_witness_item witnessScript
+{-# INLINE encode_funding_witness #-}
+
+-- transaction encoding --------------------------------------------------------
+
+-- | Encode a commitment transaction (SegWit format with witness).
+--
+-- SegWit format:
+--
+-- * version (4 bytes LE)
+-- * marker (0x00)
+-- * flag (0x01)
+-- * input count (varint)
+-- * inputs
+-- * output count (varint)
+-- * outputs
+-- * witness data
+-- * locktime (4 bytes LE)
+--
+-- Note: The witness is empty (just count=0) since the commitment tx
+-- spending the funding output requires external signatures.
+encode_tx :: CommitmentTx -> BS.ByteString
+encode_tx !tx = BSL.toStrict $ BSB.toLazyByteString $
+ -- Version
+ BSB.word32LE (ctx_version tx) <>
+ -- SegWit marker and flag
+ BSB.word8 0x00 <>
+ BSB.word8 0x01 <>
+ -- Input count (always 1 for commitment tx)
+ varint_builder 1 <>
+ -- Input: outpoint + empty scriptSig + sequence
+ BSB.byteString (encode_outpoint (ctx_input_outpoint tx)) <>
+ varint_builder 0 <> -- scriptSig length (empty for SegWit)
+ BSB.word32LE (unSequence $ ctx_input_sequence tx) <>
+ -- Output count
+ varint_builder (fromIntegral $ length $ ctx_outputs tx) <>
+ -- Outputs
+ mconcat (map (BSB.byteString . encode_output) (ctx_outputs tx)) <>
+ -- Witness (empty stack for unsigned tx)
+ varint_builder 0 <>
+ -- Locktime
+ BSB.word32LE (unLocktime $ ctx_locktime tx)
+
+-- | Encode an HTLC transaction (SegWit format with witness).
+--
+-- HTLC transactions have a single input (the commitment tx HTLC output)
+-- and a single output (the to_local-style delayed output).
+encode_htlc_tx :: HTLCTx -> BS.ByteString
+encode_htlc_tx !tx = BSL.toStrict $ BSB.toLazyByteString $
+ -- Version
+ BSB.word32LE (htx_version tx) <>
+ -- SegWit marker and flag
+ BSB.word8 0x00 <>
+ BSB.word8 0x01 <>
+ -- Input count (always 1)
+ varint_builder 1 <>
+ -- Input: outpoint + empty scriptSig + sequence
+ BSB.byteString (encode_outpoint (htx_input_outpoint tx)) <>
+ varint_builder 0 <> -- scriptSig length (empty for SegWit)
+ BSB.word32LE (unSequence $ htx_input_sequence tx) <>
+ -- Output count (always 1)
+ varint_builder 1 <>
+ -- Output: value + scriptPubKey
+ BSB.word64LE (unSatoshi $ htx_output_value tx) <>
+ let !script = unScript (htx_output_script tx)
+ !scriptLen = fromIntegral (BS.length script) :: Word64
+ in varint_builder scriptLen <> BSB.byteString script <>
+ -- Witness (empty stack for unsigned tx)
+ varint_builder 0 <>
+ -- Locktime
+ BSB.word32LE (unLocktime $ htx_locktime tx)
+
+-- | Encode a closing transaction (SegWit format with witness).
+--
+-- Closing transactions have a single input (the funding output) and
+-- one or two outputs (to_local and/or to_remote).
+encode_closing_tx :: ClosingTx -> BS.ByteString
+encode_closing_tx !tx = BSL.toStrict $ BSB.toLazyByteString $
+ -- Version
+ BSB.word32LE (cltx_version tx) <>
+ -- SegWit marker and flag
+ BSB.word8 0x00 <>
+ BSB.word8 0x01 <>
+ -- Input count (always 1)
+ varint_builder 1 <>
+ -- Input: outpoint + empty scriptSig + sequence
+ BSB.byteString (encode_outpoint (cltx_input_outpoint tx)) <>
+ varint_builder 0 <> -- scriptSig length (empty for SegWit)
+ BSB.word32LE (unSequence $ cltx_input_sequence tx) <>
+ -- Output count
+ varint_builder (fromIntegral $ length $ cltx_outputs tx) <>
+ -- Outputs
+ mconcat (map (BSB.byteString . encode_output) (cltx_outputs tx)) <>
+ -- Witness (empty stack for unsigned tx)
+ varint_builder 0 <>
+ -- Locktime
+ BSB.word32LE (unLocktime $ cltx_locktime tx)
+
+-- | Encode a commitment transaction for signing (stripped format).
+--
+-- The stripped format omits the SegWit marker, flag, and witness data.
+-- This is the format used to compute the sighash for signing.
+--
+-- Format:
+--
+-- * version (4 bytes LE)
+-- * input count (varint)
+-- * inputs
+-- * output count (varint)
+-- * outputs
+-- * locktime (4 bytes LE)
+encode_tx_for_signing :: CommitmentTx -> BS.ByteString
+encode_tx_for_signing !tx = BSL.toStrict $ BSB.toLazyByteString $
+ -- Version
+ BSB.word32LE (ctx_version tx) <>
+ -- Input count (always 1 for commitment tx)
+ varint_builder 1 <>
+ -- Input: outpoint + empty scriptSig + sequence
+ BSB.byteString (encode_outpoint (ctx_input_outpoint tx)) <>
+ varint_builder 0 <> -- scriptSig length (empty for SegWit)
+ BSB.word32LE (unSequence $ ctx_input_sequence tx) <>
+ -- Output count
+ varint_builder (fromIntegral $ length $ ctx_outputs tx) <>
+ -- Outputs
+ mconcat (map (BSB.byteString . encode_output) (ctx_outputs tx)) <>
+ -- Locktime
+ BSB.word32LE (unLocktime $ ctx_locktime tx)
+
+-- internal helpers ------------------------------------------------------------
+
+-- | Build a varint directly to Builder.
+varint_builder :: Word64 -> BSB.Builder
+varint_builder !n
+ | n < 0xFD = BSB.word8 (fromIntegral n)
+ | n <= 0xFFFF = BSB.word8 0xFD <> BSB.word16LE (fromIntegral n)
+ | n <= 0xFFFFFFFF = BSB.word8 0xFE <> BSB.word32LE (fromIntegral n)
+ | otherwise = BSB.word8 0xFF <> BSB.word64LE n
+{-# INLINE varint_builder #-}