Skip to content

Commit 36be98d

Browse files
committed
Add end-to-end auto-splice integration test
Drives the full close → on-chain → reopen → auto-splice loop end to end and asserts the on-chain balance lands inside the LSP channel. 1. Pay an invoice — LSP opens JIT channel #1. 2. /closechannel + mine until the close output is spendable on the client side. Snapshot onchain_after_close. 3. Pay another invoice — LSP opens a fresh JIT channel #2. 4. Mine blocks while polling /getbalance until onchainBalanceSat drops below 10% of onchain_after_close. Client splices are not 0-conf, so the splice tx must confirm and the LSP must return splice_locked before we observe the drop. 5. Snapshot the post-splice channel capacity and assert it covers the consumed on-chain delta. The test asserts conservation across two observations rather than racing a pre-splice capacity snapshot: on-chain drops AND channel capacity grew by at least that delta. Together they pin the funds to the channel without depending on snapshot timing. Test config sets [splice] poll_interval_secs = 1 (default 30s would balloon test runtime). The fast tick is harmless for the existing tests: none of them pair an on-chain balance with a usable LSP channel inside the same test, so the splice manager hits the NoUsableLspChannel skip every tick.
1 parent 179a0d7 commit 36be98d

2 files changed

Lines changed: 172 additions & 0 deletions

File tree

tests/common/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ dir_path = "{storage_dir}"
133133
rpc_address = "{rpc_address}"
134134
rpc_user = "{rpc_user}"
135135
rpc_password = "{rpc_password}"
136+
137+
[splice]
138+
enabled = true
139+
poll_interval_secs = 1
136140
"#,
137141
storage_dir = storage_dir.display(),
138142
);

tests/integration.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,174 @@ async fn test_decodeoffer() {
12131213
assert!(decoded["amountMsat"].is_null());
12141214
}
12151215

1216+
/// End-to-end auto-splice flow: pay → JIT channel #1 → close → on-chain funds
1217+
/// → pay again → JIT channel #2 → splice manager ticks → on-chain drained
1218+
/// into channel #2.
1219+
#[tokio::test(flavor = "multi_thread")]
1220+
async fn test_auto_splice_after_channel_close_and_reopen() {
1221+
let bitcoind = TestBitcoind::new();
1222+
let lsp = LspNode::new(&bitcoind);
1223+
fund_lsp(&bitcoind, &lsp).await;
1224+
1225+
let server = MdkdHandle::start(&bitcoind, None, Some(&lsp), &random_mnemonic()).await;
1226+
let payer = PayerNode::new(&bitcoind);
1227+
setup_payer_lsp_channel(&bitcoind, &payer, &lsp, 500_000).await;
1228+
1229+
// First payment opens JIT channel #1.
1230+
let invoice: serde_json::Value = server
1231+
.post_form(
1232+
"/createinvoice",
1233+
&[
1234+
("amountSat", "100000"),
1235+
("description", "splice setup"),
1236+
("expirySeconds", "3600"),
1237+
],
1238+
)
1239+
.await
1240+
.json()
1241+
.await
1242+
.unwrap();
1243+
let invoice_str = invoice["serialized"].as_str().unwrap();
1244+
let payment_hash = invoice["paymentHash"].as_str().unwrap().to_string();
1245+
payer.pay_invoice(invoice_str);
1246+
1247+
let start = std::time::Instant::now();
1248+
loop {
1249+
let resp: serde_json::Value = server
1250+
.get(&format!("/payments/incoming/{payment_hash}"))
1251+
.await
1252+
.json()
1253+
.await
1254+
.unwrap();
1255+
if resp["isPaid"].as_bool().unwrap() {
1256+
break;
1257+
}
1258+
if start.elapsed() > Duration::from_secs(60) {
1259+
panic!("Timed out waiting for first payment to settle");
1260+
}
1261+
bitcoind.mine_blocks(1);
1262+
tokio::time::sleep(Duration::from_secs(2)).await;
1263+
}
1264+
1265+
// Close channel #1 cooperatively.
1266+
let channels: Vec<serde_json::Value> = server.get("/listchannels").await.json().await.unwrap();
1267+
assert_eq!(channels.len(), 1);
1268+
let channel_id = channels[0]["channelId"].as_str().unwrap().to_string();
1269+
let resp = server
1270+
.post_form("/closechannel", &[("channelId", &channel_id)])
1271+
.await;
1272+
assert_eq!(resp.status(), 200);
1273+
1274+
let start = std::time::Instant::now();
1275+
loop {
1276+
bitcoind.mine_blocks(1);
1277+
tokio::time::sleep(Duration::from_secs(2)).await;
1278+
let channels: Vec<serde_json::Value> =
1279+
server.get("/listchannels").await.json().await.unwrap();
1280+
if channels.is_empty() {
1281+
break;
1282+
}
1283+
if start.elapsed() > Duration::from_secs(60) {
1284+
panic!("Timed out waiting for channel #1 to close");
1285+
}
1286+
}
1287+
1288+
// Wait for the close output to become spendable on the server side.
1289+
bitcoind.mine_blocks(6);
1290+
let start = std::time::Instant::now();
1291+
let onchain_after_close = loop {
1292+
let balance: serde_json::Value = server.get("/getbalance").await.json().await.unwrap();
1293+
let onchain = balance["onchainBalanceSat"].as_u64().unwrap();
1294+
if onchain > 0 {
1295+
break onchain;
1296+
}
1297+
if start.elapsed() > Duration::from_secs(30) {
1298+
panic!("Timed out waiting for spendable on-chain balance: {balance}");
1299+
}
1300+
bitcoind.mine_blocks(1);
1301+
tokio::time::sleep(Duration::from_secs(1)).await;
1302+
};
1303+
assert!(
1304+
onchain_after_close > 50_000,
1305+
"Expected meaningful on-chain balance after close, got {onchain_after_close}"
1306+
);
1307+
1308+
// Second payment opens JIT channel #2 — the splice target.
1309+
let invoice: serde_json::Value = server
1310+
.post_form(
1311+
"/createinvoice",
1312+
&[
1313+
("amountSat", "100000"),
1314+
("description", "splice trigger"),
1315+
("expirySeconds", "3600"),
1316+
],
1317+
)
1318+
.await
1319+
.json()
1320+
.await
1321+
.unwrap();
1322+
let invoice_str = invoice["serialized"].as_str().unwrap();
1323+
let payment_hash = invoice["paymentHash"].as_str().unwrap().to_string();
1324+
payer.pay_invoice(invoice_str);
1325+
1326+
let start = std::time::Instant::now();
1327+
loop {
1328+
let resp: serde_json::Value = server
1329+
.get(&format!("/payments/incoming/{payment_hash}"))
1330+
.await
1331+
.json()
1332+
.await
1333+
.unwrap();
1334+
if resp["isPaid"].as_bool().unwrap() {
1335+
break;
1336+
}
1337+
if start.elapsed() > Duration::from_secs(60) {
1338+
panic!("Timed out waiting for second JIT payment to settle");
1339+
}
1340+
bitcoind.mine_blocks(1);
1341+
tokio::time::sleep(Duration::from_secs(2)).await;
1342+
}
1343+
1344+
// Splice manager polls every 1s (test config). Rather than racing a
1345+
// pre-splice capacity snapshot, assert conservation across two
1346+
// observations: on-chain drops AND channel capacity grew by at least
1347+
// that delta. Together they pin the funds to the channel without
1348+
// depending on snapshot timing. Mine blocks while waiting so the
1349+
// splice tx confirms and the LSP returns splice_locked (client-
1350+
// initiated splices are not 0-conf).
1351+
let splice_threshold = onchain_after_close / 10;
1352+
let start = std::time::Instant::now();
1353+
let onchain_after_splice = loop {
1354+
bitcoind.mine_blocks(1);
1355+
tokio::time::sleep(Duration::from_secs(1)).await;
1356+
let balance: serde_json::Value = server.get("/getbalance").await.json().await.unwrap();
1357+
let onchain = balance["onchainBalanceSat"].as_u64().unwrap();
1358+
if onchain < splice_threshold {
1359+
break onchain;
1360+
}
1361+
if start.elapsed() > Duration::from_secs(120) {
1362+
let channels: serde_json::Value =
1363+
server.get("/listchannels").await.json().await.unwrap();
1364+
panic!(
1365+
"Auto-splice did not consume on-chain balance. \
1366+
onchain_after_close={onchain_after_close}, balance={balance}, \
1367+
channels={channels}"
1368+
);
1369+
}
1370+
};
1371+
1372+
// Sanity: the channel absorbed at least the consumed on-chain funds.
1373+
let channels: Vec<serde_json::Value> = server.get("/listchannels").await.json().await.unwrap();
1374+
assert_eq!(channels.len(), 1);
1375+
let post_splice_capacity = channels[0]["capacitySat"].as_u64().unwrap();
1376+
let consumed_onchain = onchain_after_close - onchain_after_splice;
1377+
assert!(
1378+
post_splice_capacity > consumed_onchain,
1379+
"Expected channel capacity to absorb the spliced funds. \
1380+
capacity={post_splice_capacity}, consumed_onchain={consumed_onchain}"
1381+
);
1382+
}
1383+
12161384
#[tokio::test(flavor = "multi_thread")]
12171385
async fn test_decodeoffer_invalid() {
12181386
let bitcoind = TestBitcoind::new();

0 commit comments

Comments
 (0)