Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
disclosed. The method is stateless: the payment id, preimage, and invoice are taken from
`Event::PaymentSuccessful` and nothing is persisted. Payments settled via a static invoice,
i.e., async payments, don't support payer proofs. (#1045)
- `Node::splice_in`, `Node::splice_in_with_all`, and `Node::splice_out` now take an optional
`min_fee_rate`. If set, it is used as the minimum feerate for our contribution to the splice
transaction instead of a feerate estimate retrieved from the configured chain source. For
`splice_in_with_all`, the amount spliced in is determined at `min_fee_rate`, or at the estimate
if unset.
`Node::bump_channel_funding_fee` takes the same optional `min_fee_rate` for the replacement
transaction.

## Bug Fixes and Improvements
- Building a fresh node against a Bitcoin Core RPC or REST chain source that fails to return the
Expand Down
8 changes: 4 additions & 4 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ interface Node {
[Throws=NodeError]
UserChannelId open_0reserve_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, FeeRate? fee_rate, ChannelConfig? channel_config);
[Throws=NodeError]
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats, FeeRate? min_fee_rate);
[Throws=NodeError]
void splice_in_with_all([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
void splice_in_with_all([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, FeeRate? min_fee_rate);
[Throws=NodeError]
void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats);
void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats, FeeRate? min_fee_rate);
[Throws=NodeError]
void bump_channel_funding_fee([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
void bump_channel_funding_fee([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, FeeRate? min_fee_rate);
[Throws=NodeError]
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
[Throws=NodeError]
Expand Down
4 changes: 2 additions & 2 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2819,9 +2819,9 @@ mod tests {
node.update_channel_config(&user_channel_id, node_id, Default::default()),
Err(Error::NotRunning)
);
assert_eq!(node.splice_in(&user_channel_id, node_id, 1_000), Err(Error::NotRunning));
assert_eq!(node.splice_in(&user_channel_id, node_id, 1_000, None), Err(Error::NotRunning));
assert_eq!(
node.bump_channel_funding_fee(&user_channel_id, node_id),
node.bump_channel_funding_fee(&user_channel_id, node_id, None),
Err(Error::NotRunning)
);
assert_eq!(node.bolt11_payment().fail_for_id(payment_id), Err(Error::NotRunning));
Expand Down
60 changes: 50 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1805,15 +1805,16 @@ impl Node {

fn splice_in_inner(
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
splice_amount_sats: FundingAmount,
splice_amount_sats: FundingAmount, min_fee_rate: Option<bitcoin::FeeRate>,
) -> Result<(), Error> {
let open_channels =
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
if let Some(channel_details) =
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
{
let min_feerate =
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
let min_feerate = min_fee_rate.unwrap_or_else(|| {
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding)
});
let max_feerate = max_funding_feerate(min_feerate);

let splice_amount_sats = match splice_amount_sats {
Expand Down Expand Up @@ -1939,13 +1940,19 @@ impl Node {
/// it. Once negotiation with the counterparty is complete, the channel remains operational
/// while waiting for a new funding transaction to confirm.
///
/// If `min_fee_rate` is set, it will be used as the minimum feerate for our contribution to the
/// splice transaction instead of a feerate estimate retrieved from the configured chain source.
/// As with the estimate, we may pay up to 1.5 times this feerate: when the channel already has a
/// pending splice that requires a higher feerate to be replaced, or when the counterparty
/// initiates a splice concurrently and our contribution is added to theirs.
///
/// # Experimental API
///
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
/// this classification may change in the future.
pub fn splice_in(
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
splice_amount_sats: u64,
splice_amount_sats: u64, min_fee_rate: Option<FfiFeeRate>,
) -> Result<(), Error> {
if !*self.is_running.read().expect("lock") {
return Err(Error::NotRunning);
Expand All @@ -1955,6 +1962,7 @@ impl Node {
user_channel_id,
counterparty_node_id,
FundingAmount::Exact { amount_sats: splice_amount_sats },
maybe_map_fee_rate_opt(min_fee_rate),
)
}

Expand All @@ -1967,17 +1975,31 @@ impl Node {
/// it. Once negotiation with the counterparty is complete, the channel remains operational
/// while waiting for a new funding transaction to confirm.
///
/// If `min_fee_rate` is set, it will be used as the minimum feerate for our contribution to the
/// splice transaction instead of a feerate estimate retrieved from the configured chain source.
/// As with the estimate, we may pay up to 1.5 times this feerate: when the channel already has a
/// pending splice that requires a higher feerate to be replaced, or when the counterparty
/// initiates a splice concurrently and our contribution is added to theirs.
///
/// The amount spliced in is determined at `min_fee_rate`, or at the estimate if unset.
///
/// # Experimental API
///
/// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but
/// this classification may change in the future.
pub fn splice_in_with_all(
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
min_fee_rate: Option<FfiFeeRate>,
) -> Result<(), Error> {
if !*self.is_running.read().expect("lock") {
return Err(Error::NotRunning);
}
self.splice_in_inner(user_channel_id, counterparty_node_id, FundingAmount::Max)
self.splice_in_inner(
user_channel_id,
counterparty_node_id,
FundingAmount::Max,
maybe_map_fee_rate_opt(min_fee_rate),
)
}

/// Remove funds from an existing channel, sending them to an on-chain address.
Expand All @@ -1986,19 +2008,27 @@ impl Node {
/// it. Once negotiation with the counterparty is complete, the channel remains operational
/// while waiting for a new funding transaction to confirm.
///
/// If `min_fee_rate` is set, it will be used as the minimum feerate for our contribution to the
/// splice transaction instead of a feerate estimate retrieved from the configured chain source.
/// As with the estimate, we may pay up to 1.5 times this feerate: when the channel already has a
/// pending splice that requires a higher feerate to be replaced, or when the counterparty
/// initiates a splice concurrently and our contribution is added to theirs.
///
/// # Experimental API
///
/// This API is experimental. Currently, a splice-out will be marked as an inbound payment if
/// paid to an address associated with the on-chain wallet, but this classification may change
/// in the future.
pub fn splice_out(
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, address: &Address,
splice_amount_sats: u64,
splice_amount_sats: u64, min_fee_rate: Option<FfiFeeRate>,
) -> Result<(), Error> {
if !*self.is_running.read().expect("lock") {
return Err(Error::NotRunning);
}

let min_fee_rate = maybe_map_fee_rate_opt(min_fee_rate);

let open_channels =
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
if let Some(channel_details) =
Expand All @@ -2012,8 +2042,9 @@ impl Node {

self.wallet.parse_and_validate_address(address)?;

let min_feerate =
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
let min_feerate = min_fee_rate.unwrap_or_else(|| {
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding)
});
let max_feerate = max_funding_feerate(min_feerate);

let funding_template = self
Expand Down Expand Up @@ -2077,20 +2108,29 @@ impl Node {
/// Fee-bumps the pending splice on a channel by replacing its in-flight funding transaction
/// (RBF). The splice's amount and destination are preserved; only the fee rate is raised.
/// Errors if the channel has no pending splice to bump.
///
/// If `min_fee_rate` is set, it will be used as the minimum feerate for the replacement instead
/// of a feerate estimate retrieved from the configured chain source. As with the estimate, we
/// pay the feerate required to replace the pending splice if it is higher, but no more than 1.5
/// times this feerate: if replacing it requires more, the bump fails.
pub fn bump_channel_funding_fee(
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey,
min_fee_rate: Option<FfiFeeRate>,
) -> Result<(), Error> {
if !*self.is_running.read().expect("lock") {
return Err(Error::NotRunning);
}

let min_fee_rate = maybe_map_fee_rate_opt(min_fee_rate);

let open_channels =
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
if let Some(channel_details) =
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
{
let min_feerate =
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
let min_feerate = min_fee_rate.unwrap_or_else(|| {
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding)
});

let funding_template = self
.channel_manager
Expand Down
6 changes: 3 additions & 3 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ pub async fn open_channel_with_all(
pub async fn splice_in_with_all(
node_a: &TestNode, node_b: &TestNode, user_channel_id: &UserChannelId, electrsd: &ElectrsD,
) {
node_a.splice_in_with_all(user_channel_id, node_b.node_id()).unwrap();
node_a.splice_in_with_all(user_channel_id, node_b.node_id(), None).unwrap();

let splice_txo = expect_splice_negotiated_event!(node_a, node_b.node_id());
wait_for_tx(&electrsd.client, splice_txo.txid).await;
Expand Down Expand Up @@ -1681,7 +1681,7 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
let available_splice_out_sat = node_b.list_channels()[0].outbound_capacity_msat / 1000;
let splice_out_sat = available_splice_out_sat / 2;
assert!(splice_out_sat > 500_000);
node_b.splice_out(&user_channel_id_b, node_a.node_id(), &addr_a, splice_out_sat).unwrap();
node_b.splice_out(&user_channel_id_b, node_a.node_id(), &addr_a, splice_out_sat, None).unwrap();

let splice_out_txo = expect_splice_negotiated_event!(node_b, node_a.node_id());
wait_for_tx(electrsd, splice_out_txo.txid).await;
Expand All @@ -1703,7 +1703,7 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(

println!("\nA splices in the splice-out payment from B");
let splice_in_sat = splice_out_sat;
node_a.splice_in(&user_channel_id_a, node_b.node_id(), splice_in_sat).unwrap();
node_a.splice_in(&user_channel_id_a, node_b.node_id(), splice_in_sat, None).unwrap();

let splice_in_txo = expect_splice_negotiated_event!(node_a, node_b.node_id());
wait_for_tx(electrsd, splice_in_txo.txid).await;
Expand Down
2 changes: 1 addition & 1 deletion tests/common/scenarios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ pub(crate) async fn splice_in_scenario<E: ElectrumApi>(
)
.await;
let ext_node_id = peer.get_node_id().await.unwrap();
node.splice_in(&user_ch, ext_node_id, 500_000).unwrap();
node.splice_in(&user_ch, ext_node_id, 500_000, None).unwrap();
let splice_txo = expect_splice_negotiated_event!(node, ext_node_id);
wait_for_tx(electrs, splice_txo.txid).await;
generate_blocks_and_wait(bitcoind, electrs, 6).await;
Expand Down
Loading
Loading