From 4e7516bf9c508532f7a86d2b45ff6724947dc87f Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:22:44 +0000 Subject: [PATCH 01/11] Increase the size of the broadcast package queue In 202474948014c5d9b43ba17b31263704717a7c54, we started taking one slot in the package queue for each transaction broadcasted by the wallet upon `WalletEvent::ChainTipChanged`, so we increase the number of slots available in the queue. --- src/tx_broadcaster.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 491a9cbde..782112dad 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -18,7 +18,7 @@ use crate::logger::{log_error, LdkLogger}; use crate::types::Wallet; use crate::Error; -const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; +const BCAST_PACKAGE_QUEUE_SIZE: usize = 256; /// A package of transactions that LDK handed to the broadcaster in one `broadcast_transactions` /// call, along with each transaction's type. Queued until the background task classifies and From 3da69d842ec6c2e857a68dd9f0a697e4c13c940e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:22:56 +0000 Subject: [PATCH 02/11] f: Draw the rebroadcast tx batch at random Co-Authored-By: HAL 9000 --- src/wallet/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f8d9d521e..f569d2985 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -63,6 +63,7 @@ use crate::payment::{ }; use crate::runtime::Runtime; use crate::types::{Broadcaster, PaymentStore, PendingPaymentStore}; +use crate::util::random_range; use crate::{ChainSource, Error}; pub(crate) enum OnchainSendAmount { @@ -326,7 +327,7 @@ impl Wallet { } if !unconfirmed_outbound_txids.is_empty() { - let txs_to_broadcast: Vec = { + let mut txs_to_broadcast: Vec = { let locked_wallet = self.inner.lock().expect("lock"); unconfirmed_outbound_txids .iter() @@ -337,6 +338,10 @@ impl Wallet { }) .collect() }; + for i in (1..txs_to_broadcast.len()).rev() { + let j = random_range(0, i as u64) as usize; + txs_to_broadcast.swap(i, j); + } if !txs_to_broadcast.is_empty() { let tx_count = txs_to_broadcast.len(); From 05efb2f8741d435cad439be12c98f6f0ad71b907 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:22:58 +0000 Subject: [PATCH 03/11] Stop the chain source upon startup checks failure This is particularly relevant for the electrum chain source; if we fail to fetch feerates, or zero fee commitments validation fails, and we do not stop the electrum chain source before returning an error, then the user will hit a debug assertion on the next restart. --- src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b22c1538c..e833dd878 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -336,14 +336,18 @@ impl Node { // // TODO: drop 0FC chain source validation when support is ubiquitous let chain_source = Arc::clone(&self.chain_source); - self.runtime.block_on(async move { + let startup_chain_check_res = self.runtime.block_on(async move { tokio::try_join!( chain_source.update_fee_rate_estimates(), chain_source.validate_zero_fee_commitments_support_if_required( zero_fee_commitments_support_required ) ) - })?; + }); + if let Err(e) = startup_chain_check_res { + self.chain_source.stop(); + return Err(e); + } // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); From 670d952c8afc69d3448ebdffa725e2ad6b7b1be8 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:23:00 +0000 Subject: [PATCH 04/11] f: Stop chain source after *all* startup failures Co-Authored-By: HAL 9000 --- src/lib.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e833dd878..14ee734a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -292,6 +292,16 @@ impl Node { return Err(Error::AlreadyRunning); } + match self.start_inner(&mut is_running_lock) { + Ok(()) => Ok(()), + Err(e) => { + self.chain_source.stop(); + Err(e) + }, + } + } + + fn start_inner(&self, is_running_lock: &mut bool) -> Result<(), Error> { log_info!( self.logger, "Starting up LDK Node with node ID {} on network: {}", @@ -336,18 +346,14 @@ impl Node { // // TODO: drop 0FC chain source validation when support is ubiquitous let chain_source = Arc::clone(&self.chain_source); - let startup_chain_check_res = self.runtime.block_on(async move { + self.runtime.block_on(async move { tokio::try_join!( chain_source.update_fee_rate_estimates(), chain_source.validate_zero_fee_commitments_support_if_required( zero_fee_commitments_support_required ) ) - }); - if let Err(e) = startup_chain_check_res { - self.chain_source.stop(); - return Err(e); - } + })?; // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); From 052303a691f93b7949210668f4019f4c9fef16b9 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 24 Jul 2026 23:28:08 +0000 Subject: [PATCH 05/11] f: Bind listeners before spawning startup tasks Resolve and bind configured listening addresses before starting wallet sync, gossip sync, pathfinding score sync, and the remaining background loops. If listener setup now fails, startup returns while only the chain source needs cleanup, so a retry cannot leave duplicate loops behind. AI-assisted-by: OpenAI Codex --- src/lib.rs | 171 +++++++++++++++++++++++++++-------------------------- 1 file changed, 87 insertions(+), 84 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 14ee734a3..2f68eed46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -355,6 +355,55 @@ impl Node { ) })?; + // Bind listeners before spawning background tasks so startup errors cannot leave loops + // running while the node is still marked stopped. + let listeners = if let Some(listening_addresses) = &self.config.listening_addresses { + let logger = Arc::clone(&self.logger); + let listening_addrs = listening_addresses.clone(); + self.runtime.block_on(async move { + let mut bind_addrs = Vec::with_capacity(listening_addrs.len()); + + for listening_addr in &listening_addrs { + let resolved = + tokio::net::lookup_host(listening_addr.to_string()).await.map_err(|e| { + log_error!( + logger, + "Unable to resolve listening address: {:?}. Error details: {}", + listening_addr, + e, + ); + Error::InvalidSocketAddress + })?; + bind_addrs.extend(resolved); + } + + let mut listeners = Vec::new(); + + // Try to bind to all addresses + for addr in &bind_addrs { + match tokio::net::TcpListener::bind(addr).await { + Ok(listener) => { + log_trace!(logger, "Listener bound to {}", addr); + listeners.push(listener); + }, + Err(e) => { + log_error!( + logger, + "Failed to bind to {}: {} - is something else already listening?", + addr, + e + ); + return Err(Error::InvalidSocketAddress); + }, + } + } + + Ok(listeners) + })? + } else { + Vec::new() + }; + // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); let chain_source = Arc::clone(&self.chain_source); @@ -425,98 +474,52 @@ impl Node { ); } - if let Some(listening_addresses) = &self.config.listening_addresses { - // Setup networking - let peer_manager_connection_handler = Arc::clone(&self.peer_manager); - let listening_logger = Arc::clone(&self.logger); - + // Setup networking + let peer_manager_connection_handler = Arc::clone(&self.peer_manager); + let listening_logger = Arc::clone(&self.logger); + for listener in listeners { let logger = Arc::clone(&listening_logger); - let listening_addrs = listening_addresses.clone(); - let listeners = self.runtime.block_on(async move { - let mut bind_addrs = Vec::with_capacity(listening_addrs.len()); - - for listening_addr in &listening_addrs { - let resolved = - tokio::net::lookup_host(listening_addr.to_string()).await.map_err(|e| { - log_error!( - logger, - "Unable to resolve listening address: {:?}. Error details: {}", - listening_addr, - e, - ); - Error::InvalidSocketAddress - })?; - bind_addrs.extend(resolved); - } - - let mut listeners = Vec::new(); - - // Try to bind to all addresses - for addr in &bind_addrs { - match tokio::net::TcpListener::bind(addr).await { - Ok(listener) => { - log_trace!(logger, "Listener bound to {}", addr); - listeners.push(listener); - }, - Err(e) => { - log_error!( + let peer_mgr = Arc::clone(&peer_manager_connection_handler); + let mut stop_listen = self.stop_sender.subscribe(); + let runtime = Arc::clone(&self.runtime); + self.runtime.spawn_cancellable_background_task(async move { + loop { + tokio::select! { + _ = stop_listen.changed() => { + log_debug!( logger, - "Failed to bind to {}: {} - is something else already listening?", - addr, - e + "Stopping listening to inbound connections." ); - return Err(Error::InvalidSocketAddress); - }, - } - } - - Ok(listeners) - })?; - - for listener in listeners { - let logger = Arc::clone(&listening_logger); - let peer_mgr = Arc::clone(&peer_manager_connection_handler); - let mut stop_listen = self.stop_sender.subscribe(); - let runtime = Arc::clone(&self.runtime); - self.runtime.spawn_cancellable_background_task(async move { - loop { - tokio::select! { - _ = stop_listen.changed() => { - log_debug!( - logger, - "Stopping listening to inbound connections." - ); - break; - } - res = listener.accept() => { - let tcp_stream = match res { - Ok((tcp_stream, _)) => tcp_stream, + break; + } + res = listener.accept() => { + let tcp_stream = match res { + Ok((tcp_stream, _)) => tcp_stream, + Err(e) => { + log_error!(logger, "Failed to accept inbound connection: {}", e); + continue; + }, + }; + let peer_mgr = Arc::clone(&peer_mgr); + let logger = Arc::clone(&logger); + runtime.spawn_cancellable_background_task(async move { + let tcp_stream = match tcp_stream.into_std() { + Ok(tcp_stream) => tcp_stream, Err(e) => { - log_error!(logger, "Failed to accept inbound connection: {}", e); - continue; + log_error!(logger, "Failed to convert inbound connection: {}", e); + return; }, }; - let peer_mgr = Arc::clone(&peer_mgr); - let logger = Arc::clone(&logger); - runtime.spawn_cancellable_background_task(async move { - let tcp_stream = match tcp_stream.into_std() { - Ok(tcp_stream) => tcp_stream, - Err(e) => { - log_error!(logger, "Failed to convert inbound connection: {}", e); - return; - }, - }; - lightning_net_tokio::setup_inbound( - Arc::clone(&peer_mgr), - tcp_stream, - ) - .await; - }); - } + lightning_net_tokio::setup_inbound( + Arc::clone(&peer_mgr), + tcp_stream, + ) + .await; + }); } } - }); - } + } + }); } // Regularly reconnect to persisted peers. From 6fc65fabc9ae0b79e8e5fe8e44dd88e0c8841f3e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:23:03 +0000 Subject: [PATCH 06/11] Validate submitpackage behavior for v29+ Require Electrum and Esplora zero-fee commitments validation to observe the Bitcoin Core v29+ failure shape for the dummy TRUC package, instead of accepting any structured submitpackage response. Set the locktime field of the transaction to zero so that the test works at any chain-height, which is particularly helpful when starting ldk-node against test networks. Map HTTP 400 errors returned to `ChainSourceNotSupported` as this error code is returned by blockstream-electrs and mempool-electrs when running against Bitcoin Core v26. We previously would map this error to a general `ConnectionFailed` error, which is not correct for Bitcoin Core v26. Co-Authored-By: HAL 9000 --- src/chain/electrum.rs | 42 +++++++++++++++++++++++++++++++----------- src/chain/esplora.rs | 43 ++++++++++++++++++++++++++++++------------- src/chain/mod.rs | 35 ++++++++++++++++++++++++++--------- 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 59fa23a6c..0d897272a 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -19,7 +19,8 @@ use bdk_wallet::{KeychainKind as BdkKeyChainKind, Update as BdkUpdate}; use bitcoin::transaction::Version; use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid}; use electrum_client::{ - Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi, + Batch, BroadcastPackageRes, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, + ElectrumApi, }; use lightning::chain::{Confirm, Filter, WatchedOutput}; use lightning::util::ser::Writeable; @@ -321,11 +322,6 @@ impl ElectrumChainSource { return Err(Error::ConnectionFailed); }; - // TODO: Use `protocol_version` API once shipped in - // https://github.com/bitcoindevkit/rust-electrum-client/pull/213. - // - // This could still accept an Electrum server running against Bitcoin Core v26 - // through v28, which does not relay ephemeral dust. let spawn_fut = electrum_client.runtime.spawn_blocking({ let electrum_client = Arc::clone(&electrum_client.electrum_client); move || electrum_client.transaction_broadcast_package(&super::dummy_package()) @@ -336,11 +332,19 @@ impl ElectrumChainSource { ); match timeout_fut.await { - Ok(Ok(Ok(_))) => Ok(()), - Ok(Ok(Err( - e @ (electrum_client::Error::Protocol(_) - | electrum_client::Error::AllAttemptsErrored(_)), - ))) => { + Ok(Ok(Ok(result))) => { + if dummy_submit_package_result_matches_v29_or_later(&result) { + Ok(()) + } else { + log_error!( + self.logger, + "Electrum server does not support submitpackage: {:?}", + result + ); + Err(Error::ChainSourceNotSupported) + } + }, + Ok(Ok(Err(e))) if electrum_submitpackage_error_implies_unsupported(&e) => { log_error!(self.logger, "Electrum server does not support submitpackage: {:?}", e); Err(Error::ChainSourceNotSupported) }, @@ -377,6 +381,22 @@ impl ElectrumChainSource { } } +fn electrum_submitpackage_error_implies_unsupported(e: &electrum_client::Error) -> bool { + matches!(e, electrum_client::Error::Protocol(_) | electrum_client::Error::AllAttemptsErrored(_)) +} + +fn dummy_submit_package_result_matches_v29_or_later(result: &BroadcastPackageRes) -> bool { + if result.success { + return false; + } + + super::dummy_package_txids().iter().all(|expected_txid| { + result.errors.iter().any(|error| { + &error.txid == expected_txid && error.error == super::DUMMY_PACKAGE_EXPECTED_ERROR + }) + }) +} + impl Filter for ElectrumChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { self.electrum_runtime_status.write().expect("lock").register_tx(txid, script_pubkey) diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 21205bd25..465b7d124 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -13,7 +13,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use bdk_esplora::EsploraAsyncExt; use bitcoin::transaction::Version; use bitcoin::{FeeRate, Network, Script, Txid}; -use esplora_client::AsyncClient as EsploraAsyncClient; +use esplora_client::{AsyncClient as EsploraAsyncClient, SubmitPackageResult}; use lightning::chain::{Confirm, Filter, WatchedOutput}; use lightning::util::ser::Writeable; use lightning_transaction_sync::EsploraSyncClient; @@ -86,16 +86,13 @@ impl EsploraChainSource { } pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { - // This could still accept an Esplora server running against Bitcoin Core v26 - // through v28, which does not relay ephemeral dust. - self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err( - |e| { - if let esplora_client::Error::HttpResponse { status: 404, message } = e { - log_error!( - self.logger, - "Esplora server does not support submitpackage: {}", - message - ); + let result = self + .esplora_client + .submit_package(&super::dummy_package(), None, None) + .await + .map_err(|e| { + if esplora_submitpackage_error_implies_unsupported(&e) { + log_error!(self.logger, "Esplora server does not support submitpackage: {}", e); Error::ChainSourceNotSupported } else { log_error!( @@ -105,8 +102,11 @@ impl EsploraChainSource { ); Error::ConnectionFailed } - }, - )?; + })?; + if !dummy_submit_package_result_matches_v29_or_later(&result) { + log_error!(self.logger, "Esplora server does not support submitpackage: {:?}", result); + return Err(Error::ChainSourceNotSupported); + } Ok(()) } @@ -521,6 +521,23 @@ impl EsploraChainSource { } } +fn esplora_submitpackage_error_implies_unsupported(e: &esplora_client::Error) -> bool { + matches!(e, esplora_client::Error::HttpResponse { status: 400 | 404, .. }) +} + +fn dummy_submit_package_result_matches_v29_or_later(result: &SubmitPackageResult) -> bool { + if result.package_msg != "transaction failed" { + return false; + } + + super::dummy_package_txids().iter().all(|expected_txid| { + result.tx_results.values().any(|tx_result| { + &tx_result.txid == expected_txid + && tx_result.error.as_deref() == Some(super::DUMMY_PACKAGE_EXPECTED_ERROR) + }) + }) +} + impl Filter for EsploraChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { self.tx_sync.register_tx(txid, script_pubkey); diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 0f96c409f..9b2f97544 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -31,19 +31,20 @@ use crate::{Error, PersistedNodeMetrics}; /// We use this parent-child TRUC package to make sure the configured chain source supports /// broadcasting packages via the `submitpackage` Bitcoin Core RPC. -const PARENT_TXID: &str = "9a015f93fac6cb203c2b994e18b85176eb0354a22a468255516f3c6002d3f696"; +const DUMMY_PACKAGE_EXPECTED_ERROR: &str = "bad-txns-inputs-missingorspent"; +const PARENT_TXID: &str = "11105ba7c94f2fdc1b870a9fbdb136ca006bcf598c372548a1a3051001f3238f"; const PARENT_HEX: &str = - "0300000000010160d0cdb72f2ddf719f40ca32f44614c67577fc75996140544003915683c34a310000000000fd\ + "0300000000010160d0cdb72f2ddf719f40ca32f44614c67577fc75996140544003915683c34a310000000000ff\ ffffff0201000000000000000451024e73876100000000000022512042731375894dad3b25092cd0f713dc5bee4\ a71e30a95e1db3d880906d7eba1fa01409327942924218e4eb1635a7cce6706fcb37b8bbb61a2f0b86357356681\ - 4e09419a3501e02252043bb237d479304632282fe9159db9e9a6ae6ec5bedea9f0f115a97b0e00"; -const CHILD_TXID: &str = "d011b3ff78cdfb8b93822639ea87771847936b04bb83afc8763a7c02a386ae26"; + 4e09419a3501e02252043bb237d479304632282fe9159db9e9a6ae6ec5bedea9f0f11500000000"; +const CHILD_TXID: &str = "6a051464dbf0534a060a61355a6a971559de004795328f8dfd19f3197f9bb4b0"; const CHILD_HEX: &str = - "0300000000010296f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0000000000ff\ - ffffff96f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0100000000fdffffff015\ + "030000000001028f23f3011005a3a14825378c59cf6b00ca36b1bd9f0a871bdc2f4fc9a75b10110000000000ff\ + ffffff8f23f3011005a3a14825378c59cf6b00ca36b1bd9f0a871bdc2f4fc9a75b10110100000000ffffffff015\ 660000000000000225120ac18cd599a1be003595854e2eeec18dbe1c92d04b0ba05812d04445e3fcf16bc000140\ 1462a35808d77a164f0a23a84c4721d1545befd09ad19945bb8aa0ea5576953a9699038725f944b1bc429942ef4\ - 7e6504a554babf022cb15db53be2d8c1dbfe5a97b0e00"; + 7e6504a554babf022cb15db53be2d8c1dbfe500000000"; fn dummy_package() -> [bitcoin::Transaction; 2] { use bitcoin::consensus::Decodable; @@ -55,11 +56,27 @@ fn dummy_package() -> [bitcoin::Transaction; 2] { Transaction::consensus_decode(&mut &parent_tx_bytes[..]).expect("read from a constant"); let child = Transaction::consensus_decode(&mut &child_tx_bytes[..]).expect("read from a constant"); - assert_eq!(parent.compute_txid().to_string(), PARENT_TXID); - assert_eq!(child.compute_txid().to_string(), CHILD_TXID); + let [parent_txid, child_txid] = dummy_package_txids(); + assert_eq!(parent.compute_txid(), parent_txid); + assert_eq!(child.compute_txid(), child_txid); + assert_eq!(parent.lock_time, bitcoin::absolute::LockTime::ZERO); + assert_eq!(child.lock_time, bitcoin::absolute::LockTime::ZERO); + assert!(parent + .input + .iter() + .chain(child.input.iter()) + .all(|input| input.sequence == bitcoin::Sequence::MAX)); + assert!(child.input.iter().all(|input| input.previous_output.txid == parent.compute_txid())); [parent, child] } +fn dummy_package_txids() -> [Txid; 2] { + [ + PARENT_TXID.parse().expect("read from a constant"), + CHILD_TXID.parse().expect("read from a constant"), + ] +} + pub(crate) enum WalletSyncStatus { Completed, InProgress { subscribers: tokio::sync::broadcast::Sender> }, From 00826a1ac3d240d696d232363f4dcd097d5488e1 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:23:05 +0000 Subject: [PATCH 07/11] Expose `ChannelTypeFeatures` in `ChannelDetails` Add a UniFFI wrapper so bindings can inspect channel type flags. Update anchor accounting tests to use channel type features instead of inferring zero-fee commitments from the commitment feerate. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 2 + src/ffi/types.rs | 81 ++++++++++++++++++++++++++++++++- src/types.rs | 10 ++++ tests/common/mod.rs | 13 ++++-- tests/integration_tests_rust.rs | 4 +- 5 files changed, 103 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b231e8d1c..07078cfa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ - `EsploraSyncConfig` and `ElectrumSyncConfig` now support `force_wallet_full_scan`. When set, the on-chain wallet keeps using BDK `full_scan` instead of incremental sync until a full scan succeeds, allowing restored wallets to rediscover funds sent to previously-unknown addresses. +- The `ChannelDetails` returned by `Node::list_channels` now exposes the negotiated + `ChannelTypeFeatures`. - `Config::anchor_channels_config` is no longer optional, hence anchor channels can no longer be disabled. We still negotiate legacy channels if the peer does not support anchor channels. diff --git a/src/ffi/types.rs b/src/ffi/types.rs index c6b48dc96..31c1b3607 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -44,7 +44,10 @@ pub use lightning_liquidity::lsps0::ser::LSPSDateTime; pub use lightning_liquidity::lsps1::msgs::{ LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState, }; -use lightning_types::features::{InitFeatures as LdkInitFeatures, NodeFeatures as LdkNodeFeatures}; +use lightning_types::features::{ + ChannelTypeFeatures as LdkChannelTypeFeatures, InitFeatures as LdkInitFeatures, + NodeFeatures as LdkNodeFeatures, +}; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_types::string::UntrustedString; use vss_client::headers::{ @@ -1817,6 +1820,82 @@ impl From for NodeFeatures { } } +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] +#[uniffi::export(Debug, Eq)] +pub struct ChannelTypeFeatures { + pub(crate) inner: LdkChannelTypeFeatures, +} + +#[uniffi::export] +impl ChannelTypeFeatures { + /// Constructs channel type features from big-endian BOLT 9 encoded bytes. + #[uniffi::constructor] + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { inner: LdkChannelTypeFeatures::from_be_bytes(bytes.to_vec()) } + } + + /// Returns the BOLT 9 big-endian encoded representation of these features. + pub fn to_bytes(&self) -> Vec { + self.inner.encode() + } + + /// Whether this channel type advertises support for `option_static_remotekey`. + pub fn supports_static_remote_key(&self) -> bool { + self.inner.supports_static_remote_key() + } + + /// Whether this channel type requires `option_static_remotekey`. + pub fn requires_static_remote_key(&self) -> bool { + self.inner.requires_static_remote_key() + } + + /// Whether this channel type advertises support for `option_anchors_zero_fee_htlc_tx`. + pub fn supports_anchors_zero_fee_htlc_tx(&self) -> bool { + self.inner.supports_anchors_zero_fee_htlc_tx() + } + + /// Whether this channel type requires `option_anchors_zero_fee_htlc_tx`. + pub fn requires_anchors_zero_fee_htlc_tx(&self) -> bool { + self.inner.requires_anchors_zero_fee_htlc_tx() + } + + /// Whether this channel type advertises support for `option_anchors_nonzero_fee_htlc_tx`. + pub fn supports_anchors_nonzero_fee_htlc_tx(&self) -> bool { + self.inner.supports_anchors_nonzero_fee_htlc_tx() + } + + /// Whether this channel type requires `option_anchors_nonzero_fee_htlc_tx`. + pub fn requires_anchors_nonzero_fee_htlc_tx(&self) -> bool { + self.inner.requires_anchors_nonzero_fee_htlc_tx() + } + + /// Whether this channel type advertises support for `option_taproot`. + pub fn supports_taproot(&self) -> bool { + self.inner.supports_taproot() + } + + /// Whether this channel type requires `option_taproot`. + pub fn requires_taproot(&self) -> bool { + self.inner.requires_taproot() + } + + /// Whether this channel type advertises support for `option_zero_fee_commitments`. + pub fn supports_anchor_zero_fee_commitments(&self) -> bool { + self.inner.supports_anchor_zero_fee_commitments() + } + + /// Whether this channel type requires `option_zero_fee_commitments`. + pub fn requires_anchor_zero_fee_commitments(&self) -> bool { + self.inner.requires_anchor_zero_fee_commitments() + } +} + +impl From for ChannelTypeFeatures { + fn from(features: LdkChannelTypeFeatures) -> Self { + Self { inner: features } + } +} + #[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] #[uniffi::export(Debug, Eq)] pub struct InitFeatures { diff --git a/src/types.rs b/src/types.rs index 5552877ef..22429d980 100644 --- a/src/types.rs +++ b/src/types.rs @@ -39,6 +39,8 @@ use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::GossipVerifier; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; +#[cfg(not(feature = "uniffi"))] +use lightning_types::features::ChannelTypeFeatures; use crate::chain::bitcoind::UtxoSourceClient; use crate::chain::ChainSource; @@ -51,6 +53,8 @@ use crate::message_handler::NodeCustomMessageHandler; use crate::payment::{PaymentDetails, PendingPaymentDetails}; use crate::runtime::RuntimeSpawner; +#[cfg(feature = "uniffi")] +type ChannelTypeFeatures = Arc; #[cfg(not(feature = "uniffi"))] type InitFeatures = lightning::types::features::InitFeatures; #[cfg(feature = "uniffi")] @@ -642,6 +646,11 @@ pub struct ChannelDetails { /// /// See [`ReserveType`] for details on how reserves differ between anchor and legacy channels. pub reserve_type: Option, + /// The negotiated channel type features. + /// + /// Will be `None` until channel negotiation has completed and the channel type has been + /// determined. + pub channel_type: Option, } impl ChannelDetails { @@ -706,6 +715,7 @@ impl ChannelDetails { .expect("value is set for objects serialized with LDK v0.0.109+"), channel_shutdown_state: value.channel_shutdown_state, reserve_type, + channel_type: value.channel_type.map(maybe_wrap), } } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 50e2b993c..04be19629 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1578,12 +1578,15 @@ pub(crate) async fn do_channel_full_cycle( ); if disable_node_b_reserve { - let node_a_outbound_capacity_msat = node_a.list_channels()[0].outbound_capacity_msat; - let node_a_reserve_msat = - node_a.list_channels()[0].unspendable_punishment_reserve.unwrap() * 1000; - let zero_fee_commitments = node_a.list_channels()[0].feerate_sat_per_1000_weight == 0; + let node_a_channel = node_a.list_channels().into_iter().next().unwrap(); + let node_a_outbound_capacity_msat = node_a_channel.outbound_capacity_msat; + let node_a_reserve_msat = node_a_channel.unspendable_punishment_reserve.unwrap() * 1000; + let zero_fee_commitments = node_a_channel + .channel_type + .as_ref() + .map_or(false, |c| c.requires_anchor_zero_fee_commitments()); let node_a_anchors_msat = if zero_fee_commitments { 0 } else { 2 * 330 * 1000 }; - let funding_amount_msat = node_a.list_channels()[0].channel_value_sats * 1000; + let funding_amount_msat = node_a_channel.channel_value_sats * 1000; // Node B does not have any reserve, so we only subtract a few items on node A's // side to arrive at node B's capacity let node_b_capacity_msat = funding_amount_msat diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index e401c8218..e27efb031 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1706,7 +1706,9 @@ async fn splice_channel() { let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); let opening_transaction_fee_sat = 156; - let zero_fee_commitments = node_a.list_channels()[0].feerate_sat_per_1000_weight == 0; + let channel = node_a.list_channels().into_iter().next().unwrap(); + let zero_fee_commitments = + channel.channel_type.as_ref().map_or(false, |c| c.requires_anchor_zero_fee_commitments()); let closing_transaction_fee_sat = if zero_fee_commitments { 0 } else { 614 }; let anchor_output_sat = if zero_fee_commitments { 0 } else { 330 }; From 10b303094045625b6519c61c4139b6d424c52108 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 24 Jul 2026 22:07:14 +0000 Subject: [PATCH 08/11] f: expose channel type feature flags Expose typed UniFFI accessors for option_scid_alias and option_zeroconf on ChannelTypeFeatures, matching the underlying LDK feature API. Add coverage for optional and required flag forms. Co-Authored-By: HAL 9000 --- src/ffi/types.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 31c1b3607..0dc79758d 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -1879,6 +1879,26 @@ impl ChannelTypeFeatures { self.inner.requires_taproot() } + /// Whether this channel type advertises support for `option_scid_alias`. + pub fn supports_scid_privacy(&self) -> bool { + self.inner.supports_scid_privacy() + } + + /// Whether this channel type requires `option_scid_alias`. + pub fn requires_scid_privacy(&self) -> bool { + self.inner.requires_scid_privacy() + } + + /// Whether this channel type advertises support for `option_zeroconf`. + pub fn supports_zero_conf(&self) -> bool { + self.inner.supports_zero_conf() + } + + /// Whether this channel type requires `option_zeroconf`. + pub fn requires_zero_conf(&self) -> bool { + self.inner.requires_zero_conf() + } + /// Whether this channel type advertises support for `option_zero_fee_commitments`. pub fn supports_anchor_zero_fee_commitments(&self) -> bool { self.inner.supports_anchor_zero_fee_commitments() @@ -2270,6 +2290,27 @@ mod tests { (ldk_invoice, wrapped_invoice) } + #[test] + fn test_channel_type_feature_accessors() { + let mut optional = LdkChannelTypeFeatures::empty(); + optional.set_scid_privacy_optional(); + optional.set_zero_conf_optional(); + let optional = ChannelTypeFeatures::from(optional); + assert!(optional.supports_scid_privacy()); + assert!(!optional.requires_scid_privacy()); + assert!(optional.supports_zero_conf()); + assert!(!optional.requires_zero_conf()); + + let mut required = LdkChannelTypeFeatures::empty(); + required.set_scid_privacy_required(); + required.set_zero_conf_required(); + let required = ChannelTypeFeatures::from(required); + assert!(required.supports_scid_privacy()); + assert!(required.requires_scid_privacy()); + assert!(required.supports_zero_conf()); + assert!(required.requires_zero_conf()); + } + #[test] fn test_invoice_description_conversion() { let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); From 62bf39181a633407a2bda1e9a168f306d9ef1441 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:23:08 +0000 Subject: [PATCH 09/11] Add Eclair interop test for zero-fee-commitments Co-Authored-By: HAL 9000 --- .github/workflows/0fc-integration.yml | 51 ++++++++++++++++++++++++++ tests/common/scenarios/mod.rs | 22 +++++++++-- tests/docker/docker-compose-eclair.yml | 1 + 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/.github/workflows/0fc-integration.yml b/.github/workflows/0fc-integration.yml index 647067c3e..1ceb60fac 100644 --- a/.github/workflows/0fc-integration.yml +++ b/.github/workflows/0fc-integration.yml @@ -47,3 +47,54 @@ jobs: - name: Test with 0FC enabled run: | RUSTFLAGS="--cfg no_download --cfg cycle_tests --cfg tokio_unstable --cfg zero_fee_commitment_tests" cargo test -- --test-threads=1 + eclair-interop-test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Start bitcoind and electrs + run: docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml up -d bitcoin electrs + + - name: Wait for bitcoind to be healthy + run: | + for i in $(seq 1 30); do + if docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass getblockchaininfo > /dev/null 2>&1; then + echo "bitcoind is ready" + exit 0 + fi + echo "Waiting for bitcoind... ($i/30)" + sleep 2 + done + echo "ERROR: bitcoind not ready" + exit 1 + + - name: Create wallets on bitcoind + run: | + docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass createwallet eclair + docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass -rpcwallet=eclair getnewaddress + docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass createwallet ldk_node_test + + - name: Start Eclair with 0FC enabled + env: + ECLAIR_EXTRA_JAVA_OPTS: "-Declair.features.zero_fee_commitments=optional" + run: docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml up -d eclair + + - name: Wait for Eclair to be ready + run: | + for i in $(seq 1 60); do + if curl -sf -u :eclairpassword -X POST http://127.0.0.1:8080/getinfo > /dev/null 2>&1; then + echo "Eclair is ready" + exit 0 + fi + echo "Waiting for Eclair... ($i/60)" + sleep 5 + done + echo "Eclair failed to start" + docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml logs eclair + exit 1 + + - name: Run Eclair 0FC integration tests + run: | + RUSTFLAGS="--cfg eclair_test --cfg zero_fee_commitment_tests" cargo test --test integration_tests_eclair -- --show-output --test-threads=1 diff --git a/tests/common/scenarios/mod.rs b/tests/common/scenarios/mod.rs index ffbfc2b00..23b25e859 100644 --- a/tests/common/scenarios/mod.rs +++ b/tests/common/scenarios/mod.rs @@ -20,6 +20,8 @@ use std::time::Duration; use bitcoin::Amount; use electrsd::corepc_node::Client as BitcoindClient; use electrsd::electrum_client::ElectrumApi; +#[cfg(zero_fee_commitment_tests)] +use ldk_node::ReserveType; use ldk_node::{Event, Node}; use super::external_node::ExternalNode; @@ -87,15 +89,15 @@ pub(crate) async fn wait_for_htlcs_settled( panic!("HTLCs did not settle on {} channel {} within 15s", peer.name(), ext_channel_id); } -/// Build a fresh LDK node configured for interop tests. Uses electrum at the +/// Build a fresh LDK node configured for interop tests. Uses esplora at the /// docker-compose default port and bumps sync timeouts for combo stress. pub(crate) fn setup_ldk_node() -> Node { let config = crate::common::random_config(); let mut builder = ldk_node::Builder::from_config(config.node_config); - let mut sync_config = ldk_node::config::ElectrumSyncConfig::default(); + let mut sync_config = ldk_node::config::EsploraSyncConfig::default(); sync_config.timeouts_config.onchain_wallet_sync_timeout_secs = 180; sync_config.timeouts_config.lightning_wallet_sync_timeout_secs = 120; - builder.set_chain_source_electrum("tcp://127.0.0.1:50001".to_string(), Some(sync_config)); + builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), Some(sync_config)); let node = builder.build(config.node_entropy).unwrap(); node.start().unwrap(); node @@ -164,6 +166,20 @@ pub(crate) async fn basic_channel_cycle_scenario( ) .await; + #[cfg(zero_fee_commitment_tests)] + { + let ext_node_id = peer.get_node_id().await.unwrap(); + let channel = node + .list_channels() + .into_iter() + .find(|channel| channel.user_channel_id == user_ch) + .expect("opened channel should be listed"); + assert_eq!(channel.counterparty.node_id, ext_node_id); + assert!(channel.counterparty.features.supports_anchor_zero_fee_commitments()); + assert_eq!(channel.feerate_sat_per_1000_weight, 0); + assert_eq!(channel.reserve_type, Some(ReserveType::Adaptive)); + } + payment::send_bolt11_to_peer(node, peer, 10_000_000, "basic-send").await; payment::receive_bolt11_payment(node, peer, 10_000_000).await; diff --git a/tests/docker/docker-compose-eclair.yml b/tests/docker/docker-compose-eclair.yml index 94b2a4a09..29ee66b40 100644 --- a/tests/docker/docker-compose-eclair.yml +++ b/tests/docker/docker-compose-eclair.yml @@ -77,4 +77,5 @@ services: -Declair.bitcoind.zmqtx=tcp://127.0.0.1:28333 -Declair.features.keysend=optional -Declair.on-chain-fees.confirmation-priority.funding=slow + ${ECLAIR_EXTRA_JAVA_OPTS:-} -Declair.printToConsole From 86bfd989c6c666fcb1755a6221b29075ff584a9a Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 17 Jul 2026 17:23:11 +0000 Subject: [PATCH 10/11] f: move 0FC eclair interop test to eclair workflow --- .github/workflows/0fc-integration.yml | 51 ------------------------ .github/workflows/eclair-integration.yml | 16 +++++++- tests/common/scenarios/channel.rs | 17 ++++++++ tests/common/scenarios/mod.rs | 16 -------- 4 files changed, 32 insertions(+), 68 deletions(-) diff --git a/.github/workflows/0fc-integration.yml b/.github/workflows/0fc-integration.yml index 1ceb60fac..647067c3e 100644 --- a/.github/workflows/0fc-integration.yml +++ b/.github/workflows/0fc-integration.yml @@ -47,54 +47,3 @@ jobs: - name: Test with 0FC enabled run: | RUSTFLAGS="--cfg no_download --cfg cycle_tests --cfg tokio_unstable --cfg zero_fee_commitment_tests" cargo test -- --test-threads=1 - eclair-interop-test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Start bitcoind and electrs - run: docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml up -d bitcoin electrs - - - name: Wait for bitcoind to be healthy - run: | - for i in $(seq 1 30); do - if docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass getblockchaininfo > /dev/null 2>&1; then - echo "bitcoind is ready" - exit 0 - fi - echo "Waiting for bitcoind... ($i/30)" - sleep 2 - done - echo "ERROR: bitcoind not ready" - exit 1 - - - name: Create wallets on bitcoind - run: | - docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass createwallet eclair - docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass -rpcwallet=eclair getnewaddress - docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass createwallet ldk_node_test - - - name: Start Eclair with 0FC enabled - env: - ECLAIR_EXTRA_JAVA_OPTS: "-Declair.features.zero_fee_commitments=optional" - run: docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml up -d eclair - - - name: Wait for Eclair to be ready - run: | - for i in $(seq 1 60); do - if curl -sf -u :eclairpassword -X POST http://127.0.0.1:8080/getinfo > /dev/null 2>&1; then - echo "Eclair is ready" - exit 0 - fi - echo "Waiting for Eclair... ($i/60)" - sleep 5 - done - echo "Eclair failed to start" - docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml logs eclair - exit 1 - - - name: Run Eclair 0FC integration tests - run: | - RUSTFLAGS="--cfg eclair_test --cfg zero_fee_commitment_tests" cargo test --test integration_tests_eclair -- --show-output --test-threads=1 diff --git a/.github/workflows/eclair-integration.yml b/.github/workflows/eclair-integration.yml index daa4572cc..f00d8943a 100644 --- a/.github/workflows/eclair-integration.yml +++ b/.github/workflows/eclair-integration.yml @@ -8,8 +8,19 @@ concurrency: jobs: check-eclair: + name: check-eclair (${{ matrix.name }}) timeout-minutes: 60 runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: standard + eclair_extra_java_opts: "" + rustflags: "--cfg eclair_test" + - name: zero-fee-commitments + eclair_extra_java_opts: "-Declair.features.zero_fee_commitments=optional" + rustflags: "--cfg eclair_test --cfg zero_fee_commitment_tests" steps: - name: Checkout repository uses: actions/checkout@v4 @@ -37,6 +48,8 @@ jobs: docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml exec bitcoin bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass createwallet ldk_node_test - name: Start Eclair + env: + ECLAIR_EXTRA_JAVA_OPTS: ${{ matrix.eclair_extra_java_opts }} run: docker compose -p ldk-node -f tests/docker/docker-compose-eclair.yml up -d eclair - name: Wait for Eclair to be ready @@ -54,4 +67,5 @@ jobs: exit 1 - name: Run Eclair integration tests - run: RUSTFLAGS="--cfg eclair_test" cargo test --test integration_tests_eclair -- --show-output --test-threads=1 + run: | + RUSTFLAGS="${{ matrix.rustflags }}" cargo test --test integration_tests_eclair -- --show-output --test-threads=1 diff --git a/tests/common/scenarios/channel.rs b/tests/common/scenarios/channel.rs index 74e04127c..038a016ad 100644 --- a/tests/common/scenarios/channel.rs +++ b/tests/common/scenarios/channel.rs @@ -9,6 +9,8 @@ use std::time::Duration; use electrsd::corepc_node::Client as BitcoindClient; use electrsd::electrum_client::ElectrumApi; +#[cfg(all(eclair_test, zero_fee_commitment_tests))] +use ldk_node::ReserveType; use ldk_node::{Event, Node}; use super::super::external_node::ExternalNode; @@ -41,6 +43,21 @@ pub(crate) async fn open_channel_to_external( .map(|ch| ch.channel_id.clone()) .unwrap_or_else(|| panic!("Could not find channel on external node {}", peer.name())); + #[cfg(all(eclair_test, zero_fee_commitment_tests))] + { + let channel = node + .list_channels() + .into_iter() + .find(|channel| channel.user_channel_id == user_channel_id) + .expect("opened channel should be listed"); + let channel_type = channel.channel_type.as_ref().expect("channel type should be set"); + assert_eq!(channel.counterparty.node_id, ext_node_id); + assert!(channel.counterparty.features.supports_anchor_zero_fee_commitments()); + assert!(channel_type.requires_anchor_zero_fee_commitments()); + assert_eq!(channel.feerate_sat_per_1000_weight, 0); + assert_eq!(channel.reserve_type, Some(ReserveType::Adaptive)); + } + (user_channel_id, ext_channel_id) } diff --git a/tests/common/scenarios/mod.rs b/tests/common/scenarios/mod.rs index 23b25e859..5b73b6511 100644 --- a/tests/common/scenarios/mod.rs +++ b/tests/common/scenarios/mod.rs @@ -20,8 +20,6 @@ use std::time::Duration; use bitcoin::Amount; use electrsd::corepc_node::Client as BitcoindClient; use electrsd::electrum_client::ElectrumApi; -#[cfg(zero_fee_commitment_tests)] -use ldk_node::ReserveType; use ldk_node::{Event, Node}; use super::external_node::ExternalNode; @@ -166,20 +164,6 @@ pub(crate) async fn basic_channel_cycle_scenario( ) .await; - #[cfg(zero_fee_commitment_tests)] - { - let ext_node_id = peer.get_node_id().await.unwrap(); - let channel = node - .list_channels() - .into_iter() - .find(|channel| channel.user_channel_id == user_ch) - .expect("opened channel should be listed"); - assert_eq!(channel.counterparty.node_id, ext_node_id); - assert!(channel.counterparty.features.supports_anchor_zero_fee_commitments()); - assert_eq!(channel.feerate_sat_per_1000_weight, 0); - assert_eq!(channel.reserve_type, Some(ReserveType::Adaptive)); - } - payment::send_bolt11_to_peer(node, peer, 10_000_000, "basic-send").await; payment::receive_bolt11_payment(node, peer, 10_000_000).await; From 35d0a285f26a5ac4936b6d5879243f91d9f4f209 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 24 Jul 2026 22:23:41 +0000 Subject: [PATCH 11/11] f: give 0fc integration more time to complete --- .github/workflows/0fc-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/0fc-integration.yml b/.github/workflows/0fc-integration.yml index 647067c3e..9432a70e7 100644 --- a/.github/workflows/0fc-integration.yml +++ b/.github/workflows/0fc-integration.yml @@ -8,7 +8,7 @@ concurrency: jobs: build-and-test: - timeout-minutes: 60 + timeout-minutes: 75 runs-on: self-hosted steps: - name: Checkout source code