commit e33f9460d1e6e7c6174ea9f950c48bfc32fd56af
parent 4cb0c5ac13780f9af42494cbce828ab6063daec8
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:08:13 +0400
lib: make htlc_maximum_msat mandatory
BOLT #7 now requires htlc_maximum_msat in every channel_update and
says receivers ignore the must_be_one bit of message_flags. The
field was a Maybe gated on that bit.
chanUpdateHtlcMaxMsat is now a plain HtlcMaximumMsat. The decoder
reads it regardless of message_flags, and the encoder always emits
it with message_flags = 0x01. validateChannelUpdate always checks
htlc_minimum_msat <= htlc_maximum_msat.
Test and bench fixtures are updated for the new field type.
Diffstat:
6 files changed, 51 insertions(+), 72 deletions(-)
diff --git a/bench/Main.hs b/bench/Main.hs
@@ -178,7 +178,7 @@ testChannelUpdate = ChannelUpdate
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Just (HtlcMaximumMsat 1000000000)
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
{-# NOINLINE testChannelUpdate #-}
diff --git a/bench/Weight.hs b/bench/Weight.hs
@@ -170,7 +170,7 @@ mkChannelUpdate !sig !ch !scid = ChannelUpdate
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Just (HtlcMaximumMsat 1000000000)
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
-- | Construct AnnouncementSignatures message.
diff --git a/lib/Lightning/Protocol/BOLT7/Codec.hs b/lib/Lightning/Protocol/BOLT7/Codec.hs
@@ -50,7 +50,6 @@ module Lightning.Protocol.BOLT7.Codec (
) where
import Control.DeepSeq (NFData)
-import Data.Bits ((.&.))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Word (Word8, Word16, Word32, Word64)
@@ -370,39 +369,34 @@ decodeNodeAnnouncement bs = do
-- | Encode channel_update message.
--
--- The message_flags byte is derived from the presence of
--- 'chanUpdateHtlcMaxMsat': bit 0 is set when the field is
--- 'Just'.
+-- The message_flags byte is always 0x01: bit 0 (@must_be_one@) is
+-- set, as BOLT #7 requires, and no other bit is.
encodeChannelUpdate :: ChannelUpdate -> ByteString
-encodeChannelUpdate msg =
- let !msgFlagsByte = case chanUpdateHtlcMaxMsat msg of
- Nothing -> 0x00 :: Word8
- Just _ -> 0x01
- in mconcat
- [ unSignature (chanUpdateSignature msg)
- , unChainHash (chanUpdateChainHash msg)
- , scidToBytes (chanUpdateShortChanId msg)
- , Prim.encodeU32 (chanUpdateTimestamp msg)
- , BS.singleton msgFlagsByte
- , BS.singleton (encodeChannelFlags (chanUpdateChanFlags msg))
- , Prim.encodeU16
- (getCltvExpiryDelta (chanUpdateCltvExpDelta msg))
- , Prim.encodeU64
- (getHtlcMinimumMsat (chanUpdateHtlcMinMsat msg))
- , Prim.encodeU32
- (getFeeBaseMsat (chanUpdateFeeBaseMsat msg))
- , Prim.encodeU32
- (getFeeProportionalMillionths
- (chanUpdateFeeProportional msg))
- , case chanUpdateHtlcMaxMsat msg of
- Nothing -> BS.empty
- Just m -> Prim.encodeU64 (getHtlcMaximumMsat m)
- ]
+encodeChannelUpdate msg = mconcat
+ [ unSignature (chanUpdateSignature msg)
+ , unChainHash (chanUpdateChainHash msg)
+ , scidToBytes (chanUpdateShortChanId msg)
+ , Prim.encodeU32 (chanUpdateTimestamp msg)
+ , BS.singleton 0x01
+ , BS.singleton (encodeChannelFlags (chanUpdateChanFlags msg))
+ , Prim.encodeU16
+ (getCltvExpiryDelta (chanUpdateCltvExpDelta msg))
+ , Prim.encodeU64
+ (getHtlcMinimumMsat (chanUpdateHtlcMinMsat msg))
+ , Prim.encodeU32
+ (getFeeBaseMsat (chanUpdateFeeBaseMsat msg))
+ , Prim.encodeU32
+ (getFeeProportionalMillionths
+ (chanUpdateFeeProportional msg))
+ , Prim.encodeU64
+ (getHtlcMaximumMsat (chanUpdateHtlcMaxMsat msg))
+ ]
-- | Decode channel_update message.
--
--- The message_flags byte is read from the wire but not stored;
--- bit 0 determines whether 'htlc_maximum_msat' is present.
+-- The message_flags byte is read from the wire but not stored.
+-- Per BOLT #7, 'htlc_maximum_msat' is always present, and
+-- receivers ignore the @must_be_one@ bit.
decodeChannelUpdate :: ByteString
-> Either DecodeError
(ChannelUpdate, ByteString)
@@ -411,31 +405,25 @@ decodeChannelUpdate bs = do
(chainH, bs2) <- decodeChainHash bs1
(scid, bs3) <- decodeShortChannelId bs2
(timestamp, bs4) <- decodeU32 bs3
- (msgFlagsRaw, bs5) <- decodeU8 bs4
+ (_msgFlags, bs5) <- decodeU8 bs4
(chanFlagsRaw, bs6) <- decodeU8 bs5
(cltvDelta, bs7) <- decodeU16 bs6
(htlcMin, bs8) <- decodeU64 bs7
(feeBase, bs9) <- decodeU32 bs8
(feeProp, bs10) <- decodeU32 bs9
- let !chanFlags' = decodeChannelFlags chanFlagsRaw
- !htlcMaxPresent = msgFlagsRaw .&. 0x01 /= 0
- (htlcMax, rest) <- if htlcMaxPresent
- then do
- (m, r) <- decodeU64 bs10
- Right (Just (HtlcMaximumMsat m), r)
- else Right (Nothing, bs10)
+ (htlcMax, rest) <- decodeU64 bs10
let msg = ChannelUpdate
{ chanUpdateSignature = sig
, chanUpdateChainHash = chainH
, chanUpdateShortChanId = scid
, chanUpdateTimestamp = timestamp
- , chanUpdateChanFlags = chanFlags'
+ , chanUpdateChanFlags = decodeChannelFlags chanFlagsRaw
, chanUpdateCltvExpDelta = CltvExpiryDelta cltvDelta
, chanUpdateHtlcMinMsat = HtlcMinimumMsat htlcMin
, chanUpdateFeeBaseMsat = FeeBaseMsat feeBase
, chanUpdateFeeProportional =
FeeProportionalMillionths feeProp
- , chanUpdateHtlcMaxMsat = htlcMax
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat htlcMax
}
Right (msg, rest)
diff --git a/lib/Lightning/Protocol/BOLT7/Messages.hs b/lib/Lightning/Protocol/BOLT7/Messages.hs
@@ -122,9 +122,8 @@ instance NFData NodeAnnouncement
--
-- Communicates per-direction routing parameters.
--
--- The message_flags field is derived automatically during
--- encoding: bit 0 is set when 'chanUpdateHtlcMaxMsat' is
--- 'Just'.
+-- The message_flags field is not stored: the encoder always
+-- writes bit 0 (@must_be_one@), and the decoder ignores it.
data ChannelUpdate = ChannelUpdate
{ chanUpdateSignature :: !Signature
-- ^ Signature of message
@@ -144,9 +143,8 @@ data ChannelUpdate = ChannelUpdate
-- ^ Base fee msat
, chanUpdateFeeProportional :: !FeeProportionalMillionths
-- ^ Proportional fee
- , chanUpdateHtlcMaxMsat :: !(Maybe HtlcMaximumMsat)
- -- ^ Max HTLC (optional; presence sets message_flags
- -- bit 0)
+ , chanUpdateHtlcMaxMsat :: !HtlcMaximumMsat
+ -- ^ Max HTLC msat
}
deriving (Eq, Show, Generic)
diff --git a/lib/Lightning/Protocol/BOLT7/Validate.hs b/lib/Lightning/Protocol/BOLT7/Validate.hs
@@ -79,21 +79,14 @@ validateNodeAnnouncement msg = do
--
-- Checks:
--
--- * htlc_minimum_msat <= htlc_maximum_msat (if present)
---
--- Note: message_flags consistency is enforced at the type
--- level -- the flag is derived from the presence of
--- 'chanUpdateHtlcMaxMsat'.
+-- * htlc_minimum_msat <= htlc_maximum_msat
validateChannelUpdate :: ChannelUpdate
-> Either ValidationError ()
-validateChannelUpdate msg = do
- case chanUpdateHtlcMaxMsat msg of
- Nothing -> Right ()
- Just htlcMax ->
- let htlcMin = chanUpdateHtlcMinMsat msg
- in if getHtlcMinimumMsat htlcMin > getHtlcMaximumMsat htlcMax
- then Left ValidateHtlcAmounts
- else Right ()
+validateChannelUpdate msg
+ | getHtlcMinimumMsat (chanUpdateHtlcMinMsat msg)
+ > getHtlcMaximumMsat (chanUpdateHtlcMaxMsat msg)
+ = Left ValidateHtlcAmounts
+ | otherwise = Right ()
-- | Validate query_channel_range message.
--
diff --git a/test/Main.hs b/test/Main.hs
@@ -201,7 +201,7 @@ node_announcement_tests = testGroup "NodeAnnouncement" [
channel_update_tests :: TestTree
channel_update_tests = testGroup "ChannelUpdate" [
- testCase "encode/decode roundtrip without htlc_maximum_msat" $ do
+ testCase "encode/decode roundtrip (NodeTwo)" $ do
let msg = ChannelUpdate
{ chanUpdateSignature = testSignature
, chanUpdateChainHash = testChainHash
@@ -213,13 +213,13 @@ channel_update_tests = testGroup "ChannelUpdate" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Nothing
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
encoded = encodeChannelUpdate msg
case decodeChannelUpdate encoded of
Right (decoded, _) -> decoded @?= msg
Left e -> assertFailure $ "decode failed: " ++ show e
- , testCase "encode/decode roundtrip with htlc_maximum_msat" $ do
+ , testCase "encode/decode roundtrip (NodeOne)" $ do
let msg = ChannelUpdate
{ chanUpdateSignature = testSignature
, chanUpdateChainHash = testChainHash
@@ -231,7 +231,7 @@ channel_update_tests = testGroup "ChannelUpdate" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 500
, chanUpdateFeeProportional = FeeProportionalMillionths 50
- , chanUpdateHtlcMaxMsat = Just (HtlcMaximumMsat 1000000000)
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
encoded = encodeChannelUpdate msg
case decodeChannelUpdate encoded of
@@ -432,7 +432,7 @@ hash_tests = testGroup "Hash Functions" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Nothing
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
hashVal = channelUpdateHash msg
BS.length hashVal @?= 32
@@ -452,7 +452,7 @@ hash_tests = testGroup "Hash Functions" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Nothing
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
cs1 = channelUpdateChecksum msg
cs2 = channelUpdateChecksum msg
@@ -471,7 +471,7 @@ hash_tests = testGroup "Hash Functions" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Nothing
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
msg2 = encodeChannelUpdate ChannelUpdate
{ chanUpdateSignature = testSignature
@@ -485,7 +485,7 @@ hash_tests = testGroup "Hash Functions" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Nothing
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
channelUpdateChecksum msg1 @?= channelUpdateChecksum msg2
]
@@ -541,7 +541,7 @@ validation_tests = testGroup "Validation" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Just (HtlcMaximumMsat 1000000000)
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
validateChannelUpdate msg @?= Right ()
, testCase "rejects htlc_min > htlc_max" $ do
@@ -557,7 +557,7 @@ validation_tests = testGroup "Validation" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 2000000000 -- > htlcMax
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Just (HtlcMaximumMsat 1000000000)
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
validateChannelUpdate msg @?= Left ValidateHtlcAmounts
]
@@ -661,7 +661,7 @@ propChannelUpdateRoundtrip timestamp cltvDelta = property $ do
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Nothing
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
encoded = encodeChannelUpdate msg
case decodeChannelUpdate encoded of