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 499ce2d09a3377914cb20799504f006cf0960871
parent 79540cf45574e0c3e2b4d1e05c8a5b94fa65ebee
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:11:21 +0400

lib: follow the BOLT #7 address descriptor rules

decodeNodeAnnouncement rejected the whole message on any unknown
address type, a deprecated Tor v2 descriptor, or a DNS descriptor
with an empty hostname. The spec says receivers ignore these.

The address parser now:

- stops at the first unknown type (0 included) and ignores the rest
  of the address bytes, since its length is unknown;
- skips Tor v2 (type 3) descriptors by their fixed length of 12;
- skips descriptors with port 0 ("SHOULD ignore that address
  descriptor");
- skips DNS descriptors whose hostname is empty or not ASCII.

A known descriptor that addrlen cannot hold is still an error.

The hostname smart constructor now rejects non-ASCII bytes ("hostname
bytes MUST be ASCII characters"). validateNodeAnnouncement fails with
the new ValidateMultipleDns when more than one DNS hostname is
present: the origin MUST NOT announce more than one, and a receiver
MUST NOT forward such a message.

Diffstat:
Mlib/Lightning/Protocol/BOLT7/Codec.hs | 74+++++++++++++++++++++++++++++++++++++++-----------------------------------
Mlib/Lightning/Protocol/BOLT7/Types.hs | 12+++++++-----
Mlib/Lightning/Protocol/BOLT7/Validate.hs | 11+++++++++--
3 files changed, 55 insertions(+), 42 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT7/Codec.hs b/lib/Lightning/Protocol/BOLT7/Codec.hs @@ -201,6 +201,18 @@ decodeFeatureBits bs = do {-# INLINE decodeFeatureBits #-} -- | Decode addresses list (length-prefixed). +-- +-- Follows the BOLT #7 receiver rules for address descriptors: +-- +-- * parsing stops at the first descriptor of an unknown type, and +-- the remaining address bytes are ignored; +-- * deprecated Tor v2 descriptors (type 3) are skipped; +-- * descriptors with port 0 are skipped; +-- * DNS descriptors whose hostname is empty or not ASCII are +-- skipped. +-- +-- A descriptor of a known type that @addrlen@ cannot hold is a +-- decoding error. decodeAddresses :: ByteString -> Either DecodeError ([Address], ByteString) decodeAddresses bs = do (addrData, rest) <- decodeLenPrefixed bs @@ -211,41 +223,33 @@ decodeAddresses bs = do parseAddrs !d | BS.null d = Right [] | otherwise = do - (addr, d') <- parseOneAddr d - addrs <- parseAddrs d' - Right (addr : addrs) - - parseOneAddr :: ByteString -> Either DecodeError (Address, ByteString) - parseOneAddr d = do - (typ, d1) <- decodeU8 d - case typ of - 1 -> do -- IPv4 - (addrBytes, d2) <- decodeBytes ipv4AddrLen d1 - (port, d3) <- decodeU16 d2 - case ipv4Addr addrBytes of - Nothing -> Left DecodeInvalidAddress - Just a -> Right (AddrIPv4 a port, d3) - 2 -> do -- IPv6 - (addrBytes, d2) <- decodeBytes ipv6AddrLen d1 - (port, d3) <- decodeU16 d2 - case ipv6Addr addrBytes of - Nothing -> Left DecodeInvalidAddress - Just a -> Right (AddrIPv6 a port, d3) - 4 -> do -- Tor v3 - (addrBytes, d2) <- decodeBytes torV3AddrLen d1 - (port, d3) <- decodeU16 d2 - case torV3Addr addrBytes of - Nothing -> Left DecodeInvalidAddress - Just a -> Right (AddrTorV3 a port, d3) - 5 -> do -- DNS hostname - (hostLen, d2) <- decodeU8 d1 - (hostBytes, d3) <- - decodeBytes (fromIntegral hostLen) d2 - (port, d4) <- decodeU16 d3 - case hostname hostBytes of - Nothing -> Left DecodeInvalidAddress - Just h -> Right (AddrDNS h port, d4) - _ -> Left DecodeInvalidAddress -- Unknown address type + (typ, d1) <- decodeU8 d + case typ of + 1 -> fixedAddr ipv4AddrLen (fmap AddrIPv4 . ipv4Addr) d1 + 2 -> fixedAddr ipv6AddrLen (fmap AddrIPv6 . ipv6Addr) d1 + 3 -> do -- Tor v2 (deprecated; 10-byte onion + port) + (_, d2) <- decodeBytes 12 d1 + parseAddrs d2 + 4 -> fixedAddr torV3AddrLen (fmap AddrTorV3 . torV3Addr) d1 + 5 -> do -- DNS hostname + (hostLen, d2) <- decodeU8 d1 + (hostBytes, d3) <- decodeBytes (fromIntegral hostLen) d2 + (port, d4) <- decodeU16 d3 + keep (fmap AddrDNS (hostname hostBytes)) port d4 + _ -> Right [] -- unknown type: ignore the rest + + fixedAddr :: Int -> (ByteString -> Maybe (Word16 -> Address)) + -> ByteString -> Either DecodeError [Address] + fixedAddr len mk d = do + (addrBytes, d1) <- decodeBytes len d + (port, d2) <- decodeU16 d1 + keep (mk addrBytes) port d2 + + keep :: Maybe (Word16 -> Address) -> Word16 -> ByteString + -> Either DecodeError [Address] + keep mk port d = case mk of + Just f | port /= 0 -> fmap (f port :) (parseAddrs d) + _ -> parseAddrs d -- Channel announcement -------------------------------------------------------- diff --git a/lib/Lightning/Protocol/BOLT7/Types.hs b/lib/Lightning/Protocol/BOLT7/Types.hs @@ -324,11 +324,12 @@ torV3Addr !bs | otherwise = Nothing {-# INLINE torV3Addr #-} --- | DNS hostname (1-255 bytes). +-- | DNS hostname (1-255 ASCII bytes). -- -- Per BOLT #7 address descriptor type 5, the hostname is --- a length-prefixed DNS name. The length byte limits it to --- 255 bytes. +-- a length-prefixed DNS name whose bytes MUST be ASCII +-- (non-ASCII names are Punycode-encoded). The length byte +-- limits it to 255 bytes. newtype Hostname = Hostname { getHostname :: ByteString } deriving (Eq, Show, Generic) @@ -336,12 +337,13 @@ instance NFData Hostname -- | Smart constructor for Hostname. -- --- Returns Nothing if the hostname is empty or exceeds --- 255 bytes. +-- Returns Nothing if the hostname is empty, exceeds 255 +-- bytes, or contains a non-ASCII byte. hostname :: ByteString -> Maybe Hostname hostname !bs | BS.null bs = Nothing | BS.length bs > 255 = Nothing + | BS.any (> 0x7f) bs = Nothing | otherwise = Just (Hostname bs) {-# INLINE hostname #-} diff --git a/lib/Lightning/Protocol/BOLT7/Validate.hs b/lib/Lightning/Protocol/BOLT7/Validate.hs @@ -41,6 +41,7 @@ data ValidationError | ValidateHtlcAmounts -- ^ htlc_minimum_msat > htlc_maximum_msat | ValidateBlockOverflow -- ^ first_blocknum + number_of_blocks overflow | ValidateScidNotAscending -- ^ short_channel_ids not in ascending order + | ValidateMultipleDns -- ^ more than one DNS hostname address deriving (Eq, Show, Generic) instance NFData ValidationError @@ -68,12 +69,18 @@ validateChannelAnnouncement msg = do -- Checks: -- -- * Feature bits do not contain unknown even bits +-- * At most one DNS hostname address. Per BOLT #7 the origin MUST +-- NOT announce more than one; a receiver SHOULD ignore all but the +-- first, and MUST NOT forward the message. -- --- Note: Address list validation (duplicate DNS entries) and alias --- UTF-8 validation are not enforced; the spec allows non-UTF-8 aliases. +-- Note: alias UTF-8 validation is not enforced; the spec allows +-- non-UTF-8 aliases. validateNodeAnnouncement :: NodeAnnouncement -> Either ValidationError () validateNodeAnnouncement msg = do validateFeatureBits (nodeAnnFeatures msg) + if length [ () | AddrDNS _ _ <- nodeAnnAddresses msg ] > 1 + then Left ValidateMultipleDns + else Right () -- | Validate channel_update message. --