X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Frouting%2Fgossip.rs;h=c51180fee731c60197d31e4cf8ad022bf3d1f4b5;hb=1c352555b670b3e98968e7aeef4a455f9819a6a7;hp=64dbb45888b338177f54f662efc0647e620a7c49;hpb=2b898a3385d226f961ef045560e94e64195bbd4f;p=rust-lightning diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 64dbb458..c51180fe 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -9,6 +9,8 @@ //! The [`NetworkGraph`] stores the network gossip and [`P2PGossipSync`] fetches it from peers +use bitcoin::blockdata::constants::ChainHash; + use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{PublicKey, Verification}; use bitcoin::secp256k1::Secp256k1; @@ -17,14 +19,13 @@ use bitcoin::secp256k1; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hashes::Hash; use bitcoin::hashes::hex::FromHex; -use bitcoin::hash_types::BlockHash; use bitcoin::network::constants::Network; -use bitcoin::blockdata::constants::genesis_block; use crate::events::{MessageSendEvent, MessageSendEventsProvider}; +use crate::ln::ChannelId; use crate::ln::features::{ChannelFeatures, NodeFeatures, InitFeatures}; -use crate::ln::msgs::{DecodeError, ErrorAction, Init, LightningError, RoutingMessageHandler, NetAddress, MAX_VALUE_MSAT}; +use crate::ln::msgs::{DecodeError, ErrorAction, Init, LightningError, RoutingMessageHandler, SocketAddress, MAX_VALUE_MSAT}; use crate::ln::msgs::{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement, GossipTimestampFilter}; use crate::ln::msgs::{QueryChannelRange, ReplyChannelRange, QueryShortChannelIds, ReplyShortChannelIdsEnd}; use crate::ln::msgs; @@ -88,12 +89,12 @@ impl NodeId { impl fmt::Debug for NodeId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "NodeId({})", log_bytes!(self.0)) + write!(f, "NodeId({})", crate::util::logger::DebugBytes(&self.0)) } } impl fmt::Display for NodeId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", log_bytes!(self.0)) + crate::util::logger::DebugBytes(&self.0).fmt(f) } } @@ -165,7 +166,7 @@ impl FromStr for NodeId { pub struct NetworkGraph where L::Target: Logger { secp_ctx: Secp256k1, last_rapid_gossip_sync_timestamp: Mutex>, - genesis_hash: BlockHash, + chain_hash: ChainHash, logger: L, // Lock order: channels -> nodes channels: RwLock>, @@ -254,7 +255,7 @@ pub struct P2PGossipSync>, U: Deref, L: Deref> where U::Target: UtxoLookup, L::Target: Logger { network_graph: G, - utxo_lookup: Option, + utxo_lookup: RwLock>, #[cfg(feature = "std")] full_syncs_requested: AtomicUsize, pending_events: Mutex>, @@ -273,7 +274,7 @@ where U::Target: UtxoLookup, L::Target: Logger network_graph, #[cfg(feature = "std")] full_syncs_requested: AtomicUsize::new(0), - utxo_lookup, + utxo_lookup: RwLock::new(utxo_lookup), pending_events: Mutex::new(vec![]), logger, } @@ -282,8 +283,8 @@ where U::Target: UtxoLookup, L::Target: Logger /// Adds a provider used to check new announcements. Does not affect /// existing announcements unless they are updated. /// Add, update or remove the provider would replace the current one. - pub fn add_utxo_lookup(&mut self, utxo_lookup: Option) { - self.utxo_lookup = utxo_lookup; + pub fn add_utxo_lookup(&self, utxo_lookup: Option) { + *self.utxo_lookup.write().unwrap() = utxo_lookup; } /// Gets a reference to the underlying [`NetworkGraph`] which was provided in @@ -340,6 +341,9 @@ where U::Target: UtxoLookup, L::Target: Logger impl NetworkGraph where L::Target: Logger { /// Handles any network updates originating from [`Event`]s. + // + /// Note that this will skip applying any [`NetworkUpdate::ChannelUpdateMessage`] to avoid + /// leaking possibly identifying information of the sender to the public network. /// /// [`Event`]: crate::events::Event pub fn handle_network_update(&self, network_update: &NetworkUpdate) { @@ -348,8 +352,7 @@ impl NetworkGraph where L::Target: Logger { let short_channel_id = msg.contents.short_channel_id; let is_enabled = msg.contents.flags & (1 << 1) != (1 << 1); let status = if is_enabled { "enabled" } else { "disabled" }; - log_debug!(self.logger, "Updating channel with channel_update from a payment failure. Channel {} is {}.", short_channel_id, status); - let _ = self.update_channel(msg); + log_debug!(self.logger, "Skipping application of a channel update from a payment failure. Channel {} is {}.", short_channel_id, status); }, NetworkUpdate::ChannelFailure { short_channel_id, is_permanent } => { if is_permanent { @@ -367,9 +370,9 @@ impl NetworkGraph where L::Target: Logger { } } - /// Gets the genesis hash for this network graph. - pub fn get_genesis_hash(&self) -> BlockHash { - self.genesis_hash + /// Gets the chain hash for this network graph. + pub fn get_chain_hash(&self) -> ChainHash { + self.chain_hash } } @@ -382,7 +385,7 @@ macro_rules! secp_verify_sig { err: format!("Invalid signature on {} message", $msg_type), action: ErrorAction::SendWarningMessage { msg: msgs::WarningMessage { - channel_id: [0; 32], + channel_id: ChannelId::new_zero(), data: format!("Invalid signature on {} message", $msg_type), }, log_level: Level::Trace, @@ -400,7 +403,7 @@ macro_rules! get_pubkey_from_node_id { err: format!("Invalid public key on {} message", $msg_type), action: ErrorAction::SendWarningMessage { msg: msgs::WarningMessage { - channel_id: [0; 32], + channel_id: ChannelId::new_zero(), data: format!("Invalid public key on {} message", $msg_type), }, log_level: Level::Trace @@ -443,7 +446,7 @@ where U::Target: UtxoLookup, L::Target: Logger } fn handle_channel_announcement(&self, msg: &msgs::ChannelAnnouncement) -> Result { - self.network_graph.update_channel_from_announcement(msg, &self.utxo_lookup)?; + self.network_graph.update_channel_from_announcement(msg, &*self.utxo_lookup.read().unwrap())?; Ok(msg.contents.excess_data.len() <= MAX_EXCESS_BYTES_FOR_RELAY) } @@ -580,7 +583,7 @@ where U::Target: UtxoLookup, L::Target: Logger pending_events.push(MessageSendEvent::SendGossipTimestampFilter { node_id: their_node_id.clone(), msg: GossipTimestampFilter { - chain_hash: self.network_graph.genesis_hash, + chain_hash: self.network_graph.chain_hash, first_timestamp: gossip_start_time as u32, // 2106 issue! timestamp_range: u32::max_value(), }, @@ -619,7 +622,7 @@ where U::Target: UtxoLookup, L::Target: Logger let exclusive_end_scid = scid_from_parts(cmp::min(msg.end_blocknum() as u64, MAX_SCID_BLOCK), 0, 0); // Per spec, we must reply to a query. Send an empty message when things are invalid. - if msg.chain_hash != self.network_graph.genesis_hash || inclusive_start_scid.is_err() || exclusive_end_scid.is_err() || msg.number_of_blocks == 0 { + if msg.chain_hash != self.network_graph.chain_hash || inclusive_start_scid.is_err() || exclusive_end_scid.is_err() || msg.number_of_blocks == 0 { let mut pending_events = self.pending_events.lock().unwrap(); pending_events.push(MessageSendEvent::SendReplyChannelRange { node_id: their_node_id.clone(), @@ -899,7 +902,7 @@ impl ChannelInfo { impl fmt::Display for ChannelInfo { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "features: {}, node_one: {}, one_to_two: {:?}, node_two: {}, two_to_one: {:?}", - log_bytes!(self.features.encode()), log_bytes!(self.node_one.as_slice()), self.one_to_two, log_bytes!(self.node_two.as_slice()), self.two_to_one)?; + log_bytes!(self.features.encode()), &self.node_one, self.one_to_two, &self.node_two, self.two_to_one)?; Ok(()) } } @@ -1127,7 +1130,7 @@ pub struct NodeAnnouncementInfo { impl NodeAnnouncementInfo { /// Internet-level addresses via which one can connect to the node - pub fn addresses(&self) -> &[NetAddress] { + pub fn addresses(&self) -> &[SocketAddress] { self.announcement_message.as_ref() .map(|msg| msg.contents.addresses.as_slice()) .unwrap_or_default() @@ -1136,7 +1139,7 @@ impl NodeAnnouncementInfo { impl Writeable for NodeAnnouncementInfo { fn write(&self, writer: &mut W) -> Result<(), io::Error> { - let empty_addresses = Vec::::new(); + let empty_addresses = Vec::::new(); write_tlv_fields!(writer, { (0, self.features, required), (2, self.last_update, required), @@ -1151,7 +1154,7 @@ impl Writeable for NodeAnnouncementInfo { impl Readable for NodeAnnouncementInfo { fn read(reader: &mut R) -> Result { - _init_and_read_tlv_fields!(reader, { + _init_and_read_len_prefixed_tlv_fields!(reader, { (0, features, required), (2, last_update, required), (4, rgb, required), @@ -1159,7 +1162,7 @@ impl Readable for NodeAnnouncementInfo { (8, announcement_message, option), (10, _addresses, optional_vec), // deprecated, not used anymore }); - let _: Option> = _addresses; + let _: Option> = _addresses; Ok(Self { features: features.0.unwrap(), last_update: last_update.0.unwrap(), rgb: rgb.0.unwrap(), alias: alias.0.unwrap(), announcement_message }) } @@ -1235,7 +1238,7 @@ impl Writeable for NodeInfo { } // A wrapper allowing for the optional deserialization of `NodeAnnouncementInfo`. Utilizing this is -// necessary to maintain compatibility with previous serializations of `NetAddress` that have an +// necessary to maintain compatibility with previous serializations of `SocketAddress` that have an // invalid hostname set. We ignore and eat all errors until we are either able to read a // `NodeAnnouncementInfo` or hit a `ShortRead`, i.e., read the TLV field to the end. struct NodeAnnouncementInfoDeserWrapper(NodeAnnouncementInfo); @@ -1259,7 +1262,7 @@ impl Readable for NodeInfo { // with zero inbound fees, causing that heuristic to provide little gain. Worse, because it // requires additional complexity and lookups during routing, it ends up being a // performance loss. Thus, we simply ignore the old field here and no longer track it. - _init_and_read_tlv_fields!(reader, { + _init_and_read_len_prefixed_tlv_fields!(reader, { (0, _lowest_inbound_channel_fees, option), (2, announcement_info_wrap, upgradable_option), (4, channels, required_vec), @@ -1281,7 +1284,7 @@ impl Writeable for NetworkGraph where L::Target: Logger { fn write(&self, writer: &mut W) -> Result<(), io::Error> { write_ver_prefix!(writer, SERIALIZATION_VERSION, MIN_SERIALIZATION_VERSION); - self.genesis_hash.write(writer)?; + self.chain_hash.write(writer)?; let channels = self.channels.read().unwrap(); (channels.len() as u64).write(writer)?; for (ref chan_id, ref chan_info) in channels.unordered_iter() { @@ -1307,7 +1310,7 @@ impl ReadableArgs for NetworkGraph where L::Target: Logger { fn read(reader: &mut R, logger: L) -> Result, DecodeError> { let _ver = read_ver_prefix!(reader, SERIALIZATION_VERSION); - let genesis_hash: BlockHash = Readable::read(reader)?; + let chain_hash: ChainHash = Readable::read(reader)?; let channels_count: u64 = Readable::read(reader)?; let mut channels = IndexedMap::new(); for _ in 0..channels_count { @@ -1330,7 +1333,7 @@ impl ReadableArgs for NetworkGraph where L::Target: Logger { Ok(NetworkGraph { secp_ctx: Secp256k1::verification_only(), - genesis_hash, + chain_hash, logger, channels: RwLock::new(channels), nodes: RwLock::new(nodes), @@ -1350,7 +1353,7 @@ impl fmt::Display for NetworkGraph where L::Target: Logger { } writeln!(f, "[Nodes]")?; for (&node_id, val) in self.nodes.read().unwrap().unordered_iter() { - writeln!(f, " {}: {}", log_bytes!(node_id.as_slice()), val)?; + writeln!(f, " {}: {}", &node_id, val)?; } Ok(()) } @@ -1366,7 +1369,7 @@ impl PartialEq for NetworkGraph where L::Target: Logger { let b = if ord { (&other.channels, &other.nodes) } else { (&self.channels, &self.nodes) }; let (channels_a, channels_b) = (a.0.unsafe_well_ordered_double_lock_self(), b.0.unsafe_well_ordered_double_lock_self()); let (nodes_a, nodes_b) = (a.1.unsafe_well_ordered_double_lock_self(), b.1.unsafe_well_ordered_double_lock_self()); - self.genesis_hash.eq(&other.genesis_hash) && channels_a.eq(&channels_b) && nodes_a.eq(&nodes_b) + self.chain_hash.eq(&other.chain_hash) && channels_a.eq(&channels_b) && nodes_a.eq(&nodes_b) } } @@ -1375,7 +1378,7 @@ impl NetworkGraph where L::Target: Logger { pub fn new(network: Network, logger: L) -> NetworkGraph { Self { secp_ctx: Secp256k1::verification_only(), - genesis_hash: genesis_block(network).header.block_hash(), + chain_hash: ChainHash::using_genesis_block(network), logger, channels: RwLock::new(IndexedMap::new()), nodes: RwLock::new(IndexedMap::new()), @@ -1607,7 +1610,7 @@ impl NetworkGraph where L::Target: Logger { return Err(LightningError{err: "Channel announcement node had a channel with itself".to_owned(), action: ErrorAction::IgnoreError}); } - if msg.chain_hash != self.genesis_hash { + if msg.chain_hash != self.chain_hash { return Err(LightningError { err: "Channel announcement chain hash does not match genesis hash".to_owned(), action: ErrorAction::IgnoreAndLog(Level::Debug), @@ -1844,14 +1847,14 @@ impl NetworkGraph where L::Target: Logger { /// For an already known (from announcement) channel, update info about one of the directions /// of the channel. /// - /// You probably don't want to call this directly, instead relying on a P2PGossipSync's - /// RoutingMessageHandler implementation to call it indirectly. This may be useful to accept + /// You probably don't want to call this directly, instead relying on a [`P2PGossipSync`]'s + /// [`RoutingMessageHandler`] implementation to call it indirectly. This may be useful to accept /// routing messages from a source using a protocol other than the lightning P2P protocol. /// /// If built with `no-std`, any updates with a timestamp more than two weeks in the past or /// materially in the future will be rejected. pub fn update_channel(&self, msg: &msgs::ChannelUpdate) -> Result<(), LightningError> { - self.update_channel_intern(&msg.contents, Some(&msg), Some(&msg.signature)) + self.update_channel_internal(&msg.contents, Some(&msg), Some(&msg.signature), false) } /// For an already known (from announcement) channel, update info about one of the directions @@ -1861,13 +1864,26 @@ impl NetworkGraph where L::Target: Logger { /// If built with `no-std`, any updates with a timestamp more than two weeks in the past or /// materially in the future will be rejected. pub fn update_channel_unsigned(&self, msg: &msgs::UnsignedChannelUpdate) -> Result<(), LightningError> { - self.update_channel_intern(msg, None, None) + self.update_channel_internal(msg, None, None, false) + } + + /// For an already known (from announcement) channel, verify the given [`ChannelUpdate`]. + /// + /// This checks whether the update currently is applicable by [`Self::update_channel`]. + /// + /// If built with `no-std`, any updates with a timestamp more than two weeks in the past or + /// materially in the future will be rejected. + pub fn verify_channel_update(&self, msg: &msgs::ChannelUpdate) -> Result<(), LightningError> { + self.update_channel_internal(&msg.contents, Some(&msg), Some(&msg.signature), true) } - fn update_channel_intern(&self, msg: &msgs::UnsignedChannelUpdate, full_msg: Option<&msgs::ChannelUpdate>, sig: Option<&secp256k1::ecdsa::Signature>) -> Result<(), LightningError> { + fn update_channel_internal(&self, msg: &msgs::UnsignedChannelUpdate, + full_msg: Option<&msgs::ChannelUpdate>, sig: Option<&secp256k1::ecdsa::Signature>, + only_verify: bool) -> Result<(), LightningError> + { let chan_enabled = msg.flags & (1 << 1) != (1 << 1); - if msg.chain_hash != self.genesis_hash { + if msg.chain_hash != self.chain_hash { return Err(LightningError { err: "Channel update chain hash does not match genesis hash".to_owned(), action: ErrorAction::IgnoreAndLog(Level::Debug), @@ -1960,7 +1976,9 @@ impl NetworkGraph where L::Target: Logger { action: ErrorAction::IgnoreAndLog(Level::Debug) })?, "channel_update"); } - channel.two_to_one = get_new_channel_info!(); + if !only_verify { + channel.two_to_one = get_new_channel_info!(); + } } else { check_update_latest!(channel.one_to_two); if let Some(sig) = sig { @@ -1969,7 +1987,9 @@ impl NetworkGraph where L::Target: Logger { action: ErrorAction::IgnoreAndLog(Level::Debug) })?, "channel_update"); } - channel.one_to_two = get_new_channel_info!(); + if !only_verify { + channel.one_to_two = get_new_channel_info!(); + } } } } @@ -2038,7 +2058,7 @@ impl ReadOnlyNetworkGraph<'_> { /// Get network addresses by node id. /// Returns None if the requested node is completely unknown, /// or if node announcement for the node was never received. - pub fn get_addresses(&self, pubkey: &PublicKey) -> Option> { + pub fn get_addresses(&self, pubkey: &PublicKey) -> Option> { self.nodes.get(&NodeId::from_pubkey(&pubkey)) .and_then(|node| node.announcement_info.as_ref().map(|ann| ann.addresses().to_vec())) } @@ -2067,7 +2087,7 @@ pub(crate) mod tests { use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; - use bitcoin::blockdata::constants::genesis_block; + use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::TxOut; @@ -2139,7 +2159,7 @@ pub(crate) mod tests { let mut unsigned_announcement = UnsignedChannelAnnouncement { features: channelmanager::provided_channel_features(&UserConfig::default()), - chain_hash: genesis_block(Network::Testnet).header.block_hash(), + chain_hash: ChainHash::using_genesis_block(Network::Testnet), short_channel_id: 0, node_id_1: NodeId::from_pubkey(&node_id_1), node_id_2: NodeId::from_pubkey(&node_id_2), @@ -2167,7 +2187,7 @@ pub(crate) mod tests { pub(crate) fn get_signed_channel_update(f: F, node_key: &SecretKey, secp_ctx: &Secp256k1) -> ChannelUpdate { let mut unsigned_channel_update = UnsignedChannelUpdate { - chain_hash: genesis_block(Network::Testnet).header.block_hash(), + chain_hash: ChainHash::using_genesis_block(Network::Testnet), short_channel_id: 0, timestamp: 100, flags: 0, @@ -2372,7 +2392,7 @@ pub(crate) mod tests { // Test that channel announcements with the wrong chain hash are ignored (network graph is testnet, // announcement is mainnet). let incorrect_chain_announcement = get_signed_channel_announcement(|unsigned_announcement| { - unsigned_announcement.chain_hash = genesis_block(Network::Bitcoin).header.block_hash(); + unsigned_announcement.chain_hash = ChainHash::using_genesis_block(Network::Bitcoin); }, node_1_privkey, node_2_privkey, &secp_ctx); match gossip_sync.handle_channel_announcement(&incorrect_chain_announcement) { Ok(_) => panic!(), @@ -2410,6 +2430,7 @@ pub(crate) mod tests { } let valid_channel_update = get_signed_channel_update(|_| {}, node_1_privkey, &secp_ctx); + network_graph.verify_channel_update(&valid_channel_update).unwrap(); match gossip_sync.handle_channel_update(&valid_channel_update) { Ok(res) => assert!(res), _ => panic!(), @@ -2486,7 +2507,7 @@ pub(crate) mod tests { // Test that channel updates with the wrong chain hash are ignored (network graph is testnet, channel // update is mainet). let incorrect_chain_update = get_signed_channel_update(|unsigned_channel_update| { - unsigned_channel_update.chain_hash = genesis_block(Network::Bitcoin).header.block_hash(); + unsigned_channel_update.chain_hash = ChainHash::using_genesis_block(Network::Bitcoin); }, node_1_privkey, &secp_ctx); match gossip_sync.handle_channel_update(&incorrect_chain_update) { @@ -2512,7 +2533,8 @@ pub(crate) mod tests { let short_channel_id; { - // Announce a channel we will update + // Check we won't apply an update via `handle_network_update` for privacy reasons, but + // can continue fine if we manually apply it. let valid_channel_announcement = get_signed_channel_announcement(|_| {}, node_1_privkey, node_2_privkey, &secp_ctx); short_channel_id = valid_channel_announcement.contents.short_channel_id; let chain_source: Option<&test_utils::TestChainSource> = None; @@ -2523,10 +2545,11 @@ pub(crate) mod tests { assert!(network_graph.read_only().channels().get(&short_channel_id).unwrap().one_to_two.is_none()); network_graph.handle_network_update(&NetworkUpdate::ChannelUpdateMessage { - msg: valid_channel_update, + msg: valid_channel_update.clone(), }); - assert!(network_graph.read_only().channels().get(&short_channel_id).unwrap().one_to_two.is_some()); + assert!(network_graph.read_only().channels().get(&short_channel_id).unwrap().one_to_two.is_none()); + network_graph.update_channel(&valid_channel_update).unwrap(); } // Non-permanent failure doesn't touch the channel at all @@ -2924,7 +2947,7 @@ pub(crate) mod tests { let node_privkey_1 = &SecretKey::from_slice(&[42; 32]).unwrap(); let node_id_1 = PublicKey::from_secret_key(&secp_ctx, node_privkey_1); - let chain_hash = genesis_block(Network::Testnet).header.block_hash(); + let chain_hash = ChainHash::using_genesis_block(Network::Testnet); // It should ignore if gossip_queries feature is not enabled { @@ -2961,7 +2984,7 @@ pub(crate) mod tests { let network_graph = create_network_graph(); let (secp_ctx, gossip_sync) = create_gossip_sync(&network_graph); - let chain_hash = genesis_block(Network::Testnet).header.block_hash(); + let chain_hash = ChainHash::using_genesis_block(Network::Testnet); let node_1_privkey = &SecretKey::from_slice(&[42; 32]).unwrap(); let node_2_privkey = &SecretKey::from_slice(&[41; 32]).unwrap(); let node_id_2 = PublicKey::from_secret_key(&secp_ctx, node_2_privkey); @@ -3013,13 +3036,13 @@ pub(crate) mod tests { &gossip_sync, &node_id_2, QueryChannelRange { - chain_hash: genesis_block(Network::Bitcoin).header.block_hash(), + chain_hash: ChainHash::using_genesis_block(Network::Bitcoin), first_blocknum: 0, number_of_blocks: 0xffff_ffff, }, false, vec![ReplyChannelRange { - chain_hash: genesis_block(Network::Bitcoin).header.block_hash(), + chain_hash: ChainHash::using_genesis_block(Network::Bitcoin), first_blocknum: 0, number_of_blocks: 0xffff_ffff, sync_complete: true, @@ -3254,7 +3277,7 @@ pub(crate) mod tests { let node_privkey = &SecretKey::from_slice(&[41; 32]).unwrap(); let node_id = PublicKey::from_secret_key(&secp_ctx, node_privkey); - let chain_hash = genesis_block(Network::Testnet).header.block_hash(); + let chain_hash = ChainHash::using_genesis_block(Network::Testnet); let result = gossip_sync.handle_query_short_channel_ids(&node_id, QueryShortChannelIds { chain_hash, @@ -3429,6 +3452,12 @@ pub(crate) mod tests { // This serialized info has an address field but no announcement_message, therefore the addresses returned by our function will still be empty assert!(ann_info_with_addresses.addresses().is_empty()); } + + #[test] + fn test_node_id_display() { + let node_id = NodeId([42; 33]); + assert_eq!(format!("{}", &node_id), "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"); + } } #[cfg(ldk_bench)]