tx

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

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

Merge impl/bench: benchmark suites

Criterion wall-time and Weigh allocation benchmarks for:
- to_bytes, from_bytes, to_bytes_legacy, txid
- Small (1 I/O), medium (5 I/O), large (20 I/O) transactions
- Both legacy and segwit variants

24 benchmarks in each suite (48 total).

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

Diffstat:
Mbench/Main.hs | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mbench/Weight.hs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 230 insertions(+), 39 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -1,44 +1,141 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE StandaloneDeriving #-} module Main where import Control.DeepSeq import Criterion.Main -import GHC.Generics +import qualified Data.ByteString as BS import Bitcoin.Prim.Tx import Bitcoin.Prim.Tx.Sighash -- NFData instances ------------------------------------------------------------ -deriving stock instance Generic TxId instance NFData TxId - -deriving stock instance Generic OutPoint instance NFData OutPoint - -deriving stock instance Generic TxIn instance NFData TxIn - -deriving stock instance Generic TxOut instance NFData TxOut - -deriving stock instance Generic Witness instance NFData Witness - -deriving stock instance Generic Tx instance NFData Tx - -deriving stock instance Generic SighashType instance NFData SighashType +-- sample data ----------------------------------------------------------------- + +-- | Sample outpoint (references a dummy txid). +sampleOutPoint :: OutPoint +sampleOutPoint = OutPoint (TxId (BS.replicate 32 0xab)) 0 + +-- | Sample input with typical P2PKH signature (~107 bytes). +sampleInput :: TxIn +sampleInput = TxIn + { txin_prevout = sampleOutPoint + , txin_script_sig = BS.replicate 107 0x00 -- typical P2PKH sig + , txin_sequence = 0xffffffff + } + +-- | Sample input for segwit (empty scriptSig). +sampleSegwitInput :: TxIn +sampleSegwitInput = TxIn + { txin_prevout = sampleOutPoint + , txin_script_sig = BS.empty + , txin_sequence = 0xffffffff + } + +-- | Sample output with typical P2PKH script (25 bytes). +sampleOutput :: TxOut +sampleOutput = TxOut + { txout_value = 50000000 + , txout_script_pubkey = BS.replicate 25 0x00 -- typical P2PKH script + } + +-- | Sample witness stack (signature + pubkey for P2WPKH). +sampleWitness :: Witness +sampleWitness = Witness + [ BS.replicate 72 0x00 -- DER signature + , BS.replicate 33 0x00 -- compressed pubkey + ] + +-- | Create a legacy transaction with n inputs and m outputs. +mkLegacyTx :: Int -> Int -> Tx +mkLegacyTx !numInputs !numOutputs = Tx + { tx_version = 1 + , tx_inputs = replicate numInputs sampleInput + , tx_outputs = replicate numOutputs sampleOutput + , tx_witnesses = [] + , tx_locktime = 0 + } + +-- | Create a segwit transaction with n inputs and m outputs. +mkSegwitTx :: Int -> Int -> Tx +mkSegwitTx !numInputs !numOutputs = Tx + { tx_version = 2 + , tx_inputs = replicate numInputs sampleSegwitInput + , tx_outputs = replicate numOutputs sampleOutput + , tx_witnesses = replicate numInputs sampleWitness + , tx_locktime = 0 + } + +-- sample transactions --------------------------------------------------------- + +smallLegacyTx, mediumLegacyTx, largeLegacyTx :: Tx +smallLegacyTx = mkLegacyTx 1 1 +mediumLegacyTx = mkLegacyTx 5 5 +largeLegacyTx = mkLegacyTx 20 20 + +smallSegwitTx, mediumSegwitTx, largeSegwitTx :: Tx +smallSegwitTx = mkSegwitTx 1 1 +mediumSegwitTx = mkSegwitTx 5 5 +largeSegwitTx = mkSegwitTx 20 20 + +-- serialised bytes ------------------------------------------------------------ + +smallLegacyBytes, mediumLegacyBytes, largeLegacyBytes :: BS.ByteString +smallLegacyBytes = to_bytes smallLegacyTx +mediumLegacyBytes = to_bytes mediumLegacyTx +largeLegacyBytes = to_bytes largeLegacyTx + +smallSegwitBytes, mediumSegwitBytes, largeSegwitBytes :: BS.ByteString +smallSegwitBytes = to_bytes smallSegwitTx +mediumSegwitBytes = to_bytes mediumSegwitTx +largeSegwitBytes = to_bytes largeSegwitTx + -- benchmarks ------------------------------------------------------------------ main :: IO () -main = defaultMain [ - -- add benchmarks here - ] +main = defaultMain + [ bgroup "serialisation" + [ bgroup "to_bytes" + [ bench "small-legacy" $ nf to_bytes smallLegacyTx + , bench "small-segwit" $ nf to_bytes smallSegwitTx + , bench "medium-legacy" $ nf to_bytes mediumLegacyTx + , bench "medium-segwit" $ nf to_bytes mediumSegwitTx + , bench "large-legacy" $ nf to_bytes largeLegacyTx + , bench "large-segwit" $ nf to_bytes largeSegwitTx + ] + , bgroup "from_bytes" + [ bench "small-legacy" $ nf from_bytes smallLegacyBytes + , bench "small-segwit" $ nf from_bytes smallSegwitBytes + , bench "medium-legacy" $ nf from_bytes mediumLegacyBytes + , bench "medium-segwit" $ nf from_bytes mediumSegwitBytes + , bench "large-legacy" $ nf from_bytes largeLegacyBytes + , bench "large-segwit" $ nf from_bytes largeSegwitBytes + ] + , bgroup "to_bytes_legacy" + [ bench "small-legacy" $ nf to_bytes_legacy smallLegacyTx + , bench "small-segwit" $ nf to_bytes_legacy smallSegwitTx + , bench "medium-legacy" $ nf to_bytes_legacy mediumLegacyTx + , bench "medium-segwit" $ nf to_bytes_legacy mediumSegwitTx + , bench "large-legacy" $ nf to_bytes_legacy largeLegacyTx + , bench "large-segwit" $ nf to_bytes_legacy largeSegwitTx + ] + ] + , bgroup "txid" + [ bench "small-legacy" $ nf txid smallLegacyTx + , bench "small-segwit" $ nf txid smallSegwitTx + , bench "medium-legacy" $ nf txid mediumLegacyTx + , bench "medium-segwit" $ nf txid mediumSegwitTx + , bench "large-legacy" $ nf txid largeLegacyTx + , bench "large-segwit" $ nf txid largeSegwitTx + ] + ] diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -1,12 +1,10 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE BangPatterns #-} module Main where import Control.DeepSeq -import GHC.Generics +import qualified Data.ByteString as BS import qualified Weigh as W import Bitcoin.Prim.Tx @@ -14,30 +12,126 @@ import Bitcoin.Prim.Tx.Sighash -- NFData instances ------------------------------------------------------------ -deriving stock instance Generic TxId instance NFData TxId - -deriving stock instance Generic OutPoint instance NFData OutPoint - -deriving stock instance Generic TxIn instance NFData TxIn - -deriving stock instance Generic TxOut instance NFData TxOut - -deriving stock instance Generic Witness instance NFData Witness - -deriving stock instance Generic Tx instance NFData Tx - -deriving stock instance Generic SighashType instance NFData SighashType +-- sample data ----------------------------------------------------------------- + +-- | Sample outpoint (references a dummy txid). +sampleOutPoint :: OutPoint +sampleOutPoint = OutPoint (TxId (BS.replicate 32 0xab)) 0 + +-- | Sample input with typical P2PKH signature (~107 bytes). +sampleInput :: TxIn +sampleInput = TxIn + { txin_prevout = sampleOutPoint + , txin_script_sig = BS.replicate 107 0x00 -- typical P2PKH sig + , txin_sequence = 0xffffffff + } + +-- | Sample input for segwit (empty scriptSig). +sampleSegwitInput :: TxIn +sampleSegwitInput = TxIn + { txin_prevout = sampleOutPoint + , txin_script_sig = BS.empty + , txin_sequence = 0xffffffff + } + +-- | Sample output with typical P2PKH script (25 bytes). +sampleOutput :: TxOut +sampleOutput = TxOut + { txout_value = 50000000 + , txout_script_pubkey = BS.replicate 25 0x00 -- typical P2PKH script + } + +-- | Sample witness stack (signature + pubkey for P2WPKH). +sampleWitness :: Witness +sampleWitness = Witness + [ BS.replicate 72 0x00 -- DER signature + , BS.replicate 33 0x00 -- compressed pubkey + ] + +-- | Create a legacy transaction with n inputs and m outputs. +mkLegacyTx :: Int -> Int -> Tx +mkLegacyTx !numInputs !numOutputs = Tx + { tx_version = 1 + , tx_inputs = replicate numInputs sampleInput + , tx_outputs = replicate numOutputs sampleOutput + , tx_witnesses = [] + , tx_locktime = 0 + } + +-- | Create a segwit transaction with n inputs and m outputs. +mkSegwitTx :: Int -> Int -> Tx +mkSegwitTx !numInputs !numOutputs = Tx + { tx_version = 2 + , tx_inputs = replicate numInputs sampleSegwitInput + , tx_outputs = replicate numOutputs sampleOutput + , tx_witnesses = replicate numInputs sampleWitness + , tx_locktime = 0 + } + +-- sample transactions --------------------------------------------------------- + +smallLegacyTx, mediumLegacyTx, largeLegacyTx :: Tx +smallLegacyTx = mkLegacyTx 1 1 +mediumLegacyTx = mkLegacyTx 5 5 +largeLegacyTx = mkLegacyTx 20 20 + +smallSegwitTx, mediumSegwitTx, largeSegwitTx :: Tx +smallSegwitTx = mkSegwitTx 1 1 +mediumSegwitTx = mkSegwitTx 5 5 +largeSegwitTx = mkSegwitTx 20 20 + +-- serialised bytes ------------------------------------------------------------ + +smallLegacyBytes, mediumLegacyBytes, largeLegacyBytes :: BS.ByteString +smallLegacyBytes = to_bytes smallLegacyTx +mediumLegacyBytes = to_bytes mediumLegacyTx +largeLegacyBytes = to_bytes largeLegacyTx + +smallSegwitBytes, mediumSegwitBytes, largeSegwitBytes :: BS.ByteString +smallSegwitBytes = to_bytes smallSegwitTx +mediumSegwitBytes = to_bytes mediumSegwitTx +largeSegwitBytes = to_bytes largeSegwitTx + -- allocation benchmarks ------------------------------------------------------- main :: IO () main = W.mainWith $ do - -- add allocation benchmarks here - pure () + -- to_bytes + W.func "to_bytes/small-legacy" to_bytes smallLegacyTx + W.func "to_bytes/small-segwit" to_bytes smallSegwitTx + W.func "to_bytes/medium-legacy" to_bytes mediumLegacyTx + W.func "to_bytes/medium-segwit" to_bytes mediumSegwitTx + W.func "to_bytes/large-legacy" to_bytes largeLegacyTx + W.func "to_bytes/large-segwit" to_bytes largeSegwitTx + + -- from_bytes + W.func "from_bytes/small-legacy" from_bytes smallLegacyBytes + W.func "from_bytes/small-segwit" from_bytes smallSegwitBytes + W.func "from_bytes/medium-legacy" from_bytes mediumLegacyBytes + W.func "from_bytes/medium-segwit" from_bytes mediumSegwitBytes + W.func "from_bytes/large-legacy" from_bytes largeLegacyBytes + W.func "from_bytes/large-segwit" from_bytes largeSegwitBytes + + -- to_bytes_legacy + W.func "to_bytes_legacy/small-legacy" to_bytes_legacy smallLegacyTx + W.func "to_bytes_legacy/small-segwit" to_bytes_legacy smallSegwitTx + W.func "to_bytes_legacy/medium-legacy" to_bytes_legacy mediumLegacyTx + W.func "to_bytes_legacy/medium-segwit" to_bytes_legacy mediumSegwitTx + W.func "to_bytes_legacy/large-legacy" to_bytes_legacy largeLegacyTx + W.func "to_bytes_legacy/large-segwit" to_bytes_legacy largeSegwitTx + + -- txid + W.func "txid/small-legacy" txid smallLegacyTx + W.func "txid/small-segwit" txid smallSegwitTx + W.func "txid/medium-legacy" txid mediumLegacyTx + W.func "txid/medium-segwit" txid mediumSegwitTx + W.func "txid/large-legacy" txid largeLegacyTx + W.func "txid/large-segwit" txid largeSegwitTx