commit 9b55ee70b3982df87cc653cc68ea92bf4c4f3b87
parent 6056ef1dbc03a5661c63d962f6ac857a27c89f88
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:31:00 +0400
lib, test: sign anchor HTLC txs locally with SIGHASH_ALL
For option_anchors channels spend_htlc_timeout and spend_htlc_success
told the local signer to use SIGHASH_SINGLE|SIGHASH_ANYONECANPAY.
Per BOLT #5 only the peer's htlc_signature uses that flag; the local
signature MUST use SIGHASH_ALL, as the BOLT #3 Appendix F vectors
show (remote signatures end in 0x83, local ones in 0x01). Both now
return SIGHASH_ALL, and stx_sighash_type is documented as the type of
the local signature.
The tests that asserted SINGLE|ANYONECANPAY now expect SIGHASH_ALL.
New tests classify the Appendix F commitment, build its HTLC-timeout
and HTLC-success txs, compare them with the vectors and check the
vectors' signature flags against the SpendingTx.
Diffstat:
3 files changed, 83 insertions(+), 15 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT5/Spend.hs b/lib/Lightning/Protocol/BOLT5/Spend.hs
@@ -94,6 +94,11 @@ spend_to_local !op !value !revpk !delay !delayedpk
-- 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.
+--
+-- The sighash type is for the local signature, which is always
+-- @SIGHASH_ALL@. With @option_anchors@ the remote's
+-- @htlc_signature@ instead uses
+-- @SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@.
spend_htlc_timeout
:: HTLCContext
-> CommitmentKeys
@@ -111,11 +116,8 @@ spend_htlc_timeout !ctx !keys =
features
!inputValue =
msatToSat (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
+ in SpendingTx tx witnessScript inputValue SIGHASH_ALL
-- | Construct an HTLC-success second-stage transaction.
--
@@ -123,6 +125,11 @@ spend_htlc_timeout !ctx !keys =
-- have the preimage. The bolt3 'build_htlc_success_tx' function
-- constructs the HTLC-success tx; this wraps it as a
-- 'SpendingTx'.
+--
+-- The sighash type is for the local signature, which is always
+-- @SIGHASH_ALL@. With @option_anchors@ the remote's
+-- @htlc_signature@ instead uses
+-- @SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@.
spend_htlc_success
:: HTLCContext
-> CommitmentKeys
@@ -141,11 +148,8 @@ spend_htlc_success !ctx !keys =
features
!inputValue =
msatToSat (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
+ in SpendingTx tx witnessScript inputValue SIGHASH_ALL
-- | Spend a second-stage HTLC output (HTLC-timeout or
-- HTLC-success output) after the CSV delay.
diff --git a/lib/Lightning/Protocol/BOLT5/Types.hs b/lib/Lightning/Protocol/BOLT5/Types.hs
@@ -162,7 +162,7 @@ data SpendingTx = SpendingTx
, stx_input_value :: {-# UNPACK #-} !Satoshi
-- ^ Value of the input being spent (for sighash).
, stx_sighash_type :: !SighashType
- -- ^ Sighash type to use when signing.
+ -- ^ Sighash type for the local signature.
} deriving (Eq, Show, Generic)
-- penalty batching ---------------------------------------------------
diff --git a/test/Main.hs b/test/Main.hs
@@ -4,7 +4,7 @@
module Main where
import Bitcoin.Prim.Tx (TxOut(..), from_base16)
-import Bitcoin.Prim.Tx.Sighash (SighashType(..))
+import Bitcoin.Prim.Tx.Sighash (SighashType(..), encode_sighash)
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.ByteString as BS
import Data.List.NonEmpty (NonEmpty(..))
@@ -224,8 +224,14 @@ vecHtlc dir msat expiry b = HTLC
, htlc_cltv_expiry = CltvExpiry expiry
}
-vecHtlc1, vecHtlc5, vecHtlc6 :: HTLC
+vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4 :: HTLC
+vecHtlc0 = vecHtlc HTLCReceived 1000000 500 0x00
vecHtlc1 = vecHtlc HTLCReceived 2000000 501 0x01
+vecHtlc2 = vecHtlc HTLCOffered 2000000 502 0x02
+vecHtlc3 = vecHtlc HTLCOffered 3000000 503 0x03
+vecHtlc4 = vecHtlc HTLCReceived 4000000 504 0x04
+
+vecHtlc5, vecHtlc6 :: HTLC
vecHtlc5 = vecHtlc HTLCOffered 5000000 506 0x05
vecHtlc6 = vecHtlc HTLCOffered 5000001 505 0x05
@@ -299,6 +305,13 @@ vecCommitSameHash = vecCommitment (FeeratePerKw 253)
(ChannelFeatures False) (MilliSatoshi 6987999999)
[vecHtlc1, vecHtlc5, vecHtlc6]
+-- | The Appendix F (anchors) "commitment tx with seven outputs
+-- untrimmed".
+vecCommitAnchors :: Maybe CommitmentTx
+vecCommitAnchors = vecCommitment (FeeratePerKw 644)
+ (ChannelFeatures True) (MilliSatoshi 6988000000)
+ [vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4]
+
-- | Appendix C: commitment tx with 3 htlc outputs, 2 offered
-- having the same amount and preimage (signed).
vecCCommitSameHash :: BS.ByteString
@@ -371,6 +384,23 @@ vecCHtlcTimeout5 =
\8d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc\
\93ebefbb1b73f0af055dcc27a0b504ad7688ac6868fa010000"
+-- | Appendix F: HTLC-timeout tx for htlc #2 from "commitment tx
+-- with seven outputs untrimmed" (signed).
+vecFHtlcTimeout2 :: BS.ByteString
+vecFHtlcTimeout2 =
+ "02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e\
+ \6746d497ec221a03000000000100000001d0070000000000002200204adb4e2f\
+ \00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830\
+ \45022100a847bc3b8cf2441013725ae32dc589c419835d069afd6d7f3d9834d8\
+ \be7cea6e02204ce9d35ec7f0788da80e4d62e810c4ccd13617e58fa10832e38f\
+ \b7ae87bdf33883473044022026739b1adbfa34c485bf0e5a19e0cf7532f64545\
+ \bcc5c95b95643ccc2d351e1902201743bb1b34f00e031cc15f6eff0f8ab76450\
+ \8d2681ff965ba62e76e540bca1e801008876a91414011f7254d96b819c76986c\
+ \277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a21\
+ \84d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384\
+ \f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e\
+ \1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000"
+
-- | Appendix F: HTLC-success tx for htlc #1 from "commitment tx
-- with seven outputs untrimmed" (signed).
vecFHtlcSuccess1 :: BS.ByteString
@@ -441,6 +471,22 @@ assertHtlcTx stx vec = do
script : _ -> B5.stx_input_script stx @?= Script script
[] -> assertFailure "empty witness"
+-- | Check the sighash flags of the signatures in a signed BOLT #3
+-- HTLC-tx vector: the local one must be the type the 'SpendingTx'
+-- gives, the remote one the given type.
+assertSighashFlags
+ :: B5.SpendingTx -> SighashType -> BS.ByteString -> Assertion
+assertSighashFlags stx remoteType vec = do
+ items <- singleWitness =<< parseTx vec
+ case items of
+ [_, remoteSig, localSig, _, _] -> do
+ flag remoteSig @?= Just (encode_sighash remoteType)
+ flag localSig
+ @?= Just (encode_sighash (B5.stx_sighash_type stx))
+ _ -> assertFailure "unexpected HTLC-tx witness"
+ where
+ flag = fmap (fromIntegral . snd) . BS.unsnoc
+
-- types tests --------------------------------------------------------
types_tests :: TestTree
@@ -781,6 +827,7 @@ classify_tests = testGroup "Classify" [
assertHtlcTx stx0 vecCHtlcSuccess1
stx1 <- vecHtlcTx (FeeratePerKw 253) o1
assertHtlcTx stx1 vecCHtlcTimeout6
+ assertSighashFlags stx1 SIGHASH_ALL vecCHtlcTimeout6
stx2 <- vecHtlcTx (FeeratePerKw 253) o2
assertHtlcTx stx2 vecCHtlcTimeout5
_ -> assertFailure "expected at least 3 outputs"
@@ -1047,8 +1094,7 @@ htlc_spend_tests = testGroup "HTLC Spend" [
let ctx = dummyHTLCContext
{ hc_features = dummyFeaturesAnchors }
stx = B5.spend_htlc_timeout ctx dummyKeys
- B5.stx_sighash_type stx
- @?= SIGHASH_SINGLE_ANYONECANPAY
+ B5.stx_sighash_type stx @?= SIGHASH_ALL
, testCase "spend_htlc_success anchors sighash" $ do
let ctx = dummyHTLCContext
@@ -1056,8 +1102,26 @@ htlc_spend_tests = testGroup "HTLC Spend" [
, hc_features = dummyFeaturesAnchors
}
stx = B5.spend_htlc_success ctx dummyKeys
- B5.stx_sighash_type stx
- @?= SIGHASH_SINGLE_ANYONECANPAY
+ B5.stx_sighash_type stx @?= SIGHASH_ALL
+
+ , testCase "HTLC txs - Appendix F (anchors)" $ do
+ -- The local signature uses SIGHASH_ALL and the remote one
+ -- SIGHASH_SINGLE|SIGHASH_ANYONECANPAY.
+ commit <- assertJust "commitment" vecCommitAnchors
+ keys <- assertJust "keys" vecKeys
+ let outs = B5.classify_local_commit_outputs commit keys
+ (ToSelfDelay 144) (ChannelFeatures True)
+ [vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4]
+ check uo vec = do
+ stx <- vecHtlcTx (FeeratePerKw 644) uo
+ assertHtlcTx stx vec
+ B5.stx_sighash_type stx @?= SIGHASH_ALL
+ assertSighashFlags stx SIGHASH_SINGLE_ANYONECANPAY vec
+ case drop 3 outs of
+ o3 : o4 : _ -> do
+ check o3 vecFHtlcTimeout2
+ check o4 vecFHtlcSuccess1
+ _ -> assertFailure "expected at least 5 outputs"
]
-- remote spend tests -------------------------------------------------