packages feed

ppad-bolt5-0.0.1: lib/Lightning/Protocol/BOLT5/Spend.hs

{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}

-- |
-- Module: Lightning.Protocol.BOLT5.Spend
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Spending transaction construction for BOLT #5 on-chain
-- 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.

module Lightning.Protocol.BOLT5.Spend (
    -- * Local commitment spends
    spend_to_local
  , spend_htlc_timeout
  , spend_htlc_success
  , spend_htlc_output

    -- * Remote commitment spends
  , spend_remote_htlc_timeout
  , spend_remote_htlc_preimage

    -- * Revoked commitment spends
  , spend_revoked_to_local
  , spend_revoked_htlc
  , spend_revoked_htlc_output
  , spend_revoked_batch

    -- * Anchor spends
  , spend_anchor_owner
  , spend_anchor_anyone
  ) where

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
  (txout_value, txout_script)
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.
--
-- The input nSequence is set to the to_self_delay value.
spend_to_local
  :: OutPoint
  -- ^ Outpoint of the to_local output.
  -> Satoshi
  -- ^ Value of the to_local output.
  -> RevocationPubkey
  -> ToSelfDelay
  -> LocalDelayedPubkey
  -> Script
  -- ^ Destination scriptPubKey.
  -> FeeratePerKw
  -> 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
      !outputValue =
        Satoshi (unSatoshi value - unSatoshi fee)
      !tx = mk_spending_tx op
              (fromIntegral (unToSelfDelay delay))
              destScript outputValue 0
  in SpendingTx tx witnessScript value SIGHASH_ALL

-- | Construct an HTLC-timeout second-stage transaction.
--
-- 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.
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 =
        msat_to_sat (htlc_amount_msat htlc)
      !sighashType = if has_anchors features
        then SIGHASH_SINGLE_ANYONECANPAY
        else SIGHASH_ALL
      !tx = htlc_tx_to_tx htlcTx
  in SpendingTx tx witnessScript inputValue sighashType

-- | Construct an HTLC-success second-stage transaction.
--
-- 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'.
spend_htlc_success
  :: 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 =
        msat_to_sat (htlc_amount_msat htlc)
      !sighashType = if has_anchors features
        then SIGHASH_SINGLE_ANYONECANPAY
        else SIGHASH_ALL
      !tx = htlc_tx_to_tx htlcTx
  in SpendingTx tx witnessScript inputValue sighashType

-- | Spend a second-stage HTLC output (HTLC-timeout or
--   HTLC-success output) 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'.
spend_htlc_output
  :: OutPoint
  -- ^ Outpoint of the second-stage output.
  -> Satoshi
  -- ^ Value of the second-stage output.
  -> RevocationPubkey
  -> ToSelfDelay
  -> LocalDelayedPubkey
  -> Script
  -- ^ Destination scriptPubKey.
  -> FeeratePerKw
  -> SpendingTx
spend_htlc_output = spend_to_local

-- remote commitment spends -------------------------------------------

-- | Spend an offered HTLC directly after timeout on the remote
--   commitment.
--
-- On the remote commitment, their received HTLCs (our offered)
-- have timed out and we can sweep them directly.
spend_remote_htlc_timeout
  :: OutPoint
  -- ^ Outpoint of the HTLC output.
  -> Satoshi
  -- ^ Value of the HTLC output.
  -> HTLC
  -- ^ The HTLC being spent.
  -> CommitmentKeys
  -- ^ Keys for the remote commitment.
  -> ChannelFeatures
  -> Script
  -- ^ Destination scriptPubKey.
  -> FeeratePerKw
  -> 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 = accepted_htlc_penalty_input_weight
              + penalty_tx_base_weight
      !fee = spending_fee feerate weight
      !outputValue =
        Satoshi (unSatoshi value - unSatoshi fee)
      !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 SpendingTx tx witnessScript value SIGHASH_ALL

-- | Spend a received HTLC directly with preimage on the remote
--   commitment.
--
-- On the remote commitment, their offered HTLCs (our received)
-- can be claimed with the payment preimage.
spend_remote_htlc_preimage
  :: OutPoint
  -- ^ Outpoint of the HTLC output.
  -> Satoshi
  -- ^ Value of the HTLC output.
  -> HTLC
  -- ^ The HTLC being spent.
  -> CommitmentKeys
  -- ^ Keys for the remote commitment.
  -> ChannelFeatures
  -> Script
  -- ^ Destination scriptPubKey.
  -> FeeratePerKw
  -> 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 = offered_htlc_penalty_input_weight
              + penalty_tx_base_weight
      !fee = spending_fee feerate weight
      !outputValue =
        Satoshi (unSatoshi value - unSatoshi fee)
      !seqNo = if has_anchors features then 1 else 0
      !tx = mk_spending_tx op seqNo destScript
              outputValue 0
  in SpendingTx tx witnessScript value SIGHASH_ALL

-- revoked commitment spends ------------------------------------------

-- | Spend a revoked to_local output using the revocation key.
--
-- The caller signs with the revocation privkey and uses
-- 'to_local_witness_revoke' from bolt3.
spend_revoked_to_local
  :: OutPoint
  -- ^ Outpoint of the to_local output.
  -> Satoshi
  -- ^ Value of the to_local output.
  -> RevocationPubkey
  -> ToSelfDelay
  -> LocalDelayedPubkey
  -> Script
  -- ^ Destination scriptPubKey.
  -> FeeratePerKw
  -> 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
      !outputValue =
        Satoshi (unSatoshi value - unSatoshi fee)
      !tx = mk_spending_tx op 0xFFFFFFFF destScript
              outputValue 0
  in SpendingTx tx witnessScript value SIGHASH_ALL

-- | Spend a revoked HTLC output using 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.
spend_revoked_htlc
  :: OutPoint
  -- ^ Outpoint of the HTLC output.
  -> Satoshi
  -- ^ Value of the HTLC output.
  -> OutputType
  -- ^ Whether offered or received HTLC.
  -> RevocationPubkey
  -> CommitmentKeys
  -> ChannelFeatures
  -> PaymentHash
  -> Script
  -- ^ Destination scriptPubKey.
  -> FeeratePerKw
  -> Maybe SpendingTx
spend_revoked_htlc !op !value !otype !revpk !keys
    !features !ph !destScript !feerate =
  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.
--
-- 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_revoked_htlc_output
  :: OutPoint
  -- ^ Outpoint of the second-stage output.
  -> Satoshi
  -- ^ Value of the second-stage output.
  -> RevocationPubkey
  -> ToSelfDelay
  -> LocalDelayedPubkey
  -> Script
  -- ^ Destination scriptPubKey.
  -> FeeratePerKw
  -> 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
      !outputValue =
        Satoshi (unSatoshi value - unSatoshi fee)
      !tx = mk_spending_tx op 0xFFFFFFFF destScript
              outputValue 0
  in SpendingTx tx witnessScript value SIGHASH_ALL

-- | Construct a batched penalty transaction spending multiple
--   revoked outputs.
--
-- Per BOLT #5, up to 483 bidirectional HTLCs plus to_local can
-- be resolved in a single penalty transaction (within the
-- 400,000 weight limit). The caller signs each input with the
-- revocation privkey.
spend_revoked_batch :: PenaltyContext -> SpendingTx
spend_revoked_batch !ctx =
  let !outs = pc_outputs ctx
      !destScript = pc_destination ctx
      !feerate = pc_feerate ctx

      -- Calculate total input value and weight
      !(totalValue, totalWeight) =
        go (Satoshi 0) penalty_tx_base_weight
          (NE.toList outs)

      !fee = spending_fee feerate totalWeight
      !outputValue =
        Satoshi (unSatoshi totalValue - unSatoshi fee)

      -- Build inputs
      !txInputs = fmap mkPenaltyInput outs

      -- Single output
      !txOutput = TxOut
        (unSatoshi outputValue)
        (unScript destScript)

      !tx = Tx
        { tx_version   = 2
        , tx_inputs    = txInputs
        , tx_outputs   = txOutput :| []
        , tx_witnesses = []
        , tx_locktime  = 0
        }

      !witnessScript = Script BS.empty
  in SpendingTx tx witnessScript totalValue SIGHASH_ALL
  where
    go !totalVal !totalWt [] = (totalVal, totalWt)
    go !totalVal !totalWt (uo:rest) =
      let !w = case uo_type uo of
            Revoke _ ->
              to_local_penalty_input_weight
            RevokeHTLC _ (OutputOfferedHTLC _) ->
              offered_htlc_penalty_input_weight
            RevokeHTLC _ (OutputReceivedHTLC _) ->
              accepted_htlc_penalty_input_weight
            _ -> 0
          !v = Satoshi
            (unSatoshi totalVal + unSatoshi (uo_value uo))
      in go v (totalWt + w) rest

    mkPenaltyInput !uo =
      TxIn
        { txin_prevout = uo_outpoint uo
        , txin_script_sig = BS.empty
        , txin_sequence = 0xFFFFFFFF
        }

-- anchor spends ------------------------------------------------------

-- | Spend an anchor output as the owner (immediately).
--
-- The caller signs with the funding privkey and uses
-- 'anchor_witness_owner' from bolt3.
spend_anchor_owner
  :: OutPoint
  -- ^ Outpoint of the anchor output.
  -> Satoshi
  -- ^ Value of the anchor output (330 sats).
  -> FundingPubkey
  -> 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 an anchor output as anyone (after 16 blocks).
--
-- Uses 'anchor_witness_anyone' from bolt3 (empty signature).
spend_anchor_anyone
  :: OutPoint
  -- ^ Outpoint of the anchor output.
  -> Satoshi
  -- ^ Value of the anchor output (330 sats).
  -> FundingPubkey
  -> 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

-- internal helpers ---------------------------------------------------

-- | Build a simple single-input single-output spending tx.
mk_spending_tx
  :: 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)
       }