tx

Minimal Bitcoin transaction primitives (docs.ppad.tech/tx).
git clone git://git.ppad.tech/tx.git
Log | Files | Refs | README | LICENSE

commit 500140d29936670be7064731f40961a0915aebce
parent 46b3d68a099ce6d0616218a11c20ceb670833544
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 18:16:25 +0400

Polish: add Haddock examples and fix line lengths

- Add documentation examples to Tx.hs public API:
  to_bytes, from_bytes, to_bytes_legacy, to_base16, from_base16, txid
- Improve sighash_legacy and sighash_segwit documentation
- Fix 4 lines exceeding 80 characters in Sighash.hs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mlib/Bitcoin/Prim/Tx.hs | 44+++++++++++++++++++++++++++++++++++++++++---
Mlib/Bitcoin/Prim/Tx/Sighash.hs | 36++++++++++++++++++++++--------------
2 files changed, 63 insertions(+), 17 deletions(-)

diff --git a/lib/Bitcoin/Prim/Tx.hs b/lib/Bitcoin/Prim/Tx.hs @@ -91,6 +91,11 @@ data Tx = Tx -- | Serialise a transaction to bytes. -- -- Uses segwit format if witnesses are present, legacy otherwise. +-- +-- @ +-- -- round-trip +-- from_bytes (to_bytes tx) == Just tx +-- @ to_bytes :: Tx -> BS.ByteString to_bytes tx@Tx {..} | null tx_witnesses = to_bytes_legacy tx @@ -107,7 +112,15 @@ to_bytes tx@Tx {..} -- | Serialise a transaction to legacy format (no witness data). -- --- Used for txid computation. +-- Used for txid computation. Excludes witness data even if present. +-- +-- @ +-- -- for legacy tx (no witnesses), same as to_bytes +-- to_bytes_legacy legacyTx == to_bytes legacyTx +-- +-- -- for segwit tx, strips witnesses +-- BS.length (to_bytes_legacy segwitTx) < BS.length (to_bytes segwitTx) +-- @ to_bytes_legacy :: Tx -> BS.ByteString to_bytes_legacy Tx {..} = to_strict $ put_word32_le tx_version @@ -117,11 +130,20 @@ to_bytes_legacy Tx {..} = to_strict $ <> foldMap put_txout tx_outputs <> put_word32_le tx_locktime --- | Serialise a transaction to base16. +-- | Serialise a transaction to base16 (hex). +-- +-- @ +-- to_base16 tx = B16.encode (to_bytes tx) +-- @ to_base16 :: Tx -> BS.ByteString to_base16 tx = B16.encode (to_bytes tx) --- | Parse a transaction from base16. +-- | Parse a transaction from base16 (hex). +-- +-- @ +-- -- round-trip +-- from_base16 (to_base16 tx) == Just tx +-- @ from_base16 :: BS.ByteString -> Maybe Tx from_base16 b16 = do bs <- B16.decode b16 @@ -201,6 +223,13 @@ put_witness (Witness items) = -- -- Automatically detects segwit vs legacy format by checking for -- marker byte 0x00 followed by flag 0x01 after the version field. +-- +-- Returns 'Nothing' on invalid or truncated input. +-- +-- @ +-- -- round-trip +-- from_bytes (to_bytes tx) == Just tx +-- @ from_bytes :: BS.ByteString -> Maybe Tx from_bytes !bs = do -- need at least 4 bytes for version @@ -421,5 +450,14 @@ get_many getter !bs = go [] -- txid ------------------------------------------------------------------------ -- | Compute the transaction ID (double SHA256 of legacy serialisation). +-- +-- The txid is computed from the legacy serialisation, so segwit +-- transactions have the same txid regardless of witness data. +-- +-- @ +-- -- Satoshi->Hal tx (block 170) +-- txid satoshiHalTx == +-- TxId "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16" +-- @ txid :: Tx -> TxId txid tx = TxId (SHA256.hash (SHA256.hash (to_bytes_legacy tx))) diff --git a/lib/Bitcoin/Prim/Tx/Sighash.hs b/lib/Bitcoin/Prim/Tx/Sighash.hs @@ -93,17 +93,21 @@ hash256 :: BS.ByteString -> BS.ByteString hash256 = SHA256.hash . SHA256.hash {-# INLINE hash256 #-} --- legacy sighash --------------------------------------------------------------- +-- legacy sighash ------------------------------------------------------------- --- | Compute legacy sighash. +-- | Compute legacy sighash for P2PKH/P2SH inputs. -- -- Modifies a copy of the transaction based on sighash flags, appends -- the sighash type as 4-byte little-endian, and double SHA256s. -- --- >>> let tx = ... -- some transaction --- >>> let spk = ... -- scriptPubKey being spent --- >>> sighash_legacy tx 0 spk SIGHASH_ALL --- <32-byte hash> +-- @ +-- -- sign input 0 with SIGHASH_ALL +-- let hash = sighash_legacy tx 0 scriptPubKey SIGHASH_ALL +-- -- use hash with ECDSA signing +-- @ +-- +-- For SIGHASH_SINGLE with input index >= output count, returns the +-- special \"sighash single bug\" value (0x01 followed by 31 zero bytes). sighash_legacy :: Tx -> Int -- ^ input index @@ -146,7 +150,8 @@ modify_tx_legacy Tx{..} !idx !script_pubkey !sighash_type = zero_other_sequences !_ [] = [] zero_other_sequences !i (inp : rest) | i == idx = inp : zero_other_sequences (i + 1) rest - | otherwise = inp { txin_sequence = 0 } : zero_other_sequences (i + 1) rest + | otherwise = + inp { txin_sequence = 0 } : zero_other_sequences (i + 1) rest -- Process inputs based on sighash type !inputs_cleared = clear_scripts 0 tx_inputs @@ -216,17 +221,20 @@ put_txin_legacy TxIn{..} = <> put_word32_le txin_sequence {-# INLINE put_txin_legacy #-} --- BIP143 segwit sighash -------------------------------------------------------- +-- BIP143 segwit sighash ------------------------------------------------------- -- | Compute BIP143 segwit sighash. -- --- Required for signing segwit inputs (P2WPKH, P2WSH). +-- Required for signing segwit inputs (P2WPKH, P2WSH). Unlike legacy +-- sighash, this commits to the value being spent, preventing fee +-- manipulation attacks. -- --- >>> let tx = ... -- some transaction --- >>> let sc = ... -- scriptCode --- >>> let val = 50000 -- value in satoshis --- >>> sighash_segwit tx 0 sc val SIGHASH_ALL --- <32-byte hash> +-- @ +-- -- sign P2WPKH input 0 +-- let scriptCode = ... -- P2WPKH scriptCode +-- let hash = sighash_segwit tx 0 scriptCode inputValue SIGHASH_ALL +-- -- use hash with ECDSA signing +-- @ sighash_segwit :: Tx -> Int -- ^ input index