Skip to content

Commit 2fc1fdb

Browse files
jkczyzclaude
andcommitted
Split splice initiation into two phases
Previously, splice_channel required callers to manually construct funding inputs and pass them directly, making coin selection the caller's responsibility. This made the API difficult to use and prevented reuse of the existing CoinSelectionSource trait. Introduce a two-phase API: splice_channel now returns a FundingTemplate that callers use to build a FundingContribution via wallet-backed splice methods (e.g., splice_in_sync, splice_out_sync), which handle coin selection automatically. The completed contribution is then passed to a new funding_contributed method to begin quiescence and negotiation. This also renames SpliceContribution to FundingContribution and moves fee estimation and input validation into the funding module, co-located with the types they operate on. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 39a1e62 commit 2fc1fdb

File tree

12 files changed

+1640
-1057
lines changed

12 files changed

+1640
-1057
lines changed

.claude/settings.local.json

Lines changed: 61 additions & 0 deletions
Large diffs are not rendered by default.

fuzz/src/chanmon_consistency.rs

Lines changed: 145 additions & 178 deletions
Large diffs are not rendered by default.

fuzz/src/full_stack.rs

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use bitcoin::opcodes;
2222
use bitcoin::script::{Builder, ScriptBuf};
2323
use bitcoin::transaction::Version;
2424
use bitcoin::transaction::{Transaction, TxIn, TxOut};
25+
use bitcoin::FeeRate;
2526

2627
use bitcoin::hash_types::{BlockHash, Txid};
2728
use bitcoin::hashes::sha256::Hash as Sha256;
@@ -30,8 +31,6 @@ use bitcoin::hashes::Hash as _;
3031
use bitcoin::hex::FromHex;
3132
use bitcoin::WPubkeyHash;
3233

33-
use lightning::ln::funding::{FundingTxInput, SpliceContribution};
34-
3534
use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode};
3635
use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs};
3736
use lightning::chain;
@@ -41,7 +40,7 @@ use lightning::chain::chaininterface::{
4140
use lightning::chain::chainmonitor;
4241
use lightning::chain::transaction::OutPoint;
4342
use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen};
44-
use lightning::events::bump_transaction::sync::WalletSourceSync;
43+
use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync};
4544
use lightning::events::Event;
4645
use lightning::ln::channel_state::ChannelDetails;
4746
use lightning::ln::channelmanager::{ChainParameters, ChannelManager, InterceptId, PaymentId};
@@ -65,6 +64,7 @@ use lightning::sign::{
6564
SignerProvider,
6665
};
6766
use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
67+
use lightning::util::async_poll::{MaybeSend, MaybeSync};
6868
use lightning::util::config::{ChannelConfig, UserConfig};
6969
use lightning::util::hash_tables::*;
7070
use lightning::util::logger::Logger;
@@ -227,7 +227,7 @@ type ChannelMan<'a> = ChannelManager<
227227
Arc<dyn chain::Filter>,
228228
Arc<TestBroadcaster>,
229229
Arc<FuzzEstimator>,
230-
Arc<dyn Logger>,
230+
Arc<dyn Logger + MaybeSend + MaybeSync>,
231231
Arc<TestPersister>,
232232
Arc<KeyProvider>,
233233
>,
@@ -239,14 +239,20 @@ type ChannelMan<'a> = ChannelManager<
239239
Arc<FuzzEstimator>,
240240
&'a FuzzRouter,
241241
&'a FuzzRouter,
242-
Arc<dyn Logger>,
242+
Arc<dyn Logger + MaybeSend + MaybeSync>,
243243
>;
244244
type PeerMan<'a> = PeerManager<
245245
Peer<'a>,
246246
Arc<ChannelMan<'a>>,
247-
Arc<P2PGossipSync<Arc<NetworkGraph<Arc<dyn Logger>>>, Arc<dyn UtxoLookup>, Arc<dyn Logger>>>,
247+
Arc<
248+
P2PGossipSync<
249+
Arc<NetworkGraph<Arc<dyn Logger + MaybeSend + MaybeSync>>>,
250+
Arc<dyn UtxoLookup>,
251+
Arc<dyn Logger + MaybeSend + MaybeSync>,
252+
>,
253+
>,
248254
IgnoringMessageHandler,
249-
Arc<dyn Logger>,
255+
Arc<dyn Logger + MaybeSend + MaybeSync>,
250256
IgnoringMessageHandler,
251257
Arc<KeyProvider>,
252258
IgnoringMessageHandler,
@@ -260,7 +266,7 @@ struct MoneyLossDetector<'a> {
260266
Arc<dyn chain::Filter>,
261267
Arc<TestBroadcaster>,
262268
Arc<FuzzEstimator>,
263-
Arc<dyn Logger>,
269+
Arc<dyn Logger + MaybeSend + MaybeSync>,
264270
Arc<TestPersister>,
265271
Arc<KeyProvider>,
266272
>,
@@ -285,7 +291,7 @@ impl<'a> MoneyLossDetector<'a> {
285291
Arc<dyn chain::Filter>,
286292
Arc<TestBroadcaster>,
287293
Arc<FuzzEstimator>,
288-
Arc<dyn Logger>,
294+
Arc<dyn Logger + MaybeSend + MaybeSync>,
289295
Arc<TestPersister>,
290296
Arc<KeyProvider>,
291297
>,
@@ -520,7 +526,7 @@ impl SignerProvider for KeyProvider {
520526
}
521527

522528
#[inline]
523-
pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
529+
pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>) {
524530
if data.len() < 32 {
525531
return;
526532
}
@@ -1024,20 +1030,27 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
10241030
if splice_in_sats == 0 {
10251031
continue;
10261032
}
1027-
// Create a funding input from the coinbase transaction
1028-
if let Ok(input) = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0) {
1029-
let contribution = SpliceContribution::splice_in(
1030-
Amount::from_sat(splice_in_sats.min(900_000)), // Cap at available funds minus fees
1031-
vec![input],
1032-
Some(wallet.get_change_script().unwrap()),
1033-
);
1034-
let _ = channelmanager.splice_channel(
1035-
&chan.channel_id,
1036-
&chan.counterparty.node_id,
1037-
contribution,
1038-
253, // funding_feerate_per_kw
1033+
let chan_id = chan.channel_id;
1034+
let counterparty = chan.counterparty.node_id;
1035+
if let Ok(funding_template) = channelmanager.splice_channel(
1036+
&chan_id,
1037+
&counterparty,
1038+
None,
1039+
FeeRate::from_sat_per_kwu(253),
1040+
) {
1041+
let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
1042+
if let Ok(contribution) = funding_template.splice_in_sync(
10391043
None,
1040-
);
1044+
Amount::from_sat(splice_in_sats.min(900_000)),
1045+
&wallet_sync,
1046+
) {
1047+
let _ = channelmanager.funding_contributed(
1048+
&chan_id,
1049+
&counterparty,
1050+
contribution,
1051+
None,
1052+
);
1053+
}
10411054
}
10421055
},
10431056
// Splice-out: remove funds from a channel
@@ -1060,17 +1073,28 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
10601073
// Cap splice-out at a reasonable portion of channel capacity
10611074
let max_splice_out = chan.channel_value_satoshis / 4;
10621075
let splice_out_sats = splice_out_sats.min(max_splice_out).max(546); // At least dust limit
1063-
let contribution = SpliceContribution::splice_out(vec![TxOut {
1064-
value: Amount::from_sat(splice_out_sats),
1065-
script_pubkey: wallet.get_change_script().unwrap(),
1066-
}]);
1067-
let _ = channelmanager.splice_channel(
1068-
&chan.channel_id,
1069-
&chan.counterparty.node_id,
1070-
contribution,
1071-
253, // funding_feerate_per_kw
1076+
let chan_id = chan.channel_id;
1077+
let counterparty = chan.counterparty.node_id;
1078+
if let Ok(funding_template) = channelmanager.splice_channel(
1079+
&chan_id,
1080+
&counterparty,
10721081
None,
1073-
);
1082+
FeeRate::from_sat_per_kwu(253),
1083+
) {
1084+
let outputs = vec![TxOut {
1085+
value: Amount::from_sat(splice_out_sats),
1086+
script_pubkey: wallet.get_change_script().unwrap(),
1087+
}];
1088+
let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
1089+
if let Ok(contribution) = funding_template.splice_out_sync(outputs, &wallet_sync) {
1090+
let _ = channelmanager.funding_contributed(
1091+
&chan_id,
1092+
&counterparty,
1093+
contribution,
1094+
None,
1095+
);
1096+
}
1097+
}
10741098
},
10751099
_ => return,
10761100
}
@@ -1137,14 +1161,15 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
11371161
}
11381162
}
11391163

1140-
pub fn full_stack_test<Out: test_logger::Output>(data: &[u8], out: Out) {
1141-
let logger: Arc<dyn Logger> = Arc::new(test_logger::TestLogger::new("".to_owned(), out));
1164+
pub fn full_stack_test<Out: test_logger::Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
1165+
let logger: Arc<dyn Logger + MaybeSend + MaybeSync> =
1166+
Arc::new(test_logger::TestLogger::new("".to_owned(), out));
11421167
do_test(data, &logger);
11431168
}
11441169

11451170
#[no_mangle]
11461171
pub extern "C" fn full_stack_run(data: *const u8, datalen: usize) {
1147-
let logger: Arc<dyn Logger> =
1172+
let logger: Arc<dyn Logger + MaybeSend + MaybeSync> =
11481173
Arc::new(test_logger::TestLogger::new("".to_owned(), test_logger::DevNull {}));
11491174
do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, &logger);
11501175
}
@@ -1930,6 +1955,7 @@ pub fn write_fst_seeds(path: &str) {
19301955

19311956
#[cfg(test)]
19321957
mod tests {
1958+
use lightning::util::async_poll::{MaybeSend, MaybeSync};
19331959
use lightning::util::logger::{Logger, Record};
19341960
use std::collections::HashMap;
19351961
use std::sync::{Arc, Mutex};
@@ -1961,7 +1987,7 @@ mod tests {
19611987
let test = super::two_peer_forwarding_seed();
19621988

19631989
let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) });
1964-
super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>));
1990+
super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>));
19651991

19661992
let log_entries = logger.lines.lock().unwrap();
19671993
// 1
@@ -1996,7 +2022,7 @@ mod tests {
19962022
let test = super::gossip_exchange_seed();
19972023

19982024
let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) });
1999-
super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>));
2025+
super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>));
20002026

20012027
let log_entries = logger.lines.lock().unwrap();
20022028
assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Sending message to all peers except Some(PublicKey(0000000000000000000000000000000000000000000000000000000000000002ff00000000000000000000000000000000000000000000000000000000000002)) or the announced channel's counterparties: ChannelAnnouncement { node_signature_1: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, node_signature_2: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, bitcoin_signature_1: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, bitcoin_signature_2: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, contents: UnsignedChannelAnnouncement { features: [], chain_hash: 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000, short_channel_id: 42, node_id_1: NodeId(030303030303030303030303030303030303030303030303030303030303030303), node_id_2: NodeId(020202020202020202020202020202020202020202020202020202020202020202), bitcoin_key_1: NodeId(030303030303030303030303030303030303030303030303030303030303030303), bitcoin_key_2: NodeId(020202020202020202020202020202020202020202020202020202020202020202), excess_data: [] } }".to_string())), Some(&1));
@@ -2009,7 +2035,7 @@ mod tests {
20092035
let test = super::splice_seed();
20102036

20112037
let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) });
2012-
super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>));
2038+
super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>));
20132039

20142040
let log_entries = logger.lines.lock().unwrap();
20152041

lightning-tests/src/upgrade_downgrade_tests.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ use lightning::chain::channelmonitor::{ANTI_REORG_DELAY, HTLC_FAIL_BACK_BUFFER};
4949
use lightning::events::bump_transaction::sync::WalletSourceSync;
5050
use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType};
5151
use lightning::ln::functional_test_utils::*;
52-
use lightning::ln::funding::SpliceContribution;
5352
use lightning::ln::msgs::BaseMessageHandler as _;
5453
use lightning::ln::msgs::ChannelMessageHandler as _;
5554
use lightning::ln::msgs::MessageSendEvent;
@@ -453,11 +452,13 @@ fn do_test_0_1_htlc_forward_after_splice(fail_htlc: bool) {
453452
reconnect_b_c_args.send_announcement_sigs = (true, true);
454453
reconnect_nodes(reconnect_b_c_args);
455454

456-
let contribution = SpliceContribution::splice_out(vec![TxOut {
455+
let outputs = vec![TxOut {
457456
value: Amount::from_sat(1_000),
458457
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
459-
}]);
460-
let splice_tx = splice_channel(&nodes[0], &nodes[1], ChannelId(chan_id_bytes_a), contribution);
458+
}];
459+
let channel_id = ChannelId(chan_id_bytes_a);
460+
let funding_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
461+
let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
461462
for node in nodes.iter() {
462463
mine_transaction(node, &splice_tx);
463464
connect_blocks(node, ANTI_REORG_DELAY - 1);

lightning/src/ln/async_signer_tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
//! properly with a signer implementation that asynchronously derives signatures.
1212
1313
use crate::events::bump_transaction::sync::WalletSourceSync;
14-
use crate::ln::funding::SpliceContribution;
15-
use crate::ln::splicing_tests::negotiate_splice_tx;
14+
use crate::ln::splicing_tests::{initiate_splice_out, negotiate_splice_tx};
1615
use crate::prelude::*;
1716
use crate::util::ser::Writeable;
1817
use bitcoin::secp256k1::Secp256k1;
@@ -1573,10 +1572,11 @@ fn test_async_splice_initial_commit_sig() {
15731572
);
15741573

15751574
// Negotiate a splice up until the signature exchange.
1576-
let contribution = SpliceContribution::splice_out(vec![TxOut {
1575+
let outputs = vec![TxOut {
15771576
value: Amount::from_sat(1_000),
15781577
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
1579-
}]);
1578+
}];
1579+
let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs);
15801580
negotiate_splice_tx(initiator, acceptor, channel_id, contribution);
15811581

15821582
assert!(initiator.node.get_and_clear_pending_msg_events().is_empty());

0 commit comments

Comments
 (0)