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 bed23d8069d89e15e2497fd52905f24a11029c50
parent ead7cd3dff620c87d31f68bd7eed91929cda213a
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 19 Apr 2026 11:45:51 +0800

lib: address review findings

- PenaltyContext.pc_outputs now uses NonEmpty, eliminating the
  error partial in spend_revoked_batch
- spend_revoked_htlc returns Maybe SpendingTx instead of silently
  producing a malformed tx for non-HTLC output types
- identify_close: remove 5 unused parameters and update haddock
  to reflect actual behavior (byte comparison only)
- Remove unused SpendSecondStage constructor from OutputResolution
- Clean up redundant import in Spend.hs

Diffstat:
Mlib/Lightning/Protocol/BOLT5/Detect.hs | 49++++++++++++-------------------------------------
Mlib/Lightning/Protocol/BOLT5/Spend.hs | 80++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mlib/Lightning/Protocol/BOLT5/Types.hs | 8+++-----
3 files changed, 56 insertions(+), 81 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT5/Detect.hs b/lib/Lightning/Protocol/BOLT5/Detect.hs @@ -38,57 +38,32 @@ import Lightning.Protocol.BOLT5.Types -- | Identify the type of channel close from a transaction that -- spends the funding output. -- --- Checks the transaction against known commitment and closing --- formats to determine whether it's a mutual close, local --- commitment, remote commitment, or revoked commitment. +-- Compares the on-chain transaction bytes against the known +-- local and remote commitment transaction serializations +-- (stripped/unsigned) to determine whether it's a local or +-- remote commitment close. -- --- Returns 'Nothing' if the transaction doesn't match any known --- format. --- --- The 'SecretStore' is checked to determine if the remote --- commitment is revoked (i.e. we have the per-commitment secret --- for that commitment number). +-- Returns 'Nothing' if 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). identify_close - :: OutPoint - -- ^ Funding outpoint. - -> CommitmentKeys - -- ^ Keys for our local commitment. - -> CommitmentKeys - -- ^ Keys for the remote commitment. - -> SecretStore - -- ^ Store of received per-commitment secrets. - -> ClosingContext - -- ^ Closing tx context (for mutual close matching). - -> CommitmentTx + :: CommitmentTx -- ^ Our local commitment tx. -> CommitmentTx -- ^ The remote commitment tx (current). -> BS.ByteString -- ^ Raw serialized transaction found on chain. -> Maybe CloseType -identify_close - !_fundingOutpoint - !_localKeys - !_remoteKeys - !_secretStore - !_closingCtx - !localCommitTx - !remoteCommitTx - !onChainBytes = - -- Compare the on-chain tx bytes against known tx - -- serializations. We compare stripped (unsigned, legacy) - -- serializations. +identify_close !localCommitTx !remoteCommitTx + !onChainBytes = let !localBytes = encode_tx_for_signing localCommitTx !remoteBytes = encode_tx_for_signing remoteCommitTx in if localBytes == Just onChainBytes then Just LocalCommitClose else if remoteBytes == Just onChainBytes then Just RemoteCommitClose - -- For mutual close and revoked detection, the caller - -- should perform additional checks (e.g. comparing - -- closing tx format, checking secret store for older - -- commitment numbers). This function provides the - -- basic identification. else Nothing -- output classification ---------------------------------------------- diff --git a/lib/Lightning/Protocol/BOLT5/Spend.hs b/lib/Lightning/Protocol/BOLT5/Spend.hs @@ -37,9 +37,10 @@ module Lightning.Protocol.BOLT5.Spend ( , spend_anchor_anyone ) where -import Bitcoin.Prim.Tx (Tx(..), TxIn(..), TxOut(..)) +import Bitcoin.Prim.Tx (TxOut(..)) import Bitcoin.Prim.Tx.Sighash (SighashType(..)) import Data.List.NonEmpty (NonEmpty(..)) +import qualified Data.List.NonEmpty as NE import Data.Word (Word32) import qualified Data.ByteString as BS import Lightning.Protocol.BOLT3 hiding @@ -290,40 +291,44 @@ spend_revoked_htlc -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw - -> SpendingTx + -> Maybe SpendingTx spend_revoked_htlc !op !value !otype !revpk !keys !features !ph !destScript !feerate = - let !(witnessScript, inputWeight) = case otype of - OutputOfferedHTLC _ -> - ( offered_htlc_script - revpk - (ck_remote_htlc keys) - (ck_local_htlc keys) - ph - features - , offered_htlc_penalty_input_weight - ) - OutputReceivedHTLC expiry -> - ( received_htlc_script - revpk - (ck_remote_htlc keys) - (ck_local_htlc keys) - ph - expiry - features - , accepted_htlc_penalty_input_weight - ) - _ -> - ( Script BS.empty - , 0 - ) - !weight = inputWeight + penalty_tx_base_weight - !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in SpendingTx tx witnessScript value SIGHASH_ALL + case otype of + OutputOfferedHTLC _ -> + let !witnessScript = offered_htlc_script + revpk + (ck_remote_htlc keys) + (ck_local_htlc keys) + ph + features + !weight = offered_htlc_penalty_input_weight + + penalty_tx_base_weight + !fee = spending_fee feerate weight + !outputValue = + Satoshi (unSatoshi value - unSatoshi fee) + !tx = mk_spending_tx op 0xFFFFFFFF destScript + outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) + OutputReceivedHTLC expiry -> + let !witnessScript = received_htlc_script + revpk + (ck_remote_htlc keys) + (ck_local_htlc keys) + ph + expiry + features + !weight = accepted_htlc_penalty_input_weight + + penalty_tx_base_weight + !fee = spending_fee feerate weight + !outputValue = + Satoshi (unSatoshi value - unSatoshi fee) + !tx = mk_spending_tx op 0xFFFFFFFF destScript + outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) + _ -> Nothing -- | Spend a revoked second-stage HTLC output (HTLC-timeout or -- HTLC-success output) using the revocation key. @@ -371,18 +376,15 @@ spend_revoked_batch !ctx = -- Calculate total input value and weight !(totalValue, totalWeight) = - go (Satoshi 0) penalty_tx_base_weight outs + go (Satoshi 0) penalty_tx_base_weight + (NE.toList outs) !fee = spending_fee feerate totalWeight !outputValue = Satoshi (unSatoshi totalValue - unSatoshi fee) -- Build inputs - !inputs = map mkPenaltyInput outs - !txInputs = case inputs of - [] -> error - "spend_revoked_batch: no outputs" - (i:is) -> i :| is + !txInputs = fmap mkPenaltyInput outs -- Single output !txOutput = TxOut diff --git a/lib/Lightning/Protocol/BOLT5/Types.hs b/lib/Lightning/Protocol/BOLT5/Types.hs @@ -41,6 +41,7 @@ module Lightning.Protocol.BOLT5.Types ( import Bitcoin.Prim.Tx (Tx(..)) import Bitcoin.Prim.Tx.Sighash (SighashType(..)) +import Data.List.NonEmpty (NonEmpty) import Data.Word (Word64) import GHC.Generics (Generic) import Lightning.Protocol.BOLT3.Types @@ -93,9 +94,6 @@ data OutputResolution | SpendHTLCPreimageDirect !HTLC -- ^ Spend HTLC directly with preimage (remote commit, -- remote offer). - | SpendSecondStage - !ToSelfDelay !RevocationPubkey !LocalDelayedPubkey - -- ^ Spend second-stage HTLC output after CSV delay. | Revoke !RevocationPubkey -- ^ Spend revoked to_local with revocation key. | RevokeHTLC !RevocationPubkey !OutputType @@ -125,8 +123,8 @@ data SpendingTx = SpendingTx -- | Context for constructing batched penalty transactions. data PenaltyContext = PenaltyContext - { pc_outputs :: ![UnresolvedOutput] - -- ^ Revoked outputs to sweep. + { pc_outputs :: !(NonEmpty UnresolvedOutput) + -- ^ Revoked outputs to sweep (must be non-empty). , pc_revocation_key :: !RevocationPubkey -- ^ Revocation pubkey for all outputs. , pc_destination :: !Script