From dfb4ca60adca83441b35e81fbe475d0e4a7e8ac7 Mon Sep 17 00:00:00 2001 From: Devansh Vashisht Date: Tue, 22 Sep 2026 03:43:38 +0530 Subject: [PATCH 1/3] smite: add query_short_channel_ids codec BOLT 7 type 261. `encoded_short_ids` stays a `Vec` so that any encoding type roundtrips unchanged, leaving validation to an oracle. --- smite/src/bolt.rs | 40 ++++ smite/src/bolt/query_short_channel_ids.rs | 273 ++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 smite/src/bolt/query_short_channel_ids.rs diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index 978fa434..dfdc79ad 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -24,6 +24,7 @@ mod open_channel; mod open_channel2; mod ping; mod pong; +mod query_short_channel_ids; mod revoke_and_ack; mod shutdown; mod tlv; @@ -63,6 +64,7 @@ pub use open_channel::{OpenChannel, OpenChannelTlvs}; pub use open_channel2::{OpenChannel2, OpenChannel2Tlvs}; pub use ping::Ping; pub use pong::Pong; +pub use query_short_channel_ids::{QueryShortChannelIds, QueryShortChannelIdsTlvs}; pub use revoke_and_ack::RevokeAndAck; pub use shutdown::{Shutdown, is_acceptable_shutdown_script, is_standard_shutdown_script}; pub use tlv::{TlvRecord, TlvStream}; @@ -208,6 +210,8 @@ impl MessageType { pub const CHANNEL_UPDATE: MessageType = MessageType(258); /// `announcement_signatures` message (BOLT 7). pub const ANNOUNCEMENT_SIGNATURES: MessageType = MessageType(259); + /// `query_short_channel_ids` message (BOLT 7). + pub const QUERY_SHORT_CHANNEL_IDS: MessageType = MessageType(261); /// Gossip timestamp filter message (BOLT 7). pub const GOSSIP_TIMESTAMP_FILTER: MessageType = MessageType(265); @@ -260,6 +264,7 @@ impl MessageType { Self::NODE_ANNOUNCEMENT => "node_announcement", Self::CHANNEL_UPDATE => "channel_update", Self::ANNOUNCEMENT_SIGNATURES => "announcement_signatures", + Self::QUERY_SHORT_CHANNEL_IDS => "query_short_channel_ids", Self::GOSSIP_TIMESTAMP_FILTER => "gossip_timestamp_filter", _ => "unknown", } @@ -340,6 +345,8 @@ pub enum Message { ChannelUpdate(ChannelUpdate), /// `announcement_signatures` message (type 259). AnnouncementSignatures(AnnouncementSignatures), + /// `query_short_channel_ids` message (type 261). + QueryShortChannelIds(QueryShortChannelIds), /// Gossip timestamp filter message (type 265). GossipTimestampFilter(GossipTimestampFilter), /// Unknown message type. @@ -397,6 +404,7 @@ impl Message { Self::NodeAnnouncement(_) => MessageType::NODE_ANNOUNCEMENT, Self::ChannelUpdate(_) => MessageType::CHANNEL_UPDATE, Self::AnnouncementSignatures(_) => MessageType::ANNOUNCEMENT_SIGNATURES, + Self::QueryShortChannelIds(_) => MessageType::QUERY_SHORT_CHANNEL_IDS, Self::GossipTimestampFilter(_) => MessageType::GOSSIP_TIMESTAMP_FILTER, Self::Unknown { msg_type, .. } => *msg_type, } @@ -440,6 +448,7 @@ impl Message { Self::NodeAnnouncement(m) => out.extend(m.encode()), Self::ChannelUpdate(m) => out.extend(m.encode()), Self::AnnouncementSignatures(m) => out.extend(m.encode()), + Self::QueryShortChannelIds(m) => out.extend(m.encode()), Self::GossipTimestampFilter(m) => out.extend(m.encode()), Self::Unknown { payload, .. } => out.extend(payload), } @@ -512,6 +521,9 @@ impl Message { MessageType::ANNOUNCEMENT_SIGNATURES => Ok(Self::AnnouncementSignatures( AnnouncementSignatures::decode(cursor)?, )), + MessageType::QUERY_SHORT_CHANNEL_IDS => Ok(Self::QueryShortChannelIds( + QueryShortChannelIds::decode(cursor)?, + )), MessageType::GOSSIP_TIMESTAMP_FILTER => Ok(Self::GossipTimestampFilter( GossipTimestampFilter::decode(cursor)?, )), @@ -592,6 +604,7 @@ impl_from_message! { NodeAnnouncement => NODE_ANNOUNCEMENT, ChannelUpdate => CHANNEL_UPDATE, AnnouncementSignatures => ANNOUNCEMENT_SIGNATURES, + QueryShortChannelIds => QUERY_SHORT_CHANNEL_IDS, GossipTimestampFilter => GOSSIP_TIMESTAMP_FILTER, } @@ -1261,6 +1274,28 @@ mod tests { assert_eq!(decoded, Message::AnnouncementSignatures(ann_sigs)); } + /// Valid `QueryShortChannelIds` message for testing. + fn sample_query_short_channel_ids() -> QueryShortChannelIds { + let mut encoded_short_ids = vec![0x00]; + encoded_short_ids.extend_from_slice(&[0x00, 0x08, 0x3b, 0x04, 0x00, 0x03, 0x4d, 0x00]); + + QueryShortChannelIds { + chain_hash: [0xaa; 32], + encoded_short_ids, + tlvs: QueryShortChannelIdsTlvs { + query_flags: Some(vec![0x00, 0x1f]), + }, + } + } + + #[test] + fn message_query_short_channel_ids_roundtrip() { + let query = sample_query_short_channel_ids(); + let encoded = Message::QueryShortChannelIds(query.clone()).encode(); + let decoded = Message::decode(&encoded).unwrap(); + assert_eq!(decoded, Message::QueryShortChannelIds(query)); + } + #[test] fn message_gossip_timestamp_filter_roundtrip() { let chain_hash = [0x6f; 32]; @@ -1465,6 +1500,11 @@ mod tests { "announcement_signatures", MessageType::ANNOUNCEMENT_SIGNATURES, ), + ( + Message::QueryShortChannelIds(sample_query_short_channel_ids()), + "query_short_channel_ids", + MessageType::QUERY_SHORT_CHANNEL_IDS, + ), ( Message::GossipTimestampFilter(GossipTimestampFilter::no_gossip([0u8; 32])), "gossip_timestamp_filter", diff --git a/smite/src/bolt/query_short_channel_ids.rs b/smite/src/bolt/query_short_channel_ids.rs new file mode 100644 index 00000000..944f095b --- /dev/null +++ b/smite/src/bolt/query_short_channel_ids.rs @@ -0,0 +1,273 @@ +//! BOLT 7 `query_short_channel_ids` message. + +use super::BoltError; +use super::tlv::TlvStream; +use super::types::CHAIN_HASH_SIZE; +use super::wire::WireFormat; + +/// TLV type for the `query_flags` record. +const TLV_QUERY_FLAGS: u64 = 1; + +/// BOLT 7 `query_short_channel_ids` message (type 261). +/// +/// Asks a peer for the `channel_announcement` and `channel_update`s of a set +/// of channels, identified by their `short_channel_id`s. The peer answers with +/// the requested gossip followed by a `reply_short_channel_ids_end`. +/// +/// Wire layout (per [BOLT 7]): +/// +/// ```text +/// [chain_hash:32] +/// [u16:len] +/// [len*byte:encoded_short_ids] +/// [query_short_channel_ids_tlvs:tlvs] +/// ``` +/// +/// [BOLT 7]: https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-query_short_channel_ids-and-reply_short_channel_ids_end-messages +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct QueryShortChannelIds { + /// The 32-byte hash that uniquely identifies the chain the ids refer to. + pub chain_hash: [u8; CHAIN_HASH_SIZE], + /// The queried `short_channel_id`s, still in their wire encoding. + /// + /// The first byte is the `encoding_type`: 0 is an ascending array of + /// 8-byte ids, and 1 was zlib, which BOLT 7 now says MUST NOT be used. + /// The codec keeps the bytes as sent so that any encoding type survives a + /// decode/encode roundtrip, and leaves judging them to an oracle. + pub encoded_short_ids: Vec, + /// Optional TLV extensions. + pub tlvs: QueryShortChannelIdsTlvs, +} + +/// TLV extensions for the `query_short_channel_ids` message. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct QueryShortChannelIdsTlvs { + /// One query flag per queried `short_channel_id` (TLV type 1). + /// + /// The first byte is the `encoding_type`, as for `encoded_short_ids`, and + /// the rest is `encoded_query_flags`: one minimally-encoded `bigsize` per + /// id, selecting which of the channel's gossip messages the sender wants. + /// Kept as raw bytes for the same reason as `encoded_short_ids`. + pub query_flags: Option>, +} + +impl QueryShortChannelIds { + /// Encodes to wire format (without message type prefix). + #[must_use] + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + self.chain_hash.write(&mut out); + self.encoded_short_ids.write(&mut out); + + // Encode TLVs + let mut tlv_stream = TlvStream::new(); + if let Some(query_flags) = &self.tlvs.query_flags { + tlv_stream.add(TLV_QUERY_FLAGS, query_flags.clone()); + } + out.extend(tlv_stream.encode()); + + out + } + + /// Decodes from wire format (without message type prefix). + /// + /// # Errors + /// + /// Returns `Truncated` if the payload is too short, or TLV errors if the + /// TLV stream is malformed. + pub fn decode(payload: &[u8]) -> Result { + let mut cursor = payload; + let chain_hash = WireFormat::read(&mut cursor)?; + let encoded_short_ids = WireFormat::read(&mut cursor)?; + + // Decode TLVs (remaining bytes). `query_flags` is odd, so no even type + // is known here. + let tlv_stream = TlvStream::decode(cursor)?; + let tlvs = QueryShortChannelIdsTlvs::from_stream(&tlv_stream); + + Ok(Self { + chain_hash, + encoded_short_ids, + tlvs, + }) + } +} + +impl QueryShortChannelIdsTlvs { + /// Extracts TLVs from a parsed TLV stream. + fn from_stream(stream: &TlvStream) -> Self { + Self { + query_flags: stream.get(TLV_QUERY_FLAGS).map(<[u8]>::to_vec), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `encoded_short_ids` holding two ids under encoding type 0. + fn sample_encoded_short_ids() -> Vec { + let mut ids = vec![0x00]; + ids.extend_from_slice(&[0x00, 0x08, 0x3b, 0x04, 0x00, 0x03, 0x4d, 0x00]); + ids.extend_from_slice(&[0x00, 0x08, 0x3b, 0x05, 0x00, 0x01, 0x9a, 0x01]); + ids + } + + fn sample_msg() -> QueryShortChannelIds { + QueryShortChannelIds { + chain_hash: [0xaa; CHAIN_HASH_SIZE], + encoded_short_ids: sample_encoded_short_ids(), + tlvs: QueryShortChannelIdsTlvs::default(), + } + } + + #[test] + fn roundtrip() { + let original = sample_msg(); + let encoded = original.encode(); + let decoded = QueryShortChannelIds::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn roundtrip_with_query_flags() { + let mut msg = sample_msg(); + // encoding type 0, then one minimally-encoded bigsize flag per id + msg.tlvs.query_flags = Some(vec![0x00, 0x01, 0x1f]); + let encoded = msg.encode(); + let decoded = QueryShortChannelIds::decode(&encoded).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn roundtrip_with_empty_encoded_short_ids() { + let mut msg = sample_msg(); + msg.encoded_short_ids = Vec::new(); + let encoded = msg.encode(); + let decoded = QueryShortChannelIds::decode(&encoded).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn encode_field_order() { + let encoded = sample_msg().encode(); + assert_eq!(encoded.len(), CHAIN_HASH_SIZE + 2 + 17); + assert_eq!(encoded[..CHAIN_HASH_SIZE], [0xaa; CHAIN_HASH_SIZE]); + assert_eq!(encoded[32..34], 17u16.to_be_bytes()); + assert_eq!(encoded[34..51], sample_encoded_short_ids()[..]); + } + + #[test] + fn decode_truncated_chain_hash() { + assert_eq!( + QueryShortChannelIds::decode(&[0x00; 20]), + Err(BoltError::Truncated { + expected: CHAIN_HASH_SIZE, + actual: 20 + }) + ); + } + + #[test] + fn decode_truncated_encoded_short_ids_len() { + let data = [0xaa; CHAIN_HASH_SIZE + 1]; + assert_eq!( + QueryShortChannelIds::decode(&data), + Err(BoltError::Truncated { + expected: 2, + actual: 1 + }) + ); + } + + #[test] + fn decode_truncated_encoded_short_ids_data() { + let mut data = vec![0xaa; CHAIN_HASH_SIZE]; + // len claims 17 bytes, only 5 follow + data.extend_from_slice(&17u16.to_be_bytes()); + data.extend_from_slice(&[0x00; 5]); + assert_eq!( + QueryShortChannelIds::decode(&data), + Err(BoltError::Truncated { + expected: 17, + actual: 5 + }) + ); + } + + #[test] + fn decode_empty() { + assert_eq!( + QueryShortChannelIds::decode(&[]), + Err(BoltError::Truncated { + expected: CHAIN_HASH_SIZE, + actual: 0 + }) + ); + } + + #[test] + fn decode_preserves_zlib_encoding_type() { + let mut msg = sample_msg(); + // Encoding type 1 (zlib) MUST NOT be sent, but a target may still send + // it, so it has to survive a roundtrip rather than fail to decode. + msg.encoded_short_ids = vec![0x01, 0x78, 0x9c, 0xff]; + let encoded = msg.encode(); + let decoded = QueryShortChannelIds::decode(&encoded).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn decode_preserves_unknown_encoding_type() { + let mut msg = sample_msg(); + msg.encoded_short_ids = vec![0xff, 0x11, 0x22]; + let encoded = msg.encode(); + let decoded = QueryShortChannelIds::decode(&encoded).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn decode_preserves_partial_short_channel_id() { + let mut msg = sample_msg(); + // Not a whole number of ids: encoding byte plus 3 trailing bytes. + msg.encoded_short_ids = vec![0x00, 0x01, 0x02, 0x03]; + let encoded = msg.encode(); + let decoded = QueryShortChannelIds::decode(&encoded).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn decode_unknown_odd_tlv_ignored() { + let mut encoded = sample_msg().encode(); + // Append an unknown odd TLV (type 3, len 2, value 0xffff) + encoded.extend_from_slice(&[0x03, 0x02, 0xff, 0xff]); + let decoded = QueryShortChannelIds::decode(&encoded).unwrap(); + assert_eq!(decoded.encoded_short_ids, sample_encoded_short_ids()); + assert_eq!(decoded.tlvs.query_flags, None); + } + + #[test] + fn decode_unknown_even_tlv_rejected() { + let mut encoded = sample_msg().encode(); + // Append an unknown even TLV (type 4, len 1, value 0x00) + encoded.extend_from_slice(&[0x04, 0x01, 0x00]); + assert_eq!( + QueryShortChannelIds::decode(&encoded), + Err(BoltError::TlvUnknownEvenType(4)) + ); + } + + #[test] + fn default_tlvs_are_none() { + assert_eq!(QueryShortChannelIdsTlvs::default().query_flags, None); + } + + #[test] + #[should_panic(expected = "exceeds maximum size")] + fn encode_panics_on_oversized_encoded_short_ids() { + let mut msg = sample_msg(); + msg.encoded_short_ids = vec![0x00; usize::from(u16::MAX) + 1]; + let _ = msg.encode(); + } +} From 633d9050497f5177477b13fd19b86bc96af9f7a5 Mon Sep 17 00:00:00 2001 From: Devansh Vashisht Date: Tue, 22 Sep 2026 03:43:38 +0530 Subject: [PATCH 2/3] smite: add reply_short_channel_ids_end codec BOLT 7 type 262, a fixed 33 bytes. `full_information` is a `u8` since the spec defines only 0 and 1, and 262 now decodes instead of being rejected as an unknown even type. --- smite/src/bolt.rs | 32 ++++ smite/src/bolt/reply_short_channel_ids_end.rs | 142 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 smite/src/bolt/reply_short_channel_ids_end.rs diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index dfdc79ad..311bce69 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -25,6 +25,7 @@ mod open_channel2; mod ping; mod pong; mod query_short_channel_ids; +mod reply_short_channel_ids_end; mod revoke_and_ack; mod shutdown; mod tlv; @@ -65,6 +66,7 @@ pub use open_channel2::{OpenChannel2, OpenChannel2Tlvs}; pub use ping::Ping; pub use pong::Pong; pub use query_short_channel_ids::{QueryShortChannelIds, QueryShortChannelIdsTlvs}; +pub use reply_short_channel_ids_end::ReplyShortChannelIdsEnd; pub use revoke_and_ack::RevokeAndAck; pub use shutdown::{Shutdown, is_acceptable_shutdown_script, is_standard_shutdown_script}; pub use tlv::{TlvRecord, TlvStream}; @@ -212,6 +214,8 @@ impl MessageType { pub const ANNOUNCEMENT_SIGNATURES: MessageType = MessageType(259); /// `query_short_channel_ids` message (BOLT 7). pub const QUERY_SHORT_CHANNEL_IDS: MessageType = MessageType(261); + /// `reply_short_channel_ids_end` message (BOLT 7). + pub const REPLY_SHORT_CHANNEL_IDS_END: MessageType = MessageType(262); /// Gossip timestamp filter message (BOLT 7). pub const GOSSIP_TIMESTAMP_FILTER: MessageType = MessageType(265); @@ -265,6 +269,7 @@ impl MessageType { Self::CHANNEL_UPDATE => "channel_update", Self::ANNOUNCEMENT_SIGNATURES => "announcement_signatures", Self::QUERY_SHORT_CHANNEL_IDS => "query_short_channel_ids", + Self::REPLY_SHORT_CHANNEL_IDS_END => "reply_short_channel_ids_end", Self::GOSSIP_TIMESTAMP_FILTER => "gossip_timestamp_filter", _ => "unknown", } @@ -347,6 +352,8 @@ pub enum Message { AnnouncementSignatures(AnnouncementSignatures), /// `query_short_channel_ids` message (type 261). QueryShortChannelIds(QueryShortChannelIds), + /// `reply_short_channel_ids_end` message (type 262). + ReplyShortChannelIdsEnd(ReplyShortChannelIdsEnd), /// Gossip timestamp filter message (type 265). GossipTimestampFilter(GossipTimestampFilter), /// Unknown message type. @@ -405,6 +412,7 @@ impl Message { Self::ChannelUpdate(_) => MessageType::CHANNEL_UPDATE, Self::AnnouncementSignatures(_) => MessageType::ANNOUNCEMENT_SIGNATURES, Self::QueryShortChannelIds(_) => MessageType::QUERY_SHORT_CHANNEL_IDS, + Self::ReplyShortChannelIdsEnd(_) => MessageType::REPLY_SHORT_CHANNEL_IDS_END, Self::GossipTimestampFilter(_) => MessageType::GOSSIP_TIMESTAMP_FILTER, Self::Unknown { msg_type, .. } => *msg_type, } @@ -449,6 +457,7 @@ impl Message { Self::ChannelUpdate(m) => out.extend(m.encode()), Self::AnnouncementSignatures(m) => out.extend(m.encode()), Self::QueryShortChannelIds(m) => out.extend(m.encode()), + Self::ReplyShortChannelIdsEnd(m) => out.extend(m.encode()), Self::GossipTimestampFilter(m) => out.extend(m.encode()), Self::Unknown { payload, .. } => out.extend(payload), } @@ -524,6 +533,9 @@ impl Message { MessageType::QUERY_SHORT_CHANNEL_IDS => Ok(Self::QueryShortChannelIds( QueryShortChannelIds::decode(cursor)?, )), + MessageType::REPLY_SHORT_CHANNEL_IDS_END => Ok(Self::ReplyShortChannelIdsEnd( + ReplyShortChannelIdsEnd::decode(cursor)?, + )), MessageType::GOSSIP_TIMESTAMP_FILTER => Ok(Self::GossipTimestampFilter( GossipTimestampFilter::decode(cursor)?, )), @@ -605,6 +617,7 @@ impl_from_message! { ChannelUpdate => CHANNEL_UPDATE, AnnouncementSignatures => ANNOUNCEMENT_SIGNATURES, QueryShortChannelIds => QUERY_SHORT_CHANNEL_IDS, + ReplyShortChannelIdsEnd => REPLY_SHORT_CHANNEL_IDS_END, GossipTimestampFilter => GOSSIP_TIMESTAMP_FILTER, } @@ -1296,6 +1309,17 @@ mod tests { assert_eq!(decoded, Message::QueryShortChannelIds(query)); } + #[test] + fn message_reply_short_channel_ids_end_roundtrip() { + let reply = ReplyShortChannelIdsEnd { + chain_hash: [0xaa; 32], + full_information: 0x01, + }; + let encoded = Message::ReplyShortChannelIdsEnd(reply.clone()).encode(); + let decoded = Message::decode(&encoded).unwrap(); + assert_eq!(decoded, Message::ReplyShortChannelIdsEnd(reply)); + } + #[test] fn message_gossip_timestamp_filter_roundtrip() { let chain_hash = [0x6f; 32]; @@ -1505,6 +1529,14 @@ mod tests { "query_short_channel_ids", MessageType::QUERY_SHORT_CHANNEL_IDS, ), + ( + Message::ReplyShortChannelIdsEnd(ReplyShortChannelIdsEnd { + chain_hash: [0xaa; 32], + full_information: 0x01, + }), + "reply_short_channel_ids_end", + MessageType::REPLY_SHORT_CHANNEL_IDS_END, + ), ( Message::GossipTimestampFilter(GossipTimestampFilter::no_gossip([0u8; 32])), "gossip_timestamp_filter", diff --git a/smite/src/bolt/reply_short_channel_ids_end.rs b/smite/src/bolt/reply_short_channel_ids_end.rs new file mode 100644 index 00000000..34af16df --- /dev/null +++ b/smite/src/bolt/reply_short_channel_ids_end.rs @@ -0,0 +1,142 @@ +//! BOLT 7 `reply_short_channel_ids_end` message. + +use super::BoltError; +use super::types::CHAIN_HASH_SIZE; +use super::wire::WireFormat; + +/// BOLT 7 `reply_short_channel_ids_end` message (type 262). +/// +/// Terminates the gossip a peer sends in answer to a +/// `query_short_channel_ids`, so the querier does not have to rely on a +/// timeout to know the reply is complete. +/// +/// Wire layout (per [BOLT 7]): +/// +/// ```text +/// [chain_hash:32] +/// [byte:full_information] +/// ``` +/// +/// [BOLT 7]: https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-query_short_channel_ids-and-reply_short_channel_ids_end-messages +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReplyShortChannelIdsEnd { + /// The 32-byte hash that uniquely identifies the chain being replied for. + pub chain_hash: [u8; CHAIN_HASH_SIZE], + /// Whether the sender maintains up-to-date channel information for + /// `chain_hash`. + /// + /// BOLT 7 defines 0 (the sender does not, so look elsewhere) and 1 (it + /// does); we don't enforce that, to allow fuzzing with arbitrary data. + pub full_information: u8, +} + +impl ReplyShortChannelIdsEnd { + /// Encodes to wire format (without message type prefix). + #[must_use] + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + self.chain_hash.write(&mut out); + self.full_information.write(&mut out); + out + } + + /// Decodes from wire format (without message type prefix). + /// + /// BOLT 7 defines no `tlv_stream` for this message, so trailing bytes are + /// ignored rather than parsed as TLVs. + /// + /// # Errors + /// + /// Returns `Truncated` if the payload is too short. + pub fn decode(payload: &[u8]) -> Result { + let mut cursor = payload; + let chain_hash = WireFormat::read(&mut cursor)?; + let full_information = WireFormat::read(&mut cursor)?; + Ok(Self { + chain_hash, + full_information, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_msg() -> ReplyShortChannelIdsEnd { + ReplyShortChannelIdsEnd { + chain_hash: [0xaa; CHAIN_HASH_SIZE], + full_information: 0x01, + } + } + + #[test] + fn roundtrip() { + let original = sample_msg(); + let encoded = original.encode(); + let decoded = ReplyShortChannelIdsEnd::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn encode_fixed_field_size() { + assert_eq!(sample_msg().encode().len(), CHAIN_HASH_SIZE + 1); + } + + #[test] + fn encode_field_order() { + let encoded = sample_msg().encode(); + assert_eq!(encoded[..CHAIN_HASH_SIZE], [0xaa; CHAIN_HASH_SIZE]); + assert_eq!(encoded[CHAIN_HASH_SIZE], 0x01); + } + + #[test] + fn decode_truncated_chain_hash() { + assert_eq!( + ReplyShortChannelIdsEnd::decode(&[0x00; 20]), + Err(BoltError::Truncated { + expected: CHAIN_HASH_SIZE, + actual: 20 + }) + ); + } + + #[test] + fn decode_truncated_full_information() { + assert_eq!( + ReplyShortChannelIdsEnd::decode(&[0xaa; CHAIN_HASH_SIZE]), + Err(BoltError::Truncated { + expected: 1, + actual: 0 + }) + ); + } + + #[test] + fn decode_empty() { + assert_eq!( + ReplyShortChannelIdsEnd::decode(&[]), + Err(BoltError::Truncated { + expected: CHAIN_HASH_SIZE, + actual: 0 + }) + ); + } + + #[test] + fn decode_ignores_trailing_bytes() { + let mut encoded = sample_msg().encode(); + encoded.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + let decoded = ReplyShortChannelIdsEnd::decode(&encoded).unwrap(); + assert_eq!(decoded, sample_msg()); + } + + #[test] + fn decode_preserves_out_of_range_full_information() { + let mut msg = sample_msg(); + msg.full_information = 0xff; + let encoded = msg.encode(); + let decoded = ReplyShortChannelIdsEnd::decode(&encoded).unwrap(); + assert_eq!(msg, decoded); + } +} From 92ea64981cb22b056fd967835f75e5811cf5d95b Mon Sep 17 00:00:00 2001 From: Devansh Vashisht Date: Tue, 22 Sep 2026 03:43:38 +0530 Subject: [PATCH 3/3] smite-scenarios: skip gossip query messages Both new messages now decode, so add them to the gossip skip arm to keep them from surfacing as `UnexpectedMessage`. --- smite-scenarios/src/executor.rs | 2 ++ smite-scenarios/src/executor/tests.rs | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index bf6f6b90..6b3ae102 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -1150,6 +1150,8 @@ fn recv_non_ping(conn: &mut impl Connection, timeout: Duration) -> Result { log::debug!("skipping gossip message {msg}"); } diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index ed2f6362..63d58e43 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -8,7 +8,10 @@ use bitcoin::Amount; use bitcoin::secp256k1::{Secp256k1, SecretKey}; use harness::*; use programs::*; -use smite::bolt::{AcceptChannelTlvs, GossipTimestampFilter, Init, Ping}; +use smite::bolt::{ + AcceptChannelTlvs, GossipTimestampFilter, Init, Ping, QueryShortChannelIds, + QueryShortChannelIdsTlvs, ReplyShortChannelIdsEnd, +}; use smite_ir::Instruction; use smite_ir::builder::ProgramBuilder; use smite_ir::operation::ShutdownScriptVariant; @@ -393,9 +396,21 @@ fn execute_recv_auto_pong() { #[test] fn execute_recv_skips_gossip() { let gossip = GossipTimestampFilter::new([0u8; 32], 0, 86400); + let query = QueryShortChannelIds { + chain_hash: [0u8; 32], + // encoding type 0, then one 8-byte short_channel_id + encoded_short_ids: vec![0x00, 0x00, 0x08, 0x3b, 0x04, 0x00, 0x03, 0x4d, 0x00], + tlvs: QueryShortChannelIdsTlvs::default(), + }; + let reply_end = ReplyShortChannelIdsEnd { + chain_hash: [0u8; 32], + full_information: 1, + }; let mut fx = Fixture::new() .queue(&Message::GossipTimestampFilter(gossip)) + .queue(&Message::QueryShortChannelIds(query)) + .queue(&Message::ReplyShortChannelIdsEnd(reply_end)) .queue(&Message::AcceptChannel(sample_accept_channel())); fx.run(&negotiate_channel_program(&announced_open_channel()));