Skip to content

Commit 86e2792

Browse files
committed
Add ability to splice in with all on-chain funds
Adds splice_in_with_all which uses get_max_drain_amount with a shared input to determine the largest splice-in amount after accounting for on-chain fees and anchor reserves.
1 parent ffc3126 commit 86e2792

File tree

5 files changed

+231
-21
lines changed

5 files changed

+231
-21
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ interface Node {
180180
[Throws=NodeError]
181181
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
182182
[Throws=NodeError]
183+
void splice_in_with_all([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
184+
[Throws=NodeError]
183185
void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats);
184186
[Throws=NodeError]
185187
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);

src/lib.rs

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,17 +1380,15 @@ impl Node {
13801380
///
13811381
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
13821382
/// this classification may change in the future.
1383-
pub fn splice_in(
1383+
fn splice_in_inner(
13841384
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1385-
splice_amount_sats: u64,
1385+
splice_amount_sats: Option<u64>,
13861386
) -> Result<(), Error> {
13871387
let open_channels =
13881388
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
13891389
if let Some(channel_details) =
13901390
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
13911391
{
1392-
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
1393-
13941392
const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
13951393
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
13961394

@@ -1410,25 +1408,63 @@ impl Node {
14101408
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
14111409
};
14121410

1413-
let shared_output = bitcoin::TxOut {
1414-
value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats),
1415-
// will not actually be the exact same script pubkey after splice
1416-
// but it is the same size and good enough for coin selection purposes
1417-
script_pubkey: funding_output.script_pubkey.clone(),
1418-
};
1419-
14201411
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
14211412

1422-
let inputs = self
1423-
.wallet
1424-
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
1425-
.map_err(|()| {
1426-
log_error!(
1413+
let (splice_amount_sats, inputs) = match splice_amount_sats {
1414+
Some(amount) => {
1415+
self.check_sufficient_funds_for_channel(amount, &counterparty_node_id)?;
1416+
1417+
let shared_output = bitcoin::TxOut {
1418+
value: shared_input.previous_utxo.value + Amount::from_sat(amount),
1419+
// will not actually be the exact same script pubkey after splice
1420+
// but it is the same size and good enough for coin selection purposes
1421+
script_pubkey: funding_output.script_pubkey.clone(),
1422+
};
1423+
1424+
let inputs = self
1425+
.wallet
1426+
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
1427+
.map_err(|()| {
1428+
log_error!(
1429+
self.logger,
1430+
"Failed to splice channel: insufficient confirmed UTXOs",
1431+
);
1432+
Error::ChannelSplicingFailed
1433+
})?;
1434+
1435+
(amount, inputs)
1436+
},
1437+
None => {
1438+
let cur_anchor_reserve_sats =
1439+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
1440+
1441+
let (amount, inputs) = self
1442+
.wallet
1443+
.get_max_splice_in_amount(
1444+
shared_input,
1445+
funding_output.script_pubkey.clone(),
1446+
cur_anchor_reserve_sats,
1447+
fee_rate,
1448+
)
1449+
.map_err(|e| {
1450+
log_error!(
1451+
self.logger,
1452+
"Failed to determine max splice-in amount: {e:?}"
1453+
);
1454+
e
1455+
})?;
1456+
1457+
log_info!(
14271458
self.logger,
1428-
"Failed to splice channel: insufficient confirmed UTXOs",
1459+
"Splicing in with all balance: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)",
1460+
amount,
1461+
fee_rate.to_sat_per_kwu(),
1462+
cur_anchor_reserve_sats,
14291463
);
1430-
Error::ChannelSplicingFailed
1431-
})?;
1464+
1465+
(amount, inputs)
1466+
},
1467+
};
14321468

14331469
let change_address = self.wallet.get_new_internal_address()?;
14341470

@@ -1482,6 +1518,42 @@ impl Node {
14821518
}
14831519
}
14841520

1521+
/// Add funds to an existing channel from a transaction output you control.
1522+
///
1523+
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1524+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1525+
/// while waiting for a new funding transaction to confirm.
1526+
///
1527+
/// # Experimental API
1528+
///
1529+
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
1530+
/// this classification may change in the future.
1531+
pub fn splice_in(
1532+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1533+
splice_amount_sats: u64,
1534+
) -> Result<(), Error> {
1535+
self.splice_in_inner(user_channel_id, counterparty_node_id, Some(splice_amount_sats))
1536+
}
1537+
1538+
/// Add all available on-chain funds into an existing channel.
1539+
///
1540+
/// This is similar to [`Node::splice_in`] but uses all available confirmed on-chain funds
1541+
/// instead of requiring a specific amount.
1542+
///
1543+
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1544+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1545+
/// while waiting for a new funding transaction to confirm.
1546+
///
1547+
/// # Experimental API
1548+
///
1549+
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
1550+
/// this classification may change in the future.
1551+
pub fn splice_in_with_all(
1552+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1553+
) -> Result<(), Error> {
1554+
self.splice_in_inner(user_channel_id, counterparty_node_id, None)
1555+
}
1556+
14851557
/// Remove funds from an existing channel, sending them to an on-chain address.
14861558
///
14871559
/// This provides for decreasing a channel's outbound liquidity without re-balancing or closing

src/wallet/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,53 @@ impl Wallet {
604604
Ok(max_amount)
605605
}
606606

607+
/// Returns the maximum amount available for splicing into an existing channel, accounting for
608+
/// on-chain fees and anchor reserves, along with the wallet UTXOs to use as inputs.
609+
pub(crate) fn get_max_splice_in_amount(
610+
&self, shared_input: Input, shared_output_script: ScriptBuf, cur_anchor_reserve_sats: u64,
611+
fee_rate: FeeRate,
612+
) -> Result<(u64, Vec<FundingTxInput>), Error> {
613+
let mut locked_wallet = self.inner.lock().unwrap();
614+
615+
debug_assert!(matches!(
616+
locked_wallet.public_descriptor(KeychainKind::External),
617+
ExtendedDescriptor::Wpkh(_)
618+
));
619+
debug_assert!(matches!(
620+
locked_wallet.public_descriptor(KeychainKind::Internal),
621+
ExtendedDescriptor::Wpkh(_)
622+
));
623+
624+
let (splice_amount, tmp_psbt) = self.get_max_drain_amount(
625+
&mut locked_wallet,
626+
shared_output_script,
627+
cur_anchor_reserve_sats,
628+
fee_rate,
629+
Some(&shared_input),
630+
)?;
631+
632+
let inputs = tmp_psbt
633+
.unsigned_tx
634+
.input
635+
.iter()
636+
.filter(|txin| txin.previous_output != shared_input.outpoint)
637+
.filter_map(|txin| {
638+
locked_wallet
639+
.tx_details(txin.previous_output.txid)
640+
.map(|tx_details| tx_details.tx.deref().clone())
641+
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
642+
})
643+
.collect::<Result<Vec<_>, ()>>()
644+
.map_err(|_| {
645+
log_error!(self.logger, "Failed to collect wallet UTXOs for splice");
646+
Error::ChannelSplicingFailed
647+
})?;
648+
649+
locked_wallet.cancel_tx(&tmp_psbt.unsigned_tx);
650+
651+
Ok((splice_amount, inputs))
652+
}
653+
607654
pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
608655
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
609656
.map_err(|_| Error::InvalidAddress)?

tests/common/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use ldk_node::io::sqlite_store::SqliteStore;
3232
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
3333
use ldk_node::{
3434
Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance,
35+
UserChannelId,
3536
};
3637
use lightning::io;
3738
use lightning::ln::msgs::SocketAddress;
@@ -751,6 +752,16 @@ pub async fn open_channel_with_all(
751752
funding_txo_a
752753
}
753754

755+
pub async fn splice_in_with_all(
756+
node_a: &TestNode, node_b: &TestNode, user_channel_id: &UserChannelId, electrsd: &ElectrsD,
757+
) {
758+
node_a.splice_in_with_all(user_channel_id, node_b.node_id()).unwrap();
759+
760+
let splice_txo = expect_splice_pending_event!(node_a, node_b.node_id());
761+
expect_splice_pending_event!(node_b, node_a.node_id());
762+
wait_for_tx(&electrsd.client, splice_txo.txid).await;
763+
}
764+
754765
pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
755766
node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool,
756767
expect_anchor_channel: bool, force_close: bool,

tests/integration_tests_rust.rs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ use common::{
2323
expect_splice_pending_event, generate_blocks_and_wait, open_channel, open_channel_push_amt,
2424
open_channel_with_all, premine_and_distribute_funds, premine_blocks, prepare_rbf,
2525
random_chain_source, random_config, random_listening_addresses, setup_bitcoind_and_electrsd,
26-
setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestStoreType,
27-
TestSyncStore,
26+
setup_builder, setup_node, setup_two_nodes, splice_in_with_all, wait_for_tx, TestChainSource,
27+
TestStoreType, TestSyncStore,
2828
};
2929
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
3030
use ldk_node::entropy::NodeEntropy;
@@ -2567,3 +2567,81 @@ async fn open_channel_with_all_without_anchors() {
25672567
node_a.stop().unwrap();
25682568
node_b.stop().unwrap();
25692569
}
2570+
2571+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2572+
async fn splice_in_with_all_balance() {
2573+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2574+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2575+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
2576+
2577+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2578+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2579+
2580+
let premine_amount_sat = 5_000_000;
2581+
let channel_amount_sat = 1_000_000;
2582+
2583+
premine_and_distribute_funds(
2584+
&bitcoind.client,
2585+
&electrsd.client,
2586+
vec![addr_a, addr_b],
2587+
Amount::from_sat(premine_amount_sat),
2588+
)
2589+
.await;
2590+
node_a.sync_wallets().unwrap();
2591+
node_b.sync_wallets().unwrap();
2592+
assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
2593+
2594+
// Open a channel with a fixed amount first
2595+
let funding_txo = open_channel(&node_a, &node_b, channel_amount_sat, false, &electrsd).await;
2596+
2597+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2598+
2599+
node_a.sync_wallets().unwrap();
2600+
node_b.sync_wallets().unwrap();
2601+
2602+
let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
2603+
let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
2604+
2605+
let channels = node_a.list_channels();
2606+
assert_eq!(channels.len(), 1);
2607+
assert_eq!(channels[0].channel_value_sats, channel_amount_sat);
2608+
assert_eq!(channels[0].funding_txo.unwrap(), funding_txo);
2609+
2610+
let balance_before_splice = node_a.list_balances().spendable_onchain_balance_sats;
2611+
assert!(balance_before_splice > 0);
2612+
2613+
// Splice in with all remaining on-chain funds
2614+
splice_in_with_all(&node_a, &node_b, &user_channel_id_a, &electrsd).await;
2615+
2616+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2617+
2618+
node_a.sync_wallets().unwrap();
2619+
node_b.sync_wallets().unwrap();
2620+
2621+
let _user_channel_id_a2 = expect_channel_ready_event!(node_a, node_b.node_id());
2622+
let _user_channel_id_b2 = expect_channel_ready_event!(node_b, node_a.node_id());
2623+
2624+
// After splicing with all balance, channel value should be close to the premined amount
2625+
// minus fees and anchor reserve
2626+
let anchor_reserve_sat = 25_000;
2627+
let channels = node_a.list_channels();
2628+
assert_eq!(channels.len(), 1);
2629+
let channel = &channels[0];
2630+
assert!(
2631+
channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 1000,
2632+
"Channel value {} should be close to premined amount {} minus anchor reserve {} and fees",
2633+
channel.channel_value_sats,
2634+
premine_amount_sat,
2635+
anchor_reserve_sat,
2636+
);
2637+
2638+
// Remaining on-chain balance should be close to just the anchor reserve
2639+
let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats;
2640+
assert!(
2641+
remaining_balance < anchor_reserve_sat + 500,
2642+
"Remaining balance {remaining_balance} should be close to the anchor reserve {anchor_reserve_sat}"
2643+
);
2644+
2645+
node_a.stop().unwrap();
2646+
node_b.stop().unwrap();
2647+
}

0 commit comments

Comments
 (0)