diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 71fe0e63a..2a1b19e51 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -10,6 +10,7 @@ dictionary Config { u64 wallet_sync_interval_secs; u64 fee_rate_cache_update_interval_secs; LogLevel log_level; + sequence trusted_peers_0conf; }; interface Builder { diff --git a/src/event.rs b/src/event.rs index dff72c69a..78d73d063 100644 --- a/src/event.rs +++ b/src/event.rs @@ -231,7 +231,7 @@ where payment_store: Arc>, runtime: Arc>>, logger: L, - _config: Arc, + config: Arc, } impl EventHandler @@ -242,7 +242,7 @@ where wallet: Arc>, event_queue: Arc>, channel_manager: Arc>, network_graph: Arc, keys_manager: Arc, payment_store: Arc>, - runtime: Arc>>, logger: L, _config: Arc, + runtime: Arc>>, logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -253,7 +253,7 @@ where payment_store, logger, runtime, - _config, + config, } } @@ -545,7 +545,52 @@ where } } } - LdkEvent::OpenChannelRequest { .. } => {} + LdkEvent::OpenChannelRequest { + temporary_channel_id, + counterparty_node_id, + funding_satoshis, + channel_type: _, + push_msat: _, + } => { + let user_channel_id: u128 = rand::thread_rng().gen::(); + let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id); + let res = if allow_0conf { + self.channel_manager.accept_inbound_channel_from_trusted_peer_0conf( + &temporary_channel_id, + &counterparty_node_id, + user_channel_id, + ) + } else { + self.channel_manager.accept_inbound_channel( + &temporary_channel_id, + &counterparty_node_id, + user_channel_id, + ) + }; + + match res { + Ok(()) => { + log_info!( + self.logger, + "Accepting inbound{} channel of {}sats from{} peer {}", + if allow_0conf { " 0conf" } else { "" }, + funding_satoshis, + if allow_0conf { " trusted" } else { "" }, + counterparty_node_id, + ); + } + Err(e) => { + log_error!( + self.logger, + "Error while accepting inbound{} channel from{} peer {}: {:?}", + if allow_0conf { " 0conf" } else { "" }, + counterparty_node_id, + if allow_0conf { " trusted" } else { "" }, + e, + ); + } + } + } LdkEvent::PaymentForwarded { prev_channel_id, next_channel_id, diff --git a/src/lib.rs b/src/lib.rs index 85b085028..bfd7878f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,6 +224,7 @@ const WALLET_KEYS_SEED_LEN: usize = 64; /// | `onchain_wallet_sync_interval_secs` | 60 | /// | `wallet_sync_interval_secs` | 20 | /// | `fee_rate_cache_update_interval_secs` | 600 | +/// | `trusted_peers_0conf` | [] | /// | `log_level` | `Debug` | /// pub struct Config { @@ -247,6 +248,12 @@ pub struct Config { /// /// **Note:** A minimum of 10 seconds is always enforced. pub fee_rate_cache_update_interval_secs: u64, + /// A list of peers that we allow to establish zero confirmation channels to us. + /// + /// **Note:** Allowing payments via zero-confirmation channels is potentially insecure if the + /// funding transaction ends up never being confirmed on-chain. Zero-confirmation channels + /// should therefore only be accepted from trusted peers. + pub trusted_peers_0conf: Vec, /// The level at which we log messages. /// /// Any messages below this level will be excluded from the logs. @@ -263,6 +270,7 @@ impl Default for Config { onchain_wallet_sync_interval_secs: DEFAULT_BDK_WALLET_SYNC_INTERVAL_SECS, wallet_sync_interval_secs: DEFAULT_LDK_WALLET_SYNC_INTERVAL_SECS, fee_rate_cache_update_interval_secs: DEFAULT_FEE_RATE_CACHE_UPDATE_INTERVAL_SECS, + trusted_peers_0conf: Vec::new(), log_level: DEFAULT_LOG_LEVEL, } } @@ -569,6 +577,11 @@ impl Builder { // Initialize the ChannelManager let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; + if !config.trusted_peers_0conf.is_empty() { + // Manually accept inbound channels if we expect 0conf channel requests, avoid + // generating the events otherwise. + user_config.manually_accept_inbound_channels = true; + } let channel_manager = { if let Ok(mut reader) = kv_store .read(CHANNEL_MANAGER_PERSISTENCE_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_KEY) diff --git a/src/peer_store.rs b/src/peer_store.rs index d22560aa1..cae22d22f 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -32,6 +32,10 @@ where pub(crate) fn add_peer(&self, peer_info: PeerInfo) -> Result<(), Error> { let mut locked_peers = self.peers.write().unwrap(); + if locked_peers.contains_key(&peer_info.node_id) { + return Ok(()); + } + locked_peers.insert(peer_info.node_id, peer_info); self.persist_peers(&*locked_peers) } diff --git a/src/test/functional_tests.rs b/src/test/functional_tests.rs index dcacd2631..2e4058cc0 100644 --- a/src/test/functional_tests.rs +++ b/src/test/functional_tests.rs @@ -1,8 +1,13 @@ +use crate::io::KVStore; use crate::test::utils::*; use crate::test::utils::{expect_event, random_config}; -use crate::{Builder, Error, Event, PaymentDirection, PaymentStatus}; +use crate::{Builder, Error, Event, Node, PaymentDirection, PaymentStatus}; use bitcoin::Amount; +use electrsd::bitcoind::BitcoinD; +use electrsd::ElectrsD; + +use std::sync::Arc; #[test] fn channel_full_cycle() { @@ -14,7 +19,6 @@ fn channel_full_cycle() { builder_a.set_esplora_server(esplora_url.clone()); let node_a = builder_a.build(); node_a.start().unwrap(); - let addr_a = node_a.new_funding_address().unwrap(); println!("\n== Node B =="); let config_b = random_config(); @@ -22,6 +26,39 @@ fn channel_full_cycle() { builder_b.set_esplora_server(esplora_url); let node_b = builder_b.build(); node_b.start().unwrap(); + + do_channel_full_cycle(node_a, node_b, &bitcoind, &electrsd, false); +} + +#[test] +fn channel_full_cycle_0conf() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + println!("== Node A =="); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let config_a = random_config(); + let builder_a = Builder::from_config(config_a); + builder_a.set_esplora_server(esplora_url.clone()); + let node_a = builder_a.build(); + node_a.start().unwrap(); + + println!("\n== Node B =="); + let mut config_b = random_config(); + config_b.trusted_peers_0conf.push(node_a.node_id()); + + let builder_b = Builder::from_config(config_b); + builder_b.set_esplora_server(esplora_url.clone()); + let node_b = builder_b.build(); + + node_b.start().unwrap(); + + do_channel_full_cycle(node_a, node_b, &bitcoind, &electrsd, true) +} + +fn do_channel_full_cycle( + node_a: Arc>, node_b: Arc>, bitcoind: &BitcoinD, electrsd: &ElectrsD, + allow_0conf: bool, +) { + let addr_a = node_a.new_funding_address().unwrap(); let addr_b = node_b.new_funding_address().unwrap(); let premine_amount_sat = 100_000; @@ -71,8 +108,11 @@ fn channel_full_cycle() { wait_for_tx(&electrsd, funding_txo.txid); - println!("\n .. generating blocks, syncing wallets .. "); - generate_blocks_and_wait(&bitcoind, &electrsd, 6); + if !allow_0conf { + println!("\n .. generating blocks .."); + generate_blocks_and_wait(&bitcoind, &electrsd, 6); + } + node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); @@ -271,7 +311,7 @@ fn channel_open_fails_when_funds_insufficient() { node_b.listening_address().unwrap().into(), 120000, None, - true + true, ) ); }