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 6a81645d68127087b8ab800dd034c74dadac9be5
parent d5d564b902d72569c6b28836a0e0064322c7ed43
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:15:24 +0400

lib: port to ppad-bolt1 0.1.0, ppad-bolt3 0.1.0 and ppad-tx 0.2.0

Amounts, points and hashes are bolt1's types; ChannelFeatures is
replaced by bolt3's CommitmentFormat. Classification reads each HTLC
output's HTLC from bolt3's OutputHTLC rather than matching rebuilt
scripts against a caller-supplied HTLC list, so the classifiers no
longer take one (nor the parameters only the matching used).

spend_htlc_tx replaces spend_htlc_timeout and spend_htlc_success,
wrapping bolt3's build_htlc_tx and htlc_to_tx. spending_fee
saturates at 21 million BTC.

Only Lightning.Protocol.BOLT5 is exposed; the Internal module is
removed. Names are snake_case (htlc_output_type,
htlc_output_type_weight) and every public type has an NFData
instance.

Diffstat:
Mlib/Lightning/Protocol/BOLT5.hs | 115+++++++++++++++++++++++++++++++++++++------------------------------------------
Mlib/Lightning/Protocol/BOLT5/Detect.hs | 432+++++++++++++++++++++++++++----------------------------------------------------
Dlib/Lightning/Protocol/BOLT5/Internal.hs | 26--------------------------
Mlib/Lightning/Protocol/BOLT5/Spend.hs | 540+++++++++++++++++++++++++++++++------------------------------------------------
Mlib/Lightning/Protocol/BOLT5/Types.hs | 215+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mppad-bolt5.cabal | 11++++++++---
6 files changed, 541 insertions(+), 798 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT5.hs b/lib/Lightning/Protocol/BOLT5.hs @@ -9,84 +9,50 @@ -- On-chain transaction handling for the Lightning Network, per -- [BOLT #5](https://github.com/lightning/bolts/blob/master/05-onchain.md). -- --- This module implements the logic for handling channel closures: --- --- * Mutual close - cooperative closure agreed by both parties --- * Unilateral close - one party publishes their commitment --- transaction --- * Revoked transaction close - penalty for publishing old state +-- This library's scope is being redesigned, and it is not yet ready +-- for release. It covers unilateral closes only partially: it +-- identifies a local or remote commitment on chain, classifies its +-- outputs, extracts preimages from HTLC spends, and builds unsigned, +-- single-input spending transactions. It doesn't detect mutual or +-- revoked closes, assemble witnesses, or attach fee inputs to anchor +-- spends and zero-fee HTLC transactions. -- -- = Design -- --- This is a stateless toolkit of pure functions. The caller --- manages channel state (which outputs are resolved, current --- block height, etc.) and provides explicit inputs. Functions --- produce unsigned 'SpendingTx' values; the caller signs and --- assembles witnesses using bolt3 constructors. +-- This is a stateless toolkit of pure functions. The caller manages +-- channel state (which outputs are resolved, the current block +-- height, etc.) and provides explicit inputs. Commitment +-- transactions are those built by ppad-bolt3, and amounts are +-- ppad-bolt1's. Functions produce unsigned 'SpendingTx' values; the +-- caller signs them and assembles witnesses with bolt3's witness +-- functions. -- -- = Usage -- -- @ +-- import qualified Lightning.Protocol.BOLT1 as BOLT1 -- import Lightning.Protocol.BOLT3 -- import Lightning.Protocol.BOLT5 -- --- -- Classify outputs of our local commitment --- let outputs = classify_local_commit_outputs --- commitTx keys delay features htlcs +-- -- classify the outputs of our local commitment +-- let outputs = classify_local_commit_outputs commit keys delay fmt -- --- -- For each unresolved output, construct spending tx +-- -- for each unresolved output, build a spending transaction -- case uo_type output of -- SpendToLocal delay revpk delayedpk -> --- spend_to_local (uo_outpoint output) --- (uo_value output) revpk delay delayedpk --- destScript feerate +-- spend_to_local (uo_outpoint output) (uo_value output) +-- revpk delay delayedpk dest feerate -- ... -- @ module Lightning.Protocol.BOLT5 ( - -- * Types - -- ** Close identification - CloseType(..) - - -- ** Output classification - , UnresolvedOutput(..) - , OutputResolution(..) - - -- ** HTLC output type - , HTLCOutputType(..) - , htlcOutputType - , htlcOutputTypeWeight - - -- ** Spending transactions - , SpendingTx(..) - - -- ** Revoked outputs - , RevokedOutput(..) - , RevokedOutputType(..) - , revoked_output_weight - - -- * Weight constants (Appendix A) - , to_local_penalty_witness_weight - , offered_htlc_penalty_witness_weight - , accepted_htlc_penalty_witness_weight - , offered_htlc_penalty_witness_weight_anchors - , accepted_htlc_penalty_witness_weight_anchors - , to_local_penalty_input_weight - , offered_htlc_penalty_input_weight - , accepted_htlc_penalty_input_weight - , offered_htlc_penalty_input_weight_anchors - , accepted_htlc_penalty_input_weight_anchors - , to_remote_input_weight - , penalty_tx_base_weight - , max_standard_weight - - -- * Fee calculation - , spending_fee - -- * Close identification + CloseType(..) , identify_close -- * Output classification + , UnresolvedOutput(..) + , OutputResolution(..) , classify_local_commit_outputs , classify_remote_commit_outputs , classify_revoked_commit_outputs @@ -98,11 +64,12 @@ module Lightning.Protocol.BOLT5 ( -- * Timeout check , htlc_timed_out - -- * Spending transaction construction + -- * Spending transactions + , SpendingTx(..) + -- ** Local commitment , spend_to_local - , spend_htlc_timeout - , spend_htlc_success + , spend_htlc_tx , spend_htlc_output -- ** Remote commitment @@ -117,8 +84,34 @@ module Lightning.Protocol.BOLT5 ( -- ** Anchor outputs , spend_anchor_owner , spend_anchor_anyone + + -- * Revoked outputs + , HTLCOutputType(..) + , htlc_output_type + , RevokedOutput(..) + , RevokedOutputType(..) + + -- * Weights (Appendix A) + , revoked_output_weight + , htlc_output_type_weight + , to_local_penalty_witness_weight + , offered_htlc_penalty_witness_weight + , accepted_htlc_penalty_witness_weight + , offered_htlc_penalty_witness_weight_anchors + , accepted_htlc_penalty_witness_weight_anchors + , to_local_penalty_input_weight + , offered_htlc_penalty_input_weight + , accepted_htlc_penalty_input_weight + , offered_htlc_penalty_input_weight_anchors + , accepted_htlc_penalty_input_weight_anchors + , to_remote_input_weight + , penalty_tx_base_weight + , max_standard_weight + + -- * Fees + , spending_fee ) where -import Lightning.Protocol.BOLT5.Types import Lightning.Protocol.BOLT5.Detect import Lightning.Protocol.BOLT5.Spend +import Lightning.Protocol.BOLT5.Types diff --git a/lib/Lightning/Protocol/BOLT5/Detect.hs b/lib/Lightning/Protocol/BOLT5/Detect.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} -- | @@ -27,11 +27,14 @@ module Lightning.Protocol.BOLT5.Detect ( , htlc_timed_out ) where +import Bitcoin.Prim.Tx (OutPoint(..), TxId, Witness(..)) import qualified Bitcoin.Prim.Tx as BT import qualified Crypto.Hash.SHA256 as SHA256 -import Data.List (mapAccumL) -import Data.Word (Word32) import qualified Data.ByteString as BS +import qualified Data.List.NonEmpty as NE +import Data.Word (Word32) +import Lightning.Protocol.BOLT1 (PaymentHash, PaymentPreimage) +import qualified Lightning.Protocol.BOLT1 as BOLT1 import Lightning.Protocol.BOLT3 import Lightning.Protocol.BOLT5.Types @@ -40,17 +43,16 @@ import Lightning.Protocol.BOLT5.Types -- | Identify the type of channel close from a transaction that -- spends the funding output. -- --- Parses the on-chain transaction and compares its txid against --- the txids of the known local and remote commitment --- transactions. Comparing txids rather than bytes means the --- signed (segwit) serialization found on chain matches the --- unsigned commitment. +-- Parses the on-chain transaction and compares its txid with the +-- txids of the known local and remote commitment transactions. +-- Comparing txids rather than bytes means the signed (segwit) +-- serialization found on chain matches the unsigned commitment. -- --- Returns 'Nothing' if the bytes don't parse as a transaction or --- the transaction doesn't match either commitment. Mutual close --- and revoked commitment detection require additional checks by --- the caller (e.g. comparing closing tx format, checking a secret --- store for older commitment numbers). +-- Returns 'Nothing' if the bytes don't parse as a transaction or +-- the transaction matches neither commitment. Mutual close and +-- revoked commitment detection are left to the caller (e.g. +-- comparing with the closing transaction, or checking a secret +-- store for older commitment numbers). identify_close :: CommitmentTx -- ^ Our local commitment tx. @@ -59,31 +61,27 @@ identify_close -> BS.ByteString -- ^ Raw serialized transaction found on chain. -> Maybe CloseType -identify_close !localCommitTx !remoteCommitTx - !onChainBytes = - case BT.from_bytes onChainBytes of - Nothing -> Nothing - Just !onChainTx -> - let !onChainTxid = Just (BT.txid onChainTx) - in if commitment_txid localCommitTx == onChainTxid - then Just LocalCommitClose - else if commitment_txid remoteCommitTx == onChainTxid - then Just RemoteCommitClose - else Nothing +identify_close !local !remote !bytes = do + !tx <- BT.from_bytes bytes + let !txid = BT.txid tx + if commitment_txid local == txid + then Just LocalCommitClose + else if commitment_txid remote == txid + then Just RemoteCommitClose + else Nothing -- output classification ---------------------------------------------- --- | Classify outputs of our local commitment transaction. +-- | Classify the outputs of our local commitment transaction. -- --- Per BOLT #5: when we discover our local commitment on chain, --- we must resolve each output. to_local requires a CSV-delayed --- spend, to_remote is resolved by the commitment itself, HTLC --- outputs need second-stage transactions, and anchors can be --- spent immediately. +-- Per BOLT #5: when our local commitment is on chain, we must +-- resolve each output. to_local requires a CSV-delayed spend, +-- to_remote is resolved by the commitment itself, HTLC outputs +-- need second-stage transactions, and our anchor can be spent +-- immediately. -- --- An HTLC output is matched to the HTLC with the same direction, --- script, amount and CLTV expiry, and each HTLC matches at most one --- output. +-- Each HTLC output's HTLC is read from its bolt3 'OutputHTLC', so +-- the commitment must be the one bolt3 built for this state. classify_local_commit_outputs :: CommitmentTx -- ^ Our local commitment transaction. @@ -91,306 +89,166 @@ classify_local_commit_outputs -- ^ Derived keys for this commitment. -> ToSelfDelay -- ^ Remote's to_self_delay (CSV delay for our outputs). - -> ChannelFeatures - -- ^ Channel feature flags. - -> [HTLC] - -- ^ HTLCs in this commitment. + -> CommitmentFormat -> [UnresolvedOutput] -classify_local_commit_outputs !commitTx !keys !delay - !features !htlcs = - case commitment_txid commitTx of - Nothing -> [] - Just !txid -> - let !outputs = ctx_outputs commitTx - !revpk = ck_revocation_pubkey keys - !delayedpk = ck_local_delayed keys - classify cands (idx, out) = - classifyLocalOutput txid revpk delayedpk - delay features keys cands idx out - in snd (mapAccumL classify - (htlc_candidates keys features htlcs) - (zip [0..] outputs)) - --- | Classify a single output from a local commitment tx, removing --- any matched HTLC from the candidates. -classifyLocalOutput - :: TxId - -> RevocationPubkey - -> LocalDelayedPubkey - -> ToSelfDelay - -> ChannelFeatures - -> CommitmentKeys - -> [(HTLC, Script)] - -> Word32 - -> TxOutput - -> ([(HTLC, Script)], UnresolvedOutput) -classifyLocalOutput !txid !revpk !delayedpk !delay - !features !keys !cands !idx !out = - let !op = OutPoint txid idx - !val = txout_value out - !(rest, resolution) = case txout_type out of - OutputToLocal -> - (cands, SpendToLocal delay revpk delayedpk) - OutputToRemote -> - (cands, Resolved) - OutputLocalAnchor -> - (cands, AnchorSpend (ck_local_funding keys)) - OutputRemoteAnchor -> - (cands, Resolved) - OutputOfferedHTLC expiry -> - case take_htlc HTLCOffered expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCTimeout htlc keys features) - (Nothing, cands') -> (cands', Resolved) - OutputReceivedHTLC expiry -> - case take_htlc HTLCReceived expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCSuccess htlc keys features) - (Nothing, cands') -> (cands', Resolved) - in (rest, UnresolvedOutput op val resolution) - --- | Classify outputs of the remote commitment transaction. --- --- Per BOLT #5: when we discover the remote commitment on chain, --- there are no CSV delays on our outputs. We can spend offered --- HTLCs directly after timeout, and received HTLCs directly --- with the preimage. --- --- HTLC outputs are matched as in 'classify_local_commit_outputs'. +classify_local_commit_outputs !tx !keys !delay !fmt = + classify_outputs resolve tx + where + resolve t = case t of + OutputToLocal -> + SpendToLocal delay (ck_revocation_pubkey keys) + (ck_local_delayed keys) + OutputToRemote -> Resolved + OutputLocalAnchor -> AnchorSpend (ck_local_funding keys) + OutputRemoteAnchor -> Resolved + OutputHTLC h -> case htlc_direction h of + HTLCOffered -> SpendHTLCTimeout h keys fmt + HTLCReceived -> SpendHTLCSuccess h keys fmt + +-- | Classify the outputs of the remote commitment transaction. +-- +-- Per BOLT #5: when the remote commitment is on chain, there are +-- no CSV delays on our outputs. We can spend HTLCs we offered +-- directly after timeout, and HTLCs we received directly with the +-- preimage. +-- +-- The commitment, its keys and its HTLCs are from the remote's +-- point of view (as bolt3 builds the remote's commitment): an +-- HTLC the remote offered is one we received. classify_remote_commit_outputs :: CommitmentTx -- ^ The remote commitment transaction. -> CommitmentKeys - -- ^ Derived keys for this commitment (from remote's - -- perspective, so local/remote are swapped). - -> ChannelFeatures - -- ^ Channel feature flags. - -> [HTLC] - -- ^ HTLCs in this commitment. + -- ^ Derived keys for this commitment (from the remote's point + -- of view, so local and remote are swapped). -> [UnresolvedOutput] -classify_remote_commit_outputs !commitTx !keys - !features !htlcs = - case commitment_txid commitTx of - Nothing -> [] - Just !txid -> - let !outputs = ctx_outputs commitTx - classify cands (idx, out) = - classifyRemoteOutput txid keys cands idx out - in snd (mapAccumL classify - (htlc_candidates keys features htlcs) - (zip [0..] outputs)) - --- | Classify a single output from a remote commitment tx, removing --- any matched HTLC from the candidates. -classifyRemoteOutput - :: TxId - -> CommitmentKeys - -> [(HTLC, Script)] - -> Word32 - -> TxOutput - -> ([(HTLC, Script)], UnresolvedOutput) -classifyRemoteOutput !txid !keys !cands !idx !out = - let !op = OutPoint txid idx - !val = txout_value out - !(rest, resolution) = case txout_type out of - OutputToLocal -> - (cands, Resolved) -- Remote's to_local; not ours - OutputToRemote -> - (cands, Resolved) -- Our to_remote; resolved by commitment - OutputLocalAnchor -> - (cands, Resolved) -- Remote's anchor - OutputRemoteAnchor -> - (cands, AnchorSpend (ck_remote_funding keys)) - OutputOfferedHTLC expiry -> - -- On remote's commit, their offered = our received. - -- We can claim with preimage. - case take_htlc HTLCOffered expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCPreimageDirect htlc) - (Nothing, cands') -> (cands', Resolved) - OutputReceivedHTLC expiry -> - -- On remote's commit, their received = our offered. - -- We can claim after timeout. - case take_htlc HTLCReceived expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCTimeoutDirect htlc) - (Nothing, cands') -> (cands', Resolved) - in (rest, UnresolvedOutput op val resolution) - --- | Classify outputs of a revoked commitment transaction. --- --- Per BOLT #5: when we discover a revoked commitment, we can --- claim everything using the revocation key. to_local is spent --- via revocation, HTLCs are spent via revocation, and we can --- also optionally sweep to_remote. +classify_remote_commit_outputs !tx !keys = + classify_outputs resolve tx + where + resolve t = case t of + OutputToLocal -> Resolved -- the remote's to_local + OutputToRemote -> Resolved -- ours; resolved by the commitment + OutputLocalAnchor -> Resolved -- the remote's anchor + OutputRemoteAnchor -> AnchorSpend (ck_remote_funding keys) + OutputHTLC h -> case htlc_direction h of + HTLCOffered -> SpendHTLCPreimageDirect h -- we received it + HTLCReceived -> SpendHTLCTimeoutDirect h -- we offered it + +-- | Classify the outputs of a revoked commitment transaction. +-- +-- Per BOLT #5: when a revoked commitment is on chain, we can +-- claim to_local and every HTLC output with the revocation key. +-- to_remote (ours) and the anchors are marked 'Resolved'. classify_revoked_commit_outputs :: CommitmentTx -- ^ The revoked commitment transaction. - -> CommitmentKeys - -- ^ Derived keys for the revoked commitment. -> RevocationPubkey -- ^ Revocation pubkey (derived from the revealed secret). - -> ChannelFeatures - -- ^ Channel feature flags. - -> [HTLC] - -- ^ HTLCs in the revoked commitment. -> [UnresolvedOutput] -classify_revoked_commit_outputs !commitTx !_keys - !revpk !_features !_htlcs = - case commitment_txid commitTx of - Nothing -> [] - Just !txid -> - let !outputs = ctx_outputs commitTx - in zipWith (classifyRevokedOutput txid revpk) - [0..] outputs - --- | Classify a single output from a revoked commitment tx. -classifyRevokedOutput - :: TxId - -> RevocationPubkey - -> Word32 - -> TxOutput - -> UnresolvedOutput -classifyRevokedOutput !txid !revpk !idx !out = - let !op = OutPoint txid idx - !val = txout_value out - !resolution = case txout_type out of - OutputToLocal -> - Revoke revpk - OutputToRemote -> - Resolved -- Our funds; resolved by commitment - OutputLocalAnchor -> - Resolved -- Can be swept by anyone after 16 blocks - OutputRemoteAnchor -> - Resolved -- Our anchor - OutputOfferedHTLC expiry -> - RevokeHTLC revpk (HTLCOfferedOutput expiry) - OutputReceivedHTLC expiry -> - RevokeHTLC revpk (HTLCReceivedOutput expiry) - in UnresolvedOutput op val resolution +classify_revoked_commit_outputs !tx !revpk = + classify_outputs resolve tx + where + resolve t = case t of + OutputToLocal -> Revoke revpk + OutputToRemote -> Resolved + OutputLocalAnchor -> Resolved + OutputRemoteAnchor -> Resolved + OutputHTLC h -> RevokeHTLC revpk $ case htlc_direction h of + HTLCOffered -> HTLCOfferedOutput (htlc_cltv_expiry h) + HTLCReceived -> HTLCReceivedOutput (htlc_cltv_expiry h) + +-- | Pair each commitment output with its outpoint, value and +-- resolution. +classify_outputs + :: (OutputType -> OutputResolution) + -> CommitmentTx + -> [UnresolvedOutput] +classify_outputs resolve tx = + zipWith classify [0..] (NE.toList (ctx_outputs tx)) + where + !txid = commitment_txid tx + classify :: Word32 -> CommitmentOutput -> UnresolvedOutput + classify !idx !out = UnresolvedOutput + { uo_outpoint = OutPoint txid idx + , uo_value = co_value out + , uo_type = resolve (co_type out) + } -- preimage extraction ------------------------------------------------ --- | Extract a payment preimage from an offered HTLC witness. +-- | Extract a payment preimage from the witness of an input +-- spending an offered HTLC output. -- --- When the remote party claims an offered HTLC on our local --- commitment, the witness contains the preimage. The witness --- stack for a preimage claim is: +-- When the remote party claims an offered HTLC on our local +-- commitment, the witness (see bolt3's +-- 'offered_htlc_witness_preimage') is: -- --- @\<remotehtlcsig\> \<paymentPreimage\> \<witnessScript\>@ +-- @\<remotehtlcsig\> \<payment_preimage\> \<witness_script\>@ -- --- Returns the preimage only if it hashes to the given payment --- hash. Any other spend of the output, e.g. by revocation, yields --- 'Nothing'. +-- Returns the preimage only if it hashes to the given payment +-- hash. Any other spend of the output, e.g. by revocation, yields +-- 'Nothing'. extract_preimage_offered :: PaymentHash -- ^ Payment hash of the HTLC. -> Witness -- ^ Witness of the input spending the HTLC output. -> Maybe PaymentPreimage -extract_preimage_offered !ph (Witness items) = - case items of - [_sig, preimageBytes, _script] -> - check_preimage ph preimageBytes - _ -> Nothing +extract_preimage_offered !ph (Witness items) = case items of + [_sig, preimage, _script] -> check_preimage ph preimage + _ -> Nothing --- | Extract a payment preimage from an HTLC-success transaction --- witness. +-- | Extract a payment preimage from the witness of an HTLC-success +-- transaction's input. -- --- When the remote party uses an HTLC-success tx on their --- commitment to claim a received HTLC, the witness contains the --- preimage. The witness stack is: +-- When the remote party claims an HTLC it received on its +-- commitment with an HTLC-success transaction, the witness (see +-- bolt3's 'htlc_success_witness') is: -- --- @0 \<remotehtlcsig\> \<localhtlcsig\> \<paymentPreimage\> --- \<witnessScript\>@ +-- @0 \<remotehtlcsig\> \<localhtlcsig\> \<payment_preimage\> +-- \<witness_script\>@ -- --- Returns the preimage only if it hashes to the given payment --- hash. +-- Returns the preimage only if it hashes to the given payment +-- hash. extract_preimage_htlc_success :: PaymentHash -- ^ Payment hash of the HTLC. -> Witness -- ^ Witness of the HTLC-success transaction input. -> Maybe PaymentPreimage -extract_preimage_htlc_success !ph (Witness items) = - case items of - [_zero, _remoteSig, _localSig, preimageBytes, _script] -> - check_preimage ph preimageBytes - _ -> Nothing +extract_preimage_htlc_success !ph (Witness items) = case items of + [_zero, _remote_sig, _local_sig, preimage, _script] -> + check_preimage ph preimage + _ -> Nothing -- | Accept a candidate preimage only if it hashes to the payment -- hash. check_preimage :: PaymentHash -> BS.ByteString -> Maybe PaymentPreimage check_preimage !ph !bs - | SHA256.hash bs == unPaymentHash ph = paymentPreimage bs + | SHA256.hash bs == BOLT1.un_payment_hash ph = BOLT1.payment_preimage bs | otherwise = Nothing -- timeout check ------------------------------------------------------ --- | Check if an HTLC has timed out at the given block height. +-- | Whether an HTLC has timed out at the given block height: whether +-- the height is at least the HTLC's CLTV expiry. -- --- An HTLC has timed out when the current block height is equal --- to or greater than the HTLC's CLTV expiry. +-- >>> let Just amt = BOLT1.milli_satoshi 1000000 +-- >>> let Just ph = BOLT1.payment_hash (BS.replicate 32 0xaa) +-- >>> let htlc = HTLC HTLCOffered amt ph (CltvExpiry 800000) +-- >>> htlc_timed_out 799999 htlc +-- False +-- >>> htlc_timed_out 800000 htlc +-- True htlc_timed_out :: Word32 -> HTLC -> Bool -htlc_timed_out !currentHeight !htlc = - currentHeight >= unCltvExpiry (htlc_cltv_expiry htlc) +htlc_timed_out !height !htlc = + let CltvExpiry expiry = htlc_cltv_expiry htlc + in height >= expiry {-# INLINE htlc_timed_out #-} -- internal helpers --------------------------------------------------- --- | Compute the txid of a commitment transaction. --- --- Returns 'Nothing' if the commitment has no outputs. -commitment_txid :: CommitmentTx -> Maybe TxId -commitment_txid !tx = fmap BT.txid (commitment_to_tx tx) - --- | Pair each HTLC with the P2WSH script of its commitment output. -htlc_candidates - :: CommitmentKeys - -> ChannelFeatures - -> [HTLC] - -> [(HTLC, Script)] -htlc_candidates !keys !features = map (\htlc -> (htlc, script htlc)) - where - script htlc = case htlc_direction htlc of - HTLCOffered -> - to_p2wsh $ offered_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - features - HTLCReceived -> - to_p2wsh $ received_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - (htlc_cltv_expiry htlc) - features - --- | Find the HTLC for an HTLC output and remove it from the --- candidates. --- --- The HTLC must agree with the output on direction, script, amount --- and CLTV expiry. The offered HTLC script doesn't commit to the --- expiry and no HTLC script commits to the amount, so HTLCs sharing --- a payment hash differ only in these. Removing the match ensures --- no two outputs resolve to the same HTLC. -take_htlc - :: HTLCDirection - -> CltvExpiry - -- ^ Expiry recorded for the output. - -> TxOutput - -> [(HTLC, Script)] - -> (Maybe HTLC, [(HTLC, Script)]) -take_htlc !dir !expiry !out !cands = - case break matches cands of - (pre, (htlc, _) : post) -> (Just htlc, pre ++ post) - (_, []) -> (Nothing, cands) - where - matches (htlc, script) = - htlc_direction htlc == dir - && htlc_cltv_expiry htlc == expiry - && msatToSat (htlc_amount_msat htlc) == txout_value out - && script == txout_script out +-- | The txid of a commitment transaction. +commitment_txid :: CommitmentTx -> TxId +commitment_txid = BT.txid . commitment_to_tx +{-# INLINE commitment_txid #-} diff --git a/lib/Lightning/Protocol/BOLT5/Internal.hs b/lib/Lightning/Protocol/BOLT5/Internal.hs @@ -1,26 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} - --- | --- Module: Lightning.Protocol.BOLT5.Internal --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Internal definitions for BOLT #5. --- --- This module re-exports all constructors from the Types --- module, including those not exported by the public API. --- Use only in tests or trusted internal code. - -module Lightning.Protocol.BOLT5.Internal ( - -- * All type constructors (for test use) - CloseType(..) - , UnresolvedOutput(..) - , OutputResolution(..) - , HTLCOutputType(..) - , SpendingTx(..) - , RevokedOutput(..) - , RevokedOutputType(..) - ) where - -import Lightning.Protocol.BOLT5.Types diff --git a/lib/Lightning/Protocol/BOLT5/Spend.hs b/lib/Lightning/Protocol/BOLT5/Spend.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} -- | @@ -11,15 +11,13 @@ -- transaction handling. -- -- All functions produce unsigned 'SpendingTx' values. The caller --- is responsible for signing (using the sighash metadata --- provided) and assembling final witnesses via bolt3 witness --- constructors. +-- signs (using the sighash metadata provided) and assembles the +-- witness with bolt3's witness functions. module Lightning.Protocol.BOLT5.Spend ( -- * Local commitment spends spend_to_local - , spend_htlc_timeout - , spend_htlc_success + , spend_htlc_tx , spend_htlc_output -- * Remote commitment spends @@ -36,27 +34,27 @@ module Lightning.Protocol.BOLT5.Spend ( , spend_anchor_anyone ) where -import Bitcoin.Prim.Tx (TxOut(..)) +import Bitcoin.Prim.Tx (OutPoint, Tx(..), TxIn(..), TxOut(..)) import Bitcoin.Prim.Tx.Sighash (SighashType(..)) +import qualified Data.ByteString as BS import Data.List.NonEmpty (NonEmpty(..)) import Data.Word (Word32, Word64) -import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT3 hiding - (txout_value, txout_script) +import Lightning.Protocol.BOLT1 (PaymentHash, Satoshi) +import qualified Lightning.Protocol.BOLT1 as BOLT1 +import Lightning.Protocol.BOLT3 import Lightning.Protocol.BOLT5.Types -- local commitment spends -------------------------------------------- -- | Spend the to_local output of our local commitment tx. -- --- Requires waiting for the CSV delay (to_self_delay) before --- broadcasting. The caller signs with the local delayed privkey --- and uses 'to_local_witness_spend' from bolt3. --- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- The input's nSequence is the to_self_delay, so the transaction +-- is valid only once the delay has passed. The caller signs with +-- the local delayed privkey and uses bolt3's +-- 'to_local_witness_spend'. -- --- The input nSequence is set to the to_self_delay value. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_to_local :: OutPoint -- ^ Outpoint of the to_local output. @@ -69,94 +67,47 @@ spend_to_local -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_to_local !op !value !revpk !delay !delayedpk - !destScript !feerate = - let !witnessScript = - to_local_script revpk delay delayedpk - !weight = to_local_penalty_input_weight - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op - (fromIntegral (unToSelfDelay delay)) - destScript outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_to_local !op !value !revpk delay@(ToSelfDelay d) !delayedpk + !dest !feerate = + sweep op value (to_local_script revpk delay delayedpk) + to_local_penalty_input_weight (fromIntegral d) 0 dest feerate --- | Construct an HTLC-timeout second-stage transaction. +-- | Build the HTLC-timeout (for an offered HTLC) or HTLC-success +-- (for a received HTLC) transaction spending an HTLC output of +-- our local commitment, with bolt3's 'build_htlc_tx'. -- --- Used when we offered an HTLC on our local commitment and it --- has timed out. The bolt3 'build_htlc_timeout_tx' function --- constructs the HTLC-timeout tx; this wraps it as a --- 'SpendingTx' with the witness script and sighash metadata. --- --- The sighash type is for the local signature, which is always --- @SIGHASH_ALL@. With @option_anchors@ the remote's --- @htlc_signature@ instead uses --- @SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@. -spend_htlc_timeout - :: HTLCContext - -> CommitmentKeys - -- ^ Full commitment keys (needed for witness script). - -> SpendingTx -spend_htlc_timeout !ctx !keys = - let !htlcTx = build_htlc_timeout_tx ctx - !htlc = hc_htlc ctx - !features = hc_features ctx - !witnessScript = offered_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - features - !inputValue = - msatToSat (htlc_amount_msat htlc) - !tx = htlc_tx_to_tx htlcTx - in SpendingTx tx witnessScript inputValue SIGHASH_ALL - --- | Construct an HTLC-success second-stage transaction. +-- The sighash type is that of the local signature, which is +-- always @SIGHASH_ALL@. The remote's @htlc_signature@ uses bolt3's +-- 'remote_htlc_sighash' (@SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@ +-- under 'Anchors'). The witness is bolt3's 'htlc_timeout_witness' +-- or 'htlc_success_witness'. -- --- Used when we received an HTLC on our local commitment and --- have the preimage. The bolt3 'build_htlc_success_tx' function --- constructs the HTLC-success tx; this wraps it as a --- 'SpendingTx'. +-- Under 'Anchors' the transaction pays no fee; it must be +-- combined with other inputs before broadcast. -- --- The sighash type is for the local signature, which is always --- @SIGHASH_ALL@. With @option_anchors@ the remote's --- @htlc_signature@ instead uses --- @SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@. -spend_htlc_success +-- Returns 'Nothing' if bolt3 can't build the transaction (the fee +-- exceeds the HTLC amount). +spend_htlc_tx :: HTLCContext -> CommitmentKeys - -- ^ Full commitment keys (needed for witness script). - -> SpendingTx -spend_htlc_success !ctx !keys = - let !htlcTx = build_htlc_success_tx ctx - !htlc = hc_htlc ctx - !features = hc_features ctx - !witnessScript = received_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - (htlc_cltv_expiry htlc) - features - !inputValue = - msatToSat (htlc_amount_msat htlc) - !tx = htlc_tx_to_tx htlcTx - in SpendingTx tx witnessScript inputValue SIGHASH_ALL + -- ^ Keys for our local commitment (for the witness script). + -> Maybe SpendingTx +spend_htlc_tx !ctx !keys = do + htx <- build_htlc_tx ctx + let !h = hc_htlc ctx + pure SpendingTx + { stx_tx = htlc_to_tx htx + , stx_input_script = htlc_script keys (hc_format ctx) h + , stx_input_value = BOLT1.msat_to_sat (htlc_amount_msat h) + , stx_sighash_type = SIGHASH_ALL + } --- | Spend a second-stage HTLC output (HTLC-timeout or --- HTLC-success output) after the CSV delay. +-- | Spend the output of an HTLC-timeout or HTLC-success +-- transaction after the CSV delay. -- --- The output of an HTLC-timeout or HTLC-success tx uses the --- same to_local script. The caller signs with the local --- delayed privkey and uses 'htlc_output_witness_spend'. --- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- The output uses the to_local script, so this is +-- 'spend_to_local'. The caller signs with the local delayed +-- privkey and uses bolt3's 'to_local_witness_spend'. spend_htlc_output :: OutPoint -- ^ Outpoint of the second-stage output. @@ -173,14 +124,15 @@ spend_htlc_output = spend_to_local -- remote commitment spends ------------------------------------------- --- | Spend an offered HTLC directly after timeout on the remote --- commitment. +-- | Spend an HTLC we offered directly, after it times out, from the +-- remote commitment (where it is a received HTLC). -- --- On the remote commitment, their received HTLCs (our offered) --- have timed out and we can sweep them directly. +-- The locktime is the HTLC's CLTV expiry. The caller signs with +-- our HTLC privkey and uses bolt3's +-- 'received_htlc_witness_timeout'. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_remote_htlc_timeout :: OutPoint -- ^ Outpoint of the HTLC output. @@ -190,44 +142,28 @@ spend_remote_htlc_timeout -- ^ The HTLC being spent. -> CommitmentKeys -- ^ Keys for the remote commitment. - -> ChannelFeatures + -> CommitmentFormat -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_remote_htlc_timeout !op !value !htlc !keys - !features !destScript !feerate = - let !witnessScript = received_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - (htlc_cltv_expiry htlc) - features - !weight = htlcOutputTypeWeight features - (HTLCReceivedOutput (htlc_cltv_expiry htlc)) - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !locktime = - unCltvExpiry (htlc_cltv_expiry htlc) - !seqNo = - if has_anchors features then 1 else 0 - !tx = mk_spending_tx op seqNo destScript - outputValue locktime - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_remote_htlc_timeout !op !value !htlc !keys !fmt !dest !feerate = + let expiry@(CltvExpiry e) = htlc_cltv_expiry htlc + !script = received_htlc_script (ck_revocation_pubkey keys) + (ck_remote_htlc keys) (ck_local_htlc keys) + (htlc_payment_hash htlc) expiry fmt + in sweep op value script + (htlc_output_type_weight fmt (HTLCReceivedOutput expiry)) + (remote_htlc_sequence fmt) e dest feerate --- | Spend a received HTLC directly with preimage on the remote --- commitment. +-- | Spend an HTLC we received directly with the preimage, from the +-- remote commitment (where it is an offered HTLC). -- --- On the remote commitment, their offered HTLCs (our received) --- can be claimed with the payment preimage. +-- The caller signs with our HTLC privkey and uses bolt3's +-- 'offered_htlc_witness_preimage'. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_remote_htlc_preimage :: OutPoint -- ^ Outpoint of the HTLC output. @@ -237,42 +173,29 @@ spend_remote_htlc_preimage -- ^ The HTLC being spent. -> CommitmentKeys -- ^ Keys for the remote commitment. - -> ChannelFeatures + -> CommitmentFormat -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_remote_htlc_preimage !op !value !htlc !keys - !features !destScript !feerate = - let !witnessScript = offered_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - features - !weight = htlcOutputTypeWeight features - (HTLCOfferedOutput (htlc_cltv_expiry htlc)) - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !seqNo = - if has_anchors features then 1 else 0 - !tx = mk_spending_tx op seqNo destScript - outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_remote_htlc_preimage !op !value !htlc !keys !fmt !dest !feerate = + let !expiry = htlc_cltv_expiry htlc + !script = offered_htlc_script (ck_revocation_pubkey keys) + (ck_remote_htlc keys) (ck_local_htlc keys) + (htlc_payment_hash htlc) fmt + in sweep op value script + (htlc_output_type_weight fmt (HTLCOfferedOutput expiry)) + (remote_htlc_sequence fmt) 0 dest feerate -- revoked commitment spends ------------------------------------------ --- | Spend a revoked to_local output using the revocation key. +-- | Spend a revoked to_local output with the revocation key. -- --- The caller signs with the revocation privkey and uses --- 'to_local_witness_revoke' from bolt3. +-- The caller signs with the revocation privkey and uses bolt3's +-- 'to_local_witness_revoke'. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_revoked_to_local :: OutPoint -- ^ Outpoint of the to_local output. @@ -285,30 +208,19 @@ spend_revoked_to_local -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_revoked_to_local !op !value !revpk !delay - !delayedpk !destScript !feerate = - let !witnessScript = - to_local_script revpk delay delayedpk - !weight = to_local_penalty_input_weight - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_revoked_to_local !op !value !revpk !delay !delayedpk !dest + !feerate = + sweep op value (to_local_script revpk delay delayedpk) + to_local_penalty_input_weight 0xFFFFFFFF 0 dest feerate --- | Spend a revoked HTLC output using the revocation key. +-- | Spend a revoked HTLC output with the revocation key. -- --- The caller signs with the revocation privkey and uses --- 'offered_htlc_witness_revoke' or --- 'received_htlc_witness_revoke' from bolt3, depending on --- the output type. +-- The caller signs with the revocation privkey and uses bolt3's +-- 'offered_htlc_witness_revoke' or 'received_htlc_witness_revoke', +-- according to the output type. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_revoked_htlc :: OutPoint -- ^ Outpoint of the HTLC output. @@ -318,60 +230,30 @@ spend_revoked_htlc -- ^ Whether offered or received HTLC. -> RevocationPubkey -> CommitmentKeys - -> ChannelFeatures + -> CommitmentFormat -> PaymentHash -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_revoked_htlc !op !value !htype !revpk !keys - !features !ph !destScript !feerate = - case htype of - HTLCOfferedOutput _ -> - let !witnessScript = offered_htlc_script - revpk - (ck_remote_htlc keys) - (ck_local_htlc keys) - ph - features - !weight = htlcOutputTypeWeight features htype - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF - destScript outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) - HTLCReceivedOutput expiry -> - let !witnessScript = received_htlc_script - revpk - (ck_remote_htlc keys) - (ck_local_htlc keys) - ph - expiry - features - !weight = htlcOutputTypeWeight features htype - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF - destScript outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_revoked_htlc !op !value !htype !revpk !keys !fmt !ph !dest + !feerate = + let !remote = ck_remote_htlc keys + !local = ck_local_htlc keys + !script = case htype of + HTLCOfferedOutput _ -> + offered_htlc_script revpk remote local ph fmt + HTLCReceivedOutput expiry -> + received_htlc_script revpk remote local ph expiry fmt + in sweep op value script (htlc_output_type_weight fmt htype) + 0xFFFFFFFF 0 dest feerate --- | Spend a revoked second-stage HTLC output (HTLC-timeout or --- HTLC-success output) using the revocation key. --- --- The output of a revoked HTLC-timeout/success tx uses the --- to_local script. The caller signs with the revocation privkey --- and uses 'htlc_output_witness_revoke'. +-- | Spend the output of a revoked HTLC-timeout or HTLC-success +-- transaction with the revocation key. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- The output uses the to_local script, so this is +-- 'spend_revoked_to_local'. The caller signs with the revocation +-- privkey and uses bolt3's 'to_local_witness_revoke'. spend_revoked_htlc_output :: OutPoint -- ^ Outpoint of the second-stage output. @@ -384,27 +266,16 @@ spend_revoked_htlc_output -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_revoked_htlc_output !op !value !revpk !delay - !delayedpk !destScript !feerate = - let !witnessScript = - to_local_script revpk delay delayedpk - !weight = to_local_penalty_input_weight - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_revoked_htlc_output = spend_revoked_to_local -- anchor spends ------------------------------------------------------ --- | Spend an anchor output as the owner (immediately). +-- | Spend an anchor output as its owner (immediately). -- --- The caller signs with the funding privkey and uses --- 'anchor_witness_owner' from bolt3. +-- The transaction pays no fee and has a single input: it must be +-- combined with other inputs before broadcast. The caller signs +-- with the funding privkey and uses bolt3's +-- 'anchor_witness_owner'. spend_anchor_owner :: OutPoint -- ^ Outpoint of the anchor output. @@ -414,15 +285,16 @@ spend_anchor_owner -> Script -- ^ Destination scriptPubKey. -> SpendingTx -spend_anchor_owner !op !value !fundpk !destScript = - let !witnessScript = anchor_script fundpk - !tx = mk_spending_tx op 0xFFFFFFFE destScript - value 0 - in SpendingTx tx witnessScript value SIGHASH_ALL +spend_anchor_owner !op !value !fundpk !dest = + SpendingTx (mk_spending_tx op 0xFFFFFFFE dest value 0) + (anchor_script fundpk) value SIGHASH_ALL --- | Spend an anchor output as anyone (after 16 blocks). +-- | Spend an anchor output as anyone (16 blocks after the +-- commitment confirms). -- --- Uses 'anchor_witness_anyone' from bolt3 (empty signature). +-- As 'spend_anchor_owner', the transaction pays no fee. The +-- witness is bolt3's 'anchor_witness_anyone', which needs no +-- signature. spend_anchor_anyone :: OutPoint -- ^ Outpoint of the anchor output. @@ -432,98 +304,108 @@ spend_anchor_anyone -> Script -- ^ Destination scriptPubKey. -> SpendingTx -spend_anchor_anyone !op !value !fundpk !destScript = - let !witnessScript = anchor_script fundpk - !tx = mk_spending_tx op 16 destScript value 0 - in SpendingTx tx witnessScript value SIGHASH_ALL +spend_anchor_anyone !op !value !fundpk !dest = + SpendingTx (mk_spending_tx op 16 dest value 0) + (anchor_script fundpk) value SIGHASH_ALL -- internal helpers --------------------------------------------------- +-- | A single-input, single-output sweep paying the input value, less +-- the fee for the given input weight, to the destination. +sweep + :: OutPoint + -> Satoshi -- ^ input value + -> Script -- ^ input witness script + -> Word64 -- ^ input weight + -> Word32 -- ^ input nSequence + -> Word32 -- ^ locktime + -> Script -- ^ destination scriptPubKey + -> FeeratePerKw + -> Maybe SpendingTx +sweep !op !value !script !weight !sq !locktime !dest !feerate = do + let !fee = spending_fee feerate (weight + penalty_tx_base_weight) + out <- sweep_value value fee dest + pure (SpendingTx (mk_spending_tx op sq dest out locktime) script + value SIGHASH_ALL) + -- | Value of a sweep's single output: the input value less the fee. -- --- 'Nothing' if the fee meets or exceeds the input value, or if the --- output would be below the dust threshold of its script. +-- 'Nothing' if the fee exceeds the input value, or if the output +-- would be below the dust threshold of its script. sweep_value :: Satoshi -> Satoshi -> Script -> Maybe Satoshi -sweep_value !value !fee !destScript - | unSatoshi fee >= unSatoshi value = Nothing - | unSatoshi out < unSatoshi (dust_threshold destScript) = Nothing - | otherwise = Just out - where - out = Satoshi (unSatoshi value - unSatoshi fee) +sweep_value !value !fee (Script dest) = do + out <- BOLT1.sub_sat value fee + if BOLT1.un_satoshi out < dust_threshold dest + then Nothing + else Just out --- | Dust threshold for an output paying to a script. +-- | Dust threshold, in satoshis, for an output paying to a script. -- --- As Bitcoin Core computes it for spendable outputs at the default --- dust relay feerate of 3 sat/vbyte: three times the size of the --- output plus that of an input spending it (67 vbytes for a --- witness program, 148 otherwise). This gives 546 sats for P2PKH, --- 540 for P2SH, 294 for P2WPKH, and 330 for P2WSH and P2TR. -dust_threshold :: Script -> Satoshi -dust_threshold (Script s) = +-- As Bitcoin Core computes it for spendable outputs at the default +-- dust relay feerate of 3 sat/vbyte: three times the size of the +-- output plus that of an input spending it (67 vbytes for a +-- witness program, 148 otherwise). This gives 546 sats for P2PKH, +-- 540 for P2SH, 294 for P2WPKH, and 330 for P2WSH and P2TR. +dust_threshold :: BS.ByteString -> Word64 +dust_threshold s = let !len = fromIntegral (BS.length s) :: Word64 - !lenSize = if len < 0xfd then 1 - else if len <= 0xffff then 3 - else 5 - !spendSize = if is_witness_program s then 67 else 148 - in Satoshi (3 * (8 + lenSize + len + spendSize)) + !len_size + | len < 0xfd = 1 + | len <= 0xffff = 3 + | otherwise = 5 + !spend_size = if is_witness_program s then 67 else 148 + in 3 * (8 + len_size + len + spend_size) -- | Whether a script is a segwit witness program: a version opcode -- (OP_0 or OP_1 to OP_16) followed by a single 2-40 byte push. is_witness_program :: BS.ByteString -> Bool is_witness_program s = case BS.unpack (BS.take 2 s) of - [version, pushLen] -> + [version, push_len] -> (version == 0x00 || (version >= 0x51 && version <= 0x60)) - && pushLen >= 2 && pushLen <= 40 - && fromIntegral pushLen + 2 == BS.length s + && push_len >= 2 && push_len <= 40 + && fromIntegral push_len + 2 == BS.length s _ -> False --- | Build a simple single-input single-output spending tx. +-- | The nSequence of a direct HTLC spend from the remote commitment: +-- 1 under 'Anchors' (for the HTLC scripts' CSV), 0 otherwise. +remote_htlc_sequence :: CommitmentFormat -> Word32 +remote_htlc_sequence fmt = case fmt of + StaticRemotekey -> 0 + Anchors -> 1 + +-- | The witness script of an HTLC output of a commitment with the +-- given keys. +htlc_script :: CommitmentKeys -> CommitmentFormat -> HTLC -> Script +htlc_script !keys !fmt !h = + let !revpk = ck_revocation_pubkey keys + !remote = ck_remote_htlc keys + !local = ck_local_htlc keys + !ph = htlc_payment_hash h + in case htlc_direction h of + HTLCOffered -> offered_htlc_script revpk remote local ph fmt + HTLCReceived -> + received_htlc_script revpk remote local ph + (htlc_cltv_expiry h) fmt + +-- | Build a version 2, single-input, single-output transaction. mk_spending_tx - :: OutPoint -- ^ Input outpoint - -> Word32 -- ^ Input nSequence - -> Script -- ^ Output scriptPubKey - -> Satoshi -- ^ Output value - -> Word32 -- ^ Locktime + :: OutPoint -- ^ input outpoint + -> Word32 -- ^ input nSequence + -> Script -- ^ output scriptPubKey + -> Satoshi -- ^ output value + -> Word32 -- ^ locktime -> Tx -mk_spending_tx !op !seqNo !destScript !outputValue - !locktime = - let !txIn = TxIn - { txin_prevout = op - , txin_script_sig = BS.empty - , txin_sequence = seqNo - } - !txOut = TxOut - { txout_value = unSatoshi outputValue - , txout_script_pubkey = unScript destScript - } - in Tx - { tx_version = 2 - , tx_inputs = txIn :| [] - , tx_outputs = txOut :| [] - , tx_witnesses = [] - , tx_locktime = locktime - } - --- | Convert a bolt3 HTLCTx to a ppad-tx Tx. -htlc_tx_to_tx :: HTLCTx -> Tx -htlc_tx_to_tx !htx = - let !txIn = TxIn - { txin_prevout = htx_input_outpoint htx - , txin_script_sig = BS.empty - , txin_sequence = - unSequence (htx_input_sequence htx) - } - !txOut = TxOut - { txout_value = - unSatoshi (htx_output_value htx) - , txout_script_pubkey = - unScript (htx_output_script htx) - } - in Tx - { tx_version = htx_version htx - , tx_inputs = txIn :| [] - , tx_outputs = txOut :| [] - , tx_witnesses = [] - , tx_locktime = - unLocktime (htx_locktime htx) - } +mk_spending_tx !op !sq (Script dest) !value !locktime = Tx + { tx_version = 2 + , tx_inputs = TxIn + { txin_prevout = op + , txin_script_sig = BS.empty + , txin_sequence = sq + } :| [] + , tx_outputs = TxOut + { txout_value = BOLT1.un_satoshi value + , txout_script_pubkey = dest + } :| [] + , tx_witnesses = [] + , tx_locktime = locktime + } diff --git a/lib/Lightning/Protocol/BOLT5/Types.hs b/lib/Lightning/Protocol/BOLT5/Types.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} @@ -20,8 +20,8 @@ module Lightning.Protocol.BOLT5.Types ( -- * HTLC output type , HTLCOutputType(..) - , htlcOutputType - , htlcOutputTypeWeight + , htlc_output_type + , htlc_output_type_weight -- * Spending transactions , SpendingTx(..) @@ -50,19 +50,22 @@ module Lightning.Protocol.BOLT5.Types ( , spending_fee ) where -import Bitcoin.Prim.Tx (Tx(..)) -import Bitcoin.Prim.Tx.Sighash (SighashType(..)) +import Bitcoin.Prim.Tx (OutPoint, Tx) +import Bitcoin.Prim.Tx.Sighash (SighashType) +import Control.DeepSeq (NFData(..)) import Data.Word (Word64) import GHC.Generics (Generic) -import Lightning.Protocol.BOLT3.Types -import Lightning.Protocol.BOLT3.Tx ( - CommitmentKeys(..) - , OutputType(..) - ) +import Lightning.Protocol.BOLT1 (Satoshi) +import qualified Lightning.Protocol.BOLT1 as BOLT1 +import Lightning.Protocol.BOLT3 -- close identification ----------------------------------------------- -- | What kind of close was detected on chain. +-- +-- @identify_close@ produces only 'LocalCommitClose' and +-- 'RemoteCommitClose'; recognising mutual and revoked closes is +-- left to the caller. data CloseType = MutualClose -- ^ Cooperative closure agreed by both parties. @@ -74,6 +77,8 @@ data CloseType -- ^ A revoked (outdated) commitment transaction was broadcast. deriving (Eq, Show, Generic) +instance NFData CloseType + -- output classification ---------------------------------------------- -- | An unresolved commitment transaction output. @@ -83,42 +88,46 @@ data UnresolvedOutput = UnresolvedOutput , uo_type :: !OutputResolution } deriving (Eq, Show, Generic) +instance NFData UnresolvedOutput + -- | How to resolve an output, per BOLT #5 rules. data OutputResolution = Resolved - -- ^ Already resolved (e.g. to_remote on local commit). + -- ^ Nothing to do (e.g. to_remote on our local commitment). | SpendToLocal !ToSelfDelay !RevocationPubkey !LocalDelayedPubkey - -- ^ Spend to_local after CSV delay. + -- ^ Spend to_local after the CSV delay. | SpendHTLCTimeout - !HTLC !CommitmentKeys !ChannelFeatures - -- ^ Spend via HTLC-timeout second-stage tx (local commit, - -- local offer). + !HTLC !CommitmentKeys !CommitmentFormat + -- ^ Spend via an HTLC-timeout transaction (our commitment, an + -- HTLC we offered). | SpendHTLCSuccess - !HTLC !CommitmentKeys !ChannelFeatures - -- ^ Spend via HTLC-success second-stage tx (local commit, - -- remote offer). + !HTLC !CommitmentKeys !CommitmentFormat + -- ^ Spend via an HTLC-success transaction (our commitment, an + -- HTLC we received). | SpendHTLCTimeoutDirect !HTLC - -- ^ Spend HTLC directly after timeout (remote commit, - -- local offer). + -- ^ Spend the HTLC directly after it times out (the remote + -- commitment, an HTLC we offered). | SpendHTLCPreimageDirect !HTLC - -- ^ Spend HTLC directly with preimage (remote commit, - -- remote offer). + -- ^ Spend the HTLC directly with the preimage (the remote + -- commitment, an HTLC we received). | Revoke !RevocationPubkey - -- ^ Spend revoked to_local with revocation key. + -- ^ Spend a revoked to_local output with the revocation key. | RevokeHTLC !RevocationPubkey !HTLCOutputType - -- ^ Spend revoked HTLC output with revocation key. + -- ^ Spend a revoked HTLC output with the revocation key. | AnchorSpend !FundingPubkey - -- ^ Spend anchor output. + -- ^ Spend our anchor output. deriving (Eq, Show, Generic) +instance NFData OutputResolution + -- HTLC output type ------------------------------------------------- --- | Type of HTLC output, restricted to only HTLC variants. +-- | The kind of an HTLC output, from the commitment owner's point of +-- view. -- --- Unlike bolt3's 'OutputType' which includes all six output --- types, this ADT makes it impossible to confuse HTLC outputs --- with to_local, to_remote, or anchor outputs. +-- Unlike bolt3's 'OutputType', this can't describe a to_local, +-- to_remote or anchor output. data HTLCOutputType = HTLCOfferedOutput {-# UNPACK #-} !CltvExpiry -- ^ Offered HTLC output with CLTV expiry. @@ -126,63 +135,74 @@ data HTLCOutputType -- ^ Received HTLC output with CLTV expiry. deriving (Eq, Show, Generic) --- | Extract an 'HTLCOutputType' from a bolt3 'OutputType'. +instance NFData HTLCOutputType + +-- | The 'HTLCOutputType' of a bolt3 'OutputType', or 'Nothing' if +-- it isn't an HTLC output. -- --- Returns 'Nothing' for non-HTLC output types. +-- >>> htlc_output_type OutputToLocal +-- Nothing +htlc_output_type :: OutputType -> Maybe HTLCOutputType +htlc_output_type t = case t of + OutputHTLC h -> Just $ case htlc_direction h of + HTLCOffered -> HTLCOfferedOutput (htlc_cltv_expiry h) + HTLCReceived -> HTLCReceivedOutput (htlc_cltv_expiry h) + OutputToLocal -> Nothing + OutputToRemote -> Nothing + OutputLocalAnchor -> Nothing + OutputRemoteAnchor -> Nothing +{-# INLINE htlc_output_type #-} + +-- | Penalty input weight for an HTLC output. -- --- >>> htlcOutputType (OutputOfferedHTLC (CltvExpiry 500)) --- Just (HTLCOfferedOutput (CltvExpiry ...)) --- >>> htlcOutputType OutputToLocal --- Nothing -htlcOutputType :: OutputType -> Maybe HTLCOutputType -htlcOutputType (OutputOfferedHTLC e) = - Just (HTLCOfferedOutput e) -htlcOutputType (OutputReceivedHTLC e) = - Just (HTLCReceivedOutput e) -htlcOutputType _ = Nothing -{-# INLINE htlcOutputType #-} - --- | Penalty input weight for an HTLC output type. -htlcOutputTypeWeight :: ChannelFeatures -> HTLCOutputType -> Word64 -htlcOutputTypeWeight !features (HTLCOfferedOutput _) - | has_anchors features = offered_htlc_penalty_input_weight_anchors - | otherwise = offered_htlc_penalty_input_weight -htlcOutputTypeWeight !features (HTLCReceivedOutput _) - | has_anchors features = accepted_htlc_penalty_input_weight_anchors - | otherwise = accepted_htlc_penalty_input_weight -{-# INLINE htlcOutputTypeWeight #-} +-- >>> htlc_output_type_weight Anchors (HTLCOfferedOutput (CltvExpiry 500)) +-- 410 +htlc_output_type_weight :: CommitmentFormat -> HTLCOutputType -> Word64 +htlc_output_type_weight !fmt !t = case (fmt, t) of + (StaticRemotekey, HTLCOfferedOutput _) -> + offered_htlc_penalty_input_weight + (StaticRemotekey, HTLCReceivedOutput _) -> + accepted_htlc_penalty_input_weight + (Anchors, HTLCOfferedOutput _) -> + offered_htlc_penalty_input_weight_anchors + (Anchors, HTLCReceivedOutput _) -> + accepted_htlc_penalty_input_weight_anchors +{-# INLINE htlc_output_type_weight #-} -- spending transactions ---------------------------------------------- --- | Unsigned spending transaction, ready for caller to sign. +-- | An unsigned spending transaction, with what the caller needs to +-- sign its (single) input. -- --- The caller uses bolt3 witness constructors to assemble the --- final witness after signing. +-- The caller signs the input's BIP143 sighash (ppad-tx's +-- @sighash_segwit@ over 'stx_tx', input 0, 'stx_input_script', +-- 'stx_input_value' and 'stx_sighash_type') and assembles the +-- witness with bolt3's witness functions. data SpendingTx = SpendingTx { stx_tx :: !Tx -- ^ The unsigned transaction. , stx_input_script :: !Script - -- ^ Witness script for the input being spent. + -- ^ Witness script of the output being spent. , stx_input_value :: {-# UNPACK #-} !Satoshi - -- ^ Value of the input being spent (for sighash). + -- ^ Value of the output being spent. , stx_sighash_type :: !SighashType - -- ^ Sighash type for the local signature. + -- ^ Sighash type of the local signature. } deriving (Eq, Show, Generic) +instance NFData SpendingTx where + rnf (SpendingTx tx s v h) = rnf tx `seq` rnf s `seq` rnf v `seq` h `seq` () + -- revoked outputs ---------------------------------------------------- -- | A revoked output that can be swept with a revocation key. --- --- This type restricts penalty transaction inputs to only --- valid revocation targets: to_local outputs and HTLC outputs. --- Other output types (to_remote, anchors) cannot appear in --- penalty transactions. data RevokedOutput = RevokedOutput { ro_outpoint :: !OutPoint , ro_value :: {-# UNPACK #-} !Satoshi , ro_type :: !RevokedOutputType } deriving (Eq, Show, Generic) +instance NFData RevokedOutput + -- | What kind of revoked output is being swept. data RevokedOutputType = RevokedToLocal @@ -191,93 +211,104 @@ data RevokedOutputType -- ^ Revoked HTLC output. deriving (Eq, Show, Generic) +instance NFData RevokedOutputType + -- | Penalty input weight for a revoked output. -revoked_output_weight :: ChannelFeatures -> RevokedOutput -> Word64 -revoked_output_weight !features !ro = case ro_type ro of +revoked_output_weight :: CommitmentFormat -> RevokedOutput -> Word64 +revoked_output_weight !fmt !ro = case ro_type ro of RevokedToLocal -> to_local_penalty_input_weight RevokedHTLC !htype -> - htlcOutputTypeWeight features htype + htlc_output_type_weight fmt htype {-# INLINE revoked_output_weight #-} -- weight constants (BOLT #5 Appendix A) ------------------------------ -- | Expected weight of the to_local penalty transaction witness --- (160 bytes). +-- (160 weight units). to_local_penalty_witness_weight :: Word64 to_local_penalty_witness_weight = 160 -- | Expected weight of the offered_htlc penalty transaction --- witness (243 bytes), for a 133-byte witness script (no --- option_anchors). +-- witness (243 weight units), for a 133-byte witness script +-- (no option_anchors). offered_htlc_penalty_witness_weight :: Word64 offered_htlc_penalty_witness_weight = 243 -- | Expected weight of the accepted_htlc penalty transaction --- witness (249 bytes), for a 139-byte witness script (no --- option_anchors). +-- witness (249 weight units), for a 139-byte witness script +-- (no option_anchors). accepted_htlc_penalty_witness_weight :: Word64 accepted_htlc_penalty_witness_weight = 249 -- | Expected weight of the offered_htlc penalty transaction --- witness with option_anchors (246 bytes), whose witness script --- is 136 bytes. +-- witness with option_anchors (246 weight units), whose witness +-- script is 136 bytes. offered_htlc_penalty_witness_weight_anchors :: Word64 offered_htlc_penalty_witness_weight_anchors = 246 -- | Expected weight of the accepted_htlc penalty transaction --- witness with option_anchors (252 bytes), whose witness script --- is 142 bytes. +-- witness with option_anchors (252 weight units), whose witness +-- script is 142 bytes. accepted_htlc_penalty_witness_weight_anchors :: Word64 accepted_htlc_penalty_witness_weight_anchors = 252 --- | Weight of a to_local penalty input (164 + 160 = 324 bytes). +-- | Weight of a to_local penalty input (164 + 160 = 324). to_local_penalty_input_weight :: Word64 to_local_penalty_input_weight = 324 --- | Weight of an offered_htlc penalty input --- (164 + 243 = 407 bytes). +-- | Weight of an offered_htlc penalty input (164 + 243 = 407). offered_htlc_penalty_input_weight :: Word64 offered_htlc_penalty_input_weight = 407 --- | Weight of an accepted_htlc penalty input --- (164 + 249 = 413 bytes). +-- | Weight of an accepted_htlc penalty input (164 + 249 = 413). accepted_htlc_penalty_input_weight :: Word64 accepted_htlc_penalty_input_weight = 413 -- | Weight of an offered_htlc penalty input with option_anchors --- (164 + 246 = 410 bytes). +-- (164 + 246 = 410). offered_htlc_penalty_input_weight_anchors :: Word64 offered_htlc_penalty_input_weight_anchors = 410 -- | Weight of an accepted_htlc penalty input with option_anchors --- (164 + 252 = 416 bytes). +-- (164 + 252 = 416). accepted_htlc_penalty_input_weight_anchors :: Word64 accepted_htlc_penalty_input_weight_anchors = 416 --- | Weight of a to_remote P2WPKH input --- (108 + 164 = 272 bytes). +-- | Weight of a to_remote P2WPKH input (108 + 164 = 272). to_remote_input_weight :: Word64 to_remote_input_weight = 272 --- | Base weight of a penalty transaction (4*53 + 2 = 214 bytes). +-- | Base weight of a penalty transaction (4 * 53 + 2 = 214). -- --- Non-witness: version(4) + input_count(1) + output_count(1) + --- value(8) + script_len(1) + p2wsh_script(34) + locktime(4) = 53 --- Witness header: 2 bytes. +-- Non-witness: version (4) + input count (1) + output count (1) + +-- value (8) + script length (1) + P2WSH script (34) + locktime (4) +-- = 53 bytes. Witness header: 2 bytes. +-- +-- The spending transactions here use it as their base weight +-- whatever their destination script. penalty_tx_base_weight :: Word64 penalty_tx_base_weight = 214 --- | Maximum standard transaction weight (400,000 bytes). +-- | Maximum standard transaction weight (400,000). max_standard_weight :: Word64 max_standard_weight = 400000 -- fee calculation ---------------------------------------------------- --- | Calculate the fee for a spending transaction given its weight. +-- | The fee for a transaction of the given weight: +-- +-- @fee = feerate_per_kw * weight / 1000@ +-- +-- rounded down, and saturating at 21 million BTC. -- --- @fee = feerate_per_kw * weight / 1000@ +-- >>> spending_fee (FeeratePerKw 253) 324 +-- Satoshi 81 spending_fee :: FeeratePerKw -> Word64 -> Satoshi spending_fee (FeeratePerKw !rate) !weight = - Satoshi ((fromIntegral rate * weight) `div` 1000) + let !f = toInteger rate * toInteger weight `quot` 1000 + !m = BOLT1.un_satoshi BOLT1.max_satoshi + in case BOLT1.satoshi (fromInteger (min f (toInteger m))) of + Just s -> s + Nothing -> BOLT1.max_satoshi {-# INLINE spending_fee #-} diff --git a/ppad-bolt5.cabal b/ppad-bolt5.cabal @@ -25,16 +25,18 @@ library -Wall exposed-modules: Lightning.Protocol.BOLT5 + other-modules: Lightning.Protocol.BOLT5.Detect - Lightning.Protocol.BOLT5.Internal Lightning.Protocol.BOLT5.Spend Lightning.Protocol.BOLT5.Types build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 - , ppad-bolt3 >= 0.0.1 && < 0.1 + , deepseq >= 1.4 && < 1.6 + , ppad-bolt1 >= 0.1 && < 0.2 + , ppad-bolt3 >= 0.1 && < 0.2 , ppad-sha256 >= 0.3.2 && < 0.4 - , ppad-tx >= 0.1 && < 0.2 + , ppad-tx >= 0.2 && < 0.3 test-suite bolt5-tests type: exitcode-stdio-1.0 @@ -48,6 +50,7 @@ test-suite bolt5-tests build-depends: base , bytestring + , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 , ppad-sha256 @@ -71,6 +74,7 @@ benchmark bolt5-bench , bytestring , criterion , deepseq + , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 , ppad-tx @@ -88,6 +92,7 @@ benchmark bolt5-weigh base , bytestring , deepseq + , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 , ppad-tx