commit 0f09078de350d0965f1039ac39b26f288530742b
parent 25defad8bbaddc05c969872947968e2de35be3fe
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 19:02:19 +0400
Merge impl/api-0.2: abstract TxId, per-input witnesses
Breaking changes for 0.2.0:
- TxId is abstract (mk_txid, un_txid, null_txid).
- Witnesses live on their inputs (txin_witness); Tx no longer has a
tx_witnesses list that could disagree with its inputs.
- The serialisation builders are internal.
Diffstat:
9 files changed, 338 insertions(+), 263 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
@@ -1,6 +1,14 @@
# Changelog
- 0.2.0 (UNRELEASED)
+ * Breaking: TxId is abstract. Construct one with mk_txid (replacing
+ mkTxId), unwrap it with un_txid; null_txid is the all-zero txid
+ coinbase inputs reference.
+ * Breaking: each TxIn carries its own witness (txin_witness), and
+ Tx's tx_witnesses field is gone, so a transaction can no longer
+ hold a different number of witnesses than inputs.
+ * Breaking: the serialisation builders (put_compact, put_outpoint,
+ to_strict, etc.) are no longer exported.
* Widens sighash_legacy and sighash_segwit to take a raw 32-bit
hashType, matching consensus (the full value is committed to the
preimage; only the low byte determines behaviour). The SighashType
diff --git a/README.md b/README.md
@@ -38,8 +38,7 @@ A sample GHCi session:
> let Just tx = from_base16 raw
>
> -- txids are displayed byte-reversed
- > let TxId tid = txid tx
- > B16.encode (BS.reverse tid)
+ > B16.encode (BS.reverse (un_txid (txid tx)))
"f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"
>
> from_bytes (to_bytes tx) == Just tx
diff --git a/bench/Main.hs b/bench/Main.hs
@@ -11,13 +11,14 @@ import Data.Word (Word64)
import Bitcoin.Prim.Tx
import Bitcoin.Prim.Tx.Sighash
--- NFData instances ------------------------------------------------------------
+-- NFData instances -----------------------------------------------------------
--- sample data -----------------------------------------------------------------
+-- sample data ----------------------------------------------------------------
-- | Sample outpoint (references a dummy txid).
sampleOutPoint :: OutPoint
-sampleOutPoint = OutPoint (TxId (BS.replicate 32 0xab)) 0
+sampleOutPoint =
+ OutPoint (maybe null_txid id (mk_txid (BS.replicate 32 0xab))) 0
-- | Sample input with typical P2PKH signature (~107 bytes).
sampleInput :: TxIn
@@ -25,14 +26,16 @@ sampleInput = TxIn
{ txin_prevout = sampleOutPoint
, txin_script_sig = BS.replicate 107 0x00 -- typical P2PKH sig
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
--- | Sample input for segwit (empty scriptSig).
+-- | Sample input for segwit (empty scriptSig, P2WPKH-style witness).
sampleSegwitInput :: TxIn
sampleSegwitInput = TxIn
{ txin_prevout = sampleOutPoint
, txin_script_sig = BS.empty
, txin_sequence = 0xffffffff
+ , txin_witness = sampleWitness
}
-- | Sample output with typical P2PKH script (25 bytes).
@@ -56,7 +59,6 @@ mkLegacyTx !numInputs !numOutputs = Tx
{ tx_version = 1
, tx_inputs = sampleInput :| replicate (numInputs - 1) sampleInput
, tx_outputs = sampleOutput :| replicate (numOutputs - 1) sampleOutput
- , tx_witnesses = []
, tx_locktime = 0
}
@@ -65,13 +67,13 @@ mkLegacyTx !numInputs !numOutputs = Tx
mkSegwitTx :: Int -> Int -> Tx
mkSegwitTx !numInputs !numOutputs = Tx
{ tx_version = 2
- , tx_inputs = sampleSegwitInput :| replicate (numInputs - 1) sampleSegwitInput
+ , tx_inputs =
+ sampleSegwitInput :| replicate (numInputs - 1) sampleSegwitInput
, tx_outputs = sampleOutput :| replicate (numOutputs - 1) sampleOutput
- , tx_witnesses = replicate numInputs sampleWitness
, tx_locktime = 0
}
--- sample transactions ---------------------------------------------------------
+-- sample transactions --------------------------------------------------------
smallLegacyTx, mediumLegacyTx, largeLegacyTx :: Tx
smallLegacyTx = mkLegacyTx 1 1
@@ -83,7 +85,7 @@ smallSegwitTx = mkSegwitTx 1 1
mediumSegwitTx = mkSegwitTx 5 5
largeSegwitTx = mkSegwitTx 20 20
--- serialised bytes ------------------------------------------------------------
+-- serialised bytes -----------------------------------------------------------
smallLegacyBytes, mediumLegacyBytes, largeLegacyBytes :: BS.ByteString
smallLegacyBytes = to_bytes smallLegacyTx
@@ -95,7 +97,7 @@ smallSegwitBytes = to_bytes smallSegwitTx
mediumSegwitBytes = to_bytes mediumSegwitTx
largeSegwitBytes = to_bytes largeSegwitTx
--- sighash inputs --------------------------------------------------------------
+-- sighash inputs -------------------------------------------------------------
-- | Typical P2PKH scriptPubKey (25 bytes).
sampleScriptPubKey :: BS.ByteString
@@ -125,7 +127,7 @@ taprootSpks n = replicate n sampleTaprootSpk
sampleTapLeaf :: BS.ByteString
sampleTapLeaf = BS.replicate 32 0x00
--- benchmarks ------------------------------------------------------------------
+-- benchmarks -----------------------------------------------------------------
main :: IO ()
main = defaultMain
diff --git a/bench/Weight.hs b/bench/Weight.hs
@@ -11,13 +11,14 @@ import qualified Weigh as W
import Bitcoin.Prim.Tx
import Bitcoin.Prim.Tx.Sighash
--- NFData instances ------------------------------------------------------------
+-- NFData instances -----------------------------------------------------------
--- sample data -----------------------------------------------------------------
+-- sample data ----------------------------------------------------------------
-- | Sample outpoint (references a dummy txid).
sampleOutPoint :: OutPoint
-sampleOutPoint = OutPoint (TxId (BS.replicate 32 0xab)) 0
+sampleOutPoint =
+ OutPoint (maybe null_txid id (mk_txid (BS.replicate 32 0xab))) 0
-- | Sample input with typical P2PKH signature (~107 bytes).
sampleInput :: TxIn
@@ -25,14 +26,16 @@ sampleInput = TxIn
{ txin_prevout = sampleOutPoint
, txin_script_sig = BS.replicate 107 0x00 -- typical P2PKH sig
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
--- | Sample input for segwit (empty scriptSig).
+-- | Sample input for segwit (empty scriptSig, P2WPKH-style witness).
sampleSegwitInput :: TxIn
sampleSegwitInput = TxIn
{ txin_prevout = sampleOutPoint
, txin_script_sig = BS.empty
, txin_sequence = 0xffffffff
+ , txin_witness = sampleWitness
}
-- | Sample output with typical P2PKH script (25 bytes).
@@ -56,7 +59,6 @@ mkLegacyTx !numInputs !numOutputs = Tx
{ tx_version = 1
, tx_inputs = sampleInput :| replicate (numInputs - 1) sampleInput
, tx_outputs = sampleOutput :| replicate (numOutputs - 1) sampleOutput
- , tx_witnesses = []
, tx_locktime = 0
}
@@ -65,13 +67,13 @@ mkLegacyTx !numInputs !numOutputs = Tx
mkSegwitTx :: Int -> Int -> Tx
mkSegwitTx !numInputs !numOutputs = Tx
{ tx_version = 2
- , tx_inputs = sampleSegwitInput :| replicate (numInputs - 1) sampleSegwitInput
+ , tx_inputs =
+ sampleSegwitInput :| replicate (numInputs - 1) sampleSegwitInput
, tx_outputs = sampleOutput :| replicate (numOutputs - 1) sampleOutput
- , tx_witnesses = replicate numInputs sampleWitness
, tx_locktime = 0
}
--- sample transactions ---------------------------------------------------------
+-- sample transactions --------------------------------------------------------
smallLegacyTx, mediumLegacyTx, largeLegacyTx :: Tx
smallLegacyTx = mkLegacyTx 1 1
@@ -83,7 +85,7 @@ smallSegwitTx = mkSegwitTx 1 1
mediumSegwitTx = mkSegwitTx 5 5
largeSegwitTx = mkSegwitTx 20 20
--- serialised bytes ------------------------------------------------------------
+-- serialised bytes -----------------------------------------------------------
smallLegacyBytes, mediumLegacyBytes, largeLegacyBytes :: BS.ByteString
smallLegacyBytes = to_bytes smallLegacyTx
@@ -95,7 +97,7 @@ smallSegwitBytes = to_bytes smallSegwitTx
mediumSegwitBytes = to_bytes mediumSegwitTx
largeSegwitBytes = to_bytes largeSegwitTx
--- sighash inputs --------------------------------------------------------------
+-- sighash inputs -------------------------------------------------------------
sampleScriptPubKey :: BS.ByteString
sampleScriptPubKey = BS.replicate 25 0x00
@@ -118,7 +120,7 @@ taprootSpks n = replicate n sampleTaprootSpk
sampleTapLeaf :: BS.ByteString
sampleTapLeaf = BS.replicate 32 0x00
--- allocation benchmarks -------------------------------------------------------
+-- allocation benchmarks ------------------------------------------------------
main :: IO ()
main = W.mainWith $ do
diff --git a/lib/Bitcoin/Prim/Tx.hs b/lib/Bitcoin/Prim/Tx.hs
@@ -1,6 +1,5 @@
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
@@ -20,8 +19,10 @@ module Bitcoin.Prim.Tx (
, TxOut(..)
, OutPoint(..)
, Witness(..)
- , TxId(..)
- , mkTxId
+ , TxId
+ , mk_txid
+ , un_txid
+ , null_txid
-- * Serialisation
, to_bytes
@@ -32,92 +33,40 @@ module Bitcoin.Prim.Tx (
-- * TxId
, txid
-
- -- * Internal (for Sighash)
- , put_word32_le
- , put_word64_le
- , put_compact
- , put_outpoint
- , put_txout
- , to_strict
) where
-import Control.DeepSeq (NFData(..))
+import Bitcoin.Prim.Tx.Internal
import qualified Crypto.Hash.SHA256 as SHA256
import Data.Bits ((.|.), shiftL)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as B16
import qualified Data.ByteString.Builder as BSB
-import qualified Data.ByteString.Lazy as BL
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NE
import Data.Word (Word32, Word64)
-import GHC.Generics (Generic)
-
--- | Transaction ID (32 bytes, little-endian double-SHA256).
-newtype TxId = TxId BS.ByteString
- deriving (Eq, Ord, Show, Generic)
-
-instance NFData TxId
--- | Construct a TxId from a 32-byte ByteString (in internal byte
--- order, i.e. the reverse of the usual hex display).
+-- | Construct a 'TxId' from 32 bytes in internal byte order (the
+-- reverse of the usual hex display).
--
-- Returns 'Nothing' if the input is not exactly 32 bytes.
--
--- >>> fmap (\(TxId b) -> BS.length b) (mkTxId (BS.replicate 32 0x00))
+-- >>> fmap (BS.length . un_txid) (mk_txid (BS.replicate 32 0x00))
-- Just 32
--- >>> mkTxId (BS.replicate 31 0x00)
+-- >>> mk_txid (BS.replicate 31 0x00)
-- Nothing
-mkTxId :: BS.ByteString -> Maybe TxId
-mkTxId bs
+mk_txid :: BS.ByteString -> Maybe TxId
+mk_txid bs
| BS.length bs == 32 = Just (TxId bs)
| otherwise = Nothing
--- | Transaction outpoint (txid + output index).
-data OutPoint = OutPoint
- { op_txid :: {-# UNPACK #-} !TxId
- , op_vout :: {-# UNPACK #-} !Word32
- } deriving (Eq, Ord, Show, Generic)
-
-instance NFData OutPoint
-
--- | Transaction input.
-data TxIn = TxIn
- { txin_prevout :: {-# UNPACK #-} !OutPoint
- , txin_script_sig :: !BS.ByteString
- , txin_sequence :: {-# UNPACK #-} !Word32
- } deriving (Eq, Show, Generic)
+-- | The 32 bytes of a 'TxId', in internal byte order.
+un_txid :: TxId -> BS.ByteString
+un_txid (TxId bs) = bs
+{-# INLINE un_txid #-}
-instance NFData TxIn
-
--- | Transaction output.
-data TxOut = TxOut
- { txout_value :: {-# UNPACK #-} !Word64 -- ^ satoshis
- , txout_script_pubkey :: !BS.ByteString
- } deriving (Eq, Show, Generic)
-
-instance NFData TxOut
-
--- | Witness stack for a single input.
-newtype Witness = Witness [BS.ByteString]
- deriving (Eq, Show, Generic)
-
-instance NFData Witness
-
--- | Complete transaction.
---
--- Bitcoin requires at least one input and one output, enforced here
--- via 'NonEmpty' lists.
-data Tx = Tx
- { tx_version :: {-# UNPACK #-} !Word32
- , tx_inputs :: !(NonEmpty TxIn)
- , tx_outputs :: !(NonEmpty TxOut)
- , tx_witnesses :: ![Witness] -- ^ empty list for legacy tx
- , tx_locktime :: {-# UNPACK #-} !Word32
- } deriving (Eq, Show, Generic)
-
-instance NFData Tx
+-- | The all-zero 'TxId', referenced by coinbase inputs' outpoints.
+null_txid :: TxId
+null_txid = TxId (BS.replicate 32 0x00)
-- serialisation --------------------------------------------------------------
@@ -132,8 +81,8 @@ instance NFData Tx
-- @
to_bytes :: Tx -> BS.ByteString
to_bytes tx@Tx {..}
- | not (has_witness tx_witnesses) = to_bytes_legacy tx
- | otherwise = to_strict $
+ | not (any (has_witness . txin_witness) tx_inputs) = to_bytes_legacy tx
+ | otherwise = to_strict $
put_word32_le tx_version
<> BSB.word8 0x00 -- marker
<> BSB.word8 0x01 -- flag
@@ -141,12 +90,12 @@ to_bytes tx@Tx {..}
<> foldMap put_txin tx_inputs
<> put_compact (fromIntegral (NE.length tx_outputs))
<> foldMap put_txout tx_outputs
- <> foldMap put_witness tx_witnesses
+ <> foldMap (put_witness . txin_witness) tx_inputs
<> put_word32_le tx_locktime
--- whether any witness stack is non-empty
-has_witness :: [Witness] -> Bool
-has_witness = any (\(Witness items) -> not (null items))
+-- whether a witness stack is non-empty
+has_witness :: Witness -> Bool
+has_witness (Witness items) = not (null items)
{-# INLINE has_witness #-}
-- | Serialise a transaction to legacy format (no witness data).
@@ -154,11 +103,11 @@ has_witness = any (\(Witness items) -> not (null items))
-- 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 a legacy tx (no witnesses), the same as to_bytes
+-- to_bytes_legacy legacy_tx == to_bytes legacy_tx
--
--- -- for segwit tx, strips witnesses
--- BS.length (to_bytes_legacy segwitTx) < BS.length (to_bytes segwitTx)
+-- -- for a segwit tx, strips the witnesses
+-- BS.length (to_bytes_legacy segwit_tx) < BS.length (to_bytes segwit_tx)
-- @
to_bytes_legacy :: Tx -> BS.ByteString
to_bytes_legacy Tx {..} = to_strict $
@@ -188,74 +137,6 @@ from_base16 b16 = do
bs <- B16.decode b16
from_bytes bs
--- internal: builders ---------------------------------------------------------
-
--- | Convert a Builder to a strict ByteString.
-to_strict :: BSB.Builder -> BS.ByteString
-to_strict = BL.toStrict . BSB.toLazyByteString
-{-# INLINE to_strict #-}
-
--- | Encode a Word32 as little-endian bytes.
-put_word32_le :: Word32 -> BSB.Builder
-put_word32_le = BSB.word32LE
-{-# INLINE put_word32_le #-}
-
--- | Encode a Word64 as little-endian bytes.
-put_word64_le :: Word64 -> BSB.Builder
-put_word64_le = BSB.word64LE
-{-# INLINE put_word64_le #-}
-
--- | Encode a Word64 as Bitcoin compactSize (varint).
---
--- Encoding:
--- - 0x00-0xfc: 1 byte (value itself)
--- - 0xfd-0xffff: 0xfd ++ 2 bytes LE
--- - 0x10000-0xffffffff: 0xfe ++ 4 bytes LE
--- - larger: 0xff ++ 8 bytes LE
-put_compact :: Word64 -> BSB.Builder
-put_compact !n
- | n <= 0xfc = 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_compact #-}
-
--- | Encode an OutPoint (txid + vout).
-put_outpoint :: OutPoint -> BSB.Builder
-put_outpoint OutPoint {..} =
- let !(TxId !txid_bs) = op_txid
- in BSB.byteString txid_bs <> put_word32_le op_vout
-{-# INLINE put_outpoint #-}
-
--- | Encode a TxIn.
-put_txin :: TxIn -> BSB.Builder
-put_txin TxIn {..} =
- put_outpoint txin_prevout
- <> put_compact (fromIntegral (BS.length txin_script_sig))
- <> BSB.byteString txin_script_sig
- <> put_word32_le txin_sequence
-{-# INLINE put_txin #-}
-
--- | Encode a TxOut.
-put_txout :: TxOut -> BSB.Builder
-put_txout TxOut {..} =
- put_word64_le txout_value
- <> put_compact (fromIntegral (BS.length txout_script_pubkey))
- <> BSB.byteString txout_script_pubkey
-{-# INLINE put_txout #-}
-
--- | Encode a Witness stack.
-put_witness :: Witness -> BSB.Builder
-put_witness (Witness items) =
- put_compact (fromIntegral (length items))
- <> foldMap put_witness_item items
- where
- put_witness_item :: BS.ByteString -> BSB.Builder
- put_witness_item !item =
- put_compact (fromIntegral (BS.length item))
- <> BSB.byteString item
-{-# INLINE put_witness #-}
-
-- decoding -------------------------------------------------------------------
-- | Parse a transaction from bytes.
@@ -301,7 +182,7 @@ parse_legacy !bs !version !off0 = do
!off5 = off4 + 4
-- should have consumed all bytes
guard (off5 == BS.length bs)
- pure $! Tx version inputs outputs [] locktime
+ pure $! Tx version inputs outputs locktime
-- Parse segwit transaction (with witness data)
parse_segwit :: BS.ByteString -> Word32 -> Int -> Maybe Tx
@@ -320,14 +201,23 @@ parse_segwit !bs !version !off0 = do
(witnesses, off5) <- get_many get_witness bs off4 input_count
-- a marker and flag with no witness data is invalid (Bitcoin Core:
-- "superfluous witness record")
- guard (has_witness witnesses)
+ guard (any has_witness witnesses)
-- locktime (4 bytes)
guard (BS.length bs >= off5 + 4)
let !locktime = get_word32_le bs off5
!off6 = off5 + 4
-- should have consumed all bytes
guard (off6 == BS.length bs)
- pure $! Tx version inputs outputs witnesses locktime
+ pure $! Tx version (attach_witnesses inputs witnesses) outputs locktime
+
+-- Pair each input with its witness (get_many returns exactly one per
+-- input).
+attach_witnesses :: NonEmpty TxIn -> [Witness] -> NonEmpty TxIn
+attach_witnesses (i :| is) ws = case ws of
+ (w : rest) -> set i w :| zipWith set is rest
+ [] -> i :| is
+ where
+ set x w = x { txin_witness = w }
-- internal helpers -----------------------------------------------------------
@@ -439,7 +329,7 @@ get_txin !bs !off0 = do
guard (BS.length bs >= off3 + 4)
let !seqn = get_word32_le bs off3
!off4 = off3 + 4
- pure (TxIn outpoint script_sig seqn, off4)
+ pure (TxIn outpoint script_sig seqn (Witness []), off4)
-- | Decode a transaction output.
-- Returns (TxOut, new_offset).
@@ -500,8 +390,7 @@ get_many getter !bs !off0 !count
--
-- @
-- -- Satoshi->Hal tx (block 170)
--- let TxId t = txid satoshi_hal_tx
--- B16.encode (BS.reverse t)
+-- B16.encode (BS.reverse (un_txid (txid satoshi_hal_tx)))
-- == "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"
-- @
txid :: Tx -> TxId
diff --git a/lib/Bitcoin/Prim/Tx/Internal.hs b/lib/Bitcoin/Prim/Tx/Internal.hs
@@ -0,0 +1,168 @@
+{-# OPTIONS_HADDOCK hide #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- |
+-- Module: Bitcoin.Prim.Tx.Internal
+-- Copyright: (c) 2025 Jared Tobin
+-- License: MIT
+-- Maintainer: Jared Tobin <jared@ppad.tech>
+--
+-- Transaction types and serialisation builders, shared by the public
+-- modules.
+
+module Bitcoin.Prim.Tx.Internal (
+ -- * Transaction types
+ TxId(..)
+ , OutPoint(..)
+ , TxIn(..)
+ , TxOut(..)
+ , Witness(..)
+ , Tx(..)
+
+ -- * Builders
+ , to_strict
+ , put_word32_le
+ , put_word64_le
+ , put_compact
+ , put_bytes
+ , put_outpoint
+ , put_txin
+ , put_txout
+ , put_witness
+ ) where
+
+import Control.DeepSeq (NFData(..))
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Builder as BSB
+import qualified Data.ByteString.Lazy as BL
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.Word (Word32, Word64)
+import GHC.Generics (Generic)
+
+-- types ----------------------------------------------------------------------
+
+-- | Transaction ID: 32 bytes, the double-SHA256 of the transaction's
+-- legacy serialisation, in internal byte order (the reverse of the
+-- usual hex display).
+newtype TxId = TxId BS.ByteString
+ deriving (Eq, Ord, Show, Generic)
+
+instance NFData TxId
+
+-- | Transaction outpoint (txid + output index).
+data OutPoint = OutPoint
+ { op_txid :: {-# UNPACK #-} !TxId
+ , op_vout :: {-# UNPACK #-} !Word32
+ } deriving (Eq, Ord, Show, Generic)
+
+instance NFData OutPoint
+
+-- | Witness stack for a single input (empty for a non-witness input).
+newtype Witness = Witness [BS.ByteString]
+ deriving (Eq, Show, Generic)
+
+instance NFData Witness
+
+-- | Transaction input.
+data TxIn = TxIn
+ { txin_prevout :: {-# UNPACK #-} !OutPoint
+ , txin_script_sig :: !BS.ByteString
+ , txin_sequence :: {-# UNPACK #-} !Word32
+ , txin_witness :: !Witness
+ } deriving (Eq, Show, Generic)
+
+instance NFData TxIn
+
+-- | Transaction output.
+data TxOut = TxOut
+ { txout_value :: {-# UNPACK #-} !Word64 -- ^ satoshis
+ , txout_script_pubkey :: !BS.ByteString
+ } deriving (Eq, Show, Generic)
+
+instance NFData TxOut
+
+-- | Complete transaction.
+--
+-- Bitcoin requires at least one input and one output, enforced here
+-- via 'NonEmpty' lists. Each input carries its own witness, so a
+-- transaction is serialised in the segwit format exactly when some
+-- input's witness is non-empty.
+data Tx = Tx
+ { tx_version :: {-# UNPACK #-} !Word32
+ , tx_inputs :: !(NonEmpty TxIn)
+ , tx_outputs :: !(NonEmpty TxOut)
+ , tx_locktime :: {-# UNPACK #-} !Word32
+ } deriving (Eq, Show, Generic)
+
+instance NFData Tx
+
+-- builders -------------------------------------------------------------------
+
+-- | Convert a Builder to a strict ByteString.
+to_strict :: BSB.Builder -> BS.ByteString
+to_strict = BL.toStrict . BSB.toLazyByteString
+{-# INLINE to_strict #-}
+
+-- | Encode a Word32 as little-endian bytes.
+put_word32_le :: Word32 -> BSB.Builder
+put_word32_le = BSB.word32LE
+{-# INLINE put_word32_le #-}
+
+-- | Encode a Word64 as little-endian bytes.
+put_word64_le :: Word64 -> BSB.Builder
+put_word64_le = BSB.word64LE
+{-# INLINE put_word64_le #-}
+
+-- | Encode a Word64 as Bitcoin compactSize (varint).
+--
+-- Encoding:
+-- - 0x00-0xfc: 1 byte (value itself)
+-- - 0xfd-0xffff: 0xfd ++ 2 bytes LE
+-- - 0x10000-0xffffffff: 0xfe ++ 4 bytes LE
+-- - larger: 0xff ++ 8 bytes LE
+put_compact :: Word64 -> BSB.Builder
+put_compact !n
+ | n <= 0xfc = 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_compact #-}
+
+-- | Encode compactSize-length-prefixed bytes (Bitcoin @ser_string@).
+put_bytes :: BS.ByteString -> BSB.Builder
+put_bytes !bs =
+ put_compact (fromIntegral (BS.length bs))
+ <> BSB.byteString bs
+{-# INLINE put_bytes #-}
+
+-- | Encode an OutPoint (txid + vout).
+put_outpoint :: OutPoint -> BSB.Builder
+put_outpoint OutPoint {..} =
+ let !(TxId !txid_bs) = op_txid
+ in BSB.byteString txid_bs <> put_word32_le op_vout
+{-# INLINE put_outpoint #-}
+
+-- | Encode a TxIn (without its witness, which is serialised
+-- separately).
+put_txin :: TxIn -> BSB.Builder
+put_txin TxIn {..} =
+ put_outpoint txin_prevout
+ <> put_bytes txin_script_sig
+ <> put_word32_le txin_sequence
+{-# INLINE put_txin #-}
+
+-- | Encode a TxOut.
+put_txout :: TxOut -> BSB.Builder
+put_txout TxOut {..} =
+ put_word64_le txout_value
+ <> put_bytes txout_script_pubkey
+{-# INLINE put_txout #-}
+
+-- | Encode a Witness stack.
+put_witness :: Witness -> BSB.Builder
+put_witness (Witness items) =
+ put_compact (fromIntegral (length items))
+ <> foldMap put_bytes items
+{-# INLINE put_witness #-}
diff --git a/lib/Bitcoin/Prim/Tx/Sighash.hs b/lib/Bitcoin/Prim/Tx/Sighash.hs
@@ -28,18 +28,20 @@ module Bitcoin.Prim.Tx.Sighash (
, sighash_taproot_keypath
, sighash_taproot_scriptpath
- -- * Internal
+ -- * Script code
, strip_codeseparators
) where
-import Bitcoin.Prim.Tx
+import Bitcoin.Prim.Tx.Internal
( Tx(..)
, TxIn(..)
, TxOut(..)
, put_word32_le
, put_word64_le
, put_compact
+ , put_bytes
, put_outpoint
+ , put_txin
, put_txout
, to_strict
)
@@ -267,7 +269,7 @@ serialize_legacy_sighash Tx{..} !idx !script_pubkey !ht =
in to_strict $
put_word32_le tx_version
<> put_compact (fromIntegral (length final_inputs))
- <> foldMap put_txin_legacy final_inputs
+ <> foldMap put_txin final_inputs
<> put_compact (fromIntegral (length final_outputs))
<> foldMap put_txout final_outputs
<> put_word32_le tx_locktime
@@ -298,15 +300,6 @@ safe_index (x : xs) !n
| otherwise = safe_index xs (n - 1)
{-# INLINE safe_index #-}
--- | Encode TxIn for legacy sighash (same as normal encoding).
-put_txin_legacy :: TxIn -> BSB.Builder
-put_txin_legacy TxIn{..} =
- put_outpoint txin_prevout
- <> put_compact (fromIntegral (BS.length txin_script_sig))
- <> BSB.byteString txin_script_sig
- <> put_word32_le txin_sequence
-{-# INLINE put_txin_legacy #-}
-
-- BIP143 segwit sighash ------------------------------------------------------
-- | Compute BIP143 segwit sighash.
@@ -413,13 +406,6 @@ sha :: BSB.Builder -> BS.ByteString
sha = SHA256.hash . to_strict
{-# INLINE sha #-}
--- | Compact-size length-prefixed bytes (Bitcoin @ser_string@).
-put_bytes :: BS.ByteString -> BSB.Builder
-put_bytes !bs =
- put_compact (fromIntegral (BS.length bs))
- <> BSB.byteString bs
-{-# INLINE put_bytes #-}
-
-- | Valid taproot hash types per BIP341: 0x00 (DEFAULT), 0x01..0x03,
-- 0x81..0x83. Non-canonical values are signalled as invalid in
-- contrast with legacy\/segwit, which commit arbitrary 32-bit values.
diff --git a/ppad-tx.cabal b/ppad-tx.cabal
@@ -34,6 +34,8 @@ library
exposed-modules:
Bitcoin.Prim.Tx
Bitcoin.Prim.Tx.Sighash
+ other-modules:
+ Bitcoin.Prim.Tx.Internal
build-depends:
base >= 4.9 && < 5
, bytestring >= 0.9 && < 0.13
diff --git a/test/Main.hs b/test/Main.hs
@@ -14,7 +14,7 @@ import Test.Tasty
import qualified Test.Tasty.HUnit as H
import Test.Tasty.QuickCheck as QC hiding (Witness)
--- main ------------------------------------------------------------------------
+-- main -----------------------------------------------------------------------
main :: IO ()
main = defaultMain $
@@ -46,10 +46,10 @@ main = defaultMain $
, edge_multi_witness
]
, testGroup "validation" [
- test_mkTxId_valid
- , test_mkTxId_short
- , test_mkTxId_long
- , test_mkTxId_empty
+ test_mk_txid_valid
+ , test_mk_txid_short
+ , test_mk_txid_long
+ , test_mk_txid_empty
, test_from_bytes_truncated
, test_from_bytes_trailing
, test_from_bytes_garbage
@@ -151,7 +151,7 @@ main = defaultMain $
]
]
--- helpers ---------------------------------------------------------------------
+-- helpers --------------------------------------------------------------------
-- | Decode a hex literal. Total: an invalid literal decodes to empty
-- bytes, which no vector expects, and the "fixtures parse" test
@@ -176,7 +176,7 @@ assertParses rawHex =
Nothing -> H.assertFailure "from_base16 returned Nothing"
Just _ -> pure ()
--- round-trip tests ------------------------------------------------------------
+-- round-trip tests -----------------------------------------------------------
-- Simple legacy tx: 1 input, 1 output, no witnesses
roundtrip_legacy_simple :: TestTree
@@ -187,16 +187,16 @@ roundtrip_legacy_simple = H.testCase "simple legacy tx" $
{ tx_version = 1
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = []
, tx_locktime = 0
}
txin = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0xab)
+ { op_txid = tid 0xab
, op_vout = 0
}
, txin_script_sig = hex "483045022100abcd"
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
txout = TxOut
{ txout_value = 50000
@@ -212,16 +212,16 @@ roundtrip_segwit = H.testCase "segwit tx with witnesses" $
{ tx_version = 2
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = [wit]
, tx_locktime = 500000
}
txin = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x12)
+ { op_txid = tid 0x12
, op_vout = 1
}
, txin_script_sig = BS.empty -- segwit: empty scriptSig
, txin_sequence = 0xfffffffe
+ , txin_witness = wit
}
txout = TxOut
{ txout_value = 100000000
@@ -229,7 +229,8 @@ roundtrip_segwit = H.testCase "segwit tx with witnesses" $
}
wit = Witness
[ hex "304402201234"
- , hex "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
+ , hex ("0279be667ef9dcbbac55a06295ce870b"
+ <> "07029bfcdb2dce28d959f2815b16f81798")
]
-- Multiple inputs and outputs
@@ -241,32 +242,34 @@ roundtrip_multi_io = H.testCase "multiple inputs/outputs" $
{ tx_version = 1
, tx_inputs = txin1 :| [txin2, txin3]
, tx_outputs = txout1 :| [txout2]
- , tx_witnesses = []
, tx_locktime = 123456
}
txin1 = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x11)
+ { op_txid = tid 0x11
, op_vout = 0
}
, txin_script_sig = hex "4730440220"
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
txin2 = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x22)
+ { op_txid = tid 0x22
, op_vout = 2
}
, txin_script_sig = hex "483045022100"
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
txin3 = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x33)
+ { op_txid = tid 0x33
, op_vout = 5
}
, txin_script_sig = hex "00"
, txin_sequence = 0xfffffffe
+ , txin_witness = Witness []
}
txout1 = TxOut
{ txout_value = 10000000
@@ -277,7 +280,7 @@ roundtrip_multi_io = H.testCase "multiple inputs/outputs" $
, txout_script_pubkey = hex "a914"
}
--- known vector tests ----------------------------------------------------------
+-- known vector tests ---------------------------------------------------------
-- First Bitcoin transaction ever (block 170, Satoshi to Hal Finney)
-- TxId: f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16
@@ -293,7 +296,8 @@ satoshiHalRaw =
\2e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000"
satoshiHalTxId :: BS.ByteString
-satoshiHalTxId = "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"
+satoshiHalTxId =
+ "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"
parse_satoshi_hal :: TestTree
parse_satoshi_hal = H.testCase "parse Satoshi->Hal tx (block 170)" $
@@ -304,7 +308,7 @@ txid_satoshi_hal = H.testCase "txid of Satoshi->Hal tx" $ do
case from_base16 satoshiHalRaw of
Nothing -> H.assertFailure "failed to parse tx"
Just tx -> do
- let TxId computed = txid tx
+ let computed = un_txid (txid tx)
-- txid is displayed big-endian, but stored little-endian
expected = BS.reverse (hex satoshiHalTxId)
H.assertEqual "txid mismatch" expected computed
@@ -323,7 +327,7 @@ parse_first_segwit :: TestTree
parse_first_segwit = H.testCase "parse first segwit tx (block 481824)" $
assertParses firstSegwitRaw
--- edge case tests -------------------------------------------------------------
+-- edge case tests ------------------------------------------------------------
-- Empty scriptSig (common in segwit)
edge_empty_scriptsig :: TestTree
@@ -334,16 +338,16 @@ edge_empty_scriptsig = H.testCase "empty scriptSig" $
{ tx_version = 2
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = [wit]
, tx_locktime = 0
}
txin = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0xff)
+ { op_txid = tid 0xff
, op_vout = 0
}
, txin_script_sig = BS.empty
, txin_sequence = 0xffffffff
+ , txin_witness = wit
}
txout = TxOut
{ txout_value = 1000
@@ -360,16 +364,16 @@ edge_max_sequence = H.testCase "maximum sequence (0xffffffff)" $
{ tx_version = 1
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = []
, tx_locktime = 0
}
txin = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x00)
+ { op_txid = tid 0x00
, op_vout = 0xffffffff -- max vout too
}
, txin_script_sig = hex "00"
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
txout = TxOut
{ txout_value = 0
@@ -385,16 +389,16 @@ edge_zero_locktime = H.testCase "zero locktime" $
{ tx_version = 1
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = []
, tx_locktime = 0
}
txin = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0xaa)
+ { op_txid = tid 0xaa
, op_vout = 0
}
, txin_script_sig = hex "51" -- OP_1
, txin_sequence = 0
+ , txin_witness = Witness []
}
txout = TxOut
{ txout_value = 100
@@ -410,24 +414,25 @@ edge_multi_witness = H.testCase "multiple witness items" $
{ tx_version = 2
, tx_inputs = txin1 :| [txin2]
, tx_outputs = txout :| []
- , tx_witnesses = [witness1, witness2]
, tx_locktime = 0
}
txin1 = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x01)
+ { op_txid = tid 0x01
, op_vout = 0
}
, txin_script_sig = BS.empty
, txin_sequence = 0xffffffff
+ , txin_witness = witness1
}
txin2 = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x02)
+ { op_txid = tid 0x02
, op_vout = 1
}
, txin_script_sig = BS.empty
, txin_sequence = 0xffffffff
+ , txin_witness = witness2
}
txout = TxOut
{ txout_value = 50000
@@ -449,30 +454,30 @@ edge_multi_witness = H.testCase "multiple witness items" $
-- validation tests -----------------------------------------------------------
--- mkTxId: valid 32-byte input accepted
-test_mkTxId_valid :: TestTree
-test_mkTxId_valid = H.testCase "mkTxId accepts 32 bytes" $
- case mkTxId (BS.replicate 32 0x00) of
- Nothing -> H.assertFailure "mkTxId returned Nothing"
+-- mk_txid: valid 32-byte input accepted
+test_mk_txid_valid :: TestTree
+test_mk_txid_valid = H.testCase "mk_txid accepts 32 bytes" $
+ case mk_txid (BS.replicate 32 0x00) of
+ Nothing -> H.assertFailure "mk_txid returned Nothing"
Just _ -> pure ()
--- mkTxId: 31 bytes rejected
-test_mkTxId_short :: TestTree
-test_mkTxId_short = H.testCase "mkTxId rejects 31 bytes" $
+-- mk_txid: 31 bytes rejected
+test_mk_txid_short :: TestTree
+test_mk_txid_short = H.testCase "mk_txid rejects 31 bytes" $
H.assertEqual "should be Nothing"
- Nothing (mkTxId (BS.replicate 31 0x00))
+ Nothing (mk_txid (BS.replicate 31 0x00))
--- mkTxId: 33 bytes rejected
-test_mkTxId_long :: TestTree
-test_mkTxId_long = H.testCase "mkTxId rejects 33 bytes" $
+-- mk_txid: 33 bytes rejected
+test_mk_txid_long :: TestTree
+test_mk_txid_long = H.testCase "mk_txid rejects 33 bytes" $
H.assertEqual "should be Nothing"
- Nothing (mkTxId (BS.replicate 33 0x00))
+ Nothing (mk_txid (BS.replicate 33 0x00))
--- mkTxId: empty input rejected
-test_mkTxId_empty :: TestTree
-test_mkTxId_empty = H.testCase "mkTxId rejects empty" $
+-- mk_txid: empty input rejected
+test_mk_txid_empty :: TestTree
+test_mk_txid_empty = H.testCase "mk_txid rejects empty" $
H.assertEqual "should be Nothing"
- Nothing (mkTxId BS.empty)
+ Nothing (mk_txid BS.empty)
-- from_bytes: truncated input rejected
test_from_bytes_truncated :: TestTree
@@ -543,8 +548,7 @@ test_from_bytes_superfluous_witness =
test_to_bytes_empty_witnesses :: TestTree
test_to_bytes_empty_witnesses =
H.testCase "to_bytes uses the legacy format for empty witnesses" $ do
- let tx = legacyTx1 { tx_witnesses = map (const (Witness []))
- (NE.toList (tx_inputs legacyTx1)) }
+ let tx = strip_witnesses legacyTx1
H.assertEqual "legacy" (to_bytes_legacy tx) (to_bytes tx)
-- every transaction fixture literal parses
@@ -589,17 +593,17 @@ legacyTx1 = Tx
{ tx_version = 1
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = []
, tx_locktime = 0
}
where
txin = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x00)
+ { op_txid = tid 0x00
, op_vout = 0
}
, txin_script_sig = hex "00"
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
txout = TxOut
{ txout_value = 0
@@ -617,16 +621,16 @@ sighash_legacy_minimal =
{ tx_version = 1
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = []
, tx_locktime = 0
}
txin = TxIn
{ txin_prevout = OutPoint
- { op_txid = TxId (BS.replicate 32 0x00)
+ { op_txid = tid 0x00
, op_vout = 0
}
, txin_script_sig = hex "00"
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
txout = TxOut
{ txout_value = 0
@@ -696,7 +700,7 @@ bip143_p2sh_p2wpkh = H.testCase "P2SH-P2WPKH" $ do
-- Arbitrary instances --------------------------------------------------------
instance Arbitrary TxId where
- arbitrary = TxId . BS.pack <$> vectorOf 32 arbitrary
+ arbitrary = maybe null_txid id . mk_txid . BS.pack <$> vectorOf 32 arbitrary
instance Arbitrary OutPoint where
arbitrary = OutPoint <$> arbitrary <*> arbitrary
@@ -706,6 +710,7 @@ instance Arbitrary TxIn where
<$> arbitrary
<*> arbitraryScript
<*> arbitrary
+ <*> pure (Witness [])
instance Arbitrary TxOut where
arbitrary = TxOut
@@ -749,7 +754,7 @@ genLegacyTx = do
ins <- arbitraryNonEmpty
outs <- arbitraryNonEmpty
lt <- arbitrary
- pure $ Tx ver ins outs [] lt
+ pure $ Tx ver ins outs lt
-- | Generate a valid segwit transaction (with witnesses).
genSegwitTx :: Gen Tx
@@ -763,7 +768,10 @@ genSegwitTx = do
w0 <- Witness <$> listOf1 arbitraryScript
ws <- vectorOf (numInputs - 1) arbitrary
lt <- arbitrary
- pure $ Tx ver ins outs (w0 : ws) lt
+ let set i w = i { txin_witness = w }
+ ins' = case ins of
+ i :| is -> set i w0 :| zipWith set is ws
+ pure $ Tx ver ins' outs lt
-- | Generate any valid transaction.
instance Arbitrary Tx where
@@ -793,20 +801,19 @@ prop_segwit_longer :: TestTree
prop_segwit_longer =
QC.testProperty "segwit tx: to_bytes longer than to_bytes_legacy" $
forAll genSegwitTx $ \tx ->
- not (null (tx_witnesses tx)) ==>
- BS.length (to_bytes tx) > BS.length (to_bytes_legacy tx)
+ BS.length (to_bytes tx) > BS.length (to_bytes_legacy tx)
-- TxId is always 32 bytes
prop_txid_32_bytes :: TestTree
prop_txid_32_bytes = QC.testProperty "txid is always 32 bytes" $
- \tx -> let TxId bs = txid tx in BS.length bs === 32
+ \tx -> BS.length (un_txid (txid tx)) === 32
-- TxId ignores witnesses (same txid with or without witnesses)
prop_txid_ignores_witnesses :: TestTree
prop_txid_ignores_witnesses =
QC.testProperty "txid ignores witnesses" $
forAll genSegwitTx $ \tx ->
- let txNoWit = tx { tx_witnesses = [] }
+ let txNoWit = strip_witnesses tx
in txid tx === txid txNoWit
-- sighash_legacy always returns 32 bytes
@@ -969,8 +976,8 @@ txid_first_segwit = H.testCase "txid of first-segwit fixture" $
case from_base16 firstSegwitRaw of
Nothing -> H.assertFailure "failed to parse tx"
Just tx -> do
- let TxId computed = txid tx
- expected = BS.reverse (hex firstSegwitTxId)
+ let computed = un_txid (txid tx)
+ expected = BS.reverse (hex firstSegwitTxId)
H.assertEqual "txid mismatch" expected computed
-- BIP143 P2SH-P2WSH multi-sighash vectors -----------------------------------
@@ -992,8 +999,18 @@ p2shP2wshTxHex = mconcat
-- (which the "fixtures parse" test reports).
placeholder_tx :: Tx
placeholder_tx = Tx 1
- (TxIn (OutPoint (TxId (BS.replicate 32 0x00)) 0) BS.empty 0 :| [])
- (TxOut 0 BS.empty :| []) [] 0
+ (TxIn (OutPoint null_txid 0) BS.empty 0 (Witness []) :| [])
+ (TxOut 0 BS.empty :| []) 0
+
+-- | A fixture txid: 32 copies of a byte.
+tid :: Word8 -> TxId
+tid w = maybe null_txid id (mk_txid (BS.replicate 32 w))
+
+-- | Clear every input's witness.
+strip_witnesses :: Tx -> Tx
+strip_witnesses tx =
+ tx { tx_inputs = fmap (\i -> i { txin_witness = Witness [] })
+ (tx_inputs tx) }
p2shP2wshScriptCode :: BS.ByteString
p2shP2wshScriptCode = hex $ mconcat
@@ -1309,14 +1326,14 @@ tinyTaprootTx = Tx
{ tx_version = 2
, tx_inputs = txin :| []
, tx_outputs = txout :| []
- , tx_witnesses = []
, tx_locktime = 0
}
where
txin = TxIn
- { txin_prevout = OutPoint (TxId (BS.replicate 32 0xab)) 0
+ { txin_prevout = OutPoint (tid 0xab) 0
, txin_script_sig = BS.empty
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
txout = TxOut
{ txout_value = 1000
@@ -1381,9 +1398,10 @@ taproot_single_oob :: TestTree
taproot_single_oob =
H.testCase "rejects SIGHASH_SINGLE with idx >= n_outputs" $
let txin2 = TxIn
- { txin_prevout = OutPoint (TxId (BS.replicate 32 0xcd)) 0
+ { txin_prevout = OutPoint (tid 0xcd) 0
, txin_script_sig = BS.empty
, txin_sequence = 0xffffffff
+ , txin_witness = Witness []
}
tx2 = tinyTaprootTx
{ tx_inputs = (NE.head (tx_inputs tinyTaprootTx)) :| [txin2] }
@@ -1414,7 +1432,8 @@ rb_script_path_all = H.testCase
Just tx ->
case sighash_taproot_scriptpath tx 0 [amount] [spk] Nothing
leaf 0xffffffff 0x01 of
- Nothing -> H.assertFailure "sighash_taproot_scriptpath returned Nothing"
+ Nothing ->
+ H.assertFailure "sighash_taproot_scriptpath returned Nothing"
Just res -> H.assertEqual "sighash mismatch" expected res
-- taproot properties --------------------------------------------------------