bolt5

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

Detect.hs (9179B)


      1 {-# OPTIONS_HADDOCK hide #-}
      2 {-# LANGUAGE BangPatterns #-}
      3 
      4 -- |
      5 -- Module: Lightning.Protocol.BOLT5.Detect
      6 -- Copyright: (c) 2025 Jared Tobin
      7 -- License: MIT
      8 -- Maintainer: Jared Tobin <jared@ppad.tech>
      9 --
     10 -- Close identification, output classification, and preimage
     11 -- extraction for BOLT #5 on-chain transaction handling.
     12 
     13 module Lightning.Protocol.BOLT5.Detect (
     14     -- * Close identification
     15     identify_close
     16 
     17     -- * Output classification
     18   , classify_local_commit_outputs
     19   , classify_remote_commit_outputs
     20   , classify_revoked_commit_outputs
     21 
     22     -- * Preimage extraction
     23   , extract_preimage_offered
     24   , extract_preimage_htlc_success
     25 
     26     -- * Timeout check
     27   , htlc_timed_out
     28   ) where
     29 
     30 import Bitcoin.Prim.Tx (OutPoint(..), TxId, Witness(..))
     31 import qualified Bitcoin.Prim.Tx as BT
     32 import qualified Crypto.Hash.SHA256 as SHA256
     33 import qualified Data.ByteString as BS
     34 import qualified Data.List.NonEmpty as NE
     35 import Data.Word (Word32)
     36 import Lightning.Protocol.BOLT1 (PaymentHash, PaymentPreimage)
     37 import qualified Lightning.Protocol.BOLT1 as BOLT1
     38 import Lightning.Protocol.BOLT3
     39 import Lightning.Protocol.BOLT5.Types
     40 
     41 -- close identification -----------------------------------------------
     42 
     43 -- | Identify the type of channel close from a transaction that
     44 --   spends the funding output.
     45 --
     46 --   Parses the on-chain transaction and compares its txid with the
     47 --   txids of the known local and remote commitment transactions.
     48 --   Comparing txids rather than bytes means the signed (segwit)
     49 --   serialization found on chain matches the unsigned commitment.
     50 --
     51 --   Returns 'Nothing' if the bytes don't parse as a transaction or
     52 --   the transaction matches neither commitment. Mutual close and
     53 --   revoked commitment detection are left to the caller (e.g.
     54 --   comparing with the closing transaction, or checking a secret
     55 --   store for older commitment numbers).
     56 identify_close
     57   :: CommitmentTx
     58   -- ^ Our local commitment tx.
     59   -> CommitmentTx
     60   -- ^ The remote commitment tx (current).
     61   -> BS.ByteString
     62   -- ^ Raw serialized transaction found on chain.
     63   -> Maybe CloseType
     64 identify_close !local !remote !bytes = do
     65   !tx <- BT.from_bytes bytes
     66   let !txid = BT.txid tx
     67   if   commitment_txid local == txid
     68   then Just LocalCommitClose
     69   else if commitment_txid remote == txid
     70   then Just RemoteCommitClose
     71   else Nothing
     72 
     73 -- output classification ----------------------------------------------
     74 
     75 -- | Classify the outputs of our local commitment transaction.
     76 --
     77 --   Per BOLT #5: when our local commitment is on chain, we must
     78 --   resolve each output. to_local requires a CSV-delayed spend,
     79 --   to_remote is resolved by the commitment itself, HTLC outputs
     80 --   need second-stage transactions, and our anchor can be spent
     81 --   immediately.
     82 --
     83 --   Each HTLC output's HTLC is read from its bolt3 'OutputHTLC', so
     84 --   the commitment must be the one bolt3 built for this state.
     85 classify_local_commit_outputs
     86   :: CommitmentTx
     87   -- ^ Our local commitment transaction.
     88   -> CommitmentKeys
     89   -- ^ Derived keys for this commitment.
     90   -> ToSelfDelay
     91   -- ^ Remote's to_self_delay (CSV delay for our outputs).
     92   -> CommitmentFormat
     93   -> [UnresolvedOutput]
     94 classify_local_commit_outputs !tx !keys !delay !fmt =
     95     classify_outputs resolve tx
     96   where
     97     resolve t = case t of
     98       OutputToLocal ->
     99         SpendToLocal delay (ck_revocation_pubkey keys)
    100           (ck_local_delayed keys)
    101       OutputToRemote -> Resolved
    102       OutputLocalAnchor -> AnchorSpend (ck_local_funding keys)
    103       OutputRemoteAnchor -> Resolved
    104       OutputHTLC h -> case htlc_direction h of
    105         HTLCOffered  -> SpendHTLCTimeout h keys fmt
    106         HTLCReceived -> SpendHTLCSuccess h keys fmt
    107 
    108 -- | Classify the outputs of the remote commitment transaction.
    109 --
    110 --   Per BOLT #5: when the remote commitment is on chain, there are
    111 --   no CSV delays on our outputs. We can spend HTLCs we offered
    112 --   directly after timeout, and HTLCs we received directly with the
    113 --   preimage.
    114 --
    115 --   The commitment, its keys and its HTLCs are from the remote's
    116 --   point of view (as bolt3 builds the remote's commitment): an
    117 --   HTLC the remote offered is one we received.
    118 classify_remote_commit_outputs
    119   :: CommitmentTx
    120   -- ^ The remote commitment transaction.
    121   -> CommitmentKeys
    122   -- ^ Derived keys for this commitment (from the remote's point
    123   --   of view, so local and remote are swapped).
    124   -> [UnresolvedOutput]
    125 classify_remote_commit_outputs !tx !keys =
    126     classify_outputs resolve tx
    127   where
    128     resolve t = case t of
    129       OutputToLocal -> Resolved       -- the remote's to_local
    130       OutputToRemote -> Resolved      -- ours; resolved by the commitment
    131       OutputLocalAnchor -> Resolved   -- the remote's anchor
    132       OutputRemoteAnchor -> AnchorSpend (ck_remote_funding keys)
    133       OutputHTLC h -> case htlc_direction h of
    134         HTLCOffered  -> SpendHTLCPreimageDirect h  -- we received it
    135         HTLCReceived -> SpendHTLCTimeoutDirect h   -- we offered it
    136 
    137 -- | Classify the outputs of a revoked commitment transaction.
    138 --
    139 --   Per BOLT #5: when a revoked commitment is on chain, we can
    140 --   claim to_local and every HTLC output with the revocation key.
    141 --   to_remote (ours) and the anchors are marked 'Resolved'.
    142 classify_revoked_commit_outputs
    143   :: CommitmentTx
    144   -- ^ The revoked commitment transaction.
    145   -> RevocationPubkey
    146   -- ^ Revocation pubkey (derived from the revealed secret).
    147   -> [UnresolvedOutput]
    148 classify_revoked_commit_outputs !tx !revpk =
    149     classify_outputs resolve tx
    150   where
    151     resolve t = case t of
    152       OutputToLocal -> Revoke revpk
    153       OutputToRemote -> Resolved
    154       OutputLocalAnchor -> Resolved
    155       OutputRemoteAnchor -> Resolved
    156       OutputHTLC h -> RevokeHTLC revpk $ case htlc_direction h of
    157         HTLCOffered  -> HTLCOfferedOutput (htlc_cltv_expiry h)
    158         HTLCReceived -> HTLCReceivedOutput (htlc_cltv_expiry h)
    159 
    160 -- | Pair each commitment output with its outpoint, value and
    161 --   resolution.
    162 classify_outputs
    163   :: (OutputType -> OutputResolution)
    164   -> CommitmentTx
    165   -> [UnresolvedOutput]
    166 classify_outputs resolve tx =
    167     zipWith classify [0..] (NE.toList (ctx_outputs tx))
    168   where
    169     !txid = commitment_txid tx
    170     classify :: Word32 -> CommitmentOutput -> UnresolvedOutput
    171     classify !idx !out = UnresolvedOutput
    172       { uo_outpoint = OutPoint txid idx
    173       , uo_value    = co_value out
    174       , uo_type     = resolve (co_type out)
    175       }
    176 
    177 -- preimage extraction ------------------------------------------------
    178 
    179 -- | Extract a payment preimage from the witness of an input
    180 --   spending an offered HTLC output.
    181 --
    182 --   When the remote party claims an offered HTLC on our local
    183 --   commitment, the witness (see bolt3's
    184 --   'offered_htlc_witness_preimage') is:
    185 --
    186 --   @\<remotehtlcsig\> \<payment_preimage\> \<witness_script\>@
    187 --
    188 --   Returns the preimage only if it hashes to the given payment
    189 --   hash. Any other spend of the output, e.g. by revocation, yields
    190 --   'Nothing'.
    191 extract_preimage_offered
    192   :: PaymentHash
    193   -- ^ Payment hash of the HTLC.
    194   -> Witness
    195   -- ^ Witness of the input spending the HTLC output.
    196   -> Maybe PaymentPreimage
    197 extract_preimage_offered !ph (Witness items) = case items of
    198   [_sig, preimage, _script] -> check_preimage ph preimage
    199   _ -> Nothing
    200 
    201 -- | Extract a payment preimage from the witness of an HTLC-success
    202 --   transaction's input.
    203 --
    204 --   When the remote party claims an HTLC it received on its
    205 --   commitment with an HTLC-success transaction, the witness (see
    206 --   bolt3's 'htlc_success_witness') is:
    207 --
    208 --   @0 \<remotehtlcsig\> \<localhtlcsig\> \<payment_preimage\>
    209 --   \<witness_script\>@
    210 --
    211 --   Returns the preimage only if it hashes to the given payment
    212 --   hash.
    213 extract_preimage_htlc_success
    214   :: PaymentHash
    215   -- ^ Payment hash of the HTLC.
    216   -> Witness
    217   -- ^ Witness of the HTLC-success transaction input.
    218   -> Maybe PaymentPreimage
    219 extract_preimage_htlc_success !ph (Witness items) = case items of
    220   [_zero, _remote_sig, _local_sig, preimage, _script] ->
    221     check_preimage ph preimage
    222   _ -> Nothing
    223 
    224 -- | Accept a candidate preimage only if it hashes to the payment
    225 --   hash.
    226 check_preimage :: PaymentHash -> BS.ByteString -> Maybe PaymentPreimage
    227 check_preimage !ph !bs
    228   | SHA256.hash bs == BOLT1.un_payment_hash ph = BOLT1.payment_preimage bs
    229   | otherwise = Nothing
    230 
    231 -- timeout check ------------------------------------------------------
    232 
    233 -- | Whether an HTLC has timed out at the given block height: whether
    234 --   the height is at least the HTLC's CLTV expiry.
    235 --
    236 --   >>> let Just amt = BOLT1.milli_satoshi 1000000
    237 --   >>> let Just ph = BOLT1.payment_hash (BS.replicate 32 0xaa)
    238 --   >>> let htlc = HTLC HTLCOffered amt ph (CltvExpiry 800000)
    239 --   >>> htlc_timed_out 799999 htlc
    240 --   False
    241 --   >>> htlc_timed_out 800000 htlc
    242 --   True
    243 htlc_timed_out :: Word32 -> HTLC -> Bool
    244 htlc_timed_out !height !htlc =
    245   let CltvExpiry expiry = htlc_cltv_expiry htlc
    246   in  height >= expiry
    247 {-# INLINE htlc_timed_out #-}
    248 
    249 -- internal helpers ---------------------------------------------------
    250 
    251 -- | The txid of a commitment transaction.
    252 commitment_txid :: CommitmentTx -> TxId
    253 commitment_txid = BT.txid . commitment_to_tx
    254 {-# INLINE commitment_txid #-}