Skip to content

Commit c844aff

Browse files
committed
Add simple-close interop coverage
Expose an opt-in for v2 cooperative closes and exercise both legacy and simple-close negotiations against implementations that support the protocol. Co-Authored-By: HAL 9000
1 parent 0cea341 commit c844aff

9 files changed

Lines changed: 114 additions & 43 deletions

File tree

Cargo.toml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,16 @@ harness = false
185185
#vss-client-ng = { path = "../vss-client" }
186186
#vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" }
187187
#
188-
#[patch."https://github.com/lightningdevkit/rust-lightning"]
189-
#lightning = { path = "../rust-lightning/lightning" }
190-
#lightning-types = { path = "../rust-lightning/lightning-types" }
191-
#lightning-invoice = { path = "../rust-lightning/lightning-invoice" }
192-
#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" }
193-
#lightning-persister = { path = "../rust-lightning/lightning-persister" }
194-
#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" }
195-
#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" }
196-
#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" }
197-
#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" }
198-
#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" }
199-
#lightning-macros = { path = "../rust-lightning/lightning-macros" }
200-
#lightning-dns-resolver = { path = "../rust-lightning/lightning-dns-resolver" }
188+
[patch."https://github.com/lightningdevkit/rust-lightning"]
189+
lightning = { path = "../rust-lightning/lightning" }
190+
lightning-types = { path = "../rust-lightning/lightning-types" }
191+
lightning-invoice = { path = "../rust-lightning/lightning-invoice" }
192+
lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" }
193+
lightning-persister = { path = "../rust-lightning/lightning-persister" }
194+
lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" }
195+
lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" }
196+
lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" }
197+
lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" }
198+
lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" }
199+
lightning-macros = { path = "../rust-lightning/lightning-macros" }
200+
lightning-dns-resolver = { path = "../rust-lightning/lightning-dns-resolver" }

src/config.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ pub(crate) const LNURL_AUTH_TIMEOUT_SECS: u64 = 15;
143143
/// | `announcement_addresses` | None |
144144
/// | `node_alias` | None |
145145
/// | `trusted_peers_0conf` | [] |
146+
/// | `enable_v2_channel_close` | false |
146147
/// | `probing_liquidity_limit_multiplier` | 3 |
147148
/// | `anchor_channels_config` | AnchorChannelsConfig::default() |
148149
/// | `route_parameters` | None |
@@ -182,6 +183,10 @@ pub struct Config {
182183
/// funding transaction ends up never being confirmed on-chain. Zero-confirmation channels
183184
/// should therefore only be accepted from trusted peers.
184185
pub trusted_peers_0conf: Vec<PublicKey>,
186+
/// Whether to enable the `option_simple_close` protocol for cooperative channel closures.
187+
///
188+
/// The protocol will only be used when supported by the channel counterparty.
189+
pub enable_v2_channel_close: bool,
185190
/// The liquidity factor by which we filter the outgoing channels used for sending probes.
186191
///
187192
/// Channels with available liquidity less than the required amount times this value won't be
@@ -222,6 +227,7 @@ impl Default for Config {
222227
listening_addresses: None,
223228
announcement_addresses: None,
224229
trusted_peers_0conf: Vec::new(),
230+
enable_v2_channel_close: false,
225231
probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER,
226232
anchor_channels_config: AnchorChannelsConfig::default(),
227233
tor_config: None,
@@ -428,6 +434,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig {
428434
user_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments =
429435
config.anchor_channels_config.enable_zero_fee_commitments;
430436
user_config.reject_inbound_splices = false;
437+
user_config.enable_v2_channel_close = config.enable_v2_channel_close;
431438

432439
if may_announce_channel(config).is_err() {
433440
user_config.accept_forwards_to_priv_channels = false;
@@ -776,9 +783,9 @@ mod tests {
776783
use std::str::FromStr;
777784

778785
use super::{
779-
clamp_full_scan_stop_gap, may_announce_channel, AnnounceError, Config, ElectrumSyncConfig,
780-
EsploraSyncConfig, NodeAlias, SocketAddress, DEFAULT_FULL_SCAN_STOP_GAP,
781-
MAX_FULL_SCAN_STOP_GAP, MIN_FULL_SCAN_STOP_GAP,
786+
clamp_full_scan_stop_gap, default_user_config, may_announce_channel, AnnounceError, Config,
787+
ElectrumSyncConfig, EsploraSyncConfig, NodeAlias, SocketAddress,
788+
DEFAULT_FULL_SCAN_STOP_GAP, MAX_FULL_SCAN_STOP_GAP, MIN_FULL_SCAN_STOP_GAP,
782789
};
783790

784791
#[test]
@@ -844,4 +851,16 @@ mod tests {
844851
assert_eq!(clamp_full_scan_stop_gap(0), MIN_FULL_SCAN_STOP_GAP);
845852
assert_eq!(clamp_full_scan_stop_gap(MAX_FULL_SCAN_STOP_GAP + 1), MAX_FULL_SCAN_STOP_GAP);
846853
}
854+
855+
#[test]
856+
fn v2_channel_close_config() {
857+
let default_config = Config::default();
858+
assert!(!default_user_config(&default_config).enable_v2_channel_close);
859+
860+
let enabled_config = Config { enable_v2_channel_close: true, ..Config::default() };
861+
assert!(
862+
default_user_config(&enabled_config).enable_v2_channel_close,
863+
"v2 channel close opt-in must be forwarded to LDK"
864+
);
865+
}
847866
}

tests/common/mod.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -799,21 +799,30 @@ pub(crate) async fn wait_for_tx<E: ElectrumApi>(electrs: &E, txid: Txid) {
799799
.await;
800800
}
801801

802-
pub(crate) async fn wait_for_outpoint_spend<E: ElectrumApi>(electrs: &E, outpoint: OutPoint) {
802+
pub(crate) async fn wait_for_outpoint_spend<E: ElectrumApi>(
803+
electrs: &E, outpoint: OutPoint,
804+
) -> Transaction {
803805
let tx = electrs.transaction_get(&outpoint.txid).unwrap();
804806
let txout_script = tx.output.get(outpoint.vout as usize).unwrap().clone().script_pubkey;
805807

806-
// Script history already contains the funding transaction itself, so wait until the exact
807-
// funding outpoint leaves the unspent set instead of treating any history as a spend.
808+
// Script history already contains the funding transaction itself and may contain unrelated
809+
// transactions for reused scripts, so only return a transaction spending the exact outpoint.
808810
exponential_backoff_poll(|| {
809811
electrs.ping().unwrap();
810812

811-
let is_spent = !electrs.script_list_unspent(&txout_script).unwrap().iter().any(|output| {
812-
output.tx_hash == outpoint.txid && output.tx_pos == outpoint.vout as usize
813-
});
814-
is_spent.then_some(())
813+
electrs.script_get_history(&txout_script).unwrap().into_iter().find_map(|entry| {
814+
if entry.tx_hash == outpoint.txid {
815+
return None;
816+
}
817+
let transaction = electrs.transaction_get(&entry.tx_hash).ok()?;
818+
transaction
819+
.input
820+
.iter()
821+
.any(|input| input.previous_output == outpoint)
822+
.then_some(transaction)
823+
})
815824
})
816-
.await;
825+
.await
817826
}
818827

819828
/// Polls the channel from `source_node` to `counterparty_node` until it reports `is_usable`

tests/common/scenarios/channel.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77

88
use std::time::Duration;
99

10+
use bitcoin::Sequence;
1011
use electrsd::corepc_node::Client as BitcoindClient;
1112
use electrsd::electrum_client::ElectrumApi;
1213
use ldk_node::{Event, Node};
1314

1415
use super::super::external_node::ExternalNode;
15-
use super::super::generate_blocks_and_wait;
16+
use super::super::{generate_blocks_and_wait, wait_for_outpoint_spend};
1617
use super::Side;
1718

1819
/// Open a channel from LDK to peer; returns (user_channel_id, external_channel_id).
@@ -50,6 +51,12 @@ pub(crate) async fn cooperative_close<E: ElectrumApi>(
5051
user_channel_id: &ldk_node::UserChannelId, ext_channel_id: &str, initiator: Side,
5152
) {
5253
tokio::time::sleep(Duration::from_secs(2)).await;
54+
let funding_txo = node
55+
.list_channels()
56+
.into_iter()
57+
.find(|channel| channel.user_channel_id == *user_channel_id)
58+
.and_then(|channel| channel.funding_txo)
59+
.expect("channel funding outpoint must be available before cooperative close");
5360
match initiator {
5461
Side::Ldk => {
5562
let ext_node_id = peer.get_node_id().await.unwrap();
@@ -59,6 +66,21 @@ pub(crate) async fn cooperative_close<E: ElectrumApi>(
5966
peer.close_channel(ext_channel_id).await.unwrap();
6067
},
6168
}
69+
let closing_tx = wait_for_outpoint_spend(electrs, funding_txo).await;
70+
let funding_input = closing_tx
71+
.input
72+
.iter()
73+
.find(|input| input.previous_output == funding_txo)
74+
.expect("closing transaction must spend the channel funding outpoint");
75+
let expected_sequence = if node.config().enable_v2_channel_close {
76+
Sequence::ENABLE_RBF_NO_LOCKTIME
77+
} else {
78+
Sequence::MAX
79+
};
80+
assert_eq!(
81+
funding_input.sequence, expected_sequence,
82+
"cooperative close used an unexpected transaction format"
83+
);
6284
generate_blocks_and_wait(bitcoind, electrs, 1).await;
6385
super::sync_wallets_with_retry(node).await;
6486
expect_event!(node, ChannelClosed);

tests/common/scenarios/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ pub(crate) async fn wait_for_htlcs_settled(
8989

9090
/// Build a fresh LDK node configured for interop tests. Uses electrum at the
9191
/// docker-compose default port and bumps sync timeouts for combo stress.
92-
pub(crate) fn setup_ldk_node() -> Node {
93-
let config = crate::common::random_config();
92+
pub(crate) fn setup_ldk_node(enable_v2_channel_close: bool) -> Node {
93+
let mut config = crate::common::random_config();
94+
config.node_config.enable_v2_channel_close = enable_v2_channel_close;
9495
let mut builder = ldk_node::Builder::from_config(config.node_config);
9596
let mut sync_config = ldk_node::config::ElectrumSyncConfig::default();
9697
sync_config.timeouts_config.onchain_wallet_sync_timeout_secs = 180;
@@ -138,13 +139,14 @@ pub(crate) async fn setup_interop_test<E: ElectrumApi>(
138139
/// per-impl `setup_clients` future and a scenario fn.
139140
pub(crate) async fn run_interop_scenario<N, E, F>(
140141
setup_fut: impl Future<Output = (BitcoindClient, E, N)>, scenario: F,
142+
enable_v2_channel_close: bool,
141143
) where
142144
N: ExternalNode,
143145
E: ElectrumApi,
144146
F: AsyncFnOnce(&Node, &N, &BitcoindClient, &E),
145147
{
146148
let (bitcoind, electrs, ext) = setup_fut.await;
147-
let node = setup_ldk_node();
149+
let node = setup_ldk_node(enable_v2_channel_close);
148150
setup_interop_test(&node, &ext, &bitcoind, &electrs).await;
149151
scenario(&node, &ext, &bitcoind, &electrs).await;
150152
node.stop().unwrap();

tests/docker/docker-compose-lnd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ services:
7777
- "--bitcoind.zmqpubrawblock=tcp://bitcoin:28332"
7878
- "--bitcoind.zmqpubrawtx=tcp://bitcoin:28333"
7979
- "--accept-keysend"
80+
- "--protocol.rbf-coop-close"
8081
- "--rpclisten=0.0.0.0:8081"
8182
- "--tlsextradomain=lnd"
8283
- "--tlsextraip=0.0.0.0"

tests/integration_tests_cln.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,26 @@ async fn setup_clients() -> (BitcoindClient, ElectrumClient, TestClnNode) {
3131

3232
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
3333
async fn test_basic_channel_cycle() {
34-
run_interop_scenario(setup_clients(), basic_channel_cycle_scenario).await;
34+
run_interop_scenario(setup_clients(), basic_channel_cycle_scenario, false).await;
3535
}
3636

3737
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
3838
#[ignore = "CLN <=v25.12.x keysend final_cltv=22 < LDK min 42; fixed in master (ElementsProject/lightning#9034), awaiting v26.04 Docker image"]
3939
async fn test_keysend() {
40-
run_interop_scenario(setup_clients(), keysend_scenario).await;
40+
run_interop_scenario(setup_clients(), keysend_scenario, false).await;
4141
}
4242

4343
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4444
async fn test_force_close_after_payment() {
45-
run_interop_scenario(setup_clients(), force_close_after_payment_scenario).await;
45+
run_interop_scenario(setup_clients(), force_close_after_payment_scenario, false).await;
4646
}
4747

4848
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4949
async fn test_disconnect_during_payment() {
50-
run_interop_scenario(setup_clients(), disconnect_during_payment_scenario).await;
50+
run_interop_scenario(setup_clients(), disconnect_during_payment_scenario, false).await;
5151
}
5252

5353
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5454
async fn test_splice_in() {
55-
run_interop_scenario(setup_clients(), splice_in_scenario).await;
55+
run_interop_scenario(setup_clients(), splice_in_scenario, false).await;
5656
}

tests/integration_tests_eclair.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,28 +47,37 @@ async fn setup_clients() -> (BitcoindClient, ElectrumClient, TestEclairNode) {
4747
(bitcoind, electrs, eclair)
4848
}
4949

50+
async fn do_test_basic_channel_cycle(v2_closing: bool) {
51+
run_interop_scenario(setup_clients(), basic_channel_cycle_scenario, v2_closing).await;
52+
}
53+
5054
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5155
async fn test_basic_channel_cycle() {
52-
run_interop_scenario(setup_clients(), basic_channel_cycle_scenario).await;
56+
do_test_basic_channel_cycle(false).await;
57+
}
58+
59+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
60+
async fn test_basic_channel_cycle_with_simple_close() {
61+
do_test_basic_channel_cycle(true).await;
5362
}
5463

5564
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5665
async fn test_keysend() {
57-
run_interop_scenario(setup_clients(), keysend_scenario).await;
66+
run_interop_scenario(setup_clients(), keysend_scenario, false).await;
5867
}
5968

6069
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6170
async fn test_force_close_after_payment() {
62-
run_interop_scenario(setup_clients(), force_close_after_payment_scenario).await;
71+
run_interop_scenario(setup_clients(), force_close_after_payment_scenario, false).await;
6372
}
6473

6574
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6675
async fn test_disconnect_during_payment() {
67-
run_interop_scenario(setup_clients(), disconnect_during_payment_scenario).await;
76+
run_interop_scenario(setup_clients(), disconnect_during_payment_scenario, false).await;
6877
}
6978

7079
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
7180
#[ignore = "Eclair advertises splicing via custom bit 154 instead of BOLT bit 62/63; disjoint from LDK until Eclair migrates"]
7281
async fn test_splice_in() {
73-
run_interop_scenario(setup_clients(), splice_in_scenario).await;
82+
run_interop_scenario(setup_clients(), splice_in_scenario, false).await;
7483
}

tests/integration_tests_lnd.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,37 @@ async fn setup_clients() -> (BitcoindClient, ElectrumClient, TestLndNode) {
2929
(bitcoind, electrs, lnd)
3030
}
3131

32+
async fn do_test_basic_channel_cycle(v2_closing: bool) {
33+
run_interop_scenario(setup_clients(), basic_channel_cycle_scenario, v2_closing).await;
34+
}
35+
3236
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
3337
async fn test_basic_channel_cycle() {
34-
run_interop_scenario(setup_clients(), basic_channel_cycle_scenario).await;
38+
do_test_basic_channel_cycle(false).await;
39+
}
40+
41+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
42+
async fn test_basic_channel_cycle_with_simple_close() {
43+
do_test_basic_channel_cycle(true).await;
3544
}
3645

3746
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
3847
async fn test_keysend() {
39-
run_interop_scenario(setup_clients(), keysend_scenario).await;
48+
run_interop_scenario(setup_clients(), keysend_scenario, false).await;
4049
}
4150

4251
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4352
async fn test_force_close_after_payment() {
44-
run_interop_scenario(setup_clients(), force_close_after_payment_scenario).await;
53+
run_interop_scenario(setup_clients(), force_close_after_payment_scenario, false).await;
4554
}
4655

4756
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4857
async fn test_disconnect_during_payment() {
49-
run_interop_scenario(setup_clients(), disconnect_during_payment_scenario).await;
58+
run_interop_scenario(setup_clients(), disconnect_during_payment_scenario, false).await;
5059
}
5160

5261
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5362
#[ignore = "LND does not implement BOLT splicing"]
5463
async fn test_splice_in() {
55-
run_interop_scenario(setup_clients(), splice_in_scenario).await;
64+
run_interop_scenario(setup_clients(), splice_in_scenario, false).await;
5665
}

0 commit comments

Comments
 (0)