Skip to content

Commit ffc3126

Browse files
committed
Add ability to open channel with all on-chain funds
Adds open_channel_with_all which uses get_max_drain_amount to determine the largest funding amount after accounting for on-chain fees and anchor reserves
1 parent 027a882 commit ffc3126

5 files changed

Lines changed: 288 additions & 23 deletions

File tree

bindings/ldk_node.udl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ interface Node {
174174
[Throws=NodeError]
175175
UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
176176
[Throws=NodeError]
177+
UserChannelId open_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
178+
[Throws=NodeError]
179+
UserChannelId open_announced_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
180+
[Throws=NodeError]
177181
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
178182
[Throws=NodeError]
179183
void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats);

src/lib.rs

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ impl Node {
10911091
}
10921092

10931093
fn open_channel_inner(
1094-
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64,
1094+
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: Option<u64>,
10951095
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
10961096
announce_for_forwarding: bool,
10971097
) -> Result<UserChannelId, Error> {
@@ -1111,8 +1111,38 @@ impl Node {
11111111
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
11121112
})?;
11131113

1114-
// Check funds availability after connection (includes anchor reserve calculation)
1115-
self.check_sufficient_funds_for_channel(channel_amount_sats, &node_id)?;
1114+
let channel_amount_sats = match channel_amount_sats {
1115+
Some(amount) => {
1116+
// Check funds availability after connection (includes anchor reserve
1117+
// calculation).
1118+
self.check_sufficient_funds_for_channel(amount, &peer_info.node_id)?;
1119+
amount
1120+
},
1121+
None => {
1122+
// Determine max funding amount from all available on-chain funds.
1123+
let cur_anchor_reserve_sats =
1124+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
1125+
let new_channel_reserve =
1126+
self.new_channel_anchor_reserve_sats(&peer_info.node_id)?;
1127+
let total_anchor_reserve_sats = cur_anchor_reserve_sats + new_channel_reserve;
1128+
1129+
let fee_rate =
1130+
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1131+
1132+
let amount =
1133+
self.wallet.get_max_funding_amount(total_anchor_reserve_sats, fee_rate)?;
1134+
1135+
log_info!(
1136+
self.logger,
1137+
"Opening channel with all balance: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)",
1138+
amount,
1139+
fee_rate.to_sat_per_kwu(),
1140+
total_anchor_reserve_sats,
1141+
);
1142+
1143+
amount
1144+
},
1145+
};
11161146

11171147
let mut user_config = default_user_config(&self.config);
11181148
user_config.channel_handshake_config.announce_for_forwarding = announce_for_forwarding;
@@ -1153,6 +1183,25 @@ impl Node {
11531183
}
11541184
}
11551185

1186+
fn new_channel_anchor_reserve_sats(&self, peer_node_id: &PublicKey) -> Result<u64, Error> {
1187+
let init_features = self
1188+
.peer_manager
1189+
.peer_by_node_id(peer_node_id)
1190+
.ok_or(Error::ConnectionFailed)?
1191+
.init_features;
1192+
let sats = self.config.anchor_channels_config.as_ref().map_or(0, |c| {
1193+
if init_features.requires_anchors_zero_fee_htlc_tx()
1194+
&& !c.trusted_peers_no_reserve.contains(peer_node_id)
1195+
{
1196+
c.per_channel_reserve_sats
1197+
} else {
1198+
0
1199+
}
1200+
});
1201+
1202+
Ok(sats)
1203+
}
1204+
11561205
fn check_sufficient_funds_for_channel(
11571206
&self, amount_sats: u64, peer_node_id: &PublicKey,
11581207
) -> Result<(), Error> {
@@ -1171,21 +1220,8 @@ impl Node {
11711220
}
11721221

11731222
// Fail if we have less than the channel value + anchor reserve available (if applicable).
1174-
let init_features = self
1175-
.peer_manager
1176-
.peer_by_node_id(peer_node_id)
1177-
.ok_or(Error::ConnectionFailed)?
1178-
.init_features;
1179-
let required_funds_sats = amount_sats
1180-
+ self.config.anchor_channels_config.as_ref().map_or(0, |c| {
1181-
if init_features.requires_anchors_zero_fee_htlc_tx()
1182-
&& !c.trusted_peers_no_reserve.contains(peer_node_id)
1183-
{
1184-
c.per_channel_reserve_sats
1185-
} else {
1186-
0
1187-
}
1188-
});
1223+
let required_funds_sats =
1224+
amount_sats + self.new_channel_anchor_reserve_sats(peer_node_id)?;
11891225

11901226
if spendable_amount_sats < required_funds_sats {
11911227
log_error!(self.logger,
@@ -1222,7 +1258,7 @@ impl Node {
12221258
self.open_channel_inner(
12231259
node_id,
12241260
address,
1225-
channel_amount_sats,
1261+
Some(channel_amount_sats),
12261262
push_to_counterparty_msat,
12271263
channel_config,
12281264
false,
@@ -1262,7 +1298,72 @@ impl Node {
12621298
self.open_channel_inner(
12631299
node_id,
12641300
address,
1265-
channel_amount_sats,
1301+
Some(channel_amount_sats),
1302+
push_to_counterparty_msat,
1303+
channel_config,
1304+
true,
1305+
)
1306+
}
1307+
1308+
/// Connect to a node and open a new unannounced channel, using all available on-chain funds
1309+
/// minus fees and anchor reserves.
1310+
///
1311+
/// To open an announced channel, see [`Node::open_announced_channel_with_all`].
1312+
///
1313+
/// Disconnects and reconnects are handled automatically.
1314+
///
1315+
/// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the
1316+
/// channel counterparty on channel open. This can be useful to start out with the balance not
1317+
/// entirely shifted to one side, therefore allowing to receive payments from the getgo.
1318+
///
1319+
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
1320+
///
1321+
/// [`AnchorChannelsConfig::per_channel_reserve_sats`]: crate::config::AnchorChannelsConfig::per_channel_reserve_sats
1322+
pub fn open_channel_with_all(
1323+
&self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option<u64>,
1324+
channel_config: Option<ChannelConfig>,
1325+
) -> Result<UserChannelId, Error> {
1326+
self.open_channel_inner(
1327+
node_id,
1328+
address,
1329+
None,
1330+
push_to_counterparty_msat,
1331+
channel_config,
1332+
false,
1333+
)
1334+
}
1335+
1336+
/// Connect to a node and open a new announced channel, using all available on-chain funds
1337+
/// minus fees and anchor reserves.
1338+
///
1339+
/// This will return an error if the node has not been sufficiently configured to operate as a
1340+
/// forwarding node that can properly announce its existence to the public network graph, i.e.,
1341+
/// [`Config::listening_addresses`] and [`Config::node_alias`] are unset.
1342+
///
1343+
/// To open an unannounced channel, see [`Node::open_channel_with_all`].
1344+
///
1345+
/// Disconnects and reconnects are handled automatically.
1346+
///
1347+
/// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the
1348+
/// channel counterparty on channel open. This can be useful to start out with the balance not
1349+
/// entirely shifted to one side, therefore allowing to receive payments from the getgo.
1350+
///
1351+
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
1352+
///
1353+
/// [`AnchorChannelsConfig::per_channel_reserve_sats`]: crate::config::AnchorChannelsConfig::per_channel_reserve_sats
1354+
pub fn open_announced_channel_with_all(
1355+
&self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option<u64>,
1356+
channel_config: Option<ChannelConfig>,
1357+
) -> Result<UserChannelId, Error> {
1358+
if let Err(err) = may_announce_channel(&self.config) {
1359+
log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node: {err}");
1360+
return Err(Error::ChannelCreationFailed);
1361+
}
1362+
1363+
self.open_channel_inner(
1364+
node_id,
1365+
address,
1366+
None,
12661367
push_to_counterparty_msat,
12671368
channel_config,
12681369
true,

src/wallet/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,29 @@ impl Wallet {
581581
Ok((max_amount, tmp_psbt))
582582
}
583583

584+
/// Returns the maximum amount available for funding a channel, accounting for on-chain fees
585+
/// and anchor reserves.
586+
pub(crate) fn get_max_funding_amount(
587+
&self, cur_anchor_reserve_sats: u64, fee_rate: FeeRate,
588+
) -> Result<u64, Error> {
589+
let mut locked_wallet = self.inner.lock().unwrap();
590+
591+
// Use a dummy P2WSH script (34 bytes) to match the size of a real funding output.
592+
let dummy_p2wsh_script = ScriptBuf::new().to_p2wsh();
593+
594+
let (max_amount, tmp_psbt) = self.get_max_drain_amount(
595+
&mut locked_wallet,
596+
dummy_p2wsh_script,
597+
cur_anchor_reserve_sats,
598+
fee_rate,
599+
None,
600+
)?;
601+
602+
locked_wallet.cancel_tx(&tmp_psbt.unsigned_tx);
603+
604+
Ok(max_amount)
605+
}
606+
584607
pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
585608
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
586609
.map_err(|_| Error::InvalidAddress)?

tests/common/mod.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,38 @@ pub async fn open_channel_push_amt(
719719
funding_txo_a
720720
}
721721

722+
pub async fn open_channel_with_all(
723+
node_a: &TestNode, node_b: &TestNode, should_announce: bool, electrsd: &ElectrsD,
724+
) -> OutPoint {
725+
if should_announce {
726+
node_a
727+
.open_announced_channel_with_all(
728+
node_b.node_id(),
729+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
730+
None,
731+
None,
732+
)
733+
.unwrap();
734+
} else {
735+
node_a
736+
.open_channel_with_all(
737+
node_b.node_id(),
738+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
739+
None,
740+
None,
741+
)
742+
.unwrap();
743+
}
744+
assert!(node_a.list_peers().iter().find(|c| { c.node_id == node_b.node_id() }).is_some());
745+
746+
let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id());
747+
let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id());
748+
assert_eq!(funding_txo_a, funding_txo_b);
749+
wait_for_tx(&electrsd.client, funding_txo_a.txid).await;
750+
751+
funding_txo_a
752+
}
753+
722754
pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
723755
node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool,
724756
expect_anchor_channel: bool, force_close: bool,

tests/integration_tests_rust.rs

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ use common::{
2121
expect_channel_pending_event, expect_channel_ready_event, expect_event,
2222
expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event,
2323
expect_splice_pending_event, generate_blocks_and_wait, open_channel, open_channel_push_amt,
24-
premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config,
25-
random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node,
26-
setup_two_nodes, wait_for_tx, TestChainSource, TestStoreType, TestSyncStore,
24+
open_channel_with_all, premine_and_distribute_funds, premine_blocks, prepare_rbf,
25+
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,
2728
};
2829
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
2930
use ldk_node::entropy::NodeEntropy;
@@ -2462,3 +2463,107 @@ async fn persistence_backwards_compatibility() {
24622463

24632464
node_new.stop().unwrap();
24642465
}
2466+
2467+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2468+
async fn open_channel_with_all_with_anchors() {
2469+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2470+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2471+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
2472+
2473+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2474+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2475+
2476+
let premine_amount_sat = 1_000_000;
2477+
2478+
premine_and_distribute_funds(
2479+
&bitcoind.client,
2480+
&electrsd.client,
2481+
vec![addr_a, addr_b],
2482+
Amount::from_sat(premine_amount_sat),
2483+
)
2484+
.await;
2485+
node_a.sync_wallets().unwrap();
2486+
node_b.sync_wallets().unwrap();
2487+
assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
2488+
2489+
let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await;
2490+
2491+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2492+
2493+
node_a.sync_wallets().unwrap();
2494+
node_b.sync_wallets().unwrap();
2495+
2496+
let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
2497+
let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
2498+
2499+
// After opening a channel with all balance, the remaining on-chain balance should only
2500+
// be the anchor reserve (25k sats by default) plus a small margin for change
2501+
let anchor_reserve_sat = 25_000;
2502+
let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats;
2503+
assert!(
2504+
remaining_balance < anchor_reserve_sat + 500,
2505+
"Remaining balance {remaining_balance} should be close to the anchor reserve {anchor_reserve_sat}"
2506+
);
2507+
2508+
// Verify a channel was opened with most of the funds
2509+
let channels = node_a.list_channels();
2510+
assert_eq!(channels.len(), 1);
2511+
let channel = &channels[0];
2512+
assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500);
2513+
assert_eq!(channel.counterparty_node_id, node_b.node_id());
2514+
assert_eq!(channel.funding_txo.unwrap(), funding_txo);
2515+
2516+
node_a.stop().unwrap();
2517+
node_b.stop().unwrap();
2518+
}
2519+
2520+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2521+
async fn open_channel_with_all_without_anchors() {
2522+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2523+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2524+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false);
2525+
2526+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2527+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2528+
2529+
let premine_amount_sat = 1_000_000;
2530+
2531+
premine_and_distribute_funds(
2532+
&bitcoind.client,
2533+
&electrsd.client,
2534+
vec![addr_a, addr_b],
2535+
Amount::from_sat(premine_amount_sat),
2536+
)
2537+
.await;
2538+
node_a.sync_wallets().unwrap();
2539+
node_b.sync_wallets().unwrap();
2540+
assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
2541+
2542+
let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await;
2543+
2544+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2545+
2546+
node_a.sync_wallets().unwrap();
2547+
node_b.sync_wallets().unwrap();
2548+
2549+
let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
2550+
let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
2551+
2552+
// Without anchors, there should be no remaining balance
2553+
let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats;
2554+
assert_eq!(
2555+
remaining_balance, 0,
2556+
"Remaining balance {remaining_balance} should be zero without anchor reserve"
2557+
);
2558+
2559+
// Verify a channel was opened with all the funds accounting for fees
2560+
let channels = node_a.list_channels();
2561+
assert_eq!(channels.len(), 1);
2562+
let channel = &channels[0];
2563+
assert!(channel.channel_value_sats > premine_amount_sat - 500);
2564+
assert_eq!(channel.counterparty_node_id, node_b.node_id());
2565+
assert_eq!(channel.funding_txo.unwrap(), funding_txo);
2566+
2567+
node_a.stop().unwrap();
2568+
node_b.stop().unwrap();
2569+
}

0 commit comments

Comments
 (0)