Skip to content

Commit 4685ad1

Browse files
authored
Merge pull request #200 from tnull/2023-11-add-cln-integration-test
Add integration tests with CLN
2 parents a6b6d8d + 73f5b8f commit 4685ad1

File tree

5 files changed

+443
-0
lines changed

5 files changed

+443
-0
lines changed

.github/workflows/cln.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Continuous Integration Checks - CLN
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
check-cln:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Checkout repository
10+
uses: actions/checkout@v4
11+
12+
- name: Install dependencies
13+
run: |
14+
sudo apt-get update -y
15+
sudo apt-get install -y socat
16+
17+
- name: Start bitcoind, electrs, and lightningd
18+
run: docker compose -f docker-compose-cln.yml up -d
19+
20+
- name: Forward lightningd RPC socket
21+
run: |
22+
docker exec ldk-node-cln-1 sh -c "socat -d -d TCP-LISTEN:9937,fork,reuseaddr UNIX-CONNECT:/root/.lightning/regtest/lightning-rpc&"
23+
socat -d -d UNIX-LISTEN:/tmp/lightning-rpc,reuseaddr,fork TCP:127.0.0.1:9937&
24+
25+
- name: Run CLN integration tests
26+
run: RUSTFLAGS="--cfg cln_test" cargo test --test integration_tests_cln

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ electrum-client = "0.12.0"
8181
proptest = "1.0.0"
8282
regex = "1.5.6"
8383

84+
[target.'cfg(cln_test)'.dev-dependencies]
85+
clightningrpc = { version = "0.3.0-beta.8", default-features = false }
86+
bitcoincore-rpc = { version = "0.16.0", default-features = false }
87+
8488
[build-dependencies]
8589
uniffi = { version = "0.25.1", features = ["build"], optional = true }
8690

docker-compose-cln.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
version: '3'
2+
3+
services:
4+
bitcoin:
5+
image: blockstream/bitcoind:24.1
6+
platform: linux/amd64
7+
command:
8+
[
9+
"bitcoind",
10+
"-printtoconsole",
11+
"-regtest=1",
12+
"-rpcallowip=0.0.0.0/0",
13+
"-rpcbind=0.0.0.0",
14+
"-rpcuser=user",
15+
"-rpcpassword=pass",
16+
"-fallbackfee=0.00001"
17+
]
18+
ports:
19+
- "18443:18443" # Regtest RPC port
20+
- "18444:18444" # Regtest P2P port
21+
networks:
22+
- bitcoin-electrs
23+
healthcheck:
24+
test: ["CMD", "bitcoin-cli", "-regtest", "-rpcuser=user", "-rpcpassword=pass", "getblockchaininfo"]
25+
interval: 5s
26+
timeout: 10s
27+
retries: 5
28+
29+
electrs:
30+
image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931
31+
platform: linux/amd64
32+
depends_on:
33+
bitcoin:
34+
condition: service_healthy
35+
command:
36+
[
37+
"/app/electrs_bitcoin/bin/electrs",
38+
"-vvvv",
39+
"--timestamp",
40+
"--jsonrpc-import",
41+
"--cookie=user:pass",
42+
"--network=regtest",
43+
"--daemon-rpc-addr=bitcoin:18443",
44+
"--http-addr=0.0.0.0:3002",
45+
"--electrum-rpc-addr=0.0.0.0:50001"
46+
]
47+
ports:
48+
- "3002:3002"
49+
- "50001:50001"
50+
networks:
51+
- bitcoin-electrs
52+
53+
cln:
54+
image: blockstream/lightningd:v23.08
55+
platform: linux/amd64
56+
depends_on:
57+
bitcoin:
58+
condition: service_healthy
59+
command:
60+
[
61+
"--bitcoin-rpcconnect=bitcoin",
62+
"--bitcoin-rpcport=18443",
63+
"--bitcoin-rpcuser=user",
64+
"--bitcoin-rpcpassword=pass",
65+
"--regtest",
66+
]
67+
ports:
68+
- "19846:19846"
69+
- "9937:9937"
70+
networks:
71+
- bitcoin-electrs
72+
73+
networks:
74+
bitcoin-electrs:
75+
driver: bridge

tests/common.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#![cfg(cln_test)]
2+
3+
use ldk_node::{Config, LogLevel};
4+
5+
use lightning::ln::msgs::SocketAddress;
6+
7+
use bitcoin::{Address, Amount, Network, OutPoint, Txid};
8+
9+
use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
10+
use bitcoincore_rpc::Client as BitcoindClient;
11+
use bitcoincore_rpc::RpcApi;
12+
13+
use electrum_client::Client as ElectrumClient;
14+
use electrum_client::ElectrumApi;
15+
16+
use rand::distributions::Alphanumeric;
17+
use rand::{thread_rng, Rng};
18+
19+
use std::path::PathBuf;
20+
use std::time::Duration;
21+
22+
macro_rules! expect_event {
23+
($node: expr, $event_type: ident) => {{
24+
match $node.wait_next_event() {
25+
ref e @ Event::$event_type { .. } => {
26+
println!("{} got event {:?}", $node.node_id(), e);
27+
$node.event_handled();
28+
}
29+
ref e => {
30+
panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
31+
}
32+
}
33+
}};
34+
}
35+
36+
pub(crate) use expect_event;
37+
38+
macro_rules! expect_channel_pending_event {
39+
($node: expr, $counterparty_node_id: expr) => {{
40+
match $node.wait_next_event() {
41+
ref e @ Event::ChannelPending { funding_txo, counterparty_node_id, .. } => {
42+
println!("{} got event {:?}", $node.node_id(), e);
43+
assert_eq!(counterparty_node_id, $counterparty_node_id);
44+
$node.event_handled();
45+
funding_txo
46+
}
47+
ref e => {
48+
panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
49+
}
50+
}
51+
}};
52+
}
53+
54+
pub(crate) use expect_channel_pending_event;
55+
56+
macro_rules! expect_channel_ready_event {
57+
($node: expr, $counterparty_node_id: expr) => {{
58+
match $node.wait_next_event() {
59+
ref e @ Event::ChannelReady { channel_id, counterparty_node_id, .. } => {
60+
println!("{} got event {:?}", $node.node_id(), e);
61+
assert_eq!(counterparty_node_id, Some($counterparty_node_id));
62+
$node.event_handled();
63+
channel_id
64+
}
65+
ref e => {
66+
panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
67+
}
68+
}
69+
}};
70+
}
71+
72+
pub(crate) use expect_channel_ready_event;
73+
74+
pub(crate) fn random_storage_path() -> PathBuf {
75+
let mut temp_path = std::env::temp_dir();
76+
let mut rng = thread_rng();
77+
let rand_dir: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect();
78+
temp_path.push(rand_dir);
79+
temp_path
80+
}
81+
82+
pub(crate) fn random_port() -> u16 {
83+
let mut rng = thread_rng();
84+
rng.gen_range(5000..65535)
85+
}
86+
87+
pub(crate) fn random_listening_addresses() -> Vec<SocketAddress> {
88+
let num_addresses = 2;
89+
let mut listening_addresses = Vec::with_capacity(num_addresses);
90+
91+
for _ in 0..num_addresses {
92+
let rand_port = random_port();
93+
let address: SocketAddress = format!("127.0.0.1:{}", rand_port).parse().unwrap();
94+
listening_addresses.push(address);
95+
}
96+
97+
listening_addresses
98+
}
99+
100+
pub(crate) fn random_config() -> Config {
101+
let mut config = Config::default();
102+
103+
config.network = Network::Regtest;
104+
println!("Setting network: {}", config.network);
105+
106+
let rand_dir = random_storage_path();
107+
println!("Setting random LDK storage dir: {}", rand_dir.display());
108+
config.storage_dir_path = rand_dir.to_str().unwrap().to_owned();
109+
110+
let rand_listening_addresses = random_listening_addresses();
111+
println!("Setting random LDK listening addresses: {:?}", rand_listening_addresses);
112+
config.listening_addresses = Some(rand_listening_addresses);
113+
114+
config.log_level = LogLevel::Gossip;
115+
116+
config
117+
}
118+
119+
pub(crate) fn generate_blocks_and_wait(
120+
bitcoind: &BitcoindClient, electrs: &ElectrumClient, num: usize,
121+
) {
122+
let _ = bitcoind.create_wallet("ldk_node_test", None, None, None, None);
123+
let _ = bitcoind.load_wallet("ldk_node_test");
124+
print!("Generating {} blocks...", num);
125+
let cur_height = bitcoind.get_block_count().expect("failed to get current block height");
126+
let address = bitcoind
127+
.get_new_address(Some("test"), Some(AddressType::Legacy))
128+
.expect("failed to get new address");
129+
// TODO: expect this Result once the WouldBlock issue is resolved upstream.
130+
let _block_hashes_res = bitcoind.generate_to_address(num as u64, &address);
131+
wait_for_block(electrs, cur_height as usize + num);
132+
print!(" Done!");
133+
println!("\n");
134+
}
135+
136+
pub(crate) fn wait_for_block(electrs: &ElectrumClient, min_height: usize) {
137+
let mut header = match electrs.block_headers_subscribe() {
138+
Ok(header) => header,
139+
Err(_) => {
140+
// While subscribing should succeed the first time around, we ran into some cases where
141+
// it didn't. Since we can't proceed without subscribing, we try again after a delay
142+
// and panic if it still fails.
143+
std::thread::sleep(Duration::from_secs(1));
144+
electrs.block_headers_subscribe().expect("failed to subscribe to block headers")
145+
}
146+
};
147+
loop {
148+
if header.height >= min_height {
149+
break;
150+
}
151+
header = exponential_backoff_poll(|| {
152+
electrs.ping().expect("failed to ping electrs");
153+
electrs.block_headers_pop().expect("failed to pop block header")
154+
});
155+
}
156+
}
157+
158+
pub(crate) fn wait_for_tx(electrs: &ElectrumClient, txid: Txid) {
159+
let mut tx_res = electrs.transaction_get(&txid);
160+
loop {
161+
if tx_res.is_ok() {
162+
break;
163+
}
164+
tx_res = exponential_backoff_poll(|| {
165+
electrs.ping().unwrap();
166+
Some(electrs.transaction_get(&txid))
167+
});
168+
}
169+
}
170+
171+
pub(crate) fn wait_for_outpoint_spend(electrs: &ElectrumClient, outpoint: OutPoint) {
172+
let tx = electrs.transaction_get(&outpoint.txid).unwrap();
173+
let txout_script = tx.output.get(outpoint.vout as usize).unwrap().clone().script_pubkey;
174+
let mut is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty();
175+
loop {
176+
if is_spent {
177+
break;
178+
}
179+
180+
is_spent = exponential_backoff_poll(|| {
181+
electrs.ping().unwrap();
182+
Some(!electrs.script_get_history(&txout_script).unwrap().is_empty())
183+
});
184+
}
185+
}
186+
187+
pub(crate) fn exponential_backoff_poll<T, F>(mut poll: F) -> T
188+
where
189+
F: FnMut() -> Option<T>,
190+
{
191+
let mut delay = Duration::from_millis(64);
192+
let mut tries = 0;
193+
loop {
194+
match poll() {
195+
Some(data) => break data,
196+
None if delay.as_millis() < 512 => {
197+
delay = delay.mul_f32(2.0);
198+
}
199+
200+
None => {}
201+
}
202+
assert!(tries < 20, "Reached max tries.");
203+
tries += 1;
204+
std::thread::sleep(delay);
205+
}
206+
}
207+
208+
pub(crate) fn premine_and_distribute_funds(
209+
bitcoind: &BitcoindClient, electrs: &ElectrumClient, addrs: Vec<Address>, amount: Amount,
210+
) {
211+
let _ = bitcoind.create_wallet("ldk_node_test", None, None, None, None);
212+
let _ = bitcoind.load_wallet("ldk_node_test");
213+
generate_blocks_and_wait(bitcoind, electrs, 101);
214+
215+
for addr in addrs {
216+
let txid =
217+
bitcoind.send_to_address(&addr, amount, None, None, None, None, None, None).unwrap();
218+
wait_for_tx(electrs, txid);
219+
}
220+
221+
generate_blocks_and_wait(bitcoind, electrs, 1);
222+
}

0 commit comments

Comments
 (0)