diff --git a/examples/custom_gossip_example.rs b/examples/custom_gossip_example.rs new file mode 100644 index 000000000..7b1b0076b --- /dev/null +++ b/examples/custom_gossip_example.rs @@ -0,0 +1,158 @@ +// Example demonstrating how to use custom gossip metadata in LDK Node + +use ldk_node::{Builder, Event, Node}; +use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::bitcoin::Network; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +fn main() -> Result<(), Box> { + // Create and configure a node with custom gossip enabled + let mut builder = Builder::new(); + builder.set_network(Network::Testnet); + builder.set_chain_source_esplora("https://blockstream.info/testnet/api".to_string(), None); + builder.set_gossip_source_rgs("https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string()); + + // Enable custom gossip functionality + builder.enable_custom_gossip(); + + let node_entropy = NodeEntropy::from_bip39_mnemonic(generate_entropy_mnemonic(None), None); + let node = builder.build(node_entropy)?; + + // Start the node + node.start()?; + + // Get the custom gossip handler + if let Some(custom_gossip) = node.custom_gossip() { + println!("Custom gossip handler is available!"); + + // Set our own metadata to advertise + let our_metadata = serde_json::json!({ + "version": "1.0", + "features": ["feature_a", "feature_b"], + "timestamp": chrono::Utc::now().timestamp(), + "description": "LDK Node with custom features" + }).to_string().into_bytes(); + + custom_gossip.set_our_metadata(our_metadata); + + // Example: Send custom metadata to a specific peer + // (This would typically be done after connecting to a peer) + let peer_metadata = serde_json::json!({ + "message": "Hello from custom gossip!", + "data": { + "custom_field": "custom_value" + } + }).to_string().into_bytes(); + + // In a real scenario, you'd have connected peers + // custom_gossip.send_metadata_to_peer(peer_node_id, peer_metadata); + + // Example: Get stored metadata for all nodes + let all_metadata = custom_gossip.get_all_metadata().clone(); + println!("Currently have metadata for {} nodes", all_metadata.len()); + + // Print metadata information + for (node_id, metadata) in all_metadata.clone() { + println!("Node {}: {} bytes of metadata", node_id, metadata.metadata.len()); + + // Try to parse as JSON + if let Ok(json_str) = String::from_utf8(metadata.metadata.clone()) { + if let Ok(json_value) = serde_json::from_str::(&json_str) { + println!(" Parsed JSON: {}", json_value); + } + } + } + + // Demonstrate event handling with custom gossip + println!("Monitoring for custom gossip events..."); + + // In a real application, you would handle events in a loop + // This is just a demonstration + for _ in 0..5 { + // Wait for events (timeout after 1 second) + std::thread::sleep(Duration::from_secs(1)); + + // In a real application, you would process events like this: + // match node.wait_next_event() { + // Event::... => { + // // Handle other events + // } + // // Custom gossip events would be handled through the custom_gossip handler + // // as they are processed automatically when messages are received + // } + } + + // Example: Check if we received any new metadata + let updated_metadata = custom_gossip.get_all_metadata(); + if updated_metadata.len() > all_metadata.clone().len() { + println!("Received new metadata from {} nodes", + updated_metadata.len() - all_metadata.len()); + } + + } else { + println!("Custom gossip not enabled. Use builder.enable_custom_gossip() to enable it."); + } + + // Stop the node + node.stop()?; + + peer_to_peer_example()?; + + Ok(()) +} + +/// Example of how to integrate custom gossip in a peer-to-peer scenario +#[allow(dead_code)] +fn peer_to_peer_example() -> Result<(), Box> { + // Create two nodes for demonstration + let mut builder1 = Builder::new(); + builder1.set_network(Network::Regtest); + builder1.enable_custom_gossip(); + let node1 = + builder1.build(NodeEntropy::from_bip39_mnemonic(generate_entropy_mnemonic(None), None))?; + + let mut builder2 = Builder::new(); + builder2.set_network(Network::Regtest); + builder2.enable_custom_gossip(); + let node2 = + builder2.build(NodeEntropy::from_bip39_mnemonic(generate_entropy_mnemonic(None), None))?; + + // Start both nodes + node1.start()?; + node2.start()?; + + // Get custom gossip handlers + let gossip1 = node1.custom_gossip().unwrap(); + let gossip2 = node2.custom_gossip().unwrap(); + + // Set metadata for each node + let metadata1 = b"Node 1 custom data".to_vec(); + let metadata2 = b"Node 2 custom data".to_vec(); + + gossip1.set_our_metadata(metadata1); + gossip2.set_our_metadata(metadata2); + + // In a real scenario, you would: + // 1. Connect the nodes to each other + // 2. Custom metadata would be automatically exchanged when peers connect + // 3. Monitor the get_all_metadata() results to see received data + + println!("Peer-to-peer custom gossip example completed"); + + // Stop nodes + node1.stop()?; + node2.stop()?; + + Ok(()) +} + +/// Example showing custom feature flags (placeholder for future implementation) +#[allow(dead_code)] +fn custom_features_example() { + println!("Custom feature flags would be implemented in the provided_node_features() method"); + println!("This allows advertising custom capabilities to peers during connection"); +} \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index c88c867cc..0d7e920dd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -71,6 +71,7 @@ use crate::io::{ use crate::liquidity::{ LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, }; +use crate::custom_gossip::CustomGossipMessageHandler; use crate::lnurl_auth::LnurlAuth; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; @@ -292,6 +293,7 @@ pub struct NodeBuilder { runtime_handle: Option, pathfinding_scores_sync_config: Option, recovery_mode: bool, + custom_gossip_enabled: bool, } impl NodeBuilder { @@ -310,6 +312,7 @@ impl NodeBuilder { let runtime_handle = None; let pathfinding_scores_sync_config = None; let recovery_mode = false; + let custom_gossip_enabled = false; Self { config, chain_data_source_config, @@ -320,6 +323,7 @@ impl NodeBuilder { async_payments_role: None, pathfinding_scores_sync_config, recovery_mode, + custom_gossip_enabled, } } @@ -500,6 +504,18 @@ impl NodeBuilder { self } + /// Enables custom gossip message support for the [`Node`] instance. + /// + /// When enabled, the node will be able to send and receive custom gossip messages + /// containing metadata extensions to the standard Lightning gossip protocol. + /// + /// Custom gossip messages use message type 32769 and can contain arbitrary metadata + /// up to 4096 bytes in length. + pub fn enable_custom_gossip(&mut self) -> &mut Self { + self.custom_gossip_enabled = true; + self + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&mut self, storage_dir_path: String) -> &mut Self { self.config.storage_dir_path = storage_dir_path; @@ -863,6 +879,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.custom_gossip_enabled, self.pathfinding_scores_sync_config.as_ref(), self.async_payments_role, self.recovery_mode, @@ -1070,6 +1087,17 @@ impl ArcedNodeBuilder { self.inner.write().expect("lock").set_liquidity_provider_lsps2(service_config); } + /// Enables custom gossip message support for the [`Node`] instance. + /// + /// When enabled, the node will be able to send and receive custom gossip messages + /// containing metadata extensions to the standard Lightning gossip protocol. + /// + /// Custom gossip messages use message type 32769 and can contain arbitrary metadata + /// up to 4096 bytes in length. + pub fn enable_custom_gossip(&self) { + self.inner.write().unwrap().enable_custom_gossip(); + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&self, storage_dir_path: String) { self.inner.write().expect("lock").set_storage_dir_path(storage_dir_path); @@ -1358,6 +1386,7 @@ fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, liquidity_source_config: Option<&LiquiditySourceConfig>, + custom_gossip_enabled: bool, pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>, async_payments_role: Option, recovery_mode: bool, seed_bytes: [u8; 64], runtime: Arc, logger: Arc, kv_store: Arc, @@ -1975,55 +2004,130 @@ fn build_with_store_internal( }, }; - let (liquidity_source, custom_message_handler) = - if let Some(lsc) = liquidity_source_config.as_ref() { - let mut liquidity_source_builder = LiquiditySourceBuilder::new( - Arc::clone(&wallet), - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - Arc::clone(&tx_broadcaster), - Arc::clone(&kv_store), - Arc::clone(&config), - Arc::clone(&logger), - ); - - lsc.lsps1_client.as_ref().map(|config| { - liquidity_source_builder.lsps1_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); - - lsc.lsps2_client.as_ref().map(|config| { - liquidity_source_builder.lsps2_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + let (liquidity_source, custom_message_handler) = { + let has_liquidity = liquidity_source_config.is_some(); + let has_custom_gossip = custom_gossip_enabled; + + match (has_liquidity, has_custom_gossip) { + (true, true) => { + // Both liquidity and custom gossip enabled + let lsc = liquidity_source_config.as_ref().unwrap(); + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + ); - let promise_secret = { - let lsps_xpriv = derive_xprv( + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); + + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); + + let promise_secret = { + let lsps_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + LSPS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + lsps_xpriv.private_key.secret_bytes() + }; + lsc.lsps2_service.as_ref().map(|config| { + liquidity_source_builder.lsps2_service(promise_secret, config.clone()) + }); + + let liquidity_source = runtime.block_on(async move { + liquidity_source_builder.build().await.map(Arc::new) + })?; + let gossip_handler = Arc::new(CustomGossipMessageHandler::new(Arc::clone(&logger))); + let custom_message_handler = Arc::new(NodeCustomMessageHandler::new_combined( + Arc::clone(&liquidity_source), + gossip_handler, + )); + (Some(liquidity_source), custom_message_handler) + }, + (true, false) => { + // Only liquidity enabled + let lsc = liquidity_source_config.as_ref().unwrap(); + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), Arc::clone(&config), - &seed_bytes, - LSPS_HARDENED_CHILD_INDEX, Arc::clone(&logger), - )?; - lsps_xpriv.private_key.secret_bytes() - }; - lsc.lsps2_service.as_ref().map(|config| { - liquidity_source_builder.lsps2_service(promise_secret, config.clone()) - }); - - let liquidity_source = runtime - .block_on(async move { liquidity_source_builder.build().await.map(Arc::new) })?; - let custom_message_handler = - Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); - (Some(liquidity_source), custom_message_handler) - } else { - (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) - }; + ); + + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); + + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); + + let promise_secret = { + let lsps_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + LSPS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + lsps_xpriv.private_key.secret_bytes() + }; + lsc.lsps2_service.as_ref().map(|config| { + liquidity_source_builder.lsps2_service(promise_secret, config.clone()) + }); + + let liquidity_source = runtime.block_on(async move { + liquidity_source_builder.build().await.map(Arc::new) + })?; + let custom_message_handler = Arc::new(NodeCustomMessageHandler::new_liquidity( + Arc::clone(&liquidity_source) + )); + (Some(liquidity_source), custom_message_handler) + }, + (false, true) => { + // Only custom gossip enabled + let gossip_handler = Arc::new(CustomGossipMessageHandler::new(Arc::clone(&logger))); + let custom_message_handler = Arc::new(NodeCustomMessageHandler::new_custom_gossip( + gossip_handler + )); + (None, custom_message_handler) + }, + (false, false) => { + // Neither enabled + (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) + } + } + }; + + // Extract custom gossip handler for later use + let custom_gossip_handler = custom_message_handler.custom_gossip_handler(); let msg_handler = match gossip_source.as_gossip_sync() { GossipSync::P2P(p2p_gossip_sync) => MessageHandler { @@ -2173,6 +2277,7 @@ fn build_with_store_internal( gossip_source, pathfinding_scores_sync_url, liquidity_source, + custom_gossip_handler, kv_store, logger, _router: router, diff --git a/src/custom_gossip.rs b/src/custom_gossip.rs new file mode 100644 index 000000000..d60178ab8 --- /dev/null +++ b/src/custom_gossip.rs @@ -0,0 +1,320 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +//! Custom gossip message handling for extending P2P gossip sync with custom metadata. + +use crate::logger::{log_debug, log_trace}; +use lightning::util::logger::Logger as LightningLogger; + +use lightning::io::{self, Read}; +use lightning::ln::msgs::LightningError; +use lightning::ln::peer_handler::CustomMessageHandler; +use lightning::ln::wire::CustomMessageReader; +use lightning::ln::wire::Type; +use lightning::util::ser::{Readable, Writeable, Writer}; +use lightning_types::features::{InitFeatures, NodeFeatures}; + +use bitcoin::secp256k1::PublicKey; + +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; + +/// Custom message type for gossip metadata extensions +pub const CUSTOM_GOSSIP_MESSAGE_TYPE: u16 = 32769; // Odd number in custom range + +/// Custom gossip message containing metadata extensions +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CustomGossipMessage { + /// The metadata payload + pub metadata: Vec, +} + +impl CustomGossipMessage { + /// Create a new custom gossip message with the given metadata + pub fn new(metadata: Vec) -> Self { + Self { metadata } + } + + /// Get the metadata payload + pub fn metadata(&self) -> &[u8] { + &self.metadata + } +} + +impl Type for CustomGossipMessage { + fn type_id(&self) -> u16 { + CUSTOM_GOSSIP_MESSAGE_TYPE + } +} + +impl Writeable for CustomGossipMessage { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + // Write length prefix (u16) followed by the metadata + (self.metadata.len() as u16).write(writer)?; + writer.write_all(&self.metadata) + } +} + +impl Readable for CustomGossipMessage { + fn read(reader: &mut R) -> Result { + let length = ::read(reader)? as usize; + + // Limit metadata size to prevent DoS attacks + if length > 4096 { + return Err(lightning::ln::msgs::DecodeError::InvalidValue); + } + + let mut metadata = vec![0u8; length]; + reader.read_exact(&mut metadata).map_err(|_| { + lightning::ln::msgs::DecodeError::ShortRead + })?; + + Ok(Self { metadata }) + } +} + +/// Metadata entry for a node +#[derive(Clone, Debug)] +pub struct NodeMetadata { + /// Node's public key + pub node_id: PublicKey, + /// Custom metadata payload + pub metadata: Vec, + /// Timestamp when metadata was received + pub timestamp: u32, +} + +/// Handler for custom gossip messages +pub struct CustomGossipMessageHandler +where + L::Target: LightningLogger, +{ + /// Logger instance + logger: L, + /// Store for node metadata + node_metadata: Arc>>, + /// Pending messages to send + pending_messages: Arc>>, + /// Our own metadata to advertise + our_metadata: Arc>>>, +} + +impl CustomGossipMessageHandler +where + L::Target: LightningLogger, +{ + /// Create a new custom gossip message handler + pub fn new(logger: L) -> Self { + Self { + logger, + node_metadata: Arc::new(Mutex::new(HashMap::new())), + pending_messages: Arc::new(Mutex::new(Vec::new())), + our_metadata: Arc::new(Mutex::new(None)), + } + } + + /// Set our own metadata to advertise to peers + pub fn set_our_metadata(&self, metadata: Vec) { + let mut our_metadata = self.our_metadata.lock().unwrap(); + *our_metadata = Some(metadata); + } + + /// Get metadata for a specific node + pub fn get_node_metadata(&self, node_id: &PublicKey) -> Option { + let metadata_store = self.node_metadata.lock().unwrap(); + metadata_store.get(node_id).cloned() + } + + /// Get all stored node metadata + pub fn get_all_metadata(&self) -> HashMap { + let metadata_store = self.node_metadata.lock().unwrap(); + metadata_store.clone() + } + + /// Send custom metadata to a specific peer + pub fn send_metadata_to_peer(&self, peer_node_id: PublicKey, metadata: Vec) { + let message = CustomGossipMessage::new(metadata); + let mut pending = self.pending_messages.lock().unwrap(); + pending.push((peer_node_id, message)); + } + + /// Broadcast our metadata to all peers + pub fn broadcast_our_metadata(&self, peer_node_ids: Vec) { + let our_metadata = self.our_metadata.lock().unwrap(); + if let Some(ref metadata) = *our_metadata { + let message = CustomGossipMessage::new(metadata.clone()); + let mut pending = self.pending_messages.lock().unwrap(); + + for node_id in peer_node_ids { + pending.push((node_id, message.clone())); + } + } + } + + /// Handle received custom gossip message + fn handle_gossip_message(&self, msg: &CustomGossipMessage, sender_node_id: PublicKey) { + log_debug!( + self.logger, + "Received custom gossip metadata from {}: {} bytes", + sender_node_id, + msg.metadata.len() + ); + + let metadata_entry = NodeMetadata { + node_id: sender_node_id, + metadata: msg.metadata.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as u32, + }; + + let mut metadata_store = self.node_metadata.lock().unwrap(); + metadata_store.insert(sender_node_id, metadata_entry); + + log_trace!( + self.logger, + "Stored metadata for node {}, total nodes: {}", + sender_node_id, + metadata_store.len() + ); + } +} + +impl CustomMessageReader for CustomGossipMessageHandler +where + L::Target: LightningLogger, +{ + type CustomMessage = CustomGossipMessage; + + fn read( + &self, message_type: u16, buffer: &mut RD, + ) -> Result, lightning::ln::msgs::DecodeError> { + if message_type == CUSTOM_GOSSIP_MESSAGE_TYPE { + log_trace!(self.logger, "Reading custom gossip message type {}", message_type); + Ok(Some(CustomGossipMessage::read(buffer)?)) + } else { + Ok(None) + } + } +} + +impl CustomMessageHandler for CustomGossipMessageHandler +where + L::Target: LightningLogger, +{ + fn handle_custom_message( + &self, msg: Self::CustomMessage, sender_node_id: PublicKey, + ) -> Result<(), LightningError> { + self.handle_gossip_message(&msg, sender_node_id); + Ok(()) + } + + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Self::CustomMessage)> { + let mut pending = self.pending_messages.lock().unwrap(); + std::mem::take(&mut *pending) + } + + fn provided_node_features(&self) -> NodeFeatures { + // Advertise that we support custom gossip messages + // You can extend this to include specific feature flags + NodeFeatures::empty() + } + + fn provided_init_features(&self, _their_node_id: PublicKey) -> InitFeatures { + // Advertise init features for custom gossip support + InitFeatures::empty() + } + + fn peer_connected( + &self, their_node_id: PublicKey, _msg: &lightning::ln::msgs::Init, _inbound: bool, + ) -> Result<(), ()> { + log_debug!(self.logger, "Peer {} connected, will broadcast our metadata", their_node_id); + + // Optionally broadcast our metadata when a peer connects + let our_metadata = self.our_metadata.lock().unwrap(); + if let Some(ref metadata) = *our_metadata { + let message = CustomGossipMessage::new(metadata.clone()); + let mut pending = self.pending_messages.lock().unwrap(); + pending.push((their_node_id, message)); + } + + Ok(()) + } + + fn peer_disconnected(&self, their_node_id: PublicKey) { + log_debug!(self.logger, "Peer {} disconnected", their_node_id); + // Optionally clean up metadata for disconnected peers + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::logger::Logger; + use bitcoin::secp256k1::{Secp256k1, SecretKey}; + use lightning::util::ser::{Readable, Writeable}; + use std::io::Cursor; + use std::sync::Arc; + + #[test] + fn test_custom_gossip_message_serialization() { + let metadata = b"custom_metadata_payload".to_vec(); + let msg = CustomGossipMessage::new(metadata.clone()); + + assert_eq!(msg.metadata(), &metadata); + assert_eq!(msg.type_id(), CUSTOM_GOSSIP_MESSAGE_TYPE); + + // Test serialization + let mut buffer = Vec::new(); + msg.write(&mut buffer).unwrap(); + + // Test deserialization + let mut cursor = Cursor::new(buffer); + let deserialized = CustomGossipMessage::read(&mut cursor).unwrap(); + + assert_eq!(msg, deserialized); + } + + #[test] + fn test_custom_gossip_handler() { + let logger = Arc::new(Logger::new_log_facade()); + let handler = CustomGossipMessageHandler::new(logger); + + // Test setting our metadata + let our_metadata = b"our_node_metadata".to_vec(); + handler.set_our_metadata(our_metadata.clone()); + + // Test handling a message + let secp_ctx = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[1; 32]).unwrap(); + let sender_node_id = PublicKey::from_secret_key(&secp_ctx, &secret_key); + + let msg = CustomGossipMessage::new(b"peer_metadata".to_vec()); + handler.handle_custom_message(msg, sender_node_id).unwrap(); + + // Verify metadata was stored + let stored_metadata = handler.get_node_metadata(&sender_node_id).unwrap(); + assert_eq!(stored_metadata.metadata, b"peer_metadata"); + assert_eq!(stored_metadata.node_id, sender_node_id); + } + + #[test] + fn test_message_size_limit() { + let large_metadata = vec![0u8; 5000]; // Exceeds 4096 byte limit + let msg = CustomGossipMessage::new(large_metadata); + + let mut buffer = Vec::new(); + msg.write(&mut buffer).unwrap(); + + let mut cursor = Cursor::new(buffer); + let result = CustomGossipMessage::read(&mut cursor); + + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7ed69031c..1e311cebb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ mod builder; mod chain; pub mod config; mod connection; +pub mod custom_gossip; mod data_store; pub mod entropy; mod error; @@ -134,6 +135,7 @@ use config::{ RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; +use custom_gossip::CustomGossipMessageHandler; pub use error::Error as NodeError; use error::Error; pub use event::Event; @@ -235,6 +237,7 @@ pub struct Node { gossip_source: Arc, pathfinding_scores_sync_url: Option, liquidity_source: Option>>>, + custom_gossip_handler: Option>>>, kv_store: Arc, logger: Arc, _router: Arc, @@ -884,6 +887,13 @@ impl Node { self.config.node_alias } + /// Expose the process_events method from the peer manager. + /// + /// This is used to process events from the peer manager. + pub fn process_events(&self) { + self.peer_manager.process_events(); + } + /// Returns a payment handler allowing to create and pay [BOLT 11] invoices. /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md @@ -1068,6 +1078,30 @@ impl Node { }) } + /// Returns a custom gossip handler allowing to send and receive custom gossip messages. + /// + /// This returns `None` if custom gossip was not enabled during node construction. + /// To enable custom gossip, call [`Builder::enable_custom_gossip`] before building the node. + /// + /// Custom gossip messages can contain arbitrary metadata up to 4096 bytes in length + /// and use message type 32769 to extend the Lightning gossip protocol. + #[cfg(not(feature = "uniffi"))] + pub fn custom_gossip(&self) -> Option<&CustomGossipMessageHandler>> { + self.custom_gossip_handler.as_ref().map(|h| h.as_ref()) + } + + /// Returns a custom gossip handler allowing to send and receive custom gossip messages. + /// + /// This returns `None` if custom gossip was not enabled during node construction. + /// To enable custom gossip, call [`Builder::enable_custom_gossip`] before building the node. + /// + /// Custom gossip messages can contain arbitrary metadata up to 4096 bytes in length + /// and use message type 32769 to extend the Lightning gossip protocol. + #[cfg(feature = "uniffi")] + pub fn custom_gossip(&self) -> Option>>> { + self.custom_gossip_handler.clone() + } + /// Returns a liquidity handler allowing to request channels via the [bLIP-51 / LSPS1] protocol. /// /// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md diff --git a/src/logger.rs b/src/logger.rs index 3ef939b6d..6af3780ef 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -251,7 +251,8 @@ impl LogWriter for Writer { } } -pub(crate) struct Logger { +/// A logger for LDK Node that can write to files, the log facade, or custom writers. +pub struct Logger { /// Specifies the logger's writer. writer: Writer, } @@ -275,10 +276,12 @@ impl Logger { Ok(Self { writer: Writer::FileWriter { file_path, max_log_level } }) } + /// Creates a new logger that forwards logs to the `log` facade. pub fn new_log_facade() -> Self { Self { writer: Writer::LogFacadeWriter } } + /// Creates a new logger with a custom writer. pub fn new_custom_writer(log_writer: Arc) -> Self { Self { writer: Writer::CustomWriter(log_writer) } } diff --git a/src/message_handler.rs b/src/message_handler.rs index fc206ec4d..20f318dcf 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -8,6 +8,8 @@ use std::ops::Deref; use std::sync::Arc; +use crate::custom_gossip::{CustomGossipMessage, CustomGossipMessageHandler}; + use bitcoin::secp256k1::PublicKey; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; @@ -24,6 +26,11 @@ where { Ignoring, Liquidity { liquidity_source: Arc> }, + CustomGossip { gossip_handler: Arc> }, + Combined { + liquidity_source: Arc>, + gossip_handler: Arc>, + }, } impl NodeCustomMessageHandler @@ -37,13 +44,58 @@ where pub(crate) fn new_ignoring() -> Self { Self::Ignoring } + + pub(crate) fn new_custom_gossip(gossip_handler: Arc>) -> Self { + Self::CustomGossip { gossip_handler } + } + + pub(crate) fn new_combined( + liquidity_source: Arc>, + gossip_handler: Arc>, + ) -> Self { + Self::Combined { liquidity_source, gossip_handler } + } + + /// Returns the custom gossip handler if available + pub(crate) fn custom_gossip_handler(&self) -> Option>> { + match self { + Self::CustomGossip { gossip_handler } => Some(Arc::clone(gossip_handler)), + Self::Combined { gossip_handler, .. } => Some(Arc::clone(gossip_handler)), + _ => None, + } + } +} + +/// Combined custom message type that can handle both LSPS and custom gossip messages +#[derive(Clone, Debug)] +pub(crate) enum NodeCustomMessage { + Lsps(RawLSPSMessage), + CustomGossip(CustomGossipMessage), +} + +impl lightning::ln::wire::Type for NodeCustomMessage { + fn type_id(&self) -> u16 { + match self { + Self::Lsps(msg) => msg.type_id(), + Self::CustomGossip(msg) => msg.type_id(), + } + } +} + +impl lightning::util::ser::Writeable for NodeCustomMessage { + fn write(&self, writer: &mut W) -> Result<(), lightning::io::Error> { + match self { + Self::Lsps(msg) => msg.write(writer), + Self::CustomGossip(msg) => msg.write(writer), + } + } } impl CustomMessageReader for NodeCustomMessageHandler where L::Target: Logger, { - type CustomMessage = RawLSPSMessage; + type CustomMessage = NodeCustomMessage; fn read( &self, message_type: u16, buffer: &mut RD, @@ -51,7 +103,28 @@ where match self { Self::Ignoring => Ok(None), Self::Liquidity { liquidity_source, .. } => { - liquidity_source.liquidity_manager().read(message_type, buffer) + if let Ok(Some(lsps_msg)) = liquidity_source.liquidity_manager().read(message_type, buffer) { + Ok(Some(NodeCustomMessage::Lsps(lsps_msg))) + } else { + Ok(None) + } + }, + Self::CustomGossip { gossip_handler, .. } => { + if let Ok(Some(gossip_msg)) = gossip_handler.read(message_type, buffer) { + Ok(Some(NodeCustomMessage::CustomGossip(gossip_msg))) + } else { + Ok(None) + } + }, + Self::Combined { liquidity_source, gossip_handler } => { + // Try LSPS first, then custom gossip + if let Ok(Some(lsps_msg)) = liquidity_source.liquidity_manager().read(message_type, buffer) { + Ok(Some(NodeCustomMessage::Lsps(lsps_msg))) + } else if let Ok(Some(gossip_msg)) = gossip_handler.read(message_type, buffer) { + Ok(Some(NodeCustomMessage::CustomGossip(gossip_msg))) + } else { + Ok(None) + } }, } } @@ -67,7 +140,36 @@ where match self { Self::Ignoring => Ok(()), // Should be unreachable!() as the reader will return `None` Self::Liquidity { liquidity_source, .. } => { - liquidity_source.liquidity_manager().handle_custom_message(msg, sender_node_id) + match msg { + NodeCustomMessage::Lsps(lsps_msg) => { + liquidity_source.liquidity_manager().handle_custom_message(lsps_msg, sender_node_id) + }, + NodeCustomMessage::CustomGossip(_) => { + // Ignoring custom gossip in liquidity-only mode + Ok(()) + }, + } + }, + Self::CustomGossip { gossip_handler, .. } => { + match msg { + NodeCustomMessage::CustomGossip(gossip_msg) => { + gossip_handler.handle_custom_message(gossip_msg, sender_node_id) + }, + NodeCustomMessage::Lsps(_) => { + // Ignoring LSPS in gossip-only mode + Ok(()) + }, + } + }, + Self::Combined { liquidity_source, gossip_handler } => { + match msg { + NodeCustomMessage::Lsps(lsps_msg) => { + liquidity_source.liquidity_manager().handle_custom_message(lsps_msg, sender_node_id) + }, + NodeCustomMessage::CustomGossip(gossip_msg) => { + gossip_handler.handle_custom_message(gossip_msg, sender_node_id) + }, + } }, } } @@ -77,6 +179,34 @@ where Self::Ignoring => Vec::new(), Self::Liquidity { liquidity_source, .. } => { liquidity_source.liquidity_manager().get_and_clear_pending_msg() + .into_iter() + .map(|(node_id, msg)| (node_id, NodeCustomMessage::Lsps(msg))) + .collect() + }, + Self::CustomGossip { gossip_handler, .. } => { + gossip_handler.get_and_clear_pending_msg() + .into_iter() + .map(|(node_id, msg)| (node_id, NodeCustomMessage::CustomGossip(msg))) + .collect() + }, + Self::Combined { liquidity_source, gossip_handler } => { + let mut pending = Vec::new(); + + // Get LSPS messages + pending.extend( + liquidity_source.liquidity_manager().get_and_clear_pending_msg() + .into_iter() + .map(|(node_id, msg)| (node_id, NodeCustomMessage::Lsps(msg))) + ); + + // Get custom gossip messages + pending.extend( + gossip_handler.get_and_clear_pending_msg() + .into_iter() + .map(|(node_id, msg)| (node_id, NodeCustomMessage::CustomGossip(msg))) + ); + + pending }, } } @@ -87,6 +217,17 @@ where Self::Liquidity { liquidity_source, .. } => { liquidity_source.liquidity_manager().provided_node_features() }, + Self::CustomGossip { gossip_handler, .. } => { + gossip_handler.provided_node_features() + }, + Self::Combined { liquidity_source, gossip_handler } => { + // Combine features from both handlers + let features = liquidity_source.liquidity_manager().provided_node_features(); + let _gossip_features = gossip_handler.provided_node_features(); + // Note: In a real implementation, you'd need to properly merge features + // For now, we'll use the liquidity features as base + features + }, } } @@ -96,6 +237,17 @@ where Self::Liquidity { liquidity_source, .. } => { liquidity_source.liquidity_manager().provided_init_features(their_node_id) }, + Self::CustomGossip { gossip_handler, .. } => { + gossip_handler.provided_init_features(their_node_id) + }, + Self::Combined { liquidity_source, gossip_handler } => { + // Combine init features from both handlers + let features = liquidity_source.liquidity_manager().provided_init_features(their_node_id); + let _gossip_features = gossip_handler.provided_init_features(their_node_id); + // Note: In a real implementation, you'd need to properly merge features + // For now, we'll use the liquidity features as base + features + }, } } @@ -107,6 +259,14 @@ where Self::Liquidity { liquidity_source, .. } => { liquidity_source.liquidity_manager().peer_connected(their_node_id, msg, inbound) }, + Self::CustomGossip { gossip_handler, .. } => { + gossip_handler.peer_connected(their_node_id, msg, inbound) + }, + Self::Combined { liquidity_source, gossip_handler } => { + // Notify both handlers + let _ = liquidity_source.liquidity_manager().peer_connected(their_node_id, msg, inbound); + gossip_handler.peer_connected(their_node_id, msg, inbound) + }, } } @@ -116,6 +276,14 @@ where Self::Liquidity { liquidity_source, .. } => { liquidity_source.liquidity_manager().peer_disconnected(their_node_id) }, + Self::CustomGossip { gossip_handler, .. } => { + gossip_handler.peer_disconnected(their_node_id) + }, + Self::Combined { liquidity_source, gossip_handler } => { + // Notify both handlers + liquidity_source.liquidity_manager().peer_disconnected(their_node_id); + gossip_handler.peer_disconnected(their_node_id); + }, } } }