commit 70c295b436c01cc5a8b550fb836edd90f1bc472a
parent 4cb0c5ac13780f9af42494cbce828ab6063daec8
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:34:19 +0400
merge: fix/phase1-spec -- BOLT #7 spec fixes
Brings the codecs in line with the current BOLT #7 text, plus
known-answer tests from real mainnet gossip.
- channel_update: htlc_maximum_msat is mandatory. The field is now a
plain HtlcMaximumMsat; the decoder reads it regardless of
message_flags (receivers ignore must_be_one), and the encoder
always emits it with message_flags = 0x01.
- node_announcement addresses follow the receiver rules: parsing
stops at the first unknown type (0 included) and ignores the rest;
Tor v2 (type 3) descriptors are skipped; descriptors with port 0,
and DNS descriptors with an empty or non-ASCII hostname, are
skipped. A truncated known descriptor is still an error. The
hostname constructor rejects non-ASCII bytes, and
validateNodeAnnouncement fails with ValidateMultipleDns when more
than one DNS hostname is present.
- Query decoders decode their TLV streams with the known types (1;
1 and 3 for reply_channel_range) and fail with DecodeTlvError on a
malformed stream or an unknown even type, rather than silently
returning an empty stream.
- Encoders with u16 length fields fail with EncodeLengthOverflow
instead of wrapping the prefix. encodeChannelAnnouncement now
returns Either EncodeError ByteString.
- validateReplyChannelRange fails with ValidateScidEncoding when it
cannot decode encoded_short_ids (zlib, unknown encoding, partial
short_channel_id).
- The hash and checksum functions return Maybe and give Nothing on
input too short for the message; the module doc states that the
input is the payload after the 2-byte type. The checksum doc now
gives the right timestamp offset (104-107).
- ppad-secp256k1 is a new test-only dependency. The test suite embeds
a real channel_announcement and two channel_updates (one with an
extension), checks decoding, byte-exact re-encoding, signature
hashes against independent digests and the real signatures, and
checksums against an independent CRC-32C.
Breaking changes:
chanUpdateHtlcMaxMsat :: ChannelUpdate -> HtlcMaximumMsat
encodeChannelAnnouncement
:: ChannelAnnouncement -> Either EncodeError ByteString
channelAnnouncementHash, nodeAnnouncementHash, channelUpdateHash
:: ByteString -> Maybe ByteString
channelUpdateChecksum :: ByteString -> Maybe Word32
ValidationError gains ValidateMultipleDns and ValidateScidEncoding
Diffstat:
12 files changed, 1004 insertions(+), 230 deletions(-)
diff --git a/bench/Main.hs b/bench/Main.hs
@@ -143,7 +143,10 @@ testChannelAnnouncement = ChannelAnnouncement
-- | Encoded ChannelAnnouncement for decode benchmarks.
encodedChannelAnnouncement :: BS.ByteString
-encodedChannelAnnouncement = encodeChannelAnnouncement testChannelAnnouncement
+encodedChannelAnnouncement =
+ case encodeChannelAnnouncement testChannelAnnouncement of
+ Right bs -> bs
+ Left _ -> error "encodedChannelAnnouncement: encode failed"
{-# NOINLINE encodedChannelAnnouncement #-}
-- | Test NodeAnnouncement message.
@@ -178,7 +181,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.
@@ -222,7 +222,10 @@ testChannelAnnouncement = mkChannelAnnouncement
-- | Encoded ChannelAnnouncement for decode benchmarks.
encodedChannelAnnouncement :: BS.ByteString
-encodedChannelAnnouncement = encodeChannelAnnouncement testChannelAnnouncement
+encodedChannelAnnouncement =
+ case encodeChannelAnnouncement testChannelAnnouncement of
+ Right bs -> bs
+ Left _ -> error "encodedChannelAnnouncement: encode failed"
{-# NOINLINE encodedChannelAnnouncement #-}
-- | Test NodeAnnouncement message.
diff --git a/flake.lock b/flake.lock
@@ -71,6 +71,40 @@
"ppad-base16_2": {
"inputs": {
"flake-utils": [
+ "ppad-secp256k1",
+ "ppad-base16",
+ "ppad-nixpkgs",
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-base16",
+ "ppad-nixpkgs",
+ "nixpkgs"
+ ],
+ "ppad-nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1780597900,
+ "narHash": "sha256-wI9ME6gE+DP+7vLYuWBff3Q9mOM/xX/N0keqJj7dKJQ=",
+ "ref": "master",
+ "rev": "881ca5128665d5e2207803751054d7d32c4c864c",
+ "revCount": 38,
+ "type": "git",
+ "url": "git://git.ppad.tech/base16.git"
+ },
+ "original": {
+ "ref": "master",
+ "type": "git",
+ "url": "git://git.ppad.tech/base16.git"
+ }
+ },
+ "ppad-base16_3": {
+ "inputs": {
+ "flake-utils": [
"ppad-sha256",
"ppad-base16",
"ppad-nixpkgs",
@@ -134,6 +168,85 @@
"url": "git://git.ppad.tech/bolt1.git"
}
},
+ "ppad-fixed": {
+ "inputs": {
+ "flake-utils": [
+ "ppad-secp256k1",
+ "ppad-fixed",
+ "ppad-nixpkgs",
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-fixed",
+ "ppad-nixpkgs",
+ "nixpkgs"
+ ],
+ "ppad-nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1785603497,
+ "narHash": "sha256-rF8KdVJ+VwUT2bEfCLZMPc05rUclr0wN2uOTYvd3z7g=",
+ "ref": "master",
+ "rev": "e303bda7578385f488c2f23baa2e1a6a2b0b2aaf",
+ "revCount": 305,
+ "type": "git",
+ "url": "git://git.ppad.tech/fixed.git"
+ },
+ "original": {
+ "ref": "master",
+ "type": "git",
+ "url": "git://git.ppad.tech/fixed.git"
+ }
+ },
+ "ppad-hmac-drbg": {
+ "inputs": {
+ "flake-utils": [
+ "ppad-secp256k1",
+ "ppad-hmac-drbg",
+ "ppad-nixpkgs",
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-hmac-drbg",
+ "ppad-nixpkgs",
+ "nixpkgs"
+ ],
+ "ppad-base16": [
+ "ppad-secp256k1",
+ "ppad-base16"
+ ],
+ "ppad-nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-nixpkgs"
+ ],
+ "ppad-sha256": [
+ "ppad-secp256k1",
+ "ppad-sha256"
+ ],
+ "ppad-sha512": [
+ "ppad-secp256k1",
+ "ppad-sha512"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783709384,
+ "narHash": "sha256-NuIDRrjy8gmJ22CvXU1JpNIEZ+pmo9ZScifieJApAxM=",
+ "owner": "ppad-tech",
+ "repo": "hmac-drbg",
+ "rev": "ff4f884296297eb7fa62f7e9326c094cdaf5f9cd",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ppad-tech",
+ "repo": "hmac-drbg",
+ "type": "github"
+ }
+ },
"ppad-nixpkgs": {
"inputs": {
"flake-utils": "flake-utils",
@@ -154,6 +267,44 @@
"url": "git://git.ppad.tech/nixpkgs.git"
}
},
+ "ppad-secp256k1": {
+ "inputs": {
+ "flake-utils": [
+ "ppad-secp256k1",
+ "ppad-nixpkgs",
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-nixpkgs",
+ "nixpkgs"
+ ],
+ "ppad-base16": "ppad-base16_2",
+ "ppad-fixed": "ppad-fixed",
+ "ppad-hmac-drbg": "ppad-hmac-drbg",
+ "ppad-nixpkgs": [
+ "ppad-nixpkgs"
+ ],
+ "ppad-sha256": [
+ "ppad-sha256"
+ ],
+ "ppad-sha512": "ppad-sha512"
+ },
+ "locked": {
+ "lastModified": 1785605654,
+ "narHash": "sha256-VDNnKXmJIZZ/3u7Tf1rK93GR4x7OiAadqykhqCwkHb4=",
+ "ref": "master",
+ "rev": "fdb80d54b86c61bd1a15e2e8db351091d02c89ee",
+ "revCount": 280,
+ "type": "git",
+ "url": "git://git.ppad.tech/secp256k1.git"
+ },
+ "original": {
+ "ref": "master",
+ "type": "git",
+ "url": "git://git.ppad.tech/secp256k1.git"
+ }
+ },
"ppad-sha256": {
"inputs": {
"flake-utils": [
@@ -166,7 +317,7 @@
"ppad-nixpkgs",
"nixpkgs"
],
- "ppad-base16": "ppad-base16_2",
+ "ppad-base16": "ppad-base16_3",
"ppad-nixpkgs": [
"ppad-nixpkgs"
]
@@ -186,6 +337,44 @@
"url": "git://git.ppad.tech/sha256.git"
}
},
+ "ppad-sha512": {
+ "inputs": {
+ "flake-utils": [
+ "ppad-secp256k1",
+ "ppad-sha512",
+ "ppad-nixpkgs",
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-sha512",
+ "ppad-nixpkgs",
+ "nixpkgs"
+ ],
+ "ppad-base16": [
+ "ppad-secp256k1",
+ "ppad-base16"
+ ],
+ "ppad-nixpkgs": [
+ "ppad-secp256k1",
+ "ppad-nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783709328,
+ "narHash": "sha256-M7Ts5a6600sauWlQZ2NHR+qpQMv0DCEPoCrNpcZAqVo=",
+ "ref": "master",
+ "rev": "287fb14136592acd6c510ae69b645e77335a214c",
+ "revCount": 58,
+ "type": "git",
+ "url": "git://git.ppad.tech/sha512.git"
+ },
+ "original": {
+ "ref": "master",
+ "type": "git",
+ "url": "git://git.ppad.tech/sha512.git"
+ }
+ },
"root": {
"inputs": {
"flake-utils": [
@@ -198,6 +387,7 @@
],
"ppad-bolt1": "ppad-bolt1",
"ppad-nixpkgs": "ppad-nixpkgs",
+ "ppad-secp256k1": "ppad-secp256k1",
"ppad-sha256": "ppad-sha256"
}
},
diff --git a/flake.nix b/flake.nix
@@ -8,6 +8,13 @@
ref = "master";
inputs.ppad-nixpkgs.follows = "ppad-nixpkgs";
};
+ ppad-secp256k1 = {
+ type = "git";
+ url = "git://git.ppad.tech/secp256k1.git";
+ ref = "master";
+ inputs.ppad-nixpkgs.follows = "ppad-nixpkgs";
+ inputs.ppad-sha256.follows = "ppad-sha256";
+ };
ppad-sha256 = {
type = "git";
url = "git://git.ppad.tech/sha256.git";
@@ -24,7 +31,7 @@
};
outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs
- , ppad-bolt1, ppad-sha256 }:
+ , ppad-bolt1, ppad-secp256k1, ppad-sha256 }:
flake-utils.lib.eachDefaultSystem (system:
let
lib = "ppad-bolt7";
@@ -40,6 +47,12 @@
(hlib.enableCabalFlag bolt1 "llvm")
[ llvm clang ];
+ secp256k1 = ppad-secp256k1.packages.${system}.default;
+ secp256k1-llvm =
+ hlib.addBuildTools
+ (hlib.enableCabalFlag secp256k1 "llvm")
+ [ llvm clang ];
+
sha256 = ppad-sha256.packages.${system}.default;
sha256-llvm =
hlib.addBuildTools
@@ -48,9 +61,11 @@
hpkgs = pkgs.haskell.packages.ghc910.extend (new: old: {
ppad-bolt1 = bolt1-llvm;
+ ppad-secp256k1 = secp256k1-llvm;
ppad-sha256 = sha256-llvm;
${lib} = new.callCabal2nix lib ./. {
ppad-bolt1 = new.ppad-bolt1;
+ ppad-secp256k1 = new.ppad-secp256k1;
ppad-sha256 = new.ppad-sha256;
};
});
diff --git a/lib/Lightning/Protocol/BOLT7.hs b/lib/Lightning/Protocol/BOLT7.hs
@@ -55,12 +55,15 @@
--
-- == Signature verification
--
--- This library provides hash computation for signature verification:
+-- This library provides hash computation for signature verification.
+-- The hash functions take the received payload (the bytes after the
+-- 2-byte message type):
--
-- @
-- -- Compute the hash that should be signed
--- let sigHash = channelAnnouncementHash encodedMessage
--- -- Verify signatures using ppad-secp256k1 (not included)
+-- case channelAnnouncementHash payload of
+-- Nothing -> rejectMessage -- too short to be a channel_announcement
+-- Just sigHash -> verifyWith sigHash -- e.g. using ppad-secp256k1
-- @
--
-- = Protocol overview
diff --git a/lib/Lightning/Protocol/BOLT7/Codec.hs b/lib/Lightning/Protocol/BOLT7/Codec.hs
@@ -50,12 +50,11 @@ 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)
import GHC.Generics (Generic)
-import Lightning.Protocol.BOLT1 (unsafeTlvStream)
+import Lightning.Protocol.BOLT1 (TlvStream)
import qualified Lightning.Protocol.BOLT1.Prim as Prim
import qualified Lightning.Protocol.BOLT1.TLV as TLV
import Lightning.Protocol.BOLT7.Messages
@@ -135,9 +134,14 @@ decodeLenPrefixed bs = do
else Right (BS.splitAt n rest)
{-# INLINE decodeLenPrefixed #-}
--- | Encode with u16 length prefix.
-encodeLenPrefixed :: ByteString -> ByteString
-encodeLenPrefixed bs = Prim.encodeU16 (fromIntegral $ BS.length bs) <> bs
+-- | Encode with u16 length prefix, failing if the length does not fit
+-- in a u16.
+encodeLenPrefixed :: ByteString -> Either EncodeError ByteString
+encodeLenPrefixed bs
+ | len > 65535 = Left EncodeLengthOverflow
+ | otherwise = Right (Prim.encodeU16 (fromIntegral len) <> bs)
+ where
+ !len = BS.length bs
{-# INLINE encodeLenPrefixed #-}
-- | Decode fixed-length validated type.
@@ -150,6 +154,15 @@ decodeFixed len err mkVal bs = do
Just v -> Right (v, rest)
{-# INLINE decodeFixed #-}
+-- | Decode a TLV stream per BOLT #1, given the known types: a
+-- malformed stream or an unknown even type is an error, and unknown
+-- odd types are skipped.
+decodeTlvs :: [Word64] -> ByteString -> Either DecodeError TlvStream
+decodeTlvs known bs = case TLV.decodeTlvStreamWith (`elem` known) bs of
+ Left _ -> Left DecodeTlvError
+ Right t -> Right t
+{-# INLINE decodeTlvs #-}
+
-- Type-specific decoders ------------------------------------------------------
-- | Decode Signature (64 bytes).
@@ -202,6 +215,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
@@ -212,59 +237,57 @@ 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 --------------------------------------------------------
-- | Encode channel_announcement message.
-encodeChannelAnnouncement :: ChannelAnnouncement -> ByteString
-encodeChannelAnnouncement msg = mconcat
- [ unSignature (channelAnnNodeSig1 msg)
- , unSignature (channelAnnNodeSig2 msg)
- , unSignature (channelAnnBitcoinSig1 msg)
- , unSignature (channelAnnBitcoinSig2 msg)
- , encodeLenPrefixed (getFeatureBits (channelAnnFeatures msg))
- , unChainHash (channelAnnChainHash msg)
- , scidToBytes (channelAnnShortChanId msg)
- , getNodeId (channelAnnNodeId1 msg)
- , getNodeId (channelAnnNodeId2 msg)
- , unPoint (channelAnnBitcoinKey1 msg)
- , unPoint (channelAnnBitcoinKey2 msg)
- ]
+--
+-- Fails with 'EncodeLengthOverflow' if the feature bits exceed 65535
+-- bytes.
+encodeChannelAnnouncement :: ChannelAnnouncement
+ -> Either EncodeError ByteString
+encodeChannelAnnouncement msg = do
+ features <- encodeLenPrefixed (getFeatureBits (channelAnnFeatures msg))
+ Right $ mconcat
+ [ unSignature (channelAnnNodeSig1 msg)
+ , unSignature (channelAnnNodeSig2 msg)
+ , unSignature (channelAnnBitcoinSig1 msg)
+ , unSignature (channelAnnBitcoinSig2 msg)
+ , features
+ , unChainHash (channelAnnChainHash msg)
+ , scidToBytes (channelAnnShortChanId msg)
+ , getNodeId (channelAnnNodeId1 msg)
+ , getNodeId (channelAnnNodeId2 msg)
+ , unPoint (channelAnnBitcoinKey1 msg)
+ , unPoint (channelAnnBitcoinKey2 msg)
+ ]
-- | Decode channel_announcement message.
decodeChannelAnnouncement :: ByteString
@@ -299,21 +322,22 @@ decodeChannelAnnouncement bs = do
-- Node announcement -----------------------------------------------------------
-- | Encode node_announcement message.
+--
+-- Fails with 'EncodeLengthOverflow' if the feature bits or the
+-- encoded addresses exceed 65535 bytes.
encodeNodeAnnouncement :: NodeAnnouncement -> Either EncodeError ByteString
encodeNodeAnnouncement msg = do
- addrData <- encodeAddresses (nodeAnnAddresses msg)
- let features = getFeatureBits (nodeAnnFeatures msg)
- if BS.length features > 65535
- then Left EncodeLengthOverflow
- else Right $ mconcat
- [ unSignature (nodeAnnSignature msg)
- , encodeLenPrefixed features
- , Prim.encodeU32 (nodeAnnTimestamp msg)
- , getNodeId (nodeAnnNodeId msg)
- , getRgbColor (nodeAnnRgbColor msg)
- , getAlias (nodeAnnAlias msg)
- , encodeLenPrefixed addrData
- ]
+ features <- encodeLenPrefixed (getFeatureBits (nodeAnnFeatures msg))
+ addrs <- encodeLenPrefixed =<< encodeAddresses (nodeAnnAddresses msg)
+ Right $ mconcat
+ [ unSignature (nodeAnnSignature msg)
+ , features
+ , Prim.encodeU32 (nodeAnnTimestamp msg)
+ , getNodeId (nodeAnnNodeId msg)
+ , getRgbColor (nodeAnnRgbColor msg)
+ , getAlias (nodeAnnAlias msg)
+ , addrs
+ ]
-- | Encode address list.
encodeAddresses :: [Address] -> Either EncodeError ByteString
@@ -370,39 +394,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 +430,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)
@@ -470,28 +483,30 @@ decodeAnnouncementSignatures bs = do
-- Query messages --------------------------------------------------------------
-- | Encode query_short_channel_ids message.
+--
+-- Fails with 'EncodeLengthOverflow' if the encoded short_channel_ids
+-- exceed 65535 bytes.
encodeQueryShortChannelIds :: QueryShortChannelIds
-> Either EncodeError ByteString
encodeQueryShortChannelIds msg = do
- let scidData = queryScidsData msg
- if BS.length scidData > 65535
- then Left EncodeLengthOverflow
- else Right $ mconcat
- [ unChainHash (queryScidsChainHash msg)
- , encodeLenPrefixed scidData
- , TLV.encodeTlvStream (queryScidsTlvs msg)
- ]
+ scidData <- encodeLenPrefixed (queryScidsData msg)
+ Right $ mconcat
+ [ unChainHash (queryScidsChainHash msg)
+ , scidData
+ , TLV.encodeTlvStream (queryScidsTlvs msg)
+ ]
-- | Decode query_short_channel_ids message.
+--
+-- The only known TLV type is 1 (query_flags). A malformed TLV
+-- stream or an unknown even type is 'DecodeTlvError'.
decodeQueryShortChannelIds :: ByteString
-> Either DecodeError
(QueryShortChannelIds, ByteString)
decodeQueryShortChannelIds bs = do
(chainH, bs1) <- decodeChainHash bs
(scidData, bs2) <- decodeLenPrefixed bs1
- let tlvs = case TLV.decodeTlvStreamRaw bs2 of
- Left _ -> unsafeTlvStream []
- Right t -> t
+ tlvs <- decodeTlvs [1] bs2
let msg = QueryShortChannelIds
{ queryScidsChainHash = chainH
, queryScidsData = scidData
@@ -531,6 +546,9 @@ encodeQueryChannelRange msg = mconcat
]
-- | Decode query_channel_range message.
+--
+-- The only known TLV type is 1 (query_option). A malformed TLV
+-- stream or an unknown even type is 'DecodeTlvError'.
decodeQueryChannelRange :: ByteString
-> Either DecodeError
(QueryChannelRange, ByteString)
@@ -538,9 +556,7 @@ decodeQueryChannelRange bs = do
(chainH, bs1) <- decodeChainHash bs
(firstBlock, bs2) <- decodeU32 bs1
(numBlocks, bs3) <- decodeU32 bs2
- let tlvs = case TLV.decodeTlvStreamRaw bs3 of
- Left _ -> unsafeTlvStream []
- Right t -> t
+ tlvs <- decodeTlvs [1] bs3
let msg = QueryChannelRange
{ queryRangeChainHash = chainH
, queryRangeFirstBlock = BlockHeight firstBlock
@@ -550,24 +566,29 @@ decodeQueryChannelRange bs = do
Right (msg, BS.empty)
-- | Encode reply_channel_range message.
+--
+-- Fails with 'EncodeLengthOverflow' if the encoded short_channel_ids
+-- exceed 65535 bytes.
encodeReplyChannelRange :: ReplyChannelRange
-> Either EncodeError ByteString
encodeReplyChannelRange msg = do
- let rangeData = replyRangeData msg
- if BS.length rangeData > 65535
- then Left EncodeLengthOverflow
- else Right $ mconcat
- [ unChainHash (replyRangeChainHash msg)
- , Prim.encodeU32
- (getBlockHeight (replyRangeFirstBlock msg))
- , Prim.encodeU32
- (getBlockCount (replyRangeNumBlocks msg))
- , BS.singleton (replyRangeSyncComplete msg)
- , encodeLenPrefixed rangeData
- , TLV.encodeTlvStream (replyRangeTlvs msg)
- ]
+ rangeData <- encodeLenPrefixed (replyRangeData msg)
+ Right $ mconcat
+ [ unChainHash (replyRangeChainHash msg)
+ , Prim.encodeU32
+ (getBlockHeight (replyRangeFirstBlock msg))
+ , Prim.encodeU32
+ (getBlockCount (replyRangeNumBlocks msg))
+ , BS.singleton (replyRangeSyncComplete msg)
+ , rangeData
+ , TLV.encodeTlvStream (replyRangeTlvs msg)
+ ]
-- | Decode reply_channel_range message.
+--
+-- The known TLV types are 1 (timestamps_tlv) and 3
+-- (checksums_tlv). A malformed TLV stream or an unknown even type
+-- is 'DecodeTlvError'.
decodeReplyChannelRange :: ByteString
-> Either DecodeError
(ReplyChannelRange, ByteString)
@@ -577,9 +598,7 @@ decodeReplyChannelRange bs = do
(numBlocks, bs3) <- decodeU32 bs2
(syncComplete, bs4) <- decodeU8 bs3
(rangeData, bs5) <- decodeLenPrefixed bs4
- let tlvs = case TLV.decodeTlvStreamRaw bs5 of
- Left _ -> unsafeTlvStream []
- Right t -> t
+ tlvs <- decodeTlvs [1, 3] bs5
let msg = ReplyChannelRange
{ replyRangeChainHash = chainH
, replyRangeFirstBlock = BlockHeight firstBlock
diff --git a/lib/Lightning/Protocol/BOLT7/Hash.hs b/lib/Lightning/Protocol/BOLT7/Hash.hs
@@ -13,6 +13,12 @@
-- These functions compute the double-SHA256 hash that is signed in each
-- message type. The hash covers the message content excluding the
-- signature field(s).
+--
+-- Each function takes the message payload as received: the bytes after
+-- the 2-byte message type. The signatures cover every byte of that
+-- payload, so hash the received bytes rather than a re-encoding of a
+-- decoded message. Each function returns 'Nothing' if the input is too
+-- short to hold the message.
module Lightning.Protocol.BOLT7.Hash (
-- * Signature hashes
@@ -28,57 +34,89 @@ import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Word (Word32)
import qualified Crypto.Hash.SHA256 as SHA256
+import qualified Lightning.Protocol.BOLT1.Prim as Prim
import Lightning.Protocol.BOLT7.CRC32C (crc32c)
-import Lightning.Protocol.BOLT7.Types (signatureLen, chainHashLen)
+import Lightning.Protocol.BOLT7.Types
+ ( signatureLen, chainHashLen, shortChannelIdLen, pointLen, nodeIdLen
+ , rgbColorLen, aliasLen )
-- | Double SHA-256 hash (used for Lightning message signing).
doubleSha256 :: ByteString -> ByteString
doubleSha256 = SHA256.hash . SHA256.hash
{-# INLINE doubleSha256 #-}
+-- | Read the big-endian u16 at an offset, if present.
+u16At :: Int -> ByteString -> Maybe Int
+u16At off bs = fmap (fromIntegral . fst) (Prim.decodeU16 (BS.drop off bs))
+{-# INLINE u16At #-}
+
+-- | Length of a channel_update without extension bytes (136).
+channelUpdateLen :: Int
+channelUpdateLen = signatureLen + chainHashLen + shortChannelIdLen + 32
+{-# INLINE channelUpdateLen #-}
+
-- | Compute signature hash for channel_announcement.
--
-- The hash covers the message starting at byte offset 256, which is after
-- the four 64-byte signatures (node_sig_1, node_sig_2, bitcoin_sig_1,
-- bitcoin_sig_2).
--
--- Returns the double-SHA256 hash (32 bytes).
-channelAnnouncementHash :: ByteString -> ByteString
-channelAnnouncementHash !msg =
- let offset = 4 * signatureLen
- payload = BS.drop offset msg
- in doubleSha256 payload
+-- Returns the double-SHA256 hash (32 bytes), or 'Nothing' if the input
+-- cannot hold the fields of a channel_announcement with its declared
+-- feature length.
+channelAnnouncementHash :: ByteString -> Maybe ByteString
+channelAnnouncementHash !msg = do
+ let !offset = 4 * signatureLen
+ flen <- u16At offset msg
+ -- features, chain_hash, short_channel_id, node_ids, bitcoin_keys
+ let !minLen = offset + 2 + flen + chainHashLen + shortChannelIdLen
+ + 2 * nodeIdLen + 2 * pointLen
+ if BS.length msg < minLen
+ then Nothing
+ else Just (doubleSha256 (BS.drop offset msg))
{-# INLINE channelAnnouncementHash #-}
-- | Compute signature hash for node_announcement.
--
-- The hash covers the message starting after the signature field (64 bytes).
--
--- Returns the double-SHA256 hash (32 bytes).
-nodeAnnouncementHash :: ByteString -> ByteString
-nodeAnnouncementHash !msg =
- let payload = BS.drop signatureLen msg
- in doubleSha256 payload
+-- Returns the double-SHA256 hash (32 bytes), or 'Nothing' if the input
+-- cannot hold the fields of a node_announcement with its declared
+-- feature and address lengths.
+nodeAnnouncementHash :: ByteString -> Maybe ByteString
+nodeAnnouncementHash !msg = do
+ flen <- u16At signatureLen msg
+ -- features, timestamp, node_id, rgb_color, alias
+ let !addrOff = signatureLen + 2 + flen + 4 + nodeIdLen + rgbColorLen
+ + aliasLen
+ addrlen <- u16At addrOff msg
+ if BS.length msg < addrOff + 2 + addrlen
+ then Nothing
+ else Just (doubleSha256 (BS.drop signatureLen msg))
{-# INLINE nodeAnnouncementHash #-}
-- | Compute signature hash for channel_update.
--
-- The hash covers the message starting after the signature field (64 bytes).
--
--- Returns the double-SHA256 hash (32 bytes).
-channelUpdateHash :: ByteString -> ByteString
-channelUpdateHash !msg =
- let payload = BS.drop signatureLen msg
- in doubleSha256 payload
+-- Returns the double-SHA256 hash (32 bytes), or 'Nothing' if the input
+-- is shorter than a channel_update (136 bytes).
+channelUpdateHash :: ByteString -> Maybe ByteString
+channelUpdateHash !msg
+ | BS.length msg < channelUpdateLen = Nothing
+ | otherwise = Just (doubleSha256 (BS.drop signatureLen msg))
{-# INLINE channelUpdateHash #-}
-- | Compute checksum for channel_update.
--
-- This is the CRC-32C of the channel_update message excluding the
--- signature field (bytes 0-63) and timestamp field (bytes 96-99).
+-- signature field (bytes 0-63) and timestamp field (bytes 104-107).
--
-- The checksum is used in the checksums_tlv of reply_channel_range.
--
+-- Returns 'Nothing' if the input is shorter than a channel_update
+-- (136 bytes).
+--
-- Message layout after signature:
-- - chain_hash: 32 bytes (offset 64-95)
-- - short_channel_id: 8 bytes (offset 96-103)
@@ -89,18 +127,20 @@ channelUpdateHash !msg =
-- - htlc_minimum_msat: 8 bytes (offset 112-119)
-- - fee_base_msat: 4 bytes (offset 120-123)
-- - fee_proportional_millionths: 4 bytes (offset 124-127)
--- - htlc_maximum_msat: 8 bytes (offset 128-135, if present)
-channelUpdateChecksum :: ByteString -> Word32
-channelUpdateChecksum !msg =
- let -- Offset 64: chain_hash (32 bytes)
- chainHash = BS.take chainHashLen (BS.drop signatureLen msg)
- -- Offset 96: short_channel_id (8 bytes)
- scid = BS.take 8 (BS.drop (signatureLen + chainHashLen) msg)
- -- Skip timestamp (4 bytes at offset 104)
- -- Offset 108 to end: rest of message
- restOffset = signatureLen + chainHashLen + 8 + 4 -- 64 + 32 + 8 + 4 = 108
- rest = BS.drop restOffset msg
- -- Concatenate: chain_hash + scid + (message without sig and timestamp)
- checksumData = BS.concat [chainHash, scid, rest]
- in crc32c checksumData
+-- - htlc_maximum_msat: 8 bytes (offset 128-135)
+channelUpdateChecksum :: ByteString -> Maybe Word32
+channelUpdateChecksum !msg
+ | BS.length msg < channelUpdateLen = Nothing
+ | otherwise = Just (crc32c checksumData)
+ where
+ -- Offset 64: chain_hash (32 bytes)
+ chainHash = BS.take chainHashLen (BS.drop signatureLen msg)
+ -- Offset 96: short_channel_id (8 bytes)
+ scid = BS.take 8 (BS.drop (signatureLen + chainHashLen) msg)
+ -- Skip timestamp (4 bytes at offset 104)
+ -- Offset 108 to end: rest of message
+ restOffset = signatureLen + chainHashLen + 8 + 4 -- 64 + 32 + 8 + 4 = 108
+ rest = BS.drop restOffset msg
+ -- Concatenate: chain_hash + scid + (message without sig and timestamp)
+ checksumData = BS.concat [chainHash, scid, rest]
{-# INLINE channelUpdateChecksum #-}
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/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,8 @@ 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
+ | ValidateScidEncoding -- ^ encoded_short_ids cannot be decoded
deriving (Eq, Show, Generic)
instance NFData ValidationError
@@ -68,32 +70,31 @@ 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.
--
-- 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.
--
@@ -117,11 +118,14 @@ validateQueryChannelRange msg = do
--
-- Checks:
--
+-- * Encoded short_channel_ids decode: the encoding type is 0
+-- (uncompressed; type 1, zlib, MUST NOT be used) and the data is
+-- a whole number of short_channel_ids
-- * Encoded short_channel_ids are in ascending order
validateReplyChannelRange :: ReplyChannelRange -> Either ValidationError ()
validateReplyChannelRange msg =
case decodeShortChannelIdList (replyRangeData msg) of
- Left _ -> Right () -- Can't decode, skip validation
+ Left _ -> Left ValidateScidEncoding
Right scids -> checkAscending scids
where
checkAscending [] = Right ()
diff --git a/ppad-bolt7.cabal b/ppad-bolt7.cabal
@@ -53,6 +53,7 @@ test-suite bolt7-tests
, ppad-base16
, ppad-bolt1 >= 0.0.1 && < 0.1
, ppad-bolt7
+ , ppad-secp256k1 >= 0.5.5 && < 0.6
, tasty
, tasty-hunit
, tasty-quickcheck
diff --git a/test/Main.hs b/test/Main.hs
@@ -2,10 +2,13 @@
module Main where
+import qualified Crypto.Curve.Secp256k1 as Secp256k1
import qualified Data.ByteString as BS
+import qualified Data.ByteString.Base16 as B16
import Data.Maybe (fromJust)
-import Data.Word (Word16, Word32)
-import Lightning.Protocol.BOLT1 (TlvStream, unsafeTlvStream)
+import Data.Word (Word8, Word16, Word32)
+import Lightning.Protocol.BOLT1
+ (TlvRecord(..), TlvStream, unTlvStream, unsafeTlvStream)
import Lightning.Protocol.BOLT7
import Lightning.Protocol.BOLT7.CRC32C (crc32c)
import Test.Tasty
@@ -24,6 +27,7 @@ main = defaultMain $ testGroup "ppad-bolt7" [
, hash_tests
, validation_tests
, error_tests
+ , mainnet_tests
, property_tests
]
@@ -79,6 +83,60 @@ mkScid b t o = case shortChannelId b t o of
Just s -> s
Nothing -> error "mkScid: invalid test fixture"
+-- | A channel_update fixture.
+testChannelUpdate :: ChannelUpdate
+testChannelUpdate = ChannelUpdate
+ { chanUpdateSignature = testSignature
+ , chanUpdateChainHash = testChainHash
+ , chanUpdateShortChanId = testShortChannelId
+ , chanUpdateTimestamp = 1234567890
+ , chanUpdateChanFlags = ChannelFlags
+ { cfDirection = NodeOne, cfStatus = Enabled }
+ , chanUpdateCltvExpDelta = CltvExpiryDelta 144
+ , chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
+ , chanUpdateFeeBaseMsat = FeeBaseMsat 1000
+ , chanUpdateFeeProportional = FeeProportionalMillionths 100
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
+ }
+
+-- | Unwrap a Maybe, failing the test on Nothing.
+expectJust :: String -> Maybe a -> IO a
+expectJust what = maybe (assertFailure ("unexpected Nothing: " ++ what)) pure
+
+-- | A node_announcement fixture with the given addresses.
+testNodeAnn :: [Address] -> NodeAnnouncement
+testNodeAnn addrs = NodeAnnouncement
+ { nodeAnnSignature = testSignature
+ , nodeAnnFeatures = emptyFeatures
+ , nodeAnnTimestamp = 1234567890
+ , nodeAnnNodeId = testNodeId
+ , nodeAnnRgbColor = testRgbColor
+ , nodeAnnAlias = testAlias
+ , nodeAnnAddresses = addrs
+ }
+
+-- | A channel_announcement fixture with the given features.
+testChannelAnn :: FeatureBits -> ChannelAnnouncement
+testChannelAnn feats = ChannelAnnouncement
+ { channelAnnNodeSig1 = testSignature
+ , channelAnnNodeSig2 = testSignature
+ , channelAnnBitcoinSig1 = testSignature
+ , channelAnnBitcoinSig2 = testSignature
+ , channelAnnFeatures = feats
+ , channelAnnChainHash = testChainHash
+ , channelAnnShortChanId = testShortChannelId
+ , channelAnnNodeId1 = testNodeId2
+ , channelAnnNodeId2 = testNodeId
+ , channelAnnBitcoinKey1 = testPoint
+ , channelAnnBitcoinKey2 = testPoint
+ }
+
+-- | Replace the byte at an offset (no-op past the end).
+setByte :: Int -> Word8 -> BS.ByteString -> BS.ByteString
+setByte i w bs
+ | i < 0 || i >= BS.length bs = bs
+ | otherwise = BS.take i bs <> BS.singleton w <> BS.drop (i + 1) bs
+
-- Type Tests ------------------------------------------------------------------
type_tests :: TestTree
@@ -154,10 +212,11 @@ channel_announcement_tests = testGroup "ChannelAnnouncement" [
, channelAnnBitcoinKey1 = testPoint
, channelAnnBitcoinKey2 = testPoint
}
- encoded = encodeChannelAnnouncement msg
- case decodeChannelAnnouncement encoded of
- Right (decoded, _) -> decoded @?= msg
- Left e -> assertFailure $ "decode failed: " ++ show e
+ case encodeChannelAnnouncement msg of
+ Left e -> assertFailure $ "encode failed: " ++ show e
+ Right encoded -> case decodeChannelAnnouncement encoded of
+ Right (decoded, _) -> decoded @?= msg
+ Left e -> assertFailure $ "decode failed: " ++ show e
]
-- Node Announcement Tests -----------------------------------------------------
@@ -195,13 +254,85 @@ node_announcement_tests = testGroup "NodeAnnouncement" [
Right encoded -> case decodeNodeAnnouncement encoded of
Right (decoded, _) -> decoded @?= msg
Left e -> assertFailure $ "decode failed: " ++ show e
+ , testGroup "address descriptors" [
+ testCase "skips deprecated Tor v2 descriptors" $ do
+ v4 <- addrV4
+ v6 <- addrV6
+ decodeAddrs (descV4 <> descTorV2 <> descV6) @?= Right [v4, v6]
+ , testCase "stops at the first unknown type" $ do
+ v4 <- addrV4
+ let wire = descV4 <> BS.pack [6, 1, 2, 3] <> descV6
+ decodeAddrs wire @?= Right [v4]
+ -- the ignored bytes are still consumed as part of addrlen
+ fmap snd (decodeNodeAnnouncement (nodeAnnWire wire))
+ @?= Right BS.empty
+ , testCase "stops at a type-0 descriptor" $
+ decodeAddrs (BS.singleton 0 <> descV4) @?= Right []
+ , testCase "skips descriptors with port 0" $ do
+ v6 <- addrV6
+ let v4port0 = BS.pack [1, 127, 0, 0, 1, 0, 0]
+ decodeAddrs (v4port0 <> descV6) @?= Right [v6]
+ , testCase "skips empty and non-ASCII hostnames" $ do
+ h <- expectJust "hostname" (hostname "ln.example.com")
+ let wire = descDns "" <> descDns "\xff\&ab"
+ <> descDns "ln.example.com"
+ decodeAddrs wire @?= Right [AddrDNS h 9735]
+ , testCase "rejects a truncated descriptor of a known type" $ do
+ decodeAddrs (BS.pack [1, 127, 0, 0])
+ @?= Left DecodeInsufficientBytes
+ decodeAddrs (BS.pack [3, 1, 2, 3])
+ @?= Left DecodeInsufficientBytes
+ decodeAddrs (BS.pack [5, 4, 0x61])
+ @?= Left DecodeInsufficientBytes
+ , testCase "hostname rejects non-ASCII bytes" $ do
+ hostname "\0\255" @?= Nothing
+ hostname "caf\233" @?= Nothing
+ fmap getHostname (hostname "xn--caf-dma") @?= Just "xn--caf-dma"
+ ]
+ ]
+
+-- | node_announcement payload carrying the given address bytes.
+nodeAnnWire :: BS.ByteString -> BS.ByteString
+nodeAnnWire addrs = mconcat
+ [ BS.replicate 64 0xee -- signature
+ , BS.pack [0, 0] -- flen
+ , BS.pack [0x49, 0x96, 0x02, 0xd2] -- timestamp
+ , getNodeId testNodeId
+ , getRgbColor testRgbColor
+ , getAlias testAlias
+ , u16be (BS.length addrs)
+ , addrs
]
+-- | Decode the addresses of a node_announcement built by 'nodeAnnWire'.
+decodeAddrs :: BS.ByteString -> Either DecodeError [Address]
+decodeAddrs = fmap (nodeAnnAddresses . fst) . decodeNodeAnnouncement
+ . nodeAnnWire
+
+-- | Big-endian u16 (low 16 bits).
+u16be :: Int -> BS.ByteString
+u16be n = BS.pack [fromIntegral (n `div` 256), fromIntegral n]
+
+-- | Address descriptors (port 9735) and the values they decode to.
+descV4, descV6, descTorV2 :: BS.ByteString
+descV4 = BS.pack [1, 127, 0, 0, 1, 0x26, 0x07]
+descV6 = BS.pack (2 : replicate 15 0 ++ [1, 0x26, 0x07])
+descTorV2 = BS.cons 3 (BS.replicate 12 0xaa)
+
+descDns :: BS.ByteString -> BS.ByteString
+descDns h = BS.pack [5, fromIntegral (BS.length h)] <> h <> u16be 9735
+
+addrV4, addrV6 :: IO Address
+addrV4 = fmap (`AddrIPv4` 9735)
+ (expectJust "ipv4" (ipv4Addr (BS.pack [127, 0, 0, 1])))
+addrV6 = fmap (`AddrIPv6` 9735)
+ (expectJust "ipv6" (ipv6Addr (BS.pack (replicate 15 0 ++ [1]))))
+
-- Channel Update Tests --------------------------------------------------------
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 +344,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,12 +362,28 @@ 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
Right (decoded, _) -> decoded @?= msg
Left e -> assertFailure $ "decode failed: " ++ show e
+ , testCase "encoder sets must_be_one and emits htlc_maximum_msat" $ do
+ let encoded = encodeChannelUpdate testChannelUpdate
+ BS.length encoded @?= 136
+ BS.take 1 (BS.drop 108 encoded) @?= BS.singleton 0x01
+ BS.drop 128 encoded @?= BS.pack [0, 0, 0, 0, 0x3b, 0x9a, 0xca, 0]
+ , testCase "decodes htlc_maximum_msat when must_be_one is clear" $ do
+ -- receivers ignore must_be_one; the field is always present
+ let wire = setByte 108 0x00 (encodeChannelUpdate testChannelUpdate)
+ case decodeChannelUpdate wire of
+ Right (decoded, rest) -> do
+ decoded @?= testChannelUpdate
+ rest @?= BS.empty
+ Left e -> assertFailure $ "decode failed: " ++ show e
+ , testCase "rejects update without htlc_maximum_msat" $ do
+ let wire = BS.take 128 (encodeChannelUpdate testChannelUpdate)
+ decodeChannelUpdate wire @?= Left DecodeInsufficientBytes
]
-- Announcement Signatures Tests -----------------------------------------------
@@ -335,6 +482,58 @@ query_tests = testGroup "Query Messages" [
Right (decoded, _) -> decoded @?= msg
Left e -> assertFailure $ "decode failed: " ++ show e
]
+ , tlvStreamTests "query_short_channel_ids TLVs" [1] $ \tlvs ->
+ fmap (unTlvStream . queryScidsTlvs . fst)
+ (decodeQueryShortChannelIds (mconcat
+ [ unChainHash testChainHash
+ , u16be 9
+ , encodeShortChannelIdList [testShortChannelId]
+ , tlvs
+ ]))
+ , tlvStreamTests "query_channel_range TLVs" [1] $ \tlvs ->
+ fmap (unTlvStream . queryRangeTlvs . fst)
+ (decodeQueryChannelRange (mconcat
+ [ unChainHash testChainHash
+ , BS.pack [0, 9, 0x27, 0xc0] -- first_blocknum 600000
+ , BS.pack [0, 0, 0x27, 0x10] -- number_of_blocks 10000
+ , tlvs
+ ]))
+ , tlvStreamTests "reply_channel_range TLVs" [1, 3] $ \tlvs ->
+ fmap (unTlvStream . replyRangeTlvs . fst)
+ (decodeReplyChannelRange (mconcat
+ [ unChainHash testChainHash
+ , BS.pack [0, 9, 0x27, 0xc0] -- first_blocknum 600000
+ , BS.pack [0, 0, 0x27, 0x10] -- number_of_blocks 10000
+ , BS.singleton 1 -- sync_complete
+ , u16be 9
+ , encodeShortChannelIdList [testShortChannelId]
+ , tlvs
+ ]))
+ ]
+
+-- | TLV stream tests for a query decoder, given its known TLV types
+-- and a function decoding a message that ends in the given TLV bytes.
+tlvStreamTests
+ :: String
+ -> [Word8]
+ -> (BS.ByteString -> Either DecodeError [TlvRecord])
+ -> TestTree
+tlvStreamTests name known dec = testGroup name [
+ testCase "empty stream" $
+ dec BS.empty @?= Right []
+ , testCase "keeps known types" $
+ dec (BS.pack (concatMap (\t -> [t, 1, 0x2a]) known))
+ @?= Right [TlvRecord (fromIntegral t) "\x2a" | t <- known]
+ , testCase "skips unknown odd types" $
+ dec (BS.pack [5, 0]) @?= Right []
+ , testCase "rejects unknown even types" $
+ dec (BS.pack [2, 0]) @?= Left DecodeTlvError
+ , testCase "rejects a length past the end" $
+ dec (BS.pack [1, 5, 0xaa]) @?= Left DecodeTlvError
+ , testCase "rejects repeated types" $
+ dec (BS.pack [1, 0, 1, 0]) @?= Left DecodeTlvError
+ , testCase "rejects a non-minimal BigSize" $
+ dec (BS.pack [0xfd, 0x00, 0x01, 0x00]) @?= Left DecodeTlvError
]
-- SCID List Tests ------------------------------------------------------------
@@ -390,7 +589,7 @@ hash_tests = testGroup "Hash Functions" [
, testGroup "Signature Hashes" [
testCase "channelAnnouncementHash produces 32 bytes" $ do
-- Create a minimal valid encoded message
- let msg = encodeChannelAnnouncement ChannelAnnouncement
+ case encodeChannelAnnouncement ChannelAnnouncement
{ channelAnnNodeSig1 = testSignature
, channelAnnNodeSig2 = testSignature
, channelAnnBitcoinSig1 = testSignature
@@ -402,9 +601,11 @@ hash_tests = testGroup "Hash Functions" [
, channelAnnNodeId2 = testNodeId2
, channelAnnBitcoinKey1 = testPoint
, channelAnnBitcoinKey2 = testPoint
- }
- hashVal = channelAnnouncementHash msg
- BS.length hashVal @?= 32
+ } of
+ Left e -> assertFailure $ "encode failed: " ++ show e
+ Right msg -> do
+ let hashVal = channelAnnouncementHash msg
+ fmap BS.length hashVal @?= Just 32
, testCase "nodeAnnouncementHash produces 32 bytes" $ do
case encodeNodeAnnouncement NodeAnnouncement
{ nodeAnnSignature = testSignature
@@ -418,7 +619,7 @@ hash_tests = testGroup "Hash Functions" [
Left e -> assertFailure $ "encode failed: " ++ show e
Right msg -> do
let hashVal = nodeAnnouncementHash msg
- BS.length hashVal @?= 32
+ fmap BS.length hashVal @?= Just 32
, testCase "channelUpdateHash produces 32 bytes" $ do
let msg = encodeChannelUpdate ChannelUpdate
{ chanUpdateSignature = testSignature
@@ -432,10 +633,10 @@ 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
+ fmap BS.length hashVal @?= Just 32
]
, testGroup "Checksum" [
testCase "channelUpdateChecksum produces consistent result" $ do
@@ -452,7 +653,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 +672,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,10 +686,41 @@ hash_tests = testGroup "Hash Functions" [
, chanUpdateHtlcMinMsat = HtlcMinimumMsat 1000
, chanUpdateFeeBaseMsat = FeeBaseMsat 1000
, chanUpdateFeeProportional = FeeProportionalMillionths 100
- , chanUpdateHtlcMaxMsat = Nothing
+ , chanUpdateHtlcMaxMsat = HtlcMaximumMsat 1000000000
}
channelUpdateChecksum msg1 @?= channelUpdateChecksum msg2
]
+ , testGroup "Input length" [
+ testCase "channel_update needs 136 bytes" $ do
+ let wire = encodeChannelUpdate testChannelUpdate
+ channelUpdateHash (BS.take 135 wire) @?= Nothing
+ channelUpdateChecksum (BS.take 135 wire) @?= Nothing
+ channelUpdateChecksum "short" @?= Nothing
+ fmap BS.length (channelUpdateHash wire) @?= Just 32
+ assertBool "checksum" (channelUpdateChecksum wire /= Nothing)
+ , testCase "channel_announcement needs its declared features" $
+ case encodeChannelAnnouncement
+ (testChannelAnn (featureBits (BS.pack [1, 2]))) of
+ Left e -> assertFailure $ "encode failed: " ++ show e
+ Right wire -> do
+ fmap BS.length (channelAnnouncementHash wire) @?= Just 32
+ channelAnnouncementHash (BS.take (BS.length wire - 1) wire)
+ @?= Nothing
+ -- claim one more feature byte than present
+ channelAnnouncementHash (setByte 257 3 wire) @?= Nothing
+ channelAnnouncementHash (BS.take 257 wire) @?= Nothing
+ , testCase "node_announcement needs its declared addresses" $ do
+ v4 <- addrV4
+ case encodeNodeAnnouncement (testNodeAnn [v4]) of
+ Left e -> assertFailure $ "encode failed: " ++ show e
+ Right wire -> do
+ fmap BS.length (nodeAnnouncementHash wire) @?= Just 32
+ nodeAnnouncementHash (BS.take (BS.length wire - 1) wire)
+ @?= Nothing
+ -- claim one more address byte than present (addrlen 8)
+ nodeAnnouncementHash (setByte 139 8 wire) @?= Nothing
+ nodeAnnouncementHash (BS.take 139 wire) @?= Nothing
+ ]
]
-- Validation Tests -----------------------------------------------------------
@@ -527,6 +759,19 @@ validation_tests = testGroup "Validation" [
}
validateChannelAnnouncement msg @?= Left ValidateNodeIdOrdering
]
+ , testGroup "NodeAnnouncement" [
+ testCase "one DNS hostname passes" $ do
+ h <- expectJust "hostname" (hostname "ln.example.com")
+ v4 <- addrV4
+ validateNodeAnnouncement (testNodeAnn [v4, AddrDNS h 9735])
+ @?= Right ()
+ , testCase "rejects more than one DNS hostname" $ do
+ h1 <- expectJust "hostname" (hostname "a.example.com")
+ h2 <- expectJust "hostname" (hostname "b.example.com")
+ validateNodeAnnouncement
+ (testNodeAnn [AddrDNS h1 9735, AddrDNS h2 9735])
+ @?= Left ValidateMultipleDns
+ ]
, testGroup "ChannelUpdate" [
testCase "valid update passes" $ do
let msg = ChannelUpdate
@@ -541,7 +786,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 +802,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
]
@@ -579,8 +824,41 @@ validation_tests = testGroup "Validation" [
}
validateQueryChannelRange msg @?= Left ValidateBlockOverflow
]
+ , testGroup "ReplyChannelRange" [
+ testCase "ascending short_channel_ids pass" $
+ validateReplyChannelRange (testReplyRange
+ (encodeShortChannelIdList [mkScid 1 0 0, mkScid 2 0 0]))
+ @?= Right ()
+ , testCase "rejects descending short_channel_ids" $
+ validateReplyChannelRange (testReplyRange
+ (encodeShortChannelIdList [mkScid 2 0 0, mkScid 1 0 0]))
+ @?= Left ValidateScidNotAscending
+ , testCase "rejects zlib encoding (type 1)" $
+ validateReplyChannelRange (testReplyRange (BS.cons 1
+ (scidToBytes (mkScid 2 0 0) <> scidToBytes (mkScid 1 0 0))))
+ @?= Left ValidateScidEncoding
+ , testCase "rejects an unknown encoding type" $
+ validateReplyChannelRange (testReplyRange (BS.cons 2
+ (scidToBytes (mkScid 1 0 0))))
+ @?= Left ValidateScidEncoding
+ , testCase "rejects a partial short_channel_id" $
+ validateReplyChannelRange (testReplyRange (BS.cons 0
+ (scidToBytes (mkScid 1 0 0) <> BS.replicate 7 0)))
+ @?= Left ValidateScidEncoding
+ ]
]
+-- | A reply_channel_range fixture with the given encoded_short_ids.
+testReplyRange :: BS.ByteString -> ReplyChannelRange
+testReplyRange scids = ReplyChannelRange
+ { replyRangeChainHash = testChainHash
+ , replyRangeFirstBlock = BlockHeight 0
+ , replyRangeNumBlocks = BlockCount 10
+ , replyRangeSyncComplete = 1
+ , replyRangeData = scids
+ , replyRangeTlvs = emptyTlvs
+ }
+
-- Error Tests -----------------------------------------------------------------
error_tests :: TestTree
@@ -603,6 +881,223 @@ error_tests = testGroup "Error Conditions" [
Left DecodeInsufficientBytes -> pure ()
other -> assertFailure $ "expected insufficient: " ++ show other
]
+ , testGroup "Length overflow" [
+ testCase "channel_announcement features" $ do
+ let feats n = featureBits (BS.replicate n 0x01)
+ encodeChannelAnnouncement (testChannelAnn (feats 65536))
+ @?= Left EncodeLengthOverflow
+ case encodeChannelAnnouncement (testChannelAnn (feats 65535)) of
+ Left e -> assertFailure $ "encode failed: " ++ show e
+ Right wire -> fmap fst (decodeChannelAnnouncement wire)
+ @?= Right (testChannelAnn (feats 65535))
+ , testCase "node_announcement features" $ do
+ let msg = (testNodeAnn [])
+ { nodeAnnFeatures = featureBits (BS.replicate 65536 0x01) }
+ encodeNodeAnnouncement msg @?= Left EncodeLengthOverflow
+ , testCase "node_announcement addresses" $ do
+ v4 <- addrV4
+ -- 10923 IPv4 descriptors take 65538 bytes
+ encodeNodeAnnouncement (testNodeAnn (replicate 10923 v4))
+ @?= Left EncodeLengthOverflow
+ case encodeNodeAnnouncement (testNodeAnn (replicate 9362 v4)) of
+ Left e -> assertFailure $ "encode failed: " ++ show e
+ Right wire -> fmap (length . nodeAnnAddresses . fst)
+ (decodeNodeAnnouncement wire) @?= Right 9362
+ , testCase "query_short_channel_ids encoded_short_ids" $ do
+ let msg = QueryShortChannelIds
+ { queryScidsChainHash = testChainHash
+ , queryScidsData = BS.replicate 65536 0x00
+ , queryScidsTlvs = emptyTlvs
+ }
+ encodeQueryShortChannelIds msg @?= Left EncodeLengthOverflow
+ , testCase "reply_channel_range encoded_short_ids" $ do
+ let msg = ReplyChannelRange
+ { replyRangeChainHash = testChainHash
+ , replyRangeFirstBlock = BlockHeight 600000
+ , replyRangeNumBlocks = BlockCount 10000
+ , replyRangeSyncComplete = 1
+ , replyRangeData = BS.replicate 65536 0x00
+ , replyRangeTlvs = emptyTlvs
+ }
+ encodeReplyChannelRange msg @?= Left EncodeLengthOverflow
+ ]
+ ]
+
+-- Mainnet Gossip Tests --------------------------------------------------------
+
+-- Real mainnet gossip for short_channel_id 850052x2992x1: its
+-- channel_announcement and one channel_update per direction. Payloads
+-- exclude the 2-byte message type. The digests and checksums were
+-- computed independently of this library.
+
+mainnet_tests :: TestTree
+mainnet_tests = testGroup "Mainnet gossip" [
+ testCase "fixtures decode from hex" $ do
+ BS.length mainnetChanAnn @?= 430
+ BS.length mainnetUpdate136 @?= 136
+ BS.length mainnetUpdate148 @?= 148
+ , testGroup "channel_announcement" [
+ testCase "decodes" $ do
+ (ann, rest) <- expectRight "decode"
+ (decodeChannelAnnouncement mainnetChanAnn)
+ rest @?= BS.empty
+ formatScid (channelAnnShortChanId ann) @?= "850052x2992x1"
+ channelAnnChainHash ann @?= mainnetChainHash
+ getFeatureBits (channelAnnFeatures ann) @?= BS.empty
+ getNodeId (channelAnnNodeId1 ann) @?= mainnetNodeId1
+ getNodeId (channelAnnNodeId2 ann) @?= mainnetNodeId2
+ validateChannelAnnouncement ann @?= Right ()
+ , testCase "re-encodes byte-exact" $ do
+ (ann, _) <- expectRight "decode"
+ (decodeChannelAnnouncement mainnetChanAnn)
+ encodeChannelAnnouncement ann @?= Right mainnetChanAnn
+ , testCase "signature hash" $
+ channelAnnouncementHash mainnetChanAnn @?= Just (hex
+ "3dc0e5a431551c44f2afd49cda2d3bf2a5f1b9cc86ef93db8953c17dd1ef4ded")
+ , testCase "all four signatures verify" $ do
+ (ann, _) <- expectRight "decode"
+ (decodeChannelAnnouncement mainnetChanAnn)
+ h <- expectJust "hash" (channelAnnouncementHash mainnetChanAnn)
+ let nid = getNodeId
+ btc = unPoint
+ assertBool "node_sig_1" $
+ verifySig h (nid (channelAnnNodeId1 ann)) (channelAnnNodeSig1 ann)
+ assertBool "node_sig_2" $
+ verifySig h (nid (channelAnnNodeId2 ann)) (channelAnnNodeSig2 ann)
+ assertBool "bitcoin_sig_1" $
+ verifySig h (btc (channelAnnBitcoinKey1 ann))
+ (channelAnnBitcoinSig1 ann)
+ assertBool "bitcoin_sig_2" $
+ verifySig h (btc (channelAnnBitcoinKey2 ann))
+ (channelAnnBitcoinSig2 ann)
+ assertBool "node_sig_1 does not verify under node_id_2" $
+ not (verifySig h (nid (channelAnnNodeId2 ann))
+ (channelAnnNodeSig1 ann))
+ ]
+ , testGroup "channel_update (136 bytes)" [
+ testCase "decodes" $ do
+ (upd, rest) <- expectRight "decode"
+ (decodeChannelUpdate mainnetUpdate136)
+ rest @?= BS.empty
+ formatScid (chanUpdateShortChanId upd) @?= "850052x2992x1"
+ chanUpdateChainHash upd @?= mainnetChainHash
+ chanUpdateTimestamp upd @?= 1776493010
+ chanUpdateChanFlags upd @?= ChannelFlags NodeTwo Disabled
+ chanUpdateCltvExpDelta upd @?= CltvExpiryDelta 80
+ chanUpdateHtlcMinMsat upd @?= HtlcMinimumMsat 1000
+ chanUpdateFeeBaseMsat upd @?= FeeBaseMsat 1000
+ chanUpdateFeeProportional upd @?= FeeProportionalMillionths 800
+ chanUpdateHtlcMaxMsat upd @?= HtlcMaximumMsat 2970000000
+ validateChannelUpdate upd @?= Right ()
+ , testCase "re-encodes byte-exact" $ do
+ (upd, _) <- expectRight "decode"
+ (decodeChannelUpdate mainnetUpdate136)
+ encodeChannelUpdate upd @?= mainnetUpdate136
+ , testCase "signature hash" $
+ channelUpdateHash mainnetUpdate136 @?= Just (hex
+ "5c9960b6fef9fa0d53073895420159e856c7c5f33fdeb694b163a832efe6b8e3")
+ , testCase "signature verifies under node_id_2" $ do
+ (upd, _) <- expectRight "decode"
+ (decodeChannelUpdate mainnetUpdate136)
+ h <- expectJust "hash" (channelUpdateHash mainnetUpdate136)
+ let sig = chanUpdateSignature upd
+ assertBool "node_id_2" (verifySig h mainnetNodeId2 sig)
+ assertBool "not node_id_1" (not (verifySig h mainnetNodeId1 sig))
+ , testCase "checksum" $
+ channelUpdateChecksum mainnetUpdate136 @?= Just 0x1da953bd
+ ]
+ , testGroup "channel_update (148 bytes, with extension)" [
+ testCase "decodes, leaving the extension as rest" $ do
+ (upd, rest) <- expectRight "decode"
+ (decodeChannelUpdate mainnetUpdate148)
+ -- an LND inbound-fee TLV (type 55555) after htlc_maximum_msat
+ rest @?= hex "fdd9030800000000ffffffed"
+ chanUpdateTimestamp upd @?= 1779047542
+ chanUpdateChanFlags upd @?= ChannelFlags NodeOne Enabled
+ chanUpdateCltvExpDelta upd @?= CltvExpiryDelta 100
+ chanUpdateHtlcMinMsat upd @?= HtlcMinimumMsat 1000
+ chanUpdateFeeBaseMsat upd @?= FeeBaseMsat 0
+ chanUpdateFeeProportional upd @?= FeeProportionalMillionths 5000
+ chanUpdateHtlcMaxMsat upd @?= HtlcMaximumMsat 2970000000
+ , testCase "re-encodes the fixed fields byte-exact" $ do
+ -- the record does not keep extension bytes
+ (upd, rest) <- expectRight "decode"
+ (decodeChannelUpdate mainnetUpdate148)
+ encodeChannelUpdate upd <> rest @?= mainnetUpdate148
+ , testCase "signature hash covers the extension" $
+ channelUpdateHash mainnetUpdate148 @?= Just (hex
+ "b1989933c26561ab2cdcc49b471aee5f6e0403a5eb7b88a908cb19cfee0556b9")
+ , testCase "signature verifies under node_id_1" $ do
+ (upd, _) <- expectRight "decode"
+ (decodeChannelUpdate mainnetUpdate148)
+ h <- expectJust "hash" (channelUpdateHash mainnetUpdate148)
+ let sig = chanUpdateSignature upd
+ assertBool "node_id_1" (verifySig h mainnetNodeId1 sig)
+ assertBool "not node_id_2" (not (verifySig h mainnetNodeId2 sig))
+ , testCase "checksum includes the extension" $
+ channelUpdateChecksum mainnetUpdate148 @?= Just 0xf0a89fda
+ ]
+ ]
+
+-- | Unwrap an Either, failing the test on Left.
+expectRight :: Show e => String -> Either e a -> IO a
+expectRight what =
+ either (\e -> assertFailure (what ++ " failed: " ++ show e)) pure
+
+-- | Decode hex (empty on malformed input; the fixture length test
+-- catches that).
+hex :: BS.ByteString -> BS.ByteString
+hex = maybe BS.empty id . B16.decode
+
+-- | Verify a compact ECDSA signature over a digest.
+verifySig :: BS.ByteString -> BS.ByteString -> Signature -> Bool
+verifySig digest pub sig =
+ case (Secp256k1.parse_point pub, Secp256k1.parse_sig (unSignature sig)) of
+ (Just p, Just s) -> Secp256k1._verify_ecdsa_no_hash digest p s
+ _ -> False
+
+mainnetNodeId1, mainnetNodeId2 :: BS.ByteString
+mainnetNodeId1 = hex
+ "02bb5e47ee26aca1654ce092d9d06eac15d953210c737f2220e2d7e54c4f108677"
+mainnetNodeId2 = hex
+ "030b828fd1e42549fbc2f1d0960ec3663362c41777d3d87fe1b8e1d4bf063abcaa"
+
+mainnetChanAnn :: BS.ByteString
+mainnetChanAnn = hex $ mconcat
+ [ "3c256c270126d2c52ab5464fa959d627e9baec8c76bd2b81d34b47c4efb03290"
+ , "40126ea7a0f8d31420976dc28ff684dc63f62620fa10dbac4fe3d417ca5690c1"
+ , "b7fe1de61f5dd2b8368886d6c98f625c9110198e57c14fac93d4eff34e19ce5f"
+ , "5dc6fef4501e8105c8f9b82d6d0b348f5840864445b071ae7a741b5eb92d2ec0"
+ , "ab85681e4e1e85144bbbd8757c14cee92468d2c5ac84345cb28931b6fed5e339"
+ , "00ed9dd363db630f31248cfa933a3667cbf87bb78a48a442e81c652686355365"
+ , "ff33164c7588f0a753d50c4d52dd4a81c611fa47b4e950dfc9e3379a12ef7834"
+ , "781c0900b90f56a5a49893446d03c8d283c3c093e3c8c682ff6cb40d70d9eebc"
+ , "00006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000"
+ , "00000cf884000bb0000102bb5e47ee26aca1654ce092d9d06eac15d953210c73"
+ , "7f2220e2d7e54c4f108677030b828fd1e42549fbc2f1d0960ec3663362c41777"
+ , "d3d87fe1b8e1d4bf063abcaa02bc3f260955062a4f427704edef1864c76d6777"
+ , "fe81bed43a5f6297dda1c7ca9203523e164f4f9fbdea978c6570d56b13ca8338"
+ , "430e6794ff74cf6aa0dbda71dd2d"
+ ]
+
+-- | Direction 1, disabled.
+mainnetUpdate136 :: BS.ByteString
+mainnetUpdate136 = hex $ mconcat
+ [ "261e3d1f1b77b4a606c20dbe5b395e35703adc82fd9320b88e892f51547b93e9"
+ , "060f793d8cfd93d2063060a692c31749d0eb3dd351f3e4bbc562d00f89472a42"
+ , "6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"
+ , "0cf884000bb0000169e321d20103005000000000000003e8000003e800000320"
+ , "00000000b1069a80"
+ ]
+
+-- | Direction 0, enabled, with a 12-byte extension.
+mainnetUpdate148 :: BS.ByteString
+mainnetUpdate148 = hex $ mconcat
+ [ "1c3f0859cb3f4494919c3dcc80d22bbf1e25e378bd95f071530833c2371c33d7"
+ , "0766012ebef1cd79b299a54d0d29bea2ef6feca1d6cfa188864c95d7eea7e9a4"
+ , "6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"
+ , "0cf884000bb000016a0a1c760100006400000000000003e80000000000001388"
+ , "00000000b1069a80fdd9030800000000ffffffed"
]
-- Property Tests --------------------------------------------------------------
@@ -641,10 +1136,11 @@ propChannelAnnouncementRoundtrip = property $ do
, channelAnnBitcoinKey1 = testPoint
, channelAnnBitcoinKey2 = testPoint
}
- encoded = encodeChannelAnnouncement msg
- case decodeChannelAnnouncement encoded of
- Right (decoded, _) -> decoded == msg
+ case encodeChannelAnnouncement msg of
Left _ -> False
+ Right encoded -> case decodeChannelAnnouncement encoded of
+ Right (decoded, _) -> decoded == msg
+ Left _ -> False
-- Property: ChannelUpdate roundtrip
propChannelUpdateRoundtrip :: Word32 -> Word16 -> Property
@@ -661,7 +1157,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