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 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:
Mbench/Main.hs | 7+++++--
Mbench/Weight.hs | 7+++++--
Mflake.lock | 192++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mflake.nix | 17++++++++++++++++-
Mlib/Lightning/Protocol/BOLT7.hs | 9++++++---
Mlib/Lightning/Protocol/BOLT7/Codec.hs | 291++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mlib/Lightning/Protocol/BOLT7/Hash.hs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mlib/Lightning/Protocol/BOLT7/Messages.hs | 10++++------
Mlib/Lightning/Protocol/BOLT7/Types.hs | 12+++++++-----
Mlib/Lightning/Protocol/BOLT7/Validate.hs | 36++++++++++++++++++++----------------
Mppad-bolt7.cabal | 1+
Mtest/Main.hs | 548+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
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