From d57c1a865381ff3f3a0165596d4111cb4ea59ebf Mon Sep 17 00:00:00 2001 From: Vu Lam Date: Thu, 3 Jul 2025 21:48:06 +0700 Subject: [PATCH 1/5] added the ability to customize the gossip data --- examples/custom_gossip_example.rs | 154 +++++++++++++++ src/builder.rs | 193 +++++++++++++----- src/custom_gossip.rs | 319 ++++++++++++++++++++++++++++++ src/lib.rs | 27 +++ src/logger.rs | 5 +- src/message_handler.rs | 173 +++++++++++++++- 6 files changed, 821 insertions(+), 50 deletions(-) create mode 100644 examples/custom_gossip_example.rs create mode 100644 src/custom_gossip.rs diff --git a/examples/custom_gossip_example.rs b/examples/custom_gossip_example.rs new file mode 100644 index 0000000000..42fc96e271 --- /dev/null +++ b/examples/custom_gossip_example.rs @@ -0,0 +1,154 @@ +// Example demonstrating how to use custom gossip metadata in LDK Node + +use ldk_node::{Builder, Event, Node}; +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 = builder.build()?; + + // 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()?; + + let mut builder2 = Builder::new(); + builder2.set_network(Network::Regtest); + builder2.enable_custom_gossip(); + let node2 = builder2.build()?; + + // 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 31a0fee456..ad6c3fdc68 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -25,6 +25,7 @@ use crate::liquidity::{ LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, }; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; +use crate::custom_gossip::CustomGossipMessageHandler; use crate::message_handler::NodeCustomMessageHandler; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -222,6 +223,7 @@ pub struct NodeBuilder { gossip_source_config: Option, liquidity_source_config: Option, log_writer_config: Option, + custom_gossip_enabled: bool, } impl NodeBuilder { @@ -238,6 +240,7 @@ impl NodeBuilder { let gossip_source_config = None; let liquidity_source_config = None; let log_writer_config = None; + let custom_gossip_enabled = false; Self { config, entropy_source_config, @@ -245,6 +248,7 @@ impl NodeBuilder { gossip_source_config, liquidity_source_config, log_writer_config, + custom_gossip_enabled, } } @@ -380,6 +384,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; @@ -609,6 +625,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, seed_bytes, logger, Arc::new(vss_store), @@ -631,6 +648,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, seed_bytes, logger, kv_store, @@ -779,6 +797,17 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().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().unwrap().set_storage_dir_path(storage_dir_path); @@ -933,8 +962,8 @@ impl ArcedNodeBuilder { fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, - liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - logger: Arc, kv_store: Arc, + liquidity_source_config: Option<&LiquiditySourceConfig>, custom_gossip_enabled: bool, + seed_bytes: [u8; 64], logger: Arc, kv_store: Arc, ) -> Result { if let Err(err) = may_announce_channel(&config) { if config.announcement_addresses.is_some() { @@ -1333,53 +1362,124 @@ 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(&chain_source), - 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(&chain_source), + 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 = Arc::new(liquidity_source_builder.build()); + 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(&chain_source), 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 = Arc::new(liquidity_source_builder.build()); - 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 = Arc::new(liquidity_source_builder.build()); + 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 { @@ -1514,6 +1614,7 @@ fn build_with_store_internal( network_graph, gossip_source, 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 0000000000..4eb39db890 --- /dev/null +++ b/src/custom_gossip.rs @@ -0,0 +1,319 @@ +// 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::test_logger; + use bitcoin::secp256k1::{Secp256k1, SecretKey}; + use lightning::util::ser::{Readable, Writeable}; + use std::io::Cursor; + + #[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 = test_logger(); + 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 b09f9a9f77..134acbf346 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ mod builder; mod chain; pub mod config; mod connection; +pub mod custom_gossip; mod data_store; mod error; mod event; @@ -130,6 +131,7 @@ use config::{ PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; +use custom_gossip::CustomGossipMessageHandler; use event::{EventHandler, EventQueue}; use gossip::GossipSource; use graph::NetworkGraph; @@ -195,6 +197,7 @@ pub struct Node { network_graph: Arc, gossip_source: Arc, liquidity_source: Option>>>, + custom_gossip_handler: Option>>>, kv_store: Arc, logger: Arc, _router: Arc, @@ -974,6 +977,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 bbd24ec207..11c82a8454 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -174,7 +174,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, } @@ -198,10 +199,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 cebd1ea07c..cf7e486638 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -5,6 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use crate::custom_gossip::{CustomGossipMessage, CustomGossipMessageHandler}; use crate::liquidity::LiquiditySource; use lightning::ln::peer_handler::CustomMessageHandler; @@ -26,6 +27,11 @@ where { Ignoring, Liquidity { liquidity_source: Arc> }, + CustomGossip { gossip_handler: Arc> }, + Combined { + liquidity_source: Arc>, + gossip_handler: Arc>, + }, } impl NodeCustomMessageHandler @@ -39,13 +45,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, @@ -53,7 +104,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) + } }, } } @@ -69,7 +141,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) + }, + } }, } } @@ -79,6 +180,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 }, } } @@ -89,6 +218,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 + }, } } @@ -98,6 +238,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 + }, } } @@ -109,6 +260,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) + }, } } @@ -118,6 +277,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); + }, } } } From 000a3309539a86d953d2ffe6b8fc2b7f62a4abae Mon Sep 17 00:00:00 2001 From: Vu Lam Date: Thu, 25 Sep 2025 04:10:48 +0700 Subject: [PATCH 2/5] added sync events action --- src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index a5b6c7652d..3691cd34f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -833,6 +833,11 @@ impl Node { self.config.node_alias } + pub fn process_events(&self) -> Arc { + self.peer_manager.process_events(); + Arc::clone(&self.peer_manager) + } + /// 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 From 8e1599b378e8a834e5bdd649c8f509c4e8637e2d Mon Sep 17 00:00:00 2001 From: datphamcode295 Date: Wed, 29 Apr 2026 15:07:27 +0700 Subject: [PATCH 3/5] fix: improve to suitable with onboarding flow --- src/config.rs | 9 +++ src/lib.rs | 72 +++++++++++--------- src/payment/bolt11.rs | 152 +++++++++++++++++++++++++++++++++++++++++- src/tx_broadcaster.rs | 10 ++- 4 files changed, 209 insertions(+), 34 deletions(-) diff --git a/src/config.rs b/src/config.rs index a2930ea5a9..5d9dd59c3c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -308,6 +308,15 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config.manually_accept_inbound_channels = true; user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = config.anchor_channels_config.is_some(); + // Allow full-capacity HTLCs. LDK's + // `max_inbound_htlc_value_in_flight_percent_of_channel` defaults to 10%, + // which rejects `temporary_channel_failure` on any forward larger than + // a tenth of the channel. Onboarding sweep needs to push ~70% of a + // single-channel capacity through its LSP in one HTLC, so we raise the + // cap to 100% for every channel this node negotiates (both as opener + // and as accepting peer). This matches the override already applied + // when we are an LSPS2 client/service (see `Builder::build_with_store`). + user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; if may_announce_channel(config).is_err() { user_config.accept_forwards_to_priv_channels = false; diff --git a/src/lib.rs b/src/lib.rs index 3691cd34f9..7809cdb4fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -833,6 +833,7 @@ impl Node { self.config.node_alias } + /// Processes pending peer manager events and returns a handle to the peer manager. pub fn process_events(&self) -> Arc { self.peer_manager.process_events(); Arc::clone(&self.peer_manager) @@ -847,6 +848,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.connection_manager), + Arc::clone(&self.keys_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), @@ -864,6 +866,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.connection_manager), + Arc::clone(&self.keys_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), @@ -1282,43 +1285,52 @@ impl Node { /// /// [`EsploraSyncConfig::background_sync_config`]: crate::config::EsploraSyncConfig::background_sync_config pub fn sync_wallets(&self) -> Result<(), Error> { + // PATCH (econ-v1 / sweep-flow E2E): this previously constructed a brand-new + // `tokio::runtime::Builder::new_multi_thread()` inside `block_in_place`. + // When `sync_wallets` is called from a non-tokio host thread (e.g. the + // cdylib FFI handler thread that our node-app builtin dispatch uses), the + // `block_in_place` panics because that API requires a tokio runtime + // context on the current thread — and the panic is silently swallowed + // since the cdylib boundary eats non-unwinding panics. The result is + // that `sync_wallets` hangs forever from the caller's perspective. + // + // Fix: reuse the node's already-initialized runtime (which is what the + // `start()` method at the top of this file does for its own sync call). + // `runtime.block_on(...)` is valid from any thread, with or without a + // surrounding tokio context, and it drives the chain-sync futures on + // the runtime's existing worker pool instead of constructing a parallel + // one per call. let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } + let runtime = rt_lock.as_ref().ok_or(Error::NotRunning)?; let chain_source = Arc::clone(&self.chain_source); let sync_cman = Arc::clone(&self.channel_manager); let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); - tokio::task::block_in_place(move || { - tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( - async move { - match chain_source.as_ref() { - ChainSource::Esplora { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) - .await?; - chain_source.sync_onchain_wallet().await?; - }, - ChainSource::Electrum { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) - .await?; - chain_source.sync_onchain_wallet().await?; - }, - ChainSource::Bitcoind { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper) - .await?; - }, - } - Ok(()) + runtime.block_on(async move { + match chain_source.as_ref() { + ChainSource::Esplora { .. } => { + chain_source.update_fee_rate_estimates().await?; + chain_source + .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) + .await?; + chain_source.sync_onchain_wallet().await?; }, - ) + ChainSource::Electrum { .. } => { + chain_source.update_fee_rate_estimates().await?; + chain_source + .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) + .await?; + chain_source.sync_onchain_wallet().await?; + }, + ChainSource::Bitcoind { .. } => { + chain_source.update_fee_rate_estimates().await?; + chain_source + .poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper) + .await?; + }, + } + Ok(()) }) } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 817a428bdf..7b90c91759 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -22,23 +22,27 @@ use crate::payment::store::{ }; use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; -use crate::types::{ChannelManager, PaymentStore}; +use crate::types::{ChannelManager, KeysManager, PaymentStore}; use lightning::ln::bolt11_payment; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, PaymentId, RecipientOnionFields, Retry, RetryableSendFailure, + MIN_FINAL_CLTV_EXPIRY_DELTA, }; -use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::routing::router::{PaymentParameters, RouteHint, RouteParameters}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; use lightning_invoice::Bolt11Invoice as LdkBolt11Invoice; use lightning_invoice::Bolt11InvoiceDescription as LdkBolt11InvoiceDescription; +use lightning_invoice::InvoiceBuilder; -use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::sha256::{self, Hash as Sha256}; use bitcoin::hashes::Hash; +use bitcoin::secp256k1::Secp256k1; use std::sync::{Arc, RwLock}; +use std::time::Duration; #[cfg(not(feature = "uniffi"))] type Bolt11Invoice = LdkBolt11Invoice; @@ -60,6 +64,7 @@ pub struct Bolt11Payment { runtime: Arc>>>, channel_manager: Arc, connection_manager: Arc>>, + keys_manager: Arc, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, @@ -72,6 +77,7 @@ impl Bolt11Payment { runtime: Arc>>>, channel_manager: Arc, connection_manager: Arc>>, + keys_manager: Arc, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, config: Arc, logger: Arc, @@ -80,6 +86,7 @@ impl Bolt11Payment { runtime, channel_manager, connection_manager, + keys_manager, liquidity_source, payment_store, peer_store, @@ -499,6 +506,145 @@ impl Bolt11Payment { Ok(maybe_wrap(invoice)) } + /// Returns a payable invoice whose route hints are supplied by the caller, bypassing + /// `ChannelManager::create_bolt11_invoice`'s filter that drops any inbound channel whose + /// counterparty has not yet sent a `channel_update` (i.e. `forwarding_info = None`). + /// + /// This is required for topologies where a leaf LSP keeps the channel private and never + /// issues a unicast `channel_update`, making the default invoice builder return an empty + /// route-hint list. The caller is responsible for building hints that match the peer's + /// actual forwarding policy. + pub fn receive_with_hints( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + route_hints: Vec, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = + self.receive_with_hints_inner(Some(amount_msat), &description, expiry_secs, None, route_hints)?; + Ok(maybe_wrap(invoice)) + } + + /// HODL variant of [`receive_with_hints`] — registers a caller-supplied `payment_hash` + /// so the caller can later release the preimage via [`claim_for_hash`]. + /// + /// [`claim_for_hash`]: Self::claim_for_hash + pub fn receive_for_hash_with_hints( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + payment_hash: PaymentHash, route_hints: Vec, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_with_hints_inner( + Some(amount_msat), + &description, + expiry_secs, + Some(payment_hash), + route_hints, + )?; + Ok(maybe_wrap(invoice)) + } + + pub(crate) fn receive_with_hints_inner( + &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, + expiry_secs: u32, manual_claim_payment_hash: Option, + route_hints: Vec, + ) -> Result { + let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA; + + let (payment_hash_ldk, payment_secret) = if let Some(manual_hash) = + manual_claim_payment_hash + { + let secret = self + .channel_manager + .create_inbound_payment_for_hash( + manual_hash, + amount_msat, + expiry_secs, + Some(min_final_cltv_expiry_delta), + ) + .map_err(|e| { + log_error!( + self.logger, + "Failed to register inbound payment for hash: {:?}", + e + ); + Error::InvoiceCreationFailed + })?; + (manual_hash, secret) + } else { + self.channel_manager + .create_inbound_payment( + amount_msat, + expiry_secs, + Some(min_final_cltv_expiry_delta), + ) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })? + }; + + let payment_hash_sha = sha256::Hash::from_slice(&payment_hash_ldk.0).map_err(|e| { + log_error!(self.logger, "Invalid payment hash: {:?}", e); + Error::InvoiceCreationFailed + })?; + + let currency = self.config.network.into(); + let mut invoice_builder = InvoiceBuilder::new(currency) + .invoice_description(invoice_description.clone()) + .payment_hash(payment_hash_sha) + .payment_secret(payment_secret) + .current_timestamp() + .min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into()) + .expiry_time(Duration::from_secs(expiry_secs.into())); + + for hint in route_hints { + invoice_builder = invoice_builder.private_route(hint); + } + + if let Some(amount_msat) = amount_msat { + invoice_builder = + invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp(); + } + + let invoice = invoice_builder + .build_signed(|hash| { + Secp256k1::new() + .sign_ecdsa_recoverable(hash, &self.keys_manager.get_node_secret_key()) + }) + .map_err(|e| { + log_error!(self.logger, "Failed to build and sign invoice: {}", e); + Error::InvoiceCreationFailed + })?; + + log_info!(self.logger, "Invoice (with manual route hints) created: {}", invoice); + + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let id = PaymentId(payment_hash.0); + let preimage = if manual_claim_payment_hash.is_none() { + self.channel_manager + .get_payment_preimage(payment_hash, invoice.payment_secret().clone()) + .ok() + } else { + None + }; + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage, + secret: Some(invoice.payment_secret().clone()), + }; + let payment = PaymentDetails::new( + id, + kind, + amount_msat, + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); + self.payment_store.insert(payment)?; + + Ok(invoice) + } + pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 09189b1371..15b135de48 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -16,7 +16,15 @@ use tokio::sync::{Mutex, MutexGuard}; use std::ops::Deref; -const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; +// Bumped from 50 to 500 because LDK's onchain claim-bump logic floods the +// queue with rebroadcasts of stuck force-close commitment TXs (each new +// block triggers another retry). Once the queue fills up, new broadcasts — +// including one-shot sweep/funding TXs the onboarding flow depends on — +// are silently dropped with `try_send` returning `Full`. 500 is generous +// enough that legitimate sweep/funding broadcasts always make it through +// even when an old monitor's commitment-tx is stuck looping against +// bitcoind's "Transaction outputs already in utxo set" rejection. +const BCAST_PACKAGE_QUEUE_SIZE: usize = 500; pub(crate) struct TransactionBroadcaster where From a6e82313352251e222c9256336f92cc129097fda Mon Sep 17 00:00:00 2001 From: datphamcode295 Date: Fri, 8 May 2026 23:59:21 +0700 Subject: [PATCH 4/5] fix: opt in to forwarding HTLCs over private channelsupport paris version --- src/config.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 5d9dd59c3c..7bf91f34be 100644 --- a/src/config.rs +++ b/src/config.rs @@ -317,9 +317,23 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { // and as accepting peer). This matches the override already applied // when we are an LSPS2 client/service (see `Builder::build_with_store`). user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + // Permit forwarding HTLCs over private channels regardless of whether + // this node has a publicly-announceable identity. Onboarding's HODL-peer + // flow relies on a private last hop (Alice→Bob inbound channel kept + // private so the buyer's invoice carries a route hint). Without this, + // ChannelManager's `can_forward_htlc_to_outgoing_channel` short-circuits + // with `unknown_next_peer (0x400a)` whenever an HTLC is targeted at a + // private channel — this is by design in upstream LDK to hide + // private-channel existence from forwarders, but it breaks any + // leaf-LSP topology that relies on private channels as forwardable + // hops. The two settings (forwarding-over-private-channels vs + // announcing-our-own-channels) are conceptually independent, so we + // leave this enabled even when `may_announce_channel` reports the + // node is missing alias/addresses; only the gossip-related toggles + // are gated on announceability. + user_config.accept_forwards_to_priv_channels = true; if may_announce_channel(config).is_err() { - user_config.accept_forwards_to_priv_channels = false; user_config.channel_handshake_config.announce_for_forwarding = false; user_config.channel_handshake_limits.force_announced_channel_preference = true; } From 5bad3445bd8b4384dec33f557914f0d6da55072f Mon Sep 17 00:00:00 2001 From: datphamcode295 Date: Fri, 5 Jun 2026 08:56:41 +0700 Subject: [PATCH 5/5] fix(fee): target 3 blocks for channel funding txs so they confirm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ChannelFunding confirmation target resolved to a 12-block fee tier, which during normal mempool congestion maps to a rate low enough that funding transactions can sit unconfirmed for hours. The channel never reaches channel_ready and the UI is stuck on `sync`. Lower the target to ~3 blocks (mempool's "fast" tier) so funding txs are mined promptly. The fee is still sourced from the chain source's recommended estimates (esplora get_fee_estimates / electrum estimatefee / bitcoind estimatesmartfee) — only the targeted confirmation window changes, and only for funding transactions. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/fee_estimator.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/fee_estimator.rs b/src/fee_estimator.rs index f000245aa9..da7833d835 100644 --- a/src/fee_estimator.rs +++ b/src/fee_estimator.rs @@ -87,7 +87,11 @@ impl LdkFeeEstimator for OnchainFeeEstimator { pub(crate) fn get_num_block_defaults_for_target(target: ConfirmationTarget) -> usize { match target { ConfirmationTarget::OnchainPayment => 6, - ConfirmationTarget::ChannelFunding => 12, + // Funding txs target ~3 blocks (mempool's "fast" tier) so they confirm + // promptly. The prior 12-block target resolved to a fee low enough that + // funding txs could sit unconfirmed for hours during normal congestion, + // stalling channels in `sync` (never reaching `channel_ready`). + ConfirmationTarget::ChannelFunding => 3, ConfirmationTarget::Lightning(ldk_target) => match ldk_target { LdkConfirmationTarget::MaximumFeeEstimate => 1, LdkConfirmationTarget::UrgentOnChainSweep => 6,