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 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:
Mlib/Lightning/Protocol/BOLT5.hs | 4++++
Mlib/Lightning/Protocol/BOLT5/Spend.hs | 10++++++----
Mlib/Lightning/Protocol/BOLT5/Types.hs | 50++++++++++++++++++++++++++++++++++++++++----------
Mtest/Main.hs | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
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 ]