bolt3

Lightning transaction and script formats, per BOLT #3 (docs.ppad.tech/bolt3).
git clone git://git.ppad.tech/bolt3.git
Log | Files | Refs | README | LICENSE

commit 7be7bbb7e4903df525c62baa2bc5ece23701f337
parent 23c984f83e3c629f3b8a7c1b123a7b298c394352
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 18 Apr 2026 21:46:22 +0800

replace vestigial tx encoding/decoding with ppad-tx

Add commitment_to_tx, htlc_to_tx, closing_to_tx conversion
functions in Tx.hs that convert bolt3 domain types to ppad-tx's
generic Tx type.

Replace Encode.hs: encode_tx, encode_htlc_tx, encode_closing_tx,
and encode_tx_for_signing now delegate to ppad-tx's to_bytes and
to_bytes_legacy via the conversion functions. encode_witness is
reimplemented locally. All primitive encoders removed.

Replace Decode.hs: decode_tx is now a thin wrapper around
ppad-tx's from_bytes. RawTx, RawInput, RawOutput, DecodeError
types and all primitive decoders removed.

Update BOLT3.hs: remove vestigial encode/decode primitive exports,
add conversion functions and re-export ppad-tx's Tx, TxIn, TxOut,
and from_bytes.

Update benchmarks: remove NFData instances for deleted types,
adapt encode/decode benchmarks for new Maybe return types.

Net effect: -506 lines, eliminating all duplicated tx
serialisation code.

Diffstat:
Mbench/Main.hs | 36+++++++++++-------------------------
Mbench/Weight.hs | 45+++++++++++++++++----------------------------
Mlib/Lightning/Protocol/BOLT3.hs | 25++++++++++---------------
Mlib/Lightning/Protocol/BOLT3/Decode.hs | 376++-----------------------------------------------------------------------------
Mlib/Lightning/Protocol/BOLT3/Encode.hs | 301++++++++++++++-----------------------------------------------------------------
Mlib/Lightning/Protocol/BOLT3/Tx.hs | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 180 insertions(+), 686 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -97,17 +97,6 @@ instance NFData ToSelfDelay where instance NFData CommitmentNumber where rnf (CommitmentNumber x) = rnf x --- Parsing types -instance NFData RawTx where - rnf (RawTx v i o w l) = - rnf v `seq` rnf i `seq` rnf o `seq` rnf w `seq` rnf l - -instance NFData RawInput where - rnf (RawInput o scr sq) = rnf o `seq` rnf scr `seq` rnf sq - -instance NFData RawOutput where - rnf (RawOutput v s) = rnf v `seq` rnf s - -- Context types instance NFData CommitmentContext where rnf ctx = rnf (cc_funding_outpoint ctx) `seq` @@ -183,14 +172,6 @@ instance NFData ValidationError where rnf NoOutputs = () rnf (TooManyOutputs a) = rnf a --- Decode errors -instance NFData DecodeError where - rnf (InsufficientBytes a b) = rnf a `seq` rnf b - rnf (InvalidMarker a) = rnf a - rnf (InvalidFlag a) = rnf a - rnf InvalidVarint = () - rnf EmptyInput = () - main :: IO () main = defaultMain [ bgroup "key derivation" [ @@ -282,14 +263,11 @@ main = defaultMain [ whnf encode_closing_tx (build_closing_tx sampleClosingContext) ] , bgroup "parsing" [ - env (pure $ encode_tx $ build_commitment_tx $ - mkCommitmentContext htlcs0 noAnchors) + env (pure $ encodeTx0 htlcs0 noAnchors) $ \bs -> bench "decode_tx (0 htlcs)" $ whnf decode_tx bs - , env (pure $ encode_tx $ build_commitment_tx $ - mkCommitmentContext htlcs10 noAnchors) + , env (pure $ encodeTx0 htlcs10 noAnchors) $ \bs -> bench "decode_tx (10 htlcs)" $ whnf decode_tx bs - , env (pure $ encode_tx $ build_commitment_tx $ - mkCommitmentContext htlcs100 noAnchors) + , env (pure $ encodeTx0 htlcs100 noAnchors) $ \bs -> bench "decode_tx (100 htlcs)" $ whnf decode_tx bs ] , bgroup "validation" [ @@ -381,6 +359,14 @@ main = defaultMain [ 0x5e, 0xaf, 0x46, 0x7a, 0x4a, 0x1c, 0x2a, 0x45, 0xce, 0x14, 0x86] samplePubkey3 = samplePubkey1 + -- Helper to encode a commitment tx for decode benchmarks + encodeTx0 :: [HTLC] -> ChannelFeatures -> BS.ByteString + encodeTx0 htlcs features = + case encode_tx (build_commitment_tx + (mkCommitmentContext htlcs features)) of + Nothing -> BS.empty + Just bs -> bs + -- Funding outpoint sampleFundingOutpoint :: OutPoint sampleFundingOutpoint = OutPoint (TxId $ BS.replicate 32 0x01) 0 diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -134,17 +134,6 @@ instance NFData ToSelfDelay where instance NFData CommitmentNumber where rnf (CommitmentNumber x) = rnf x --- Parsing types -instance NFData RawTx where - rnf (RawTx v i o w l) = - rnf v `seq` rnf i `seq` rnf o `seq` rnf w `seq` rnf l - -instance NFData RawInput where - rnf (RawInput o scr sq) = rnf o `seq` rnf scr `seq` rnf sq - -instance NFData RawOutput where - rnf (RawOutput v s) = rnf v `seq` rnf s - -- Context types instance NFData CommitmentContext where rnf ctx = rnf (cc_funding_outpoint ctx) `seq` @@ -167,14 +156,6 @@ instance NFData ClosingContext where rnf (clc_local_amount ctx) `seq` rnf (clc_remote_amount ctx) --- Error types -instance NFData DecodeError where - rnf (InsufficientBytes e a) = rnf e `seq` rnf a - rnf (InvalidMarker w) = rnf w - rnf (InvalidFlag w) = rnf w - rnf InvalidVarint = () - rnf EmptyInput = () - instance NFData ValidationError where rnf (InvalidVersion e a) = rnf e `seq` rnf a rnf (InvalidLocktime lt) = rnf lt @@ -266,11 +247,14 @@ main = mainWith $ do -- Serialization allocations func "encode_tx (0 htlcs)" - encode_tx (build_commitment_tx $ mkCommitmentContext htlcs0 noAnchors) + encode_tx (build_commitment_tx $ + mkCommitmentContext htlcs0 noAnchors) func "encode_tx (10 htlcs)" - encode_tx (build_commitment_tx $ mkCommitmentContext htlcs10 noAnchors) + encode_tx (build_commitment_tx $ + mkCommitmentContext htlcs10 noAnchors) func "encode_tx (100 htlcs)" - encode_tx (build_commitment_tx $ mkCommitmentContext htlcs100 noAnchors) + encode_tx (build_commitment_tx $ + mkCommitmentContext htlcs100 noAnchors) func "encode_htlc_tx" encode_htlc_tx (build_htlc_timeout_tx sampleHtlcContext) func "encode_closing_tx" @@ -278,14 +262,11 @@ main = mainWith $ do -- Parsing allocations func "decode_tx (0 htlcs)" - decode_tx (encode_tx $ build_commitment_tx $ - mkCommitmentContext htlcs0 noAnchors) + decode_tx (encodeTx0 htlcs0 noAnchors) func "decode_tx (10 htlcs)" - decode_tx (encode_tx $ build_commitment_tx $ - mkCommitmentContext htlcs10 noAnchors) + decode_tx (encodeTx0 htlcs10 noAnchors) func "decode_tx (100 htlcs)" - decode_tx (encode_tx $ build_commitment_tx $ - mkCommitmentContext htlcs100 noAnchors) + decode_tx (encodeTx0 htlcs100 noAnchors) -- Validation allocations func "validate_commitment_tx (valid)" @@ -372,6 +353,14 @@ main = mainWith $ do 0x5e, 0xaf, 0x46, 0x7a, 0x4a, 0x1c, 0x2a, 0x45, 0xce, 0x14, 0x86] samplePubkey3 = samplePubkey1 + -- Helper to encode a commitment tx for decode benchmarks + encodeTx0 :: [HTLC] -> ChannelFeatures -> BS.ByteString + encodeTx0 htlcs features = + case encode_tx (build_commitment_tx + (mkCommitmentContext htlcs features)) of + Nothing -> BS.empty + Just bs -> bs + -- Funding outpoint sampleFundingOutpoint :: OutPoint sampleFundingOutpoint = OutPoint (TxId $ BS.replicate 32 0x01) 0 diff --git a/lib/Lightning/Protocol/BOLT3.hs b/lib/Lightning/Protocol/BOLT3.hs @@ -235,31 +235,25 @@ module Lightning.Protocol.BOLT3 ( -- ** Output ordering , sort_outputs + -- * Conversion to ppad-tx + , commitment_to_tx + , htlc_to_tx + , closing_to_tx + -- * Serialization , encode_tx , encode_htlc_tx , encode_closing_tx , encode_tx_for_signing - , encode_varint - , encode_le32 - , encode_le64 - , encode_outpoint - , encode_output , encode_witness , encode_funding_witness -- * Parsing - , DecodeError(..) - , RawTx(..) - , RawInput(..) - , RawOutput(..) + , BT.Tx(..) + , BT.TxIn(..) + , BT.TxOut(BT.TxOut) + , BT.from_bytes , decode_tx - , decode_varint - , decode_le32 - , decode_le64 - , decode_outpoint - , decode_output - , decode_witness -- * Validation , ValidationError(..) @@ -278,6 +272,7 @@ module Lightning.Protocol.BOLT3 ( , validate_htlc_fee ) where +import qualified Bitcoin.Prim.Tx as BT import Lightning.Protocol.BOLT3.Types import Lightning.Protocol.BOLT3.Keys import Lightning.Protocol.BOLT3.Scripts diff --git a/lib/Lightning/Protocol/BOLT3/Decode.hs b/lib/Lightning/Protocol/BOLT3/Decode.hs @@ -1,6 +1,4 @@ {-# OPTIONS_HADDOCK prune #-} -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} -- | -- Module: Lightning.Protocol.BOLT3.Decode @@ -8,382 +6,22 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> -- --- Parsing for BOLT #3 transactions and scripts. +-- Parsing for BOLT #3 transactions. -- --- Decodes SegWit Bitcoin transactions from raw bytes. +-- Delegates to ppad-tx for transaction decoding. 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 + decode_tx ) where -import Data.Bits ((.|.), shiftL) -import Data.Word (Word8, Word32, Word64) +import qualified Bitcoin.Prim.Tx as BT 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 tid = BS.replicate 32 0 --- >>> let idx = BS.pack [0x01, 0x00, 0x00, 0x00] --- >>> decode_outpoint (tid <> idx) --- Right (OutPoint {op_txid = ..., op_vout = 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 !tid = 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 tid 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) +-- Just (Tx {...}) +decode_tx :: BS.ByteString -> Maybe BT.Tx +decode_tx = BT.from_bytes diff --git a/lib/Lightning/Protocol/BOLT3/Encode.hs b/lib/Lightning/Protocol/BOLT3/Encode.hs @@ -9,19 +9,7 @@ -- -- 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) +-- Delegates to ppad-tx for transaction encoding. module Lightning.Protocol.BOLT3.Encode ( -- * Transaction serialization @@ -33,267 +21,82 @@ module Lightning.Protocol.BOLT3.Encode ( -- * Witness serialization , encode_witness , encode_funding_witness - - -- * Primitive encoding - , encode_varint - , encode_le32 - , encode_le64 - , encode_outpoint - , encode_output ) where -import Data.Word (Word32, Word64) +import qualified Bitcoin.Prim.Tx as BT import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as BSB -import qualified Data.ByteString.Lazy as BSL +import Data.Word (Word64) 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 #-} +-- transaction encoding -------------------------------------------------------- --- | Encode a 64-bit value in little-endian format. +-- | Encode a commitment transaction (SegWit 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 #-} +-- Returns 'Nothing' if the transaction has no outputs. +encode_tx :: CommitmentTx -> Maybe BS.ByteString +encode_tx = fmap BT.to_bytes . commitment_to_tx --- | 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 HTLC transaction (SegWit format). +encode_htlc_tx :: HTLCTx -> BS.ByteString +encode_htlc_tx = BT.to_bytes . htlc_to_tx --- | Encode an outpoint (txid + output index). +-- | Encode a closing transaction (SegWit format). -- --- 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 $ - let !(TxId bs) = op_txid op - in BSB.byteString bs <> - BSB.word32LE (op_vout op) -{-# INLINE encode_outpoint #-} +-- Returns 'Nothing' if the transaction has no outputs. +encode_closing_tx :: ClosingTx -> Maybe BS.ByteString +encode_closing_tx = fmap BT.to_bytes . closing_to_tx --- | Encode a transaction output. --- --- Format: 8 bytes value LE + varint scriptPubKey length + scriptPubKey +-- | Encode a commitment transaction for signing (stripped +-- format, no witness). -- --- >>> 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 #-} +-- Returns 'Nothing' if the transaction has no outputs. +encode_tx_for_signing + :: CommitmentTx -> Maybe BS.ByteString +encode_tx_for_signing = + fmap BT.to_bytes_legacy . commitment_to_tx -- 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> +-- Format: varint item count, then for each item: +-- varint length followed by item data. 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_witness (Witness !items) = + BT.to_strict $ + put_varint (fromIntegral (length items)) + <> foldMap put_item items + where + put_item :: BS.ByteString -> BSB.Builder + put_item !bs = + put_varint (fromIntegral (BS.length bs)) + <> BSB.byteString bs + +-- | Encode a varint to a 'BSB.Builder'. +put_varint :: Word64 -> BSB.Builder +put_varint !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 put_varint #-} -- | 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> +-- Signatures must be ordered to match pubkey order in the +-- funding script. encode_funding_witness - :: BS.ByteString -- ^ Signature for pubkey1 (lexicographically lesser) - -> BS.ByteString -- ^ Signature for pubkey2 (lexicographically greater) + :: BS.ByteString -- ^ Signature for lesser pubkey + -> BS.ByteString -- ^ Signature for greater pubkey -> 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 #-} +encode_funding_witness !sig1 !sig2 (Script !ws) = + encode_witness + (Witness [BS.empty, sig1, sig2, ws]) diff --git a/lib/Lightning/Protocol/BOLT3/Tx.hs b/lib/Lightning/Protocol/BOLT3/Tx.hs @@ -36,6 +36,11 @@ module Lightning.Protocol.BOLT3.Tx ( , build_closing_tx , build_legacy_closing_tx + -- * Conversion to ppad-tx + , commitment_to_tx + , htlc_to_tx + , closing_to_tx + -- * Transaction outputs , TxOutput(..) , OutputType(..) @@ -56,8 +61,11 @@ module Lightning.Protocol.BOLT3.Tx ( , sort_outputs ) where +import qualified Bitcoin.Prim.Tx as BT import Data.Bits ((.&.), (.|.), shiftL, shiftR) +import qualified Data.ByteString as BS import Data.List (sortBy) +import Data.List.NonEmpty (NonEmpty(..), nonEmpty) import Data.Word (Word32, Word64) import GHC.Generics (Generic) import Lightning.Protocol.BOLT3.Keys @@ -558,6 +566,81 @@ untrimmed_htlcs dust feerate features = filter (not . is_trimmed dust feerate features) {-# INLINE untrimmed_htlcs #-} +-- conversion to ppad-tx ------------------------------------------------------- + +-- | Convert a 'TxOutput' to a ppad-tx 'BT.TxOut'. +toTxOut :: TxOutput -> BT.TxOut +toTxOut o = BT.TxOut + { BT.txout_value = + unSatoshi (txout_value o) + , BT.txout_script_pubkey = + unScript (txout_script o) + } +{-# INLINE toTxOut #-} + +-- | Convert a commitment transaction to a ppad-tx 'BT.Tx'. +-- +-- Returns 'Nothing' if the transaction has no outputs. +commitment_to_tx :: CommitmentTx -> Maybe BT.Tx +commitment_to_tx ctx = do + outs <- nonEmpty (map toTxOut (ctx_outputs ctx)) + let !input = BT.TxIn + { BT.txin_prevout = ctx_input_outpoint ctx + , BT.txin_script_sig = BS.empty + , BT.txin_sequence = + unSequence (ctx_input_sequence ctx) + } + pure $! BT.Tx + { BT.tx_version = ctx_version ctx + , BT.tx_inputs = input :| [] + , BT.tx_outputs = outs + , BT.tx_witnesses = [] + , BT.tx_locktime = unLocktime (ctx_locktime ctx) + } + +-- | Convert an HTLC transaction to a ppad-tx 'BT.Tx'. +htlc_to_tx :: HTLCTx -> BT.Tx +htlc_to_tx htx = + let !input = BT.TxIn + { BT.txin_prevout = htx_input_outpoint htx + , BT.txin_script_sig = BS.empty + , BT.txin_sequence = + unSequence (htx_input_sequence htx) + } + !output = BT.TxOut + { BT.txout_value = + unSatoshi (htx_output_value htx) + , BT.txout_script_pubkey = + unScript (htx_output_script htx) + } + in BT.Tx + { BT.tx_version = htx_version htx + , BT.tx_inputs = input :| [] + , BT.tx_outputs = output :| [] + , BT.tx_witnesses = [] + , BT.tx_locktime = unLocktime (htx_locktime htx) + } + +-- | Convert a closing transaction to a ppad-tx 'BT.Tx'. +-- +-- Returns 'Nothing' if the transaction has no outputs. +closing_to_tx :: ClosingTx -> Maybe BT.Tx +closing_to_tx ctx = do + outs <- nonEmpty (map toTxOut (cltx_outputs ctx)) + let !input = BT.TxIn + { BT.txin_prevout = cltx_input_outpoint ctx + , BT.txin_script_sig = BS.empty + , BT.txin_sequence = + unSequence (cltx_input_sequence ctx) + } + pure $! BT.Tx + { BT.tx_version = cltx_version ctx + , BT.tx_inputs = input :| [] + , BT.tx_outputs = outs + , BT.tx_witnesses = [] + , BT.tx_locktime = unLocktime (cltx_locktime ctx) + } + -- output ordering ------------------------------------------------------------- -- | Sort outputs per BOLT #3 ordering.