bolt7

Routing gossip protocol, per BOLT #7 (docs.ppad.tech/bolt7).
git clone git://git.ppad.tech/bolt7.git
Log | Files | Refs | README | LICENSE

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:
Mbench/Main.hs | 2+-
Mbench/Weight.hs | 2+-
Mlib/Lightning/Protocol/BOLT7/Codec.hs | 68++++++++++++++++++++++++++++----------------------------------------
Mlib/Lightning/Protocol/BOLT7/Messages.hs | 10++++------
Mlib/Lightning/Protocol/BOLT7/Validate.hs | 19++++++-------------
Mtest/Main.hs | 22+++++++++++-----------
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