commit cd7f3e4a97b2201fb12b90d122781aef5eeed48e
parent 99dfa43640d8312375fd9836d0c0b00af021fffd
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:38:34 +0400
lib, test: use anchor-channel penalty weights
The Appendix A penalty witness weights (243/249) assume 133/139-byte
HTLC scripts. With option_anchors the scripts carry an extra
'1 OP_CHECKSEQUENCEVERIFY OP_DROP' and are 136/142 bytes, so every
anchor HTLC sweep underestimated its weight by 3. Add
offered_htlc_penalty_{witness,input}_weight_anchors (246/410) and
accepted_htlc_penalty_{witness,input}_weight_anchors (252/416), and
select them by channel type in spend_revoked_htlc,
spend_remote_htlc_timeout and spend_remote_htlc_preimage.
The weight selectors need the channel type, so:
htlcOutputTypeWeight
old: HTLCOutputType -> Word64
new: ChannelFeatures -> HTLCOutputType -> Word64
revoked_output_weight
old: RevokedOutput -> Word64
new: ChannelFeatures -> RevokedOutput -> Word64
Tests tie the constants to the sizes of the scripts bolt3 builds.
Diffstat:
4 files changed, 112 insertions(+), 17 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT5.hs b/lib/Lightning/Protocol/BOLT5.hs
@@ -69,9 +69,13 @@ module Lightning.Protocol.BOLT5 (
, 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
diff --git a/lib/Lightning/Protocol/BOLT5/Spend.hs b/lib/Lightning/Protocol/BOLT5/Spend.hs
@@ -204,7 +204,8 @@ spend_remote_htlc_timeout !op !value !htlc !keys
(htlc_payment_hash htlc)
(htlc_cltv_expiry htlc)
features
- !weight = accepted_htlc_penalty_input_weight
+ !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
@@ -249,7 +250,8 @@ spend_remote_htlc_preimage !op !value !htlc !keys
(ck_local_htlc keys)
(htlc_payment_hash htlc)
features
- !weight = offered_htlc_penalty_input_weight
+ !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
@@ -332,7 +334,7 @@ spend_revoked_htlc !op !value !htype !revpk !keys
(ck_local_htlc keys)
ph
features
- !weight = offered_htlc_penalty_input_weight
+ !weight = htlcOutputTypeWeight features htype
+ penalty_tx_base_weight
!fee = spending_fee feerate weight
in case sweep_value value fee destScript of
@@ -350,7 +352,7 @@ spend_revoked_htlc !op !value !htype !revpk !keys
ph
expiry
features
- !weight = accepted_htlc_penalty_input_weight
+ !weight = htlcOutputTypeWeight features htype
+ penalty_tx_base_weight
!fee = spending_fee feerate weight
in case sweep_value value fee destScript of
diff --git a/lib/Lightning/Protocol/BOLT5/Types.hs b/lib/Lightning/Protocol/BOLT5/Types.hs
@@ -35,9 +35,13 @@ module Lightning.Protocol.BOLT5.Types (
, 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
@@ -139,11 +143,13 @@ htlcOutputType _ = Nothing
{-# INLINE htlcOutputType #-}
-- | Penalty input weight for an HTLC output type.
-htlcOutputTypeWeight :: HTLCOutputType -> Word64
-htlcOutputTypeWeight (HTLCOfferedOutput _) =
- offered_htlc_penalty_input_weight
-htlcOutputTypeWeight (HTLCReceivedOutput _) =
- accepted_htlc_penalty_input_weight
+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 #-}
-- spending transactions ----------------------------------------------
@@ -186,12 +192,12 @@ data RevokedOutputType
deriving (Eq, Show, Generic)
-- | Penalty input weight for a revoked output.
-revoked_output_weight :: RevokedOutput -> Word64
-revoked_output_weight !ro = case ro_type ro of
+revoked_output_weight :: ChannelFeatures -> RevokedOutput -> Word64
+revoked_output_weight !features !ro = case ro_type ro of
RevokedToLocal ->
to_local_penalty_input_weight
RevokedHTLC !htype ->
- htlcOutputTypeWeight htype
+ htlcOutputTypeWeight features htype
{-# INLINE revoked_output_weight #-}
-- weight constants (BOLT #5 Appendix A) ------------------------------
@@ -202,15 +208,29 @@ to_local_penalty_witness_weight :: Word64
to_local_penalty_witness_weight = 160
-- | Expected weight of the offered_htlc penalty transaction
--- witness (243 bytes).
+-- witness (243 bytes), 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).
+-- witness (249 bytes), 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.
+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.
+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).
to_local_penalty_input_weight :: Word64
to_local_penalty_input_weight = 324
@@ -225,6 +245,16 @@ offered_htlc_penalty_input_weight = 407
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).
+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).
+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).
to_remote_input_weight :: Word64
diff --git a/test/Main.hs b/test/Main.hs
@@ -511,6 +511,8 @@ types_tests = testGroup "Types" [
B5.to_local_penalty_input_weight @?= 324
B5.offered_htlc_penalty_input_weight @?= 407
B5.accepted_htlc_penalty_input_weight @?= 413
+ B5.offered_htlc_penalty_input_weight_anchors @?= 410
+ B5.accepted_htlc_penalty_input_weight_anchors @?= 416
B5.to_remote_input_weight @?= 272
B5.max_standard_weight @?= 400000
]
@@ -1221,6 +1223,53 @@ weight_tests = testGroup "Weight" [
-- accepted: 164 + 249 = 413
B5.accepted_htlc_penalty_input_weight @?=
(164 + B5.accepted_htlc_penalty_witness_weight)
+ -- with anchors: 164 + 246 = 410, 164 + 252 = 416
+ B5.offered_htlc_penalty_input_weight_anchors @?=
+ (164 + B5.offered_htlc_penalty_witness_weight_anchors)
+ B5.accepted_htlc_penalty_input_weight_anchors @?=
+ (164 + B5.accepted_htlc_penalty_witness_weight_anchors)
+
+ , testCase "penalty witness weights match HTLC scripts" $ do
+ -- 1 (item count) + 1 + 73 (signature) + 1 + 33
+ -- (revocationpubkey) + 1 + script, for a 3-byte CLTV expiry
+ let witnessWeight script =
+ fromIntegral (110 + BS.length (unScript script))
+ offered = offered_htlc_script
+ dummyRevocationPubkey dummyRemoteHtlcPubkey
+ dummyLocalHtlcPubkey dummyPaymentHash
+ received = received_htlc_script
+ dummyRevocationPubkey dummyRemoteHtlcPubkey
+ dummyLocalHtlcPubkey dummyPaymentHash
+ (CltvExpiry 500000)
+ witnessWeight (offered dummyFeatures)
+ @?= B5.offered_htlc_penalty_witness_weight
+ witnessWeight (received dummyFeatures)
+ @?= B5.accepted_htlc_penalty_witness_weight
+ witnessWeight (offered dummyFeaturesAnchors)
+ @?= B5.offered_htlc_penalty_witness_weight_anchors
+ witnessWeight (received dummyFeaturesAnchors)
+ @?= B5.accepted_htlc_penalty_witness_weight_anchors
+
+ , testCase "spend_revoked_htlc weight by channel type" $ do
+ let value = 50000
+ outValue features htype = fmap
+ (txout_value . head' . tx_outputs . B5.stx_tx)
+ (B5.spend_revoked_htlc dummyOutPoint (Satoshi value)
+ htype dummyRevocationPubkey dummyKeys features
+ dummyPaymentHash dummyDestScript dummyFeerate)
+ expected inputWeight = Just $ value - unSatoshi
+ (B5.spending_fee dummyFeerate
+ (inputWeight + B5.penalty_tx_base_weight))
+ offered = B5.HTLCOfferedOutput (CltvExpiry 500000)
+ received = B5.HTLCReceivedOutput (CltvExpiry 500000)
+ outValue dummyFeatures offered
+ @?= expected B5.offered_htlc_penalty_input_weight
+ outValue dummyFeatures received
+ @?= expected B5.accepted_htlc_penalty_input_weight
+ outValue dummyFeaturesAnchors offered
+ @?= expected B5.offered_htlc_penalty_input_weight_anchors
+ outValue dummyFeaturesAnchors received
+ @?= expected B5.accepted_htlc_penalty_input_weight_anchors
, testCase "max HTLCs in single penalty tx" $ do
-- Per spec: (400000 - 324 - 272 - 212 - 2) / 413
@@ -1428,14 +1477,24 @@ property_tests = testGroup "Properties" [
dummyOutPoint (Satoshi 1000)
(B5.RevokedHTLC
(B5.HTLCReceivedOutput ce))
- in B5.revoked_output_weight roLocal
+ weight = B5.revoked_output_weight
+ in weight dummyFeatures roLocal
== B5.to_local_penalty_input_weight
&&
- B5.revoked_output_weight roOffered
+ weight dummyFeatures roOffered
== B5.offered_htlc_penalty_input_weight
&&
- B5.revoked_output_weight roReceived
+ weight dummyFeatures roReceived
== B5.accepted_htlc_penalty_input_weight
+ &&
+ weight dummyFeaturesAnchors roLocal
+ == B5.to_local_penalty_input_weight
+ &&
+ weight dummyFeaturesAnchors roOffered
+ == B5.offered_htlc_penalty_input_weight_anchors
+ &&
+ weight dummyFeaturesAnchors roReceived
+ == B5.accepted_htlc_penalty_input_weight_anchors
]