commit d39df6fe47e0d793b85471ad1baedc630bc6cc3e
parent 9b55ee70b3982df87cc653cc68ea92bf4c4f3b87
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:32:43 +0400
lib, test, bench: remove spend_revoked_batch
The batched penalty tx couldn't be signed. Its SpendingTx carried an
empty witness script and the total value of all inputs, while BIP143
needs each input's own script and value. RevokedOutput also lacks
what the scripts need (to_self_delay and the delayed key for
to_local; payment hash, HTLC keys and expiry for HTLCs), and
PenaltyContext's revocation key was never read. Fixing it needs
per-input signing metadata, i.e. a redesign of SpendingTx, so remove
spend_revoked_batch and PenaltyContext, its only input, rather than
ship a builder that yields invalid signatures.
RevokedOutput, RevokedOutputType and revoked_output_weight remain as
weight helpers. Single-output penalty spends (spend_revoked_to_local,
spend_revoked_htlc, spend_revoked_htlc_output) are unaffected.
Diffstat:
7 files changed, 3 insertions(+), 258 deletions(-)
diff --git a/bench/Main.hs b/bench/Main.hs
@@ -8,7 +8,6 @@ import Bitcoin.Prim.Tx.Sighash (SighashType(..))
import Control.DeepSeq (NFData(..))
import Criterion.Main
import qualified Data.ByteString as BS
-import qualified Data.List.NonEmpty as NE
import Lightning.Protocol.BOLT3 hiding
(txout_value, txout_script)
import qualified Lightning.Protocol.BOLT5 as B5
@@ -74,10 +73,6 @@ instance NFData B5.SpendingTx where
rnf (B5.SpendingTx tx scr val sh) =
rnf tx `seq` rnf scr `seq` rnf val `seq` rnf sh
-instance NFData B5.PenaltyContext where
- rnf (B5.PenaltyContext os rk d f) =
- rnf os `seq` rnf rk `seq` rnf d `seq` rnf f
-
main :: IO ()
main = defaultMain [
spend_benchmarks
@@ -118,14 +113,6 @@ dummyFeerate = FeeratePerKw 253
dummyDelay :: ToSelfDelay
dummyDelay = ToSelfDelay 144
-mkRevokedOutputs :: Int -> NE.NonEmpty B5.RevokedOutput
-mkRevokedOutputs n =
- let ro i = B5.RevokedOutput
- (OutPoint dummyTxId (fromIntegral i))
- (Satoshi 10000)
- B5.RevokedToLocal
- in ro 0 NE.:| [ ro i | i <- [1..n-1] ]
-
-- benchmarks ---------------------------------------------------------
spend_benchmarks :: Benchmark
@@ -147,23 +134,6 @@ spend_benchmarks = bgroup "spend" [
dummyOutPoint v dummyFundPk dummyDest)
(Satoshi 330)
- , bench "spend_revoked_batch/10" $
- nf B5.spend_revoked_batch
- (B5.PenaltyContext
- (mkRevokedOutputs 10)
- dummyRevPk dummyDest dummyFeerate)
-
- , bench "spend_revoked_batch/100" $
- nf B5.spend_revoked_batch
- (B5.PenaltyContext
- (mkRevokedOutputs 100)
- dummyRevPk dummyDest dummyFeerate)
-
- , bench "spend_revoked_batch/483" $
- nf B5.spend_revoked_batch
- (B5.PenaltyContext
- (mkRevokedOutputs 483)
- dummyRevPk dummyDest dummyFeerate)
]
classify_benchmarks :: Benchmark
diff --git a/bench/Weight.hs b/bench/Weight.hs
@@ -8,7 +8,6 @@ module Main where
import Bitcoin.Prim.Tx.Sighash (SighashType(..))
import Control.DeepSeq (NFData(..))
import qualified Data.ByteString as BS
-import qualified Data.List.NonEmpty as NE
import Lightning.Protocol.BOLT3 hiding
(txout_value, txout_script)
import qualified Lightning.Protocol.BOLT5 as B5
@@ -67,15 +66,10 @@ instance NFData B5.SpendingTx where
rnf (B5.SpendingTx tx scr val sh) =
rnf tx `seq` rnf scr `seq` rnf val `seq` rnf sh
-instance NFData B5.PenaltyContext where
- rnf (B5.PenaltyContext os rk d f) =
- rnf os `seq` rnf rk `seq` rnf d `seq` rnf f
-
-- note that 'weigh' doesn't work properly in a repl
main :: IO ()
main = mainWith $ do
spend_weights
- batch_weights
-- fixtures -----------------------------------------------------------
@@ -111,14 +105,6 @@ dummyFeerate = FeeratePerKw 253
dummyDelay :: ToSelfDelay
dummyDelay = ToSelfDelay 144
-mkRevokedOutputs :: Int -> NE.NonEmpty B5.RevokedOutput
-mkRevokedOutputs n =
- let ro i = B5.RevokedOutput
- (OutPoint dummyTxId (fromIntegral i))
- (Satoshi 10000)
- B5.RevokedToLocal
- in ro 0 NE.:| [ ro i | i <- [1..n-1] ]
-
-- weights ------------------------------------------------------------
spend_weights :: Weigh ()
@@ -133,23 +119,3 @@ spend_weights = wgroup "spend" $ do
(\v -> B5.spend_anchor_owner
dummyOutPoint v dummyFundPk dummyDest)
(Satoshi 330)
-
-batch_weights :: Weigh ()
-batch_weights = wgroup "batch" $ do
- func "spend_revoked_batch/10"
- B5.spend_revoked_batch
- (B5.PenaltyContext
- (mkRevokedOutputs 10)
- dummyRevPk dummyDest dummyFeerate)
-
- func "spend_revoked_batch/100"
- B5.spend_revoked_batch
- (B5.PenaltyContext
- (mkRevokedOutputs 100)
- dummyRevPk dummyDest dummyFeerate)
-
- func "spend_revoked_batch/483"
- B5.spend_revoked_batch
- (B5.PenaltyContext
- (mkRevokedOutputs 483)
- dummyRevPk dummyDest dummyFeerate)
diff --git a/lib/Lightning/Protocol/BOLT5.hs b/lib/Lightning/Protocol/BOLT5.hs
@@ -60,10 +60,9 @@ module Lightning.Protocol.BOLT5 (
-- ** Spending transactions
, SpendingTx(..)
- -- ** Penalty batching
+ -- ** Revoked outputs
, RevokedOutput(..)
, RevokedOutputType(..)
- , PenaltyContext(..)
, revoked_output_weight
-- * Weight constants (Appendix A)
@@ -110,7 +109,6 @@ module Lightning.Protocol.BOLT5 (
, spend_revoked_to_local
, spend_revoked_htlc
, spend_revoked_htlc_output
- , spend_revoked_batch
-- ** Anchor outputs
, spend_anchor_owner
diff --git a/lib/Lightning/Protocol/BOLT5/Internal.hs b/lib/Lightning/Protocol/BOLT5/Internal.hs
@@ -21,7 +21,6 @@ module Lightning.Protocol.BOLT5.Internal (
, SpendingTx(..)
, RevokedOutput(..)
, RevokedOutputType(..)
- , PenaltyContext(..)
) where
import Lightning.Protocol.BOLT5.Types
diff --git a/lib/Lightning/Protocol/BOLT5/Spend.hs b/lib/Lightning/Protocol/BOLT5/Spend.hs
@@ -30,7 +30,6 @@ module Lightning.Protocol.BOLT5.Spend (
, spend_revoked_to_local
, spend_revoked_htlc
, spend_revoked_htlc_output
- , spend_revoked_batch
-- * Anchor spends
, spend_anchor_owner
@@ -40,7 +39,6 @@ module Lightning.Protocol.BOLT5.Spend (
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
@@ -409,70 +407,6 @@ spend_revoked_htlc_output !op !value !revpk !delay
in Just (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.
---
--- Returns 'Nothing' if the total fee would exceed the total
--- input value.
-spend_revoked_batch :: PenaltyContext -> Maybe 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
- in if unSatoshi fee >= unSatoshi totalValue
- then Nothing
- else
- let !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 Just (SpendingTx tx witnessScript totalValue
- SIGHASH_ALL)
- where
- go !totalVal !totalWt [] = (totalVal, totalWt)
- go !totalVal !totalWt (ro:rest) =
- let !w = revoked_output_weight ro
- !v = Satoshi
- (unSatoshi totalVal
- + unSatoshi (ro_value ro))
- in go v (totalWt + w) rest
-
- mkPenaltyInput !ro =
- TxIn
- { txin_prevout = ro_outpoint ro
- , txin_script_sig = BS.empty
- , txin_sequence = 0xFFFFFFFF
- }
-
-- anchor spends ------------------------------------------------------
-- | Spend an anchor output as the owner (immediately).
diff --git a/lib/Lightning/Protocol/BOLT5/Types.hs b/lib/Lightning/Protocol/BOLT5/Types.hs
@@ -26,10 +26,9 @@ module Lightning.Protocol.BOLT5.Types (
-- * Spending transactions
, SpendingTx(..)
- -- * Penalty batching
+ -- * Revoked outputs
, RevokedOutput(..)
, RevokedOutputType(..)
- , PenaltyContext(..)
, revoked_output_weight
-- * Weight constants (Appendix A)
@@ -49,7 +48,6 @@ 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
@@ -165,7 +163,7 @@ data SpendingTx = SpendingTx
-- ^ Sighash type for the local signature.
} deriving (Eq, Show, Generic)
--- penalty batching ---------------------------------------------------
+-- revoked outputs ----------------------------------------------------
-- | A revoked output that can be swept with a revocation key.
--
@@ -196,18 +194,6 @@ revoked_output_weight !ro = case ro_type ro of
htlcOutputTypeWeight htype
{-# INLINE revoked_output_weight #-}
--- | Context for constructing batched penalty transactions.
-data PenaltyContext = PenaltyContext
- { pc_outputs :: !(NonEmpty RevokedOutput)
- -- ^ Revoked outputs to sweep (must be non-empty).
- , pc_revocation_key :: !RevocationPubkey
- -- ^ Revocation pubkey for all outputs.
- , pc_destination :: !Script
- -- ^ Destination scriptPubKey.
- , pc_feerate :: !FeeratePerKw
- -- ^ Fee rate for the penalty transaction.
- } deriving (Eq, Show, Generic)
-
-- weight constants (BOLT #5 Appendix A) ------------------------------
-- | Expected weight of the to_local penalty transaction witness
diff --git a/test/Main.hs b/test/Main.hs
@@ -973,77 +973,6 @@ spend_tests = testGroup "Spend" [
B5.stx_input_script stx1 @?=
B5.stx_input_script stx2
- , testCase "spend_revoked_batch total value" $ do
- let op1 = OutPoint dummyTxId 0
- op2 = OutPoint dummyTxId 1
- ro1 = B5.RevokedOutput op1 (Satoshi 50000)
- B5.RevokedToLocal
- ro2 = B5.RevokedOutput op2 (Satoshi 30000)
- B5.RevokedToLocal
- pctx = B5.PenaltyContext
- { B5.pc_outputs = ro1 :| [ro2]
- , B5.pc_revocation_key =
- dummyRevocationPubkey
- , B5.pc_destination = dummyDestScript
- , B5.pc_feerate = dummyFeerate
- }
- stx <- assertJust "spend_revoked_batch" $
- B5.spend_revoked_batch pctx
- let tx = B5.stx_tx stx
- outVal = txout_value
- (head' (tx_outputs tx))
- -- Output should be less than total input
- assertBool "output < total input"
- (outVal < 80000)
- -- Should have 2 inputs
- length (tx_inputs tx) @?= 2
-
- , testCase "spend_revoked_batch single element" $ do
- let ro = B5.RevokedOutput
- dummyOutPoint (Satoshi 50000)
- B5.RevokedToLocal
- pctx = B5.PenaltyContext
- { B5.pc_outputs = ro :| []
- , B5.pc_revocation_key =
- dummyRevocationPubkey
- , B5.pc_destination = dummyDestScript
- , B5.pc_feerate = dummyFeerate
- }
- stx <- assertJust "spend_revoked_batch" $
- B5.spend_revoked_batch pctx
- let tx = B5.stx_tx stx
- length (tx_inputs tx) @?= 1
- length (tx_outputs tx) @?= 1
-
- , testCase "spend_revoked_batch mixed types" $ do
- let op1 = OutPoint dummyTxId 0
- op2 = OutPoint dummyTxId 1
- op3 = OutPoint dummyTxId 2
- ro1 = B5.RevokedOutput op1
- (Satoshi 50000)
- B5.RevokedToLocal
- ro2 = B5.RevokedOutput op2
- (Satoshi 10000)
- (B5.RevokedHTLC
- (B5.HTLCOfferedOutput
- (CltvExpiry 500000)))
- ro3 = B5.RevokedOutput op3
- (Satoshi 10000)
- (B5.RevokedHTLC
- (B5.HTLCReceivedOutput
- (CltvExpiry 500000)))
- pctx = B5.PenaltyContext
- { B5.pc_outputs = ro1 :| [ro2, ro3]
- , B5.pc_revocation_key =
- dummyRevocationPubkey
- , B5.pc_destination = dummyDestScript
- , B5.pc_feerate = dummyFeerate
- }
- stx <- assertJust "spend_revoked_batch" $
- B5.spend_revoked_batch pctx
- let tx = B5.stx_tx stx
- length (tx_inputs tx) @?= 3
-
, testCase "spend_to_local fee underflow" $ do
B5.spend_to_local
dummyOutPoint (Satoshi 1)
@@ -1052,18 +981,6 @@ spend_tests = testGroup "Spend" [
dummyDestScript dummyFeerate
@?= Nothing
- , testCase "spend_revoked_batch fee underflow" $ do
- let ro = B5.RevokedOutput
- dummyOutPoint (Satoshi 1)
- B5.RevokedToLocal
- pctx = B5.PenaltyContext
- { B5.pc_outputs = ro :| []
- , B5.pc_revocation_key =
- dummyRevocationPubkey
- , B5.pc_destination = dummyDestScript
- , B5.pc_feerate = dummyFeerate
- }
- B5.spend_revoked_batch pctx @?= Nothing
]
-- htlc spend tests ---------------------------------------------------
@@ -1457,31 +1374,6 @@ property_tests = testGroup "Properties" [
B5.revoked_output_weight roReceived
== B5.accepted_htlc_penalty_input_weight
- , testProperty "spend_revoked_batch input count" $
- \(Positive n') ->
- let n = min (n' :: Int) 5
- ros = [ B5.RevokedOutput
- (OutPoint dummyTxId
- (fromIntegral i))
- (Satoshi 1000000)
- B5.RevokedToLocal
- | i <- [0..n-1] ]
- in case ros of
- [] -> True -- impossible with Positive
- (r:rs) ->
- let pctx = B5.PenaltyContext
- { B5.pc_outputs = r :| rs
- , B5.pc_revocation_key =
- dummyRevocationPubkey
- , B5.pc_destination =
- dummyDestScript
- , B5.pc_feerate = FeeratePerKw 253
- }
- in case B5.spend_revoked_batch pctx of
- Nothing -> False
- Just stx ->
- length (tx_inputs (B5.stx_tx stx))
- == n
]
-- helpers ------------------------------------------------------------