Skip to content

Commit 2ddb583

Browse files
committed
Add shared interop test scenarios and fix CLN implementation
1 parent 8d5f58d commit 2ddb583

9 files changed

Lines changed: 1177 additions & 12 deletions

File tree

tests/common/mod.rs

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,17 @@ use serde_json::{json, Value};
5757

5858
macro_rules! expect_event {
5959
($node:expr, $event_type:ident) => {{
60-
match $node.next_event_async().await {
60+
let event =
61+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
62+
.await
63+
.unwrap_or_else(|_| {
64+
panic!(
65+
"{} timed out waiting for {} event after 60s",
66+
$node.node_id(),
67+
std::stringify!($event_type)
68+
)
69+
});
70+
match event {
6171
ref e @ Event::$event_type { .. } => {
6272
println!("{} got event {:?}", $node.node_id(), e);
6373
$node.event_handled().unwrap();
@@ -73,7 +83,16 @@ pub(crate) use expect_event;
7383

7484
macro_rules! expect_channel_pending_event {
7585
($node:expr, $counterparty_node_id:expr) => {{
76-
match $node.next_event_async().await {
86+
let event =
87+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
88+
.await
89+
.unwrap_or_else(|_| {
90+
panic!(
91+
"{} timed out waiting for ChannelPending event after 60s",
92+
$node.node_id()
93+
)
94+
});
95+
match event {
7796
ref e @ Event::ChannelPending { funding_txo, counterparty_node_id, .. } => {
7897
println!("{} got event {:?}", $node.node_id(), e);
7998
assert_eq!(counterparty_node_id, $counterparty_node_id);
@@ -91,7 +110,13 @@ pub(crate) use expect_channel_pending_event;
91110

92111
macro_rules! expect_channel_ready_event {
93112
($node:expr, $counterparty_node_id:expr) => {{
94-
match $node.next_event_async().await {
113+
let event =
114+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
115+
.await
116+
.unwrap_or_else(|_| {
117+
panic!("{} timed out waiting for ChannelReady event after 60s", $node.node_id())
118+
});
119+
match event {
95120
ref e @ Event::ChannelReady { user_channel_id, counterparty_node_id, .. } => {
96121
println!("{} got event {:?}", $node.node_id(), e);
97122
assert_eq!(counterparty_node_id, Some($counterparty_node_id));
@@ -111,7 +136,16 @@ macro_rules! expect_channel_ready_events {
111136
($node:expr, $counterparty_node_id_a:expr, $counterparty_node_id_b:expr) => {{
112137
let mut ids = Vec::new();
113138
for _ in 0..2 {
114-
match $node.next_event_async().await {
139+
let event =
140+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
141+
.await
142+
.unwrap_or_else(|_| {
143+
panic!(
144+
"{} timed out waiting for ChannelReady event after 60s",
145+
$node.node_id()
146+
)
147+
});
148+
match event {
115149
ref e @ Event::ChannelReady { counterparty_node_id, .. } => {
116150
println!("{} got event {:?}", $node.node_id(), e);
117151
ids.push(counterparty_node_id);
@@ -137,7 +171,16 @@ pub(crate) use expect_channel_ready_events;
137171

138172
macro_rules! expect_splice_pending_event {
139173
($node:expr, $counterparty_node_id:expr) => {{
140-
match $node.next_event_async().await {
174+
let event =
175+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
176+
.await
177+
.unwrap_or_else(|_| {
178+
panic!(
179+
"{} timed out waiting for SplicePending event after 60s",
180+
$node.node_id()
181+
)
182+
});
183+
match event {
141184
ref e @ Event::SplicePending { new_funding_txo, counterparty_node_id, .. } => {
142185
println!("{} got event {:?}", $node.node_id(), e);
143186
assert_eq!(counterparty_node_id, $counterparty_node_id);
@@ -155,7 +198,16 @@ pub(crate) use expect_splice_pending_event;
155198

156199
macro_rules! expect_payment_received_event {
157200
($node:expr, $amount_msat:expr) => {{
158-
match $node.next_event_async().await {
201+
let event =
202+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
203+
.await
204+
.unwrap_or_else(|_| {
205+
panic!(
206+
"{} timed out waiting for PaymentReceived event after 60s",
207+
$node.node_id()
208+
)
209+
});
210+
match event {
159211
ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => {
160212
println!("{} got event {:?}", $node.node_id(), e);
161213
assert_eq!(amount_msat, $amount_msat);
@@ -177,7 +229,16 @@ pub(crate) use expect_payment_received_event;
177229

178230
macro_rules! expect_payment_claimable_event {
179231
($node:expr, $payment_id:expr, $payment_hash:expr, $claimable_amount_msat:expr) => {{
180-
match $node.next_event_async().await {
232+
let event =
233+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
234+
.await
235+
.unwrap_or_else(|_| {
236+
panic!(
237+
"{} timed out waiting for PaymentClaimable event after 60s",
238+
std::stringify!($node)
239+
)
240+
});
241+
match event {
181242
ref e @ Event::PaymentClaimable {
182243
payment_id,
183244
payment_hash,
@@ -202,7 +263,16 @@ pub(crate) use expect_payment_claimable_event;
202263

203264
macro_rules! expect_payment_successful_event {
204265
($node:expr, $payment_id:expr, $fee_paid_msat:expr) => {{
205-
match $node.next_event_async().await {
266+
let event =
267+
tokio::time::timeout(std::time::Duration::from_secs(60), $node.next_event_async())
268+
.await
269+
.unwrap_or_else(|_| {
270+
panic!(
271+
"{} timed out waiting for PaymentSuccessful event after 60s",
272+
$node.node_id()
273+
)
274+
});
275+
match event {
206276
ref e @ Event::PaymentSuccessful { payment_id, fee_paid_msat, .. } => {
207277
println!("{} got event {:?}", $node.node_id(), e);
208278
if let Some(fee_msat) = $fee_paid_msat {
@@ -390,6 +460,9 @@ macro_rules! setup_builder {
390460

391461
pub(crate) use setup_builder;
392462

463+
#[cfg(any(cln_test, lnd_test, eclair_test))]
464+
pub(crate) mod scenarios;
465+
393466
pub(crate) fn setup_two_nodes(
394467
chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool,
395468
anchors_trusted_no_reserve: bool,

tests/common/scenarios/channel.rs

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
use std::str::FromStr;
9+
use std::time::Duration;
10+
11+
use electrsd::corepc_node::Client as BitcoindClient;
12+
use electrum_client::ElectrumApi;
13+
use ldk_node::{Event, Node};
14+
use lightning_invoice::Bolt11Invoice;
15+
16+
use super::super::external_node::ExternalNode;
17+
use super::super::generate_blocks_and_wait;
18+
use super::payment::test_receive_bolt11_payment;
19+
use super::wait_for_external_node_block_height;
20+
use super::{setup_interop_test, CloseType, Side};
21+
22+
/// Open a channel from LDK to external node, wait for it to be confirmed.
23+
/// Returns (user_channel_id, external_channel_id).
24+
pub(crate) async fn open_channel_to_external<E: ElectrumApi>(
25+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
26+
funding_amount_sat: u64, push_msat: Option<u64>,
27+
) -> (ldk_node::UserChannelId, String) {
28+
let ext_node_id = peer.get_node_id().await.unwrap();
29+
let ext_addr = peer.get_listening_address().await.unwrap();
30+
31+
node.open_channel(ext_node_id, ext_addr, funding_amount_sat, push_msat, None).unwrap();
32+
33+
let funding_txo = expect_channel_pending_event!(node, ext_node_id);
34+
super::super::wait_for_tx(electrs, funding_txo.txid).await;
35+
generate_blocks_and_wait(bitcoind, electrs, 6).await;
36+
let expected_height = bitcoind.get_blockchain_info().unwrap().blocks as u64;
37+
wait_for_external_node_block_height(peer, expected_height).await;
38+
node.sync_wallets().unwrap();
39+
let user_channel_id = expect_channel_ready_event!(node, ext_node_id);
40+
41+
// Find the external node's channel ID for this channel
42+
let ext_channels = peer.list_channels().await.unwrap();
43+
let funding_txid_str = funding_txo.txid.to_string();
44+
let ext_channel_id = ext_channels
45+
.iter()
46+
.find(|ch| ch.funding_txid.as_deref() == Some(&funding_txid_str))
47+
.or_else(|| ext_channels.iter().find(|ch| ch.peer_id == node.node_id()))
48+
.map(|ch| ch.channel_id.clone())
49+
.unwrap_or_else(|| panic!("Could not find channel on external node {}", peer.name()));
50+
51+
(user_channel_id, ext_channel_id)
52+
}
53+
54+
/// Open a channel from external node to LDK, wait for it to be confirmed.
55+
/// Returns (user_channel_id, external_channel_id).
56+
pub(crate) async fn open_channel_from_external<E: ElectrumApi>(
57+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
58+
funding_amount_sat: u64, push_msat: Option<u64>,
59+
) -> (ldk_node::UserChannelId, String) {
60+
let ext_node_id = peer.get_node_id().await.unwrap();
61+
let ldk_addr = node.listening_addresses().unwrap().first().unwrap().clone();
62+
63+
let ext_channel_id =
64+
peer.open_channel(node.node_id(), ldk_addr, funding_amount_sat, push_msat).await.unwrap();
65+
66+
let funding_txo = expect_channel_pending_event!(node, ext_node_id);
67+
super::super::wait_for_tx(electrs, funding_txo.txid).await;
68+
generate_blocks_and_wait(bitcoind, electrs, 6).await;
69+
let expected_height = bitcoind.get_blockchain_info().unwrap().blocks as u64;
70+
wait_for_external_node_block_height(peer, expected_height).await;
71+
node.sync_wallets().unwrap();
72+
let user_channel_id = expect_channel_ready_event!(node, ext_node_id);
73+
74+
(user_channel_id, ext_channel_id)
75+
}
76+
77+
/// Cooperative close initiated by LDK.
78+
pub(crate) async fn test_cooperative_close_by_ldk(
79+
node: &Node, peer: &(impl ExternalNode + ?Sized), user_channel_id: &ldk_node::UserChannelId,
80+
) {
81+
let ext_node_id = peer.get_node_id().await.unwrap();
82+
node.close_channel(user_channel_id, ext_node_id).unwrap();
83+
expect_event!(node, ChannelClosed);
84+
}
85+
86+
/// Cooperative close initiated by external node.
87+
pub(crate) async fn test_cooperative_close_by_external(
88+
node: &Node, peer: &(impl ExternalNode + ?Sized), ext_channel_id: &str,
89+
) {
90+
peer.close_channel(ext_channel_id).await.unwrap();
91+
expect_event!(node, ChannelClosed);
92+
}
93+
94+
/// Force close by LDK.
95+
pub(crate) async fn test_force_close_by_ldk<E: ElectrumApi>(
96+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
97+
user_channel_id: &ldk_node::UserChannelId,
98+
) {
99+
let ext_node_id = peer.get_node_id().await.unwrap();
100+
node.force_close_channel(user_channel_id, ext_node_id, None).unwrap();
101+
expect_event!(node, ChannelClosed);
102+
generate_blocks_and_wait(bitcoind, electrs, 6).await;
103+
node.sync_wallets().unwrap();
104+
}
105+
106+
/// Force close by external node.
107+
pub(crate) async fn test_force_close_by_external<E: ElectrumApi>(
108+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
109+
ext_channel_id: &str,
110+
) {
111+
peer.force_close_channel(ext_channel_id).await.unwrap();
112+
generate_blocks_and_wait(bitcoind, electrs, 6).await;
113+
node.sync_wallets().unwrap();
114+
expect_event!(node, ChannelClosed);
115+
}
116+
117+
/// Helper to close a channel using the given type and initiator.
118+
pub(crate) async fn close_channel<E: ElectrumApi>(
119+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
120+
user_channel_id: &ldk_node::UserChannelId, ext_channel_id: &str, close_type: &CloseType,
121+
close_initiator: &Side,
122+
) {
123+
match (close_type, close_initiator) {
124+
(CloseType::Cooperative, Side::Ldk) => {
125+
test_cooperative_close_by_ldk(node, peer, user_channel_id).await;
126+
},
127+
(CloseType::Cooperative, Side::External) => {
128+
test_cooperative_close_by_external(node, peer, ext_channel_id).await;
129+
},
130+
(CloseType::Force, Side::Ldk) => {
131+
test_force_close_by_ldk(node, peer, bitcoind, electrs, user_channel_id).await;
132+
},
133+
(CloseType::Force, Side::External) => {
134+
test_force_close_by_external(node, peer, bitcoind, electrs, ext_channel_id).await;
135+
},
136+
}
137+
}
138+
139+
/// External node opens channel to LDK, payments flow both ways, then close.
140+
pub(crate) async fn run_inbound_channel_test<E: ElectrumApi>(
141+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
142+
close_type: CloseType, close_initiator: Side,
143+
) {
144+
setup_interop_test(node, peer, bitcoind, electrs).await;
145+
146+
// External node opens channel to LDK with push so both sides have balance
147+
let (user_channel_id, ext_channel_id) =
148+
open_channel_from_external(node, peer, bitcoind, electrs, 1_000_000, Some(500_000_000))
149+
.await;
150+
151+
// External → LDK payment
152+
test_receive_bolt11_payment(node, peer, 5_000_000).await;
153+
154+
// LDK → External payment
155+
let invoice_str = peer.create_invoice(5_000_000, "inbound-ldk-to-ext").await.unwrap();
156+
let parsed = Bolt11Invoice::from_str(&invoice_str).unwrap();
157+
node.bolt11_payment().send(&parsed, None).unwrap();
158+
expect_event!(node, PaymentSuccessful);
159+
160+
// Close
161+
close_channel(
162+
node,
163+
peer,
164+
bitcoind,
165+
electrs,
166+
&user_channel_id,
167+
&ext_channel_id,
168+
&close_type,
169+
&close_initiator,
170+
)
171+
.await;
172+
}
173+
174+
// ---------------------------------------------------------------------------
175+
// Fee rate scenarios
176+
// ---------------------------------------------------------------------------
177+
178+
/// Open a channel, mine many blocks to let fee estimates change naturally,
179+
/// then verify cooperative close still succeeds (fee negotiation works).
180+
pub(crate) async fn test_cooperative_close_after_fee_change<E: ElectrumApi>(
181+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
182+
) {
183+
setup_interop_test(node, peer, bitcoind, electrs).await;
184+
185+
let (user_channel_id, _ext_channel_id) =
186+
open_channel_to_external(node, peer, bitcoind, electrs, 1_000_000, Some(500_000_000)).await;
187+
188+
// Make a payment to confirm channel is working
189+
let invoice_str = peer.create_invoice(5_000_000, "pre-fee-change").await.unwrap();
190+
let parsed = Bolt11Invoice::from_str(&invoice_str).unwrap();
191+
node.bolt11_payment().send(&parsed, None).unwrap();
192+
expect_event!(node, PaymentSuccessful);
193+
194+
// Mine many blocks to change fee environment.
195+
// In regtest, bitcoind's fee estimation changes as blocks are mined
196+
// with varying transaction densities.
197+
generate_blocks_and_wait(bitcoind, electrs, 50).await;
198+
node.sync_wallets().unwrap();
199+
200+
// Allow fee rate updates to propagate between peers
201+
tokio::time::sleep(Duration::from_secs(3)).await;
202+
203+
// Verify payment still works after fee rate change
204+
let invoice_str = peer.create_invoice(5_000_000, "post-fee-change").await.unwrap();
205+
let parsed = Bolt11Invoice::from_str(&invoice_str).unwrap();
206+
node.bolt11_payment().send(&parsed, None).unwrap();
207+
expect_event!(node, PaymentSuccessful);
208+
209+
// Cooperative close — this exercises fee negotiation between peers
210+
let ext_node_id = peer.get_node_id().await.unwrap();
211+
node.close_channel(&user_channel_id, ext_node_id).unwrap();
212+
expect_event!(node, ChannelClosed);
213+
}
214+
215+
/// Open a channel, mine many blocks, then force close.
216+
/// Verifies that the commitment transaction fee is accepted on-chain.
217+
pub(crate) async fn test_force_close_after_fee_change<E: ElectrumApi>(
218+
node: &Node, peer: &(impl ExternalNode + ?Sized), bitcoind: &BitcoindClient, electrs: &E,
219+
) {
220+
setup_interop_test(node, peer, bitcoind, electrs).await;
221+
222+
let (user_channel_id, _ext_channel_id) =
223+
open_channel_to_external(node, peer, bitcoind, electrs, 1_000_000, Some(500_000_000)).await;
224+
225+
// Mine many blocks to shift fee environment
226+
generate_blocks_and_wait(bitcoind, electrs, 50).await;
227+
node.sync_wallets().unwrap();
228+
tokio::time::sleep(Duration::from_secs(3)).await;
229+
230+
// Force close — commitment tx must have a fee that gets accepted into mempool
231+
let ext_node_id = peer.get_node_id().await.unwrap();
232+
node.force_close_channel(&user_channel_id, ext_node_id, None).unwrap();
233+
expect_event!(node, ChannelClosed);
234+
235+
// Mine blocks to confirm the commitment tx
236+
generate_blocks_and_wait(bitcoind, electrs, 6).await;
237+
node.sync_wallets().unwrap();
238+
}

0 commit comments

Comments
 (0)