Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
45db7c8
Move wallet utils to dedicated module
jkczyz Jan 28, 2026
0ce6ba4
Move sync wallet utils to util::wallet_utils
jkczyz Jan 28, 2026
9a0a249
Make ConfirmedUtxo the primary type
jkczyz Jan 28, 2026
2a238d7
Remove use of Deref with CoinSelectionSource
jkczyz Feb 17, 2026
e24ff56
Remove redundant is_initiator from FundingTemplate/FundingContribution
jkczyz Feb 17, 2026
04130c1
Remove PersistenceNotifierGuard from splice_channel
jkczyz Feb 17, 2026
9f6c678
Make FundingContribution::net_value() infallible
jkczyz Feb 17, 2026
3b2b36e
Add expect_splice_failed_events helper
jkczyz Feb 6, 2026
580aa59
Split DiscardFunding from SpliceFailed event
jkczyz Feb 5, 2026
2dd3b10
Contribute to splice as acceptor
jkczyz Feb 10, 2026
0632a9a
Stop persisting QuiescentAction and remove legacy code
jkczyz Feb 10, 2026
758ab0a
Split InteractiveTxConstructor::new into outbound/inbound variants
jkczyz Feb 12, 2026
a886ee9
Accept tx_init_rbf for pending splice transactions
jkczyz Feb 18, 2026
059dd4f
Allow multiple RBF splice candidates in channel monitor
jkczyz Feb 19, 2026
728977f
Add rbf_channel API for initiating splice RBF
jkczyz Feb 19, 2026
08dbd35
Send tx_init_rbf instead of splice_init when a splice is pending
jkczyz Feb 19, 2026
9798e3f
Handle tx_ack_rbf on the initiator side
jkczyz Feb 19, 2026
14aadf6
Test end-to-end RBF splice initiator flow
jkczyz Feb 19, 2026
bee052d
Allow acceptor contribution to RBF splice via tx_init_rbf
jkczyz Feb 19, 2026
1552b89
Adjust acceptor's contribution for initiator's feerate in splices
jkczyz Feb 19, 2026
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
2 changes: 1 addition & 1 deletion fuzz/src/chanmon_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ use lightning::chain::{
chainmonitor, channelmonitor, BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch,
};
use lightning::events;
use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync};
use lightning::ln::channel::{
FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS,
};
Expand Down Expand Up @@ -81,6 +80,7 @@ use lightning::util::logger::Logger;
use lightning::util::ser::{LengthReadable, ReadableArgs, Writeable, Writer};
use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner};
use lightning::util::test_utils::TestWalletSource;
use lightning::util::wallet_utils::{WalletSourceSync, WalletSync};

use lightning_invoice::RawBolt11Invoice;

Expand Down
2 changes: 1 addition & 1 deletion fuzz/src/full_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ use lightning::chain::chaininterface::{
use lightning::chain::chainmonitor;
use lightning::chain::transaction::OutPoint;
use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen};
use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync};
use lightning::events::Event;
use lightning::ln::channel_state::ChannelDetails;
use lightning::ln::channelmanager::{ChainParameters, ChannelManager, InterceptId, PaymentId};
Expand Down Expand Up @@ -71,6 +70,7 @@ use lightning::util::logger::Logger;
use lightning::util::ser::{Readable, Writeable};
use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner};
use lightning::util::test_utils::TestWalletSource;
use lightning::util::wallet_utils::{WalletSourceSync, WalletSync};

use lightning_invoice::RawBolt11Invoice;

Expand Down
2 changes: 1 addition & 1 deletion lightning-tests/src/upgrade_downgrade_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ use lightning_0_0_125::routing::router as router_0_0_125;
use lightning_0_0_125::util::ser::Writeable as _;

use lightning::chain::channelmonitor::{ANTI_REORG_DELAY, HTLC_FAIL_BACK_BUFFER};
use lightning::events::bump_transaction::sync::WalletSourceSync;
use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType};
use lightning::ln::functional_test_utils::*;
use lightning::ln::msgs::BaseMessageHandler as _;
Expand All @@ -55,6 +54,7 @@ use lightning::ln::msgs::MessageSendEvent;
use lightning::ln::splicing_tests::*;
use lightning::ln::types::ChannelId;
use lightning::sign::OutputSpender;
use lightning::util::wallet_utils::WalletSourceSync;

use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};

Expand Down
11 changes: 8 additions & 3 deletions lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4039,9 +4039,14 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

if let Some(parent_funding_txid) = channel_parameters.splice_parent_funding_txid.as_ref() {
// Only one splice can be negotiated at a time after we've exchanged `channel_ready`
// (implying our funding is confirmed) that spends our currently locked funding.
if !self.pending_funding.is_empty() {
// Multiple RBF candidates for the same splice are allowed (they share the same
// parent funding txid). A new splice with a different parent while one is pending
// is not allowed.
let has_different_parent = self.pending_funding.iter().any(|funding| {
funding.channel_parameters.splice_parent_funding_txid.as_ref()
!= Some(parent_funding_txid)
});
if has_different_parent {
log_error!(
logger,
"Negotiated splice while channel is pending channel_ready/splice_locked"
Expand Down
538 changes: 15 additions & 523 deletions lightning/src/events/bump_transaction/mod.rs

Large diffs are not rendered by default.

252 changes: 3 additions & 249 deletions lightning/src/events/bump_transaction/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,258 +15,12 @@ use core::pin::pin;
use core::task;

use crate::chain::chaininterface::BroadcasterInterface;
use crate::chain::ClaimId;
use crate::prelude::*;
use crate::sign::SignerProvider;
use crate::util::async_poll::{dummy_waker, MaybeSend, MaybeSync};
use crate::util::async_poll::dummy_waker;
use crate::util::logger::Logger;
use crate::util::wallet_utils::{CoinSelectionSourceSync, CoinSelectionSourceSyncWrapper};

use bitcoin::{OutPoint, Psbt, ScriptBuf, Transaction, TxOut};

use super::BumpTransactionEvent;
use super::{
BumpTransactionEventHandler, CoinSelection, CoinSelectionSource, Input, Utxo, Wallet,
WalletSource,
};

/// An alternative to [`CoinSelectionSourceSync`] that can be implemented and used along
/// [`WalletSync`] to provide a default implementation to [`CoinSelectionSourceSync`].
///
/// For an asynchronous version of this trait, see [`WalletSource`].
// Note that updates to documentation on this trait should be copied to the asynchronous version.
pub trait WalletSourceSync {
/// Returns all UTXOs, with at least 1 confirmation each, that are available to spend.
fn list_confirmed_utxos(&self) -> Result<Vec<Utxo>, ()>;

/// Returns the previous transaction containing the UTXO referenced by the outpoint.
fn get_prevtx(&self, outpoint: OutPoint) -> Result<Transaction, ()>;

/// Returns a script to use for change above dust resulting from a successful coin selection
/// attempt.
fn get_change_script(&self) -> Result<ScriptBuf, ()>;

/// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within
/// the transaction known to the wallet (i.e., any provided via
/// [`WalletSource::list_confirmed_utxos`]).
///
/// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the
/// unsigned transaction and then sign it with your wallet.
///
/// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig
/// [`TxIn::witness`]: bitcoin::TxIn::witness
fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()>;
}

pub(crate) struct WalletSourceSyncWrapper<T: Deref>(T)
where
T::Target: WalletSourceSync;

// Implement `Deref` directly on WalletSourceSyncWrapper so that it can be used directly
// below, rather than via a wrapper.
impl<T: Deref> Deref for WalletSourceSyncWrapper<T>
where
T::Target: WalletSourceSync,
{
type Target = Self;
fn deref(&self) -> &Self {
self
}
}

impl<T: Deref> WalletSource for WalletSourceSyncWrapper<T>
where
T::Target: WalletSourceSync,
{
fn list_confirmed_utxos<'a>(
&'a self,
) -> impl Future<Output = Result<Vec<Utxo>, ()>> + MaybeSend + 'a {
let utxos = self.0.list_confirmed_utxos();
async move { utxos }
}

fn get_prevtx<'a>(
&'a self, outpoint: OutPoint,
) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a {
let prevtx = self.0.get_prevtx(outpoint);
Box::pin(async move { prevtx })
}

fn get_change_script<'a>(
&'a self,
) -> impl Future<Output = Result<ScriptBuf, ()>> + MaybeSend + 'a {
let script = self.0.get_change_script();
async move { script }
}

fn sign_psbt<'a>(
&'a self, psbt: Psbt,
) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a {
let signed_psbt = self.0.sign_psbt(psbt);
async move { signed_psbt }
}
}

/// A wrapper over [`WalletSourceSync`] that implements [`CoinSelectionSourceSync`] by preferring
/// UTXOs that would avoid conflicting double spends. If not enough UTXOs are available to do so,
/// conflicting double spends may happen.
///
/// For an asynchronous version of this wrapper, see [`Wallet`].
// Note that updates to documentation on this struct should be copied to the asynchronous version.
pub struct WalletSync<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend>
where
W::Target: WalletSourceSync + MaybeSend,
{
wallet: Wallet<WalletSourceSyncWrapper<W>, L>,
}

impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> WalletSync<W, L>
where
W::Target: WalletSourceSync + MaybeSend,
{
/// Constructs a new [`WalletSync`] instance.
pub fn new(source: W, logger: L) -> Self {
Self { wallet: Wallet::new(WalletSourceSyncWrapper(source), logger) }
}
}

impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> CoinSelectionSourceSync
for WalletSync<W, L>
where
W::Target: WalletSourceSync + MaybeSend + MaybeSync,
{
fn select_confirmed_utxos(
&self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &[TxOut],
target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> Result<CoinSelection, ()> {
let fut = self.wallet.select_confirmed_utxos(
claim_id,
must_spend,
must_pay_to,
target_feerate_sat_per_1000_weight,
max_tx_weight,
);
let mut waker = dummy_waker();
let mut ctx = task::Context::from_waker(&mut waker);
match pin!(fut).poll(&mut ctx) {
task::Poll::Ready(result) => result,
task::Poll::Pending => {
unreachable!(
"Wallet::select_confirmed_utxos should not be pending in a sync context"
);
},
}
}

fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()> {
let fut = self.wallet.sign_psbt(psbt);
let mut waker = dummy_waker();
let mut ctx = task::Context::from_waker(&mut waker);
match pin!(fut).poll(&mut ctx) {
task::Poll::Ready(result) => result,
task::Poll::Pending => {
unreachable!("Wallet::sign_psbt should not be pending in a sync context");
},
}
}
}

/// An abstraction over a bitcoin wallet that can perform coin selection over a set of UTXOs and can
/// sign for them. The coin selection method aims to mimic Bitcoin Core's `fundrawtransaction` RPC,
/// which most wallets should be able to satisfy. Otherwise, consider implementing
/// [`WalletSourceSync`], which can provide a default implementation of this trait when used with
/// [`WalletSync`].
///
/// For an asynchronous version of this trait, see [`CoinSelectionSource`].
// Note that updates to documentation on this trait should be copied to the asynchronous version.
pub trait CoinSelectionSourceSync {
/// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are
/// available to spend. Implementations are free to pick their coin selection algorithm of
/// choice, as long as the following requirements are met:
///
/// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction
/// throughout coin selection, but must not be returned as part of the result.
/// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction
/// throughout coin selection. In some cases, like when funding an anchor transaction, this
/// set is empty. Implementations should ensure they handle this correctly on their end,
/// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be
/// provided, in which case a zero-value empty OP_RETURN output can be used instead.
/// 3. Enough inputs must be selected/contributed for the resulting transaction (including the
/// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`.
/// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this
/// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC
/// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for
/// anchor transactions, we will try your coin selection again with the same input-output
/// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions
/// cannot be downsized.
///
/// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of
/// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require
/// providing the full input weight. Failing to do so may lead to underestimating fee bumps and
/// delaying block inclusion.
///
/// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they
/// can be re-used within new fee-bumped iterations of the original claiming transaction,
/// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a
/// transaction associated with it, and all of the available UTXOs have already been assigned to
/// other claims, implementations must be willing to double spend their UTXOs. The choice of
/// which UTXOs to double spend is left to the implementation, but it must strive to keep the
/// set of other claims being double spent to a minimum.
///
/// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims
fn select_confirmed_utxos(
&self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &[TxOut],
target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> Result<CoinSelection, ()>;

/// Signs and provides the full witness for all inputs within the transaction known to the
/// trait (i.e., any provided via [`CoinSelectionSourceSync::select_confirmed_utxos`]).
///
/// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the
/// unsigned transaction and then sign it with your wallet.
fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()>;
}

struct CoinSelectionSourceSyncWrapper<T: Deref>(T)
where
T::Target: CoinSelectionSourceSync;

// Implement `Deref` directly on CoinSelectionSourceSyncWrapper so that it can be used directly
// below, rather than via a wrapper.
impl<T: Deref> Deref for CoinSelectionSourceSyncWrapper<T>
where
T::Target: CoinSelectionSourceSync,
{
type Target = Self;
fn deref(&self) -> &Self {
self
}
}

impl<T: Deref> CoinSelectionSource for CoinSelectionSourceSyncWrapper<T>
where
T::Target: CoinSelectionSourceSync,
{
fn select_confirmed_utxos<'a>(
&'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a {
let coins = self.0.select_confirmed_utxos(
claim_id,
must_spend,
must_pay_to,
target_feerate_sat_per_1000_weight,
max_tx_weight,
);
async move { coins }
}

fn sign_psbt<'a>(
&'a self, psbt: Psbt,
) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a {
let psbt = self.0.sign_psbt(psbt);
async move { psbt }
}
}
use super::{BumpTransactionEvent, BumpTransactionEventHandler};

/// A handler for [`Event::BumpTransaction`] events that sources confirmed UTXOs from a
/// [`CoinSelectionSourceSync`] to fee bump transactions via Child-Pays-For-Parent (CPFP) or
Expand Down
23 changes: 11 additions & 12 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ pub enum FundingInfo {
/// The outpoint of the funding
outpoint: transaction::OutPoint,
},
/// The contributions used to for a dual funding or splice funding transaction.
Contribution {
/// UTXOs spent as inputs contributed to the funding transaction.
inputs: Vec<OutPoint>,
/// Outputs contributed to the funding transaction.
outputs: Vec<TxOut>,
},
}

impl_writeable_tlv_based_enum!(FundingInfo,
Expand All @@ -85,6 +92,10 @@ impl_writeable_tlv_based_enum!(FundingInfo,
},
(1, OutPoint) => {
(1, outpoint, required)
},
(2, Contribution) => {
(0, inputs, optional_vec),
(1, outputs, optional_vec),
}
);

Expand Down Expand Up @@ -1561,10 +1572,6 @@ pub enum Event {
abandoned_funding_txo: Option<OutPoint>,
/// The features that this channel will operate with, if available.
channel_type: Option<ChannelTypeFeatures>,
/// UTXOs spent as inputs contributed to the splice transaction.
contributed_inputs: Vec<OutPoint>,
/// Outputs contributed to the splice transaction.
contributed_outputs: Vec<TxOut>,
},
/// Used to indicate to the user that they can abandon the funding transaction and recycle the
/// inputs for another purpose.
Expand Down Expand Up @@ -2326,8 +2333,6 @@ impl Writeable for Event {
ref counterparty_node_id,
ref abandoned_funding_txo,
ref channel_type,
ref contributed_inputs,
ref contributed_outputs,
} => {
52u8.write(writer)?;
write_tlv_fields!(writer, {
Expand All @@ -2336,8 +2341,6 @@ impl Writeable for Event {
(5, user_channel_id, required),
(7, counterparty_node_id, required),
(9, abandoned_funding_txo, option),
(11, *contributed_inputs, optional_vec),
(13, *contributed_outputs, optional_vec),
});
},
// Note that, going forward, all new events must only write data inside of
Expand Down Expand Up @@ -2964,8 +2967,6 @@ impl MaybeReadable for Event {
(5, user_channel_id, required),
(7, counterparty_node_id, required),
(9, abandoned_funding_txo, option),
(11, contributed_inputs, optional_vec),
(13, contributed_outputs, optional_vec),
});

Ok(Some(Event::SpliceFailed {
Expand All @@ -2974,8 +2975,6 @@ impl MaybeReadable for Event {
counterparty_node_id: counterparty_node_id.0.unwrap(),
abandoned_funding_txo,
channel_type,
contributed_inputs: contributed_inputs.unwrap_or_default(),
contributed_outputs: contributed_outputs.unwrap_or_default(),
}))
};
f()
Expand Down
Loading