bolt5

On-chain transaction handling for Lightning (docs.ppad.tech/bolt5).
git clone git://git.ppad.tech/bolt5.git
Log | Files | Refs | README | LICENSE

commit 3ede0beb72812d79769878d1ab20978b6c563cb6
parent 9c3c9c72232c60c9a0b2aab82de6cec3378d09fe
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:26:05 +0400

bench: rewrite for the new API

Fixtures come from the BOLT #3 Appendix C parameters, built with
bolt1's checked constructors and fully evaluated before measuring;
the orphan NFData instances are gone, as the libraries now provide
them. Criterion and weigh cover close identification, all three
classifiers, preimage extraction and the spending-transaction
builders, forcing results with nf.

Diffstat:
Abench/Fixtures.hs | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbench/Main.hs | 210++++++++++++++++++++++++-------------------------------------------------------
Mbench/Weight.hs | 157++++++++++++++++++++++---------------------------------------------------------
Mppad-bolt5.cabal | 8++++++--
4 files changed, 282 insertions(+), 263 deletions(-)

diff --git a/bench/Fixtures.hs b/bench/Fixtures.hs @@ -0,0 +1,170 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} + +module Fixtures ( + Fixtures(..) + , fixtures + ) where + +import qualified Bitcoin.Prim.Tx as BT +import Control.DeepSeq (NFData, force) +import Control.Exception (evaluate) +import qualified Crypto.Hash.SHA256 as SHA256 +import qualified Data.ByteString as BS +import Data.Word (Word8, Word32, Word64) +import GHC.Generics (Generic) +import qualified Lightning.Protocol.BOLT1 as BOLT1 +import Lightning.Protocol.BOLT3 + +data Fixtures = Fixtures + { fx_keys :: !CommitmentKeys + , fx_commit :: !CommitmentTx + -- ^ BOLT #3 Appendix C commitment with five untrimmed HTLCs + , fx_other :: !CommitmentTx + -- ^ as 'fx_commit', with a different balance + , fx_raw :: !BS.ByteString + -- ^ serialized 'fx_commit' + , fx_outpoint :: !BT.OutPoint + , fx_value :: !BOLT1.Satoshi + , fx_anchor :: !BOLT1.Satoshi + , fx_dest :: !Script + , fx_feerate :: !FeeratePerKw + , fx_delay :: !ToSelfDelay + , fx_offered :: !HTLC + , fx_received :: !HTLC + , fx_htlc_ctx :: !HTLCContext + -- ^ for 'fx_offered' + , fx_hash :: !BOLT1.PaymentHash + -- ^ payment hash of 'fx_offered' and 'fx_received' + , fx_claim :: !BT.Witness + -- ^ remote preimage claim of an offered HTLC + , fx_success :: !BT.Witness + -- ^ HTLC-success transaction witness + } deriving Generic + +instance NFData Fixtures + +pt :: BS.ByteString -> Maybe BOLT1.Point +pt h = unhex h >>= BOLT1.point + +-- | Decode a hex string. +unhex :: BS.ByteString -> Maybe BS.ByteString +unhex bs + | odd (BS.length bs) = Nothing + | otherwise = BS.pack <$> go (BS.unpack bs) + where + go (a:b:rest) = (:) <$> byte a b <*> go rest + go _ = Just [] + byte a b = (\hi lo -> hi * 16 + lo) <$> nibble a <*> nibble b + nibble c + | c >= 0x30 && c <= 0x39 = Just (c - 0x30) + | c >= 0x61 && c <= 0x66 = Just (c - 0x57) + | otherwise = Nothing + +-- | Benchmark fixtures, from the BOLT #3 Appendix C parameters, fully +-- evaluated. +fixtures :: IO Fixtures +fixtures = maybe (fail "invalid fixtures") (evaluate . force) build + +build :: Maybe Fixtures +build = do + rev <- pt + "0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19" + delayed <- pt + "03fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c" + local_htlc <- pt + "030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e7" + remote_htlc <- pt + "0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b" + local_pay <- pt + "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" + remote_pay <- pt + "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + local_fund <- pt + "023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb" + remote_fund <- pt + "030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1" + funding_txid <- BT.mkTxId =<< unhex + "bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489" + cn <- commitment_number 42 + dust <- BOLT1.satoshi 546 + value <- BOLT1.satoshi 100000 + anchor <- BOLT1.satoshi 330 + to_remote <- BOLT1.milli_satoshi 3000000000 + h0 <- htlc HTLCReceived 1000000 500 0x00 + h1 <- htlc HTLCReceived 2000000 501 0x01 + h2 <- htlc HTLCOffered 2000000 502 0x02 + h3 <- htlc HTLCOffered 3000000 503 0x03 + h4 <- htlc HTLCReceived 4000000 504 0x04 + preimage <- BOLT1.payment_preimage (BS.replicate 32 0x01) + let keys = CommitmentKeys + { ck_revocation_pubkey = RevocationPubkey rev + , ck_local_delayed = LocalDelayedPubkey delayed + , ck_local_htlc = LocalHtlcPubkey local_htlc + , ck_remote_htlc = RemoteHtlcPubkey remote_htlc + , ck_remote_payment = RemotePubkey remote_pay + , ck_local_funding = FundingPubkey local_fund + , ck_remote_funding = FundingPubkey remote_fund + } + ctx to_local = do + tl <- BOLT1.milli_satoshi to_local + pure CommitmentContext + { cc_funding_outpoint = BT.OutPoint funding_txid 0 + , cc_commitment_number = cn + , cc_local_payment_bp = PaymentBasepoint local_pay + , cc_remote_payment_bp = PaymentBasepoint remote_pay + , cc_to_self_delay = ToSelfDelay 144 + , cc_dust_limit = DustLimit dust + , cc_feerate = FeeratePerKw 0 + , cc_format = StaticRemotekey + , cc_is_funder = True + , cc_to_local_msat = tl + , cc_to_remote_msat = to_remote + , cc_htlcs = [h0, h1, h2, h3, h4] + , cc_keys = keys + } + sig = BS.replicate 71 0xCC + script_of h = case htlc_direction h of + HTLCOffered -> offered_htlc_script (RevocationPubkey rev) + (RemoteHtlcPubkey remote_htlc) (LocalHtlcPubkey local_htlc) + (htlc_payment_hash h) StaticRemotekey + HTLCReceived -> received_htlc_script (RevocationPubkey rev) + (RemoteHtlcPubkey remote_htlc) (LocalHtlcPubkey local_htlc) + (htlc_payment_hash h) (htlc_cltv_expiry h) StaticRemotekey + offered = h2 { htlc_payment_hash = htlc_payment_hash h1 } + commit <- build_commitment_tx =<< ctx 6988000000 + other <- build_commitment_tx =<< ctx 6987000000 + pure Fixtures + { fx_keys = keys + , fx_commit = commit + , fx_other = other + , fx_raw = encode_commitment_tx commit + , fx_outpoint = BT.OutPoint funding_txid 1 + , fx_value = value + , fx_anchor = anchor + , fx_dest = Script (BS.pack [0x00, 0x14] <> BS.replicate 20 0xCC) + , fx_feerate = FeeratePerKw 253 + , fx_delay = ToSelfDelay 144 + , fx_offered = offered + , fx_received = h1 + , fx_htlc_ctx = HTLCContext + { hc_commitment_txid = funding_txid + , hc_output_index = 2 + , hc_htlc = offered + , hc_to_self_delay = ToSelfDelay 144 + , hc_feerate = FeeratePerKw 253 + , hc_format = StaticRemotekey + , hc_revocation_pubkey = RevocationPubkey rev + , hc_local_delayed = LocalDelayedPubkey delayed + } + , fx_hash = htlc_payment_hash h1 + , fx_claim = + offered_htlc_witness_preimage sig preimage (script_of offered) + , fx_success = htlc_success_witness sig sig preimage (script_of h1) + } + where + htlc :: HTLCDirection -> Word64 -> Word32 -> Word8 -> Maybe HTLC + htlc dir amt expiry b = do + a <- BOLT1.milli_satoshi amt + ph <- BOLT1.payment_hash (SHA256.hash (BS.replicate 32 b)) + pure (HTLC dir a ph (CltvExpiry expiry)) diff --git a/bench/Main.hs b/bench/Main.hs @@ -1,153 +1,69 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} - module Main where -import Bitcoin.Prim.Tx.Sighash (SighashType(..)) -import Control.DeepSeq (NFData(..)) import Criterion.Main -import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT3 hiding - (txout_value, txout_script) +import Fixtures +import Lightning.Protocol.BOLT3 import qualified Lightning.Protocol.BOLT5 as B5 --- NFData orphan instances for criterion ----------------------------- - -instance NFData Script where - rnf (Script bs) = rnf bs - -instance NFData Pubkey where - rnf pk = rnf (unPubkey pk) - -instance NFData RevocationPubkey where - rnf (RevocationPubkey x) = rnf x - -instance NFData LocalDelayedPubkey where - rnf (LocalDelayedPubkey p) = rnf p - -instance NFData FundingPubkey where - rnf (FundingPubkey p) = rnf p - -instance NFData FeeratePerKw where - rnf (FeeratePerKw x) = rnf x - -instance NFData ToSelfDelay where - rnf (ToSelfDelay x) = rnf x - -instance NFData CltvExpiry where - rnf (CltvExpiry x) = rnf x - -instance NFData HTLCDirection where - rnf HTLCOffered = () - rnf HTLCReceived = () - -instance NFData HTLC where - rnf (HTLC d a h c) = - rnf d `seq` rnf a `seq` rnf h `seq` rnf c - -instance NFData SighashType - -instance NFData B5.OutputResolution where - rnf B5.Resolved = () - rnf (B5.Revoke rk) = rnf rk - rnf _ = () - -instance NFData B5.HTLCOutputType where - rnf (B5.HTLCOfferedOutput e) = rnf e - rnf (B5.HTLCReceivedOutput e) = rnf e - -instance NFData B5.UnresolvedOutput where - rnf (B5.UnresolvedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.RevokedOutputType where - rnf B5.RevokedToLocal = () - rnf (B5.RevokedHTLC h) = rnf h - -instance NFData B5.RevokedOutput where - rnf (B5.RevokedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.SpendingTx where - rnf (B5.SpendingTx tx scr val sh) = - rnf tx `seq` rnf scr `seq` rnf val `seq` rnf sh - main :: IO () -main = defaultMain [ - spend_benchmarks - , classify_benchmarks - ] - --- fixtures ----------------------------------------------------------- - -dummyPubkey :: Pubkey -dummyPubkey = case pubkey (BS.pack (0x02 : replicate 32 0x01)) of - Just pk -> pk - Nothing -> error "impossible" - -dummyTxId :: TxId -dummyTxId = case mkTxId (BS.replicate 32 0x00) of - Just tid -> tid - Nothing -> error "impossible" - -dummyOutPoint :: OutPoint -dummyOutPoint = OutPoint dummyTxId 0 - -dummyRevPk :: RevocationPubkey -dummyRevPk = RevocationPubkey dummyPubkey - -dummyDelayedPk :: LocalDelayedPubkey -dummyDelayedPk = LocalDelayedPubkey dummyPubkey - -dummyFundPk :: FundingPubkey -dummyFundPk = FundingPubkey dummyPubkey - -dummyDest :: Script -dummyDest = Script $ BS.pack [0x00, 0x14] <> - BS.replicate 20 0xCC - -dummyFeerate :: FeeratePerKw -dummyFeerate = FeeratePerKw 253 - -dummyDelay :: ToSelfDelay -dummyDelay = ToSelfDelay 144 - --- benchmarks --------------------------------------------------------- - -spend_benchmarks :: Benchmark -spend_benchmarks = bgroup "spend" [ - bench "spend_to_local" $ - nf (\v -> B5.spend_to_local - dummyOutPoint v dummyRevPk dummyDelay - dummyDelayedPk dummyDest dummyFeerate) - (Satoshi 100000) - - , bench "spend_revoked_to_local" $ - nf (\v -> B5.spend_revoked_to_local - dummyOutPoint v dummyRevPk dummyDelay - dummyDelayedPk dummyDest dummyFeerate) - (Satoshi 100000) - - , bench "spend_anchor_owner" $ - nf (\v -> B5.spend_anchor_owner - dummyOutPoint v dummyFundPk dummyDest) - (Satoshi 330) - - ] - -classify_benchmarks :: Benchmark -classify_benchmarks = bgroup "classify" [ - bench "spending_fee" $ - nf (B5.spending_fee dummyFeerate) 538 - - , bench "htlc_timed_out" $ - nf (B5.htlc_timed_out 500001) - (HTLC HTLCOffered (MilliSatoshi 1000000) - dummyPaymentHash (CltvExpiry 500000)) - ] - where - dummyPaymentHash = case paymentHash - (BS.replicate 32 0xAA) of - Just ph -> ph - Nothing -> error "impossible" +main = do + fx <- fixtures + let keys = fx_keys fx + revpk = ck_revocation_pubkey keys + delayed = ck_local_delayed keys + defaultMain [ + bgroup "detect" [ + bench "identify_close" $ + nf (B5.identify_close (fx_other fx) (fx_commit fx)) (fx_raw fx) + , bench "classify_local_commit_outputs" $ + nf (\c -> B5.classify_local_commit_outputs c keys (fx_delay fx) + StaticRemotekey) + (fx_commit fx) + , bench "classify_remote_commit_outputs" $ + nf (\c -> B5.classify_remote_commit_outputs c keys) (fx_commit fx) + , bench "classify_revoked_commit_outputs" $ + nf (\c -> B5.classify_revoked_commit_outputs c revpk) + (fx_commit fx) + , bench "extract_preimage_offered" $ + nf (B5.extract_preimage_offered (fx_hash fx)) (fx_claim fx) + , bench "extract_preimage_htlc_success" $ + nf (B5.extract_preimage_htlc_success (fx_hash fx)) (fx_success fx) + , bench "htlc_timed_out" $ + nf (B5.htlc_timed_out 500001) (fx_offered fx) + ] + , bgroup "spend" [ + bench "spend_to_local" $ + nf (\v -> B5.spend_to_local (fx_outpoint fx) v revpk (fx_delay fx) + delayed (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + , bench "spend_htlc_tx" $ + nf (B5.spend_htlc_tx (fx_htlc_ctx fx)) keys + , bench "spend_remote_htlc_timeout" $ + nf (\v -> B5.spend_remote_htlc_timeout (fx_outpoint fx) v + (fx_received fx) keys StaticRemotekey (fx_dest fx) + (fx_feerate fx)) + (fx_value fx) + , bench "spend_remote_htlc_preimage" $ + nf (\v -> B5.spend_remote_htlc_preimage (fx_outpoint fx) v + (fx_offered fx) keys StaticRemotekey (fx_dest fx) + (fx_feerate fx)) + (fx_value fx) + , bench "spend_revoked_to_local" $ + nf (\v -> B5.spend_revoked_to_local (fx_outpoint fx) v revpk + (fx_delay fx) delayed (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + , bench "spend_revoked_htlc" $ + nf (\v -> B5.spend_revoked_htlc (fx_outpoint fx) v + (B5.HTLCReceivedOutput (CltvExpiry 501)) revpk keys + StaticRemotekey (fx_hash fx) (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + , bench "spend_anchor_owner" $ + nf (\v -> B5.spend_anchor_owner (fx_outpoint fx) v + (ck_local_funding keys) (fx_dest fx)) + (fx_anchor fx) + ] + , bgroup "fees" [ + bench "spending_fee" $ + nf (B5.spending_fee (fx_feerate fx)) 538 + ] + ] diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -1,121 +1,50 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} - module Main where -import Bitcoin.Prim.Tx.Sighash (SighashType(..)) -import Control.DeepSeq (NFData(..)) -import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT3 hiding - (txout_value, txout_script) +import Fixtures +import Lightning.Protocol.BOLT3 import qualified Lightning.Protocol.BOLT5 as B5 import Weigh --- NFData orphan instances for weigh --------------------------------- - -instance NFData Script where - rnf (Script bs) = rnf bs - -instance NFData Pubkey where - rnf pk = rnf (unPubkey pk) - -instance NFData RevocationPubkey where - rnf (RevocationPubkey x) = rnf x - -instance NFData LocalDelayedPubkey where - rnf (LocalDelayedPubkey p) = rnf p - -instance NFData FundingPubkey where - rnf (FundingPubkey p) = rnf p - -instance NFData FeeratePerKw where - rnf (FeeratePerKw x) = rnf x - -instance NFData ToSelfDelay where - rnf (ToSelfDelay x) = rnf x - -instance NFData CltvExpiry where - rnf (CltvExpiry x) = rnf x - -instance NFData SighashType - -instance NFData B5.OutputResolution where - rnf B5.Resolved = () - rnf (B5.Revoke rk) = rnf rk - rnf _ = () - -instance NFData B5.HTLCOutputType where - rnf (B5.HTLCOfferedOutput e) = rnf e - rnf (B5.HTLCReceivedOutput e) = rnf e - -instance NFData B5.UnresolvedOutput where - rnf (B5.UnresolvedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.RevokedOutputType where - rnf B5.RevokedToLocal = () - rnf (B5.RevokedHTLC h) = rnf h - -instance NFData B5.RevokedOutput where - rnf (B5.RevokedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.SpendingTx where - rnf (B5.SpendingTx tx scr val sh) = - rnf tx `seq` rnf scr `seq` rnf val `seq` rnf sh - -- note that 'weigh' doesn't work properly in a repl main :: IO () -main = mainWith $ do - spend_weights - --- fixtures ----------------------------------------------------------- - -dummyPubkey :: Pubkey -dummyPubkey = case pubkey (BS.pack (0x02 : replicate 32 0x01)) of - Just pk -> pk - Nothing -> error "impossible" - -dummyTxId :: TxId -dummyTxId = case mkTxId (BS.replicate 32 0x00) of - Just tid -> tid - Nothing -> error "impossible" - -dummyOutPoint :: OutPoint -dummyOutPoint = OutPoint dummyTxId 0 - -dummyRevPk :: RevocationPubkey -dummyRevPk = RevocationPubkey dummyPubkey - -dummyDelayedPk :: LocalDelayedPubkey -dummyDelayedPk = LocalDelayedPubkey dummyPubkey - -dummyFundPk :: FundingPubkey -dummyFundPk = FundingPubkey dummyPubkey - -dummyDest :: Script -dummyDest = Script $ BS.pack [0x00, 0x14] <> - BS.replicate 20 0xCC - -dummyFeerate :: FeeratePerKw -dummyFeerate = FeeratePerKw 253 - -dummyDelay :: ToSelfDelay -dummyDelay = ToSelfDelay 144 - --- weights ------------------------------------------------------------ - -spend_weights :: Weigh () -spend_weights = wgroup "spend" $ do - func "spend_to_local" - (\v -> B5.spend_to_local - dummyOutPoint v dummyRevPk dummyDelay - dummyDelayedPk dummyDest dummyFeerate) - (Satoshi 100000) - - func "spend_anchor_owner" - (\v -> B5.spend_anchor_owner - dummyOutPoint v dummyFundPk dummyDest) - (Satoshi 330) +main = do + fx <- fixtures + let keys = fx_keys fx + revpk = ck_revocation_pubkey keys + delayed = ck_local_delayed keys + mainWith $ do + wgroup "detect" $ do + func "identify_close" + (B5.identify_close (fx_other fx) (fx_commit fx)) (fx_raw fx) + func "classify_local_commit_outputs" + (\c -> B5.classify_local_commit_outputs c keys (fx_delay fx) + StaticRemotekey) + (fx_commit fx) + func "classify_remote_commit_outputs" + (\c -> B5.classify_remote_commit_outputs c keys) (fx_commit fx) + func "classify_revoked_commit_outputs" + (\c -> B5.classify_revoked_commit_outputs c revpk) (fx_commit fx) + func "extract_preimage_offered" + (B5.extract_preimage_offered (fx_hash fx)) (fx_claim fx) + func "extract_preimage_htlc_success" + (B5.extract_preimage_htlc_success (fx_hash fx)) (fx_success fx) + wgroup "spend" $ do + func "spend_to_local" + (\v -> B5.spend_to_local (fx_outpoint fx) v revpk (fx_delay fx) + delayed (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + func "spend_htlc_tx" (B5.spend_htlc_tx (fx_htlc_ctx fx)) keys + func "spend_remote_htlc_timeout" + (\v -> B5.spend_remote_htlc_timeout (fx_outpoint fx) v + (fx_received fx) keys StaticRemotekey (fx_dest fx) + (fx_feerate fx)) + (fx_value fx) + func "spend_revoked_htlc" + (\v -> B5.spend_revoked_htlc (fx_outpoint fx) v + (B5.HTLCReceivedOutput (CltvExpiry 501)) revpk keys + StaticRemotekey (fx_hash fx) (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + func "spend_anchor_owner" + (\v -> B5.spend_anchor_owner (fx_outpoint fx) v + (ck_local_funding keys) (fx_dest fx)) + (fx_anchor fx) diff --git a/ppad-bolt5.cabal b/ppad-bolt5.cabal @@ -65,9 +65,10 @@ benchmark bolt5-bench default-language: Haskell2010 hs-source-dirs: bench main-is: Main.hs + other-modules: Fixtures ghc-options: - -rtsopts -O2 -Wall -fno-warn-orphans + -rtsopts -O2 -Wall build-depends: base @@ -77,6 +78,7 @@ benchmark bolt5-bench , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 + , ppad-sha256 , ppad-tx benchmark bolt5-weigh @@ -84,9 +86,10 @@ benchmark bolt5-weigh default-language: Haskell2010 hs-source-dirs: bench main-is: Weight.hs + other-modules: Fixtures ghc-options: - -rtsopts -O2 -Wall -fno-warn-orphans + -rtsopts -O2 -Wall build-depends: base @@ -95,5 +98,6 @@ benchmark bolt5-weigh , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 + , ppad-sha256 , ppad-tx , weigh