Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
38 changes: 26 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ use io::utils::write_node_metrics;
use lightning::chain::BestBlock;
use lightning::events::bump_transaction::{Input, Wallet as LdkWallet};
use lightning::impl_writeable_tlv_based;
use lightning::ln::chan_utils::{make_funding_redeemscript, FUNDING_TRANSACTION_WITNESS_WEIGHT};
use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState};
use lightning::ln::channelmanager::PaymentId;
use lightning::ln::funding::SpliceContribution;
Expand Down Expand Up @@ -1267,29 +1267,27 @@ impl Node {
const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;

// Used for creating a redeem script for the previous funding txo and the new funding
// txo. Only needed when selecting which UTXOs to include in the funding tx that would
// be sufficient to pay for fees. Hence, the value does not matter.
let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap();

let funding_txo = channel_details.funding_txo.ok_or_else(|| {
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
Error::ChannelSplicingFailed
})?;

let funding_output = channel_details.get_funding_output().ok_or_else(|| {
log_error!(self.logger, "Failed to splice channel: channel not yet ready");
Error::ChannelSplicingFailed
})?;

let shared_input = Input {
outpoint: funding_txo.into_bitcoin_outpoint(),
previous_utxo: bitcoin::TxOut {
value: Amount::from_sat(channel_details.channel_value_satoshis),
script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey)
.to_p2wsh(),
},
previous_utxo: funding_output.clone(),
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
};

let shared_output = bitcoin::TxOut {
value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats),
script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh(),
// will not actually be the exact same script pubkey after splice
// but it is the same size and good enough for coin selection purposes
script_pubkey: funding_output.script_pubkey.clone(),
};

let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
Expand All @@ -1305,6 +1303,10 @@ impl Node {
Error::ChannelSplicingFailed
})?;

// insert channel's funding utxo into the wallet so we can later calculate fees
// correctly when viewing this splice-in.
self.wallet.insert_txo(funding_txo.into_bitcoin_outpoint(), funding_output)?;

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

let contribution = SpliceContribution::SpliceIn {
Expand Down Expand Up @@ -1400,6 +1402,18 @@ impl Node {
},
};

let funding_txo = channel_details.funding_txo.ok_or_else(|| {
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
Error::ChannelSplicingFailed
})?;

let funding_output = channel_details.get_funding_output().ok_or_else(|| {
log_error!(self.logger, "Failed to splice channel: channel not yet ready");
Error::ChannelSplicingFailed
})?;

self.wallet.insert_txo(funding_txo.into_bitcoin_outpoint(), funding_output)?;

self.channel_manager
.splice_channel(
&channel_details.channel_id,
Expand Down
15 changes: 14 additions & 1 deletion src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
use bitcoin::{
Address, Amount, FeeRate, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
WitnessProgram, WitnessVersion,
};
use lightning::chain::chaininterface::BroadcasterInterface;
Expand Down Expand Up @@ -153,6 +153,19 @@ impl Wallet {
Ok(())
}

pub(crate) fn insert_txo(&self, outpoint: OutPoint, txout: TxOut) -> Result<(), Error> {
let mut locked_wallet = self.inner.lock().unwrap();
locked_wallet.insert_txout(outpoint, txout);

let mut locked_persister = self.persister.lock().unwrap();
locked_wallet.persist(&mut locked_persister).map_err(|e| {
log_error!(self.logger, "Failed to persist wallet: {}", e);
Error::PersistenceFailed
})?;

Ok(())
}

fn update_payment_store<'a>(
&self, locked_wallet: &'a mut PersistedWallet<KVStoreWalletPersister>,
) -> Result<(), Error> {
Expand Down
37 changes: 28 additions & 9 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,10 +927,13 @@ async fn concurrent_connections_succeed() {
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn splice_channel() {
async fn run_splice_channel_test(bitcoind_chain_source: bool) {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = TestChainSource::Esplora(&electrsd);
let chain_source = if bitcoind_chain_source {
TestChainSource::BitcoindRpcSync(&bitcoind)
} else {
TestChainSource::Esplora(&electrsd)
};
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);

let address_a = node_a.onchain_payment().new_address().unwrap();
Expand Down Expand Up @@ -995,7 +998,7 @@ async fn splice_channel() {
// Splice-in funds for Node B so that it has outbound liquidity to make a payment
node_b.splice_in(&user_channel_id_b, node_a.node_id(), 4_000_000).unwrap();

expect_splice_pending_event!(node_a, node_b.node_id());
let txo = expect_splice_pending_event!(node_a, node_b.node_id());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These test changes pass on main. Can you add coverage for the cases where the previous approach would lead to inaccurate fee estimations?

Copy link
Copy Markdown
Contributor Author

@benthecarman benthecarman Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug/issue only exists with bitcoind syncing so to do so, we'd have to change the test to sync with bitcoind rpc. Is that wanted?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug/issue only exists with bitcoind syncing so to do so, we'd have to change the test to sync with bitcoind rpc. Is that wanted?

Sure, feel free to do so. Alternatively, you could also add a _bitcoind variant test case (ofc reusing the logic) so we have two test cases for bitcoind and esplora - that's if we think there would be more chain source specific edge cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

expect_splice_pending_event!(node_b, node_a.node_id());

generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
Expand All @@ -1006,11 +1009,16 @@ async fn splice_channel() {
expect_channel_ready_event!(node_a, node_b.node_id());
expect_channel_ready_event!(node_b, node_a.node_id());

let splice_in_fee_sat = 252;
let expected_splice_in_fee_sat = 252;

let payments = node_b.list_payments();
let payment =
payments.into_iter().find(|p| p.id == PaymentId(txo.txid.to_byte_array())).unwrap();
assert_eq!(payment.fee_paid_msat, Some(expected_splice_in_fee_sat * 1_000));

assert_eq!(
node_b.list_balances().total_onchain_balance_sats,
premine_amount_sat - 4_000_000 - splice_in_fee_sat
premine_amount_sat - 4_000_000 - expected_splice_in_fee_sat
);
assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000);

Expand All @@ -1033,7 +1041,7 @@ async fn splice_channel() {
let address = node_a.onchain_payment().new_address().unwrap();
node_a.splice_out(&user_channel_id_a, node_b.node_id(), &address, amount_msat / 1000).unwrap();

expect_splice_pending_event!(node_a, node_b.node_id());
let txo = expect_splice_pending_event!(node_a, node_b.node_id());
expect_splice_pending_event!(node_b, node_a.node_id());

generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
Expand All @@ -1044,18 +1052,29 @@ async fn splice_channel() {
expect_channel_ready_event!(node_a, node_b.node_id());
expect_channel_ready_event!(node_b, node_a.node_id());

let splice_out_fee_sat = 183;
let expected_splice_out_fee_sat = 183;

let payments = node_a.list_payments();
let payment =
payments.into_iter().find(|p| p.id == PaymentId(txo.txid.to_byte_array())).unwrap();
assert_eq!(payment.fee_paid_msat, Some(expected_splice_out_fee_sat * 1_000));

assert_eq!(
node_a.list_balances().total_onchain_balance_sats,
premine_amount_sat - 4_000_000 - opening_transaction_fee_sat + amount_msat / 1000
);
assert_eq!(
node_a.list_balances().total_lightning_balance_sats,
4_000_000 - closing_transaction_fee_sat - anchor_output_sat - splice_out_fee_sat
4_000_000 - closing_transaction_fee_sat - anchor_output_sat - expected_splice_out_fee_sat
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn splice_channel() {
run_splice_channel_test(false).await;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Not that it matters much, but elsewhere in LDK Node / LDK tests the usual pattern would be to name the 'inner' test methods do_splice_channel. Feel free to make that rename if you touch the code again.

run_splice_channel_test(true).await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn simple_bolt12_send_receive() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
Expand Down