Skip to content

Commit cadfed5

Browse files
jkczyzclaude
andcommitted
Test 0conf splice promotion against funding rebroadcasts
A splice on a 0conf channel locks before its funding transaction confirms, so LDK promotes the new funding immediately and re-broadcasts the still-unconfirmed transaction on every monitor-update completion, re-typed as a generic funding transaction with wallet-view figures. Exercise the full cycle end to end: the contributing side must keep a single record with the splice-time id, interactive-funding classification, and contribution-derived figures through the re-broadcasts and on to graduation, and the non-contributing side must not record anything. Generated with assistance from Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fa76ced commit cadfed5

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

tests/integration_tests_rust.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,6 +2150,129 @@ async fn splice_channel() {
21502150
);
21512151
}
21522152

2153+
/// A splice on a 0conf channel locks before its funding transaction confirms: LDK promotes the
2154+
/// new funding immediately and then re-broadcasts the still-unconfirmed transaction — re-typed as
2155+
/// a generic funding transaction with wallet-view figures and no contribution data — on every
2156+
/// monitor-update completion until it confirms. The re-broadcasts must neither disturb the
2157+
/// contribution-derived record on the contributing side nor mint spurious records on either side.
2158+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2159+
async fn zero_conf_splice_survives_funding_rebroadcasts() {
2160+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2161+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2162+
2163+
// Node A's log collector synchronizes the record checks below with the re-broadcasts.
2164+
// `setup_two_nodes` wires file loggers, so build the pair manually with a collector, Node B
2165+
// trusting Node A for 0conf so channels and splices lock without confirmations.
2166+
let logger_a = Arc::new(CollectingLogWriter::new());
2167+
let mut config_a = random_config();
2168+
config_a.log_writer = TestLogWriter::Custom(logger_a.clone());
2169+
let node_a = setup_node(&chain_source, config_a);
2170+
2171+
let mut config_b = random_config();
2172+
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
2173+
let node_b = setup_node(&chain_source, config_b);
2174+
2175+
let address_a = node_a.onchain_payment().new_address().unwrap();
2176+
let premine_amount_sat = 5_000_000;
2177+
premine_and_distribute_funds(
2178+
&bitcoind.client,
2179+
&electrsd.client,
2180+
vec![address_a],
2181+
Amount::from_sat(premine_amount_sat),
2182+
)
2183+
.await;
2184+
node_a.sync_wallets().unwrap();
2185+
2186+
open_channel(&node_a, &node_b, 2_000_000, false, &electrsd).await;
2187+
2188+
// 0conf: the channel is ready without any confirmations.
2189+
let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
2190+
expect_channel_ready_event!(node_b, node_a.node_id());
2191+
2192+
// Confirm the original funding so the splice below is the only unconfirmed funding and Node
2193+
// A's change from the open is spendable for the splice contribution.
2194+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2195+
node_a.sync_wallets().unwrap();
2196+
node_b.sync_wallets().unwrap();
2197+
2198+
node_a.splice_in(&user_channel_id_a, node_b.node_id(), 1_000_000).unwrap();
2199+
let txo = expect_splice_negotiated_event!(node_a, node_b.node_id());
2200+
wait_for_classified_funding_payment(&node_a, txo.txid).await;
2201+
2202+
// The 0conf splice locks without confirmations, re-signaled as `ChannelReady`.
2203+
expect_channel_ready_event!(node_a, node_b.node_id());
2204+
expect_channel_ready_event!(node_b, node_a.node_id());
2205+
2206+
let payment = funding_payment(&node_a, txo.txid);
2207+
let recorded_amount_msat = payment.amount_msat;
2208+
let recorded_fee_paid_msat = payment.fee_paid_msat;
2209+
assert!(matches!(
2210+
payment.kind,
2211+
PaymentKind::Onchain { tx_type: Some(TransactionType::InteractiveFunding { .. }), .. }
2212+
));
2213+
2214+
// Locking the splice completed monitor updates that re-offered the unconfirmed funding
2215+
// transaction; a payment drives further monitor updates and thus further re-broadcasts.
2216+
let amount_msat = 1_000_000;
2217+
let payment_id =
2218+
node_a.spontaneous_payment().send(amount_msat, node_b.node_id(), None).unwrap();
2219+
expect_payment_successful_event!(node_a, payment_id, None);
2220+
expect_payment_received_event!(node_b, amount_msat);
2221+
2222+
// Wait until the classification pipeline has demonstrably processed a re-offer against the
2223+
// interactive-funding record. The broadcast loop classifies sequentially, so by the second
2224+
// arrival the first re-offer's store write has completed and the checks below are
2225+
// deterministic rather than racing the queue.
2226+
let rebroadcast = format!("funding-typed rebroadcast {}", txo.txid);
2227+
assert!(
2228+
logger_a.wait_for_count(&rebroadcast, 2).await,
2229+
"no funding re-broadcast reached Node A's classification"
2230+
);
2231+
2232+
let splice_payments = |node: &Node| {
2233+
node.list_payments_with_filter(
2234+
|p| matches!(p.kind, PaymentKind::Onchain { txid, .. } if txid == txo.txid),
2235+
)
2236+
};
2237+
2238+
// The record must keep the splice-time id, classification, and contribution-derived figures
2239+
// through the re-broadcasts.
2240+
let payments = splice_payments(&node_a);
2241+
assert_eq!(payments.len(), 1);
2242+
let payment = &payments[0];
2243+
assert_eq!(payment.amount_msat, recorded_amount_msat);
2244+
assert_eq!(payment.fee_paid_msat, recorded_fee_paid_msat);
2245+
assert!(matches!(
2246+
payment.kind,
2247+
PaymentKind::Onchain { tx_type: Some(TransactionType::InteractiveFunding { .. }), .. }
2248+
));
2249+
2250+
// Node B contributed nothing and its wallet sees no activity in the splice; the
2251+
// re-broadcasts must not mint a spurious zero-amount record for it.
2252+
assert!(splice_payments(&node_b).is_empty());
2253+
2254+
// Confirmation and graduation must land on that same record.
2255+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2256+
node_a.sync_wallets().unwrap();
2257+
node_b.sync_wallets().unwrap();
2258+
2259+
let payments = splice_payments(&node_a);
2260+
assert_eq!(payments.len(), 1);
2261+
let payment = &payments[0];
2262+
assert_eq!(payment.status, PaymentStatus::Succeeded);
2263+
assert_eq!(payment.amount_msat, recorded_amount_msat);
2264+
assert_eq!(payment.fee_paid_msat, recorded_fee_paid_msat);
2265+
assert!(matches!(
2266+
payment.kind,
2267+
PaymentKind::Onchain {
2268+
status: ConfirmationStatus::Confirmed { .. },
2269+
tx_type: Some(TransactionType::InteractiveFunding { .. }),
2270+
..
2271+
}
2272+
));
2273+
assert!(splice_payments(&node_b).is_empty());
2274+
}
2275+
21532276
/// Canary for the upstream behavior the zero-activity skip in `classify_funding` works around:
21542277
/// after a 0conf splice is promoted, LDK re-broadcasts the still-unconfirmed funding transaction
21552278
/// through its generic funding path — re-typed as a plain funding transaction without its

0 commit comments

Comments
 (0)