Skip to content

Commit 4a2cefc

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 c4a45a6 commit 4a2cefc

File tree

5 files changed

+223
-17
lines changed

5 files changed

+223
-17
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: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,14 @@ use graph::NetworkGraph;
139139
use io::utils::write_node_metrics;
140140
use lightning::chain::BestBlock;
141141
use lightning::impl_writeable_tlv_based;
142+
use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
142143
use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState};
143144
use lightning::ln::channelmanager::PaymentId;
144145
use lightning::ln::msgs::SocketAddress;
145146
use lightning::routing::gossip::NodeAlias;
146147
use lightning::sign::EntropySource;
147148
use lightning::util::persist::KVStoreSync;
148-
use lightning::util::wallet_utils::Wallet as LdkWallet;
149+
use lightning::util::wallet_utils::{Input, Wallet as LdkWallet};
149150
use lightning_background_processor::process_events_async;
150151
use liquidity::{LSPS1Liquidity, LiquiditySource};
151152
use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
@@ -1374,29 +1375,73 @@ impl Node {
13741375
)
13751376
}
13761377

1377-
/// Add funds from the on-chain wallet into an existing channel.
1378-
///
1379-
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1380-
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1381-
/// while waiting for a new funding transaction to confirm.
1382-
///
1383-
/// # Experimental API
1384-
///
1385-
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
1386-
/// this classification may change in the future.
1387-
pub fn splice_in(
1378+
fn splice_in_inner(
13881379
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1389-
splice_amount_sats: u64,
1380+
splice_amount_sats: FundingAmount,
13901381
) -> Result<(), Error> {
13911382
let open_channels =
13921383
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
13931384
if let Some(channel_details) =
13941385
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
13951386
{
1396-
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
1397-
13981387
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
13991388

1389+
let splice_amount_sats = match splice_amount_sats {
1390+
FundingAmount::Exact { amount_sats } => amount_sats,
1391+
FundingAmount::Max => {
1392+
let cur_anchor_reserve_sats =
1393+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
1394+
1395+
const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1396+
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
1397+
1398+
let funding_txo = channel_details.funding_txo.ok_or_else(|| {
1399+
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
1400+
Error::ChannelSplicingFailed
1401+
})?;
1402+
1403+
let funding_output = channel_details.get_funding_output().ok_or_else(|| {
1404+
log_error!(self.logger, "Failed to splice channel: channel not yet ready");
1405+
Error::ChannelSplicingFailed
1406+
})?;
1407+
1408+
let shared_input = Input {
1409+
outpoint: funding_txo.into_bitcoin_outpoint(),
1410+
previous_utxo: funding_output.clone(),
1411+
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT
1412+
+ FUNDING_TRANSACTION_WITNESS_WEIGHT,
1413+
};
1414+
1415+
let amount = self
1416+
.wallet
1417+
.get_max_splice_in_amount(
1418+
shared_input,
1419+
funding_output.script_pubkey.clone(),
1420+
cur_anchor_reserve_sats,
1421+
fee_rate,
1422+
)
1423+
.map_err(|e| {
1424+
log_error!(
1425+
self.logger,
1426+
"Failed to determine max splice-in amount: {e:?}"
1427+
);
1428+
e
1429+
})?;
1430+
1431+
log_info!(
1432+
self.logger,
1433+
"Splicing in with all balance: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)",
1434+
amount,
1435+
fee_rate.to_sat_per_kwu(),
1436+
cur_anchor_reserve_sats,
1437+
);
1438+
1439+
amount
1440+
},
1441+
};
1442+
1443+
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;
1444+
14001445
let funding_template = self
14011446
.channel_manager
14021447
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
@@ -1438,6 +1483,46 @@ impl Node {
14381483
}
14391484
}
14401485

1486+
/// Add funds to an existing channel from a transaction output you control.
1487+
///
1488+
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1489+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1490+
/// while waiting for a new funding transaction to confirm.
1491+
///
1492+
/// # Experimental API
1493+
///
1494+
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
1495+
/// this classification may change in the future.
1496+
pub fn splice_in(
1497+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1498+
splice_amount_sats: u64,
1499+
) -> Result<(), Error> {
1500+
self.splice_in_inner(
1501+
user_channel_id,
1502+
counterparty_node_id,
1503+
FundingAmount::Exact { amount_sats: splice_amount_sats },
1504+
)
1505+
}
1506+
1507+
/// Add all available on-chain funds into an existing channel.
1508+
///
1509+
/// This is similar to [`Node::splice_in`] but uses all available confirmed on-chain funds
1510+
/// instead of requiring a specific amount.
1511+
///
1512+
/// This provides for increasing a channel's outbound liquidity without re-balancing or closing
1513+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1514+
/// while waiting for a new funding transaction to confirm.
1515+
///
1516+
/// # Experimental API
1517+
///
1518+
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
1519+
/// this classification may change in the future.
1520+
pub fn splice_in_with_all(
1521+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
1522+
) -> Result<(), Error> {
1523+
self.splice_in_inner(user_channel_id, counterparty_node_id, FundingAmount::Max)
1524+
}
1525+
14411526
/// Remove funds from an existing channel, sending them to an on-chain address.
14421527
///
14431528
/// This provides for decreasing a channel's outbound liquidity without re-balancing or closing

src/wallet/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,36 @@ impl Wallet {
662662
Ok(max_amount)
663663
}
664664

665+
/// Returns the maximum amount available for splicing into an existing channel, accounting for
666+
/// on-chain fees and anchor reserves, along with the wallet UTXOs to use as inputs.
667+
pub(crate) fn get_max_splice_in_amount(
668+
&self, shared_input: Input, shared_output_script: ScriptBuf, cur_anchor_reserve_sats: u64,
669+
fee_rate: FeeRate,
670+
) -> Result<u64, Error> {
671+
let mut locked_wallet = self.inner.lock().unwrap();
672+
673+
debug_assert!(matches!(
674+
locked_wallet.public_descriptor(KeychainKind::External),
675+
ExtendedDescriptor::Wpkh(_)
676+
));
677+
debug_assert!(matches!(
678+
locked_wallet.public_descriptor(KeychainKind::Internal),
679+
ExtendedDescriptor::Wpkh(_)
680+
));
681+
682+
let (splice_amount, tmp_psbt) = self.get_max_drain_amount(
683+
&mut locked_wallet,
684+
shared_output_script,
685+
cur_anchor_reserve_sats,
686+
fee_rate,
687+
Some(&shared_input),
688+
)?;
689+
690+
locked_wallet.cancel_tx(&tmp_psbt.unsigned_tx);
691+
692+
Ok(splice_amount)
693+
}
694+
665695
pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
666696
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
667697
.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;
@@ -779,6 +780,16 @@ pub async fn open_channel_with_all(
779780
funding_txo_a
780781
}
781782

783+
pub async fn splice_in_with_all(
784+
node_a: &TestNode, node_b: &TestNode, user_channel_id: &UserChannelId, electrsd: &ElectrsD,
785+
) {
786+
node_a.splice_in_with_all(user_channel_id, node_b.node_id()).unwrap();
787+
788+
let splice_txo = expect_splice_pending_event!(node_a, node_b.node_id());
789+
expect_splice_pending_event!(node_b, node_a.node_id());
790+
wait_for_tx(&electrsd.client, splice_txo.txid).await;
791+
}
792+
782793
pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
783794
node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool,
784795
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_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait,
2424
open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds,
2525
premine_blocks, prepare_rbf, random_chain_source, random_config, random_listening_addresses,
26-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx,
27-
TestChainSource, TestStoreType, TestSyncStore,
26+
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all,
27+
wait_for_tx, TestChainSource, TestStoreType, TestSyncStore,
2828
};
2929
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
3030
use ldk_node::entropy::NodeEntropy;
@@ -2731,3 +2731,81 @@ async fn open_channel_with_all_without_anchors() {
27312731
node_a.stop().unwrap();
27322732
node_b.stop().unwrap();
27332733
}
2734+
2735+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2736+
async fn splice_in_with_all_balance() {
2737+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2738+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2739+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
2740+
2741+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2742+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2743+
2744+
let premine_amount_sat = 5_000_000;
2745+
let channel_amount_sat = 1_000_000;
2746+
2747+
premine_and_distribute_funds(
2748+
&bitcoind.client,
2749+
&electrsd.client,
2750+
vec![addr_a, addr_b],
2751+
Amount::from_sat(premine_amount_sat),
2752+
)
2753+
.await;
2754+
node_a.sync_wallets().unwrap();
2755+
node_b.sync_wallets().unwrap();
2756+
assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
2757+
2758+
// Open a channel with a fixed amount first
2759+
let funding_txo = open_channel(&node_a, &node_b, channel_amount_sat, false, &electrsd).await;
2760+
2761+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2762+
2763+
node_a.sync_wallets().unwrap();
2764+
node_b.sync_wallets().unwrap();
2765+
2766+
let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
2767+
let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
2768+
2769+
let channels = node_a.list_channels();
2770+
assert_eq!(channels.len(), 1);
2771+
assert_eq!(channels[0].channel_value_sats, channel_amount_sat);
2772+
assert_eq!(channels[0].funding_txo.unwrap(), funding_txo);
2773+
2774+
let balance_before_splice = node_a.list_balances().spendable_onchain_balance_sats;
2775+
assert!(balance_before_splice > 0);
2776+
2777+
// Splice in with all remaining on-chain funds
2778+
splice_in_with_all(&node_a, &node_b, &user_channel_id_a, &electrsd).await;
2779+
2780+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2781+
2782+
node_a.sync_wallets().unwrap();
2783+
node_b.sync_wallets().unwrap();
2784+
2785+
let _user_channel_id_a2 = expect_channel_ready_event!(node_a, node_b.node_id());
2786+
let _user_channel_id_b2 = expect_channel_ready_event!(node_b, node_a.node_id());
2787+
2788+
// After splicing with all balance, channel value should be close to the premined amount
2789+
// minus fees and anchor reserve
2790+
let anchor_reserve_sat = 25_000;
2791+
let channels = node_a.list_channels();
2792+
assert_eq!(channels.len(), 1);
2793+
let channel = &channels[0];
2794+
assert!(
2795+
channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 1000,
2796+
"Channel value {} should be close to premined amount {} minus anchor reserve {} and fees",
2797+
channel.channel_value_sats,
2798+
premine_amount_sat,
2799+
anchor_reserve_sat,
2800+
);
2801+
2802+
// Remaining on-chain balance should be close to just the anchor reserve
2803+
let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats;
2804+
assert!(
2805+
remaining_balance < anchor_reserve_sat + 500,
2806+
"Remaining balance {remaining_balance} should be close to the anchor reserve {anchor_reserve_sat}"
2807+
);
2808+
2809+
node_a.stop().unwrap();
2810+
node_b.stop().unwrap();
2811+
}

0 commit comments

Comments
 (0)