Skip to content

Commit 0b9b403

Browse files
jkczyzclaude
andcommitted
Tie funding payment status transitions to Lightning lifecycle events
Channel-opening and splice transactions transition to Succeeded when ChannelReady fires, not after ANTI_REORG_DELAY confirmations. This matches the point at which the Lightning layer considers the channel usable: a zero-conf channel graduates as soon as its counterparty signals, and a high-conf channel waits however many confirmations the peer requires, rather than always stopping at six. For splice RBF, the payment records whichever candidate actually confirmed, with that candidate's amount and this node's share of the fee — not the fee-estimate used for weight at coin-selection time, and not the whole-tx fee for a multi-contributor splice. A channel closure whose funding or splice never confirmed discards its payment record instead of leaving it pending forever. Generated with assistance from Claude Code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ec1393c commit 0b9b403

7 files changed

Lines changed: 707 additions & 46 deletions

File tree

Cargo.toml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,18 @@ default = []
4040
#lightning-macros = { version = "0.2.0" }
4141
#lightning-dns-resolver = { version = "0.3.0" }
4242

43-
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89", features = ["std"] }
44-
lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89" }
45-
lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89", features = ["std"] }
46-
lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89" }
47-
lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89", features = ["tokio"] }
48-
lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89" }
49-
lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89" }
50-
lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89", features = ["rest-client", "rpc-client", "tokio"] }
51-
lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89", features = ["esplora-async-https", "time", "electrum-rustls-ring"] }
52-
lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89", features = ["std"] }
53-
lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89" }
54-
lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89" }
43+
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0", features = ["std"] }
44+
lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0" }
45+
lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0", features = ["std"] }
46+
lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0" }
47+
lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0", features = ["tokio"] }
48+
lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0" }
49+
lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0" }
50+
lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0", features = ["rest-client", "rpc-client", "tokio"] }
51+
lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] }
52+
lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0", features = ["std"] }
53+
lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0" }
54+
lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0" }
5555

5656
bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] }
5757
bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]}
@@ -81,13 +81,13 @@ async-trait = { version = "0.1", default-features = false }
8181
vss-client = { package = "vss-client-ng", version = "0.5" }
8282
prost = { version = "0.11.6", default-features = false}
8383
#bitcoin-payment-instructions = { version = "0.6" }
84-
bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "4b176efe2e0258767b949904c7c835b3d0d142dd" }
84+
bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "e75be4ef1e4f1d30eefac3cd1a2b8f495dd4ba66" }
8585

8686
[target.'cfg(windows)'.dependencies]
8787
winapi = { version = "0.3", features = ["winbase"] }
8888

8989
[dev-dependencies]
90-
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "23b620a6016abcf1614659eb75b9bc1fd8579e89", features = ["std", "_test_utils"] }
90+
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38f5651ce0c423d27d7dc4d7abb1b2adc5cc4eb0", features = ["std", "_test_utils"] }
9191
rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] }
9292
proptest = "1.0.0"
9393
regex = "1.5.6"

src/builder.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,6 +1518,8 @@ fn build_with_store_internal(
15181518
Arc::clone(&pending_payment_store),
15191519
));
15201520

1521+
tx_broadcaster.set_wallet(Arc::downgrade(&wallet));
1522+
15211523
// Initialize the KeysManager
15221524
let cur_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_err(|e| {
15231525
log_error!(logger, "Failed to get current time: {}", e);

src/event.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,18 @@ where
15561556
);
15571557
}
15581558

1559+
if let Err(e) =
1560+
self.wallet.handle_channel_ready(channel_id, funding_txo.map(|txo| txo.txid))
1561+
{
1562+
log_error!(
1563+
self.logger,
1564+
"Failed to graduate funding payment on ChannelReady for channel {}: {:?}",
1565+
channel_id,
1566+
e,
1567+
);
1568+
return Err(ReplayEvent());
1569+
}
1570+
15591571
if let Some(liquidity_source) = self.liquidity_source.as_ref() {
15601572
liquidity_source
15611573
.handle_channel_ready(user_channel_id, &channel_id, &counterparty_node_id)
@@ -1585,6 +1597,16 @@ where
15851597
} => {
15861598
log_info!(self.logger, "Channel {} closed due to: {}", channel_id, reason);
15871599

1600+
if let Err(e) = self.wallet.handle_channel_closed(channel_id) {
1601+
log_error!(
1602+
self.logger,
1603+
"Failed to handle ChannelClosed for channel {}: {:?}",
1604+
channel_id,
1605+
e,
1606+
);
1607+
return Err(ReplayEvent());
1608+
}
1609+
15881610
let event = Event::ChannelClosed {
15891611
channel_id,
15901612
user_channel_id: UserChannelId(user_channel_id),

src/payment/pending_payment_store.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,52 @@
66
// accordance with one or both of these licenses.
77

88
use bitcoin::Txid;
9+
use lightning::chain::chaininterface::FundingCandidate;
910
use lightning::impl_writeable_tlv_based;
1011
use lightning::ln::channelmanager::PaymentId;
1112

1213
use crate::data_store::{StorableObject, StorableObjectUpdate};
1314
use crate::payment::store::PaymentDetailsUpdate;
1415
use crate::payment::PaymentDetails;
1516

17+
/// Marks an on-chain payment as belonging to an interactive-funding negotiation. The
18+
/// last entry in `candidates` is the currently-broadcast tx; earlier entries are RBF
19+
/// predecessors that may still confirm if reorgs intervene.
20+
#[derive(Clone, Debug, PartialEq, Eq)]
21+
pub struct FundingDetails {
22+
/// Every negotiated candidate, oldest first.
23+
pub candidates: Vec<FundingCandidate>,
24+
}
25+
26+
impl_writeable_tlv_based!(FundingDetails, {
27+
(0, candidates, optional_vec),
28+
});
29+
1630
/// Represents a pending payment
1731
#[derive(Clone, Debug, PartialEq, Eq)]
1832
pub struct PendingPaymentDetails {
1933
/// The full payment details
2034
pub details: PaymentDetails,
2135
/// Transaction IDs that have replaced or conflict with this payment.
2236
pub conflicting_txids: Vec<Txid>,
37+
/// Set when the payment's transaction is an interactive-funding broadcast (channel
38+
/// open or splice). The record transitions to [`PaymentStatus::Succeeded`] on
39+
/// `ChannelReady` instead of after [`ANTI_REORG_DELAY`] confirmations.
40+
///
41+
/// [`PaymentStatus::Succeeded`]: crate::payment::store::PaymentStatus::Succeeded
42+
/// [`ANTI_REORG_DELAY`]: lightning::chain::channelmonitor::ANTI_REORG_DELAY
43+
pub funding_details: Option<FundingDetails>,
2344
}
2445

2546
impl PendingPaymentDetails {
2647
pub(crate) fn new(details: PaymentDetails, conflicting_txids: Vec<Txid>) -> Self {
27-
Self { details, conflicting_txids }
48+
Self { details, conflicting_txids, funding_details: None }
49+
}
50+
51+
pub(crate) fn with_funding_details(
52+
details: PaymentDetails, conflicting_txids: Vec<Txid>, funding_details: FundingDetails,
53+
) -> Self {
54+
Self { details, conflicting_txids, funding_details: Some(funding_details) }
2855
}
2956

3057
/// Convert to finalized payment for the main payment store
@@ -36,13 +63,15 @@ impl PendingPaymentDetails {
3663
impl_writeable_tlv_based!(PendingPaymentDetails, {
3764
(0, details, required),
3865
(2, conflicting_txids, optional_vec),
66+
(4, funding_details, option),
3967
});
4068

4169
#[derive(Clone, Debug, PartialEq, Eq)]
4270
pub(crate) struct PendingPaymentDetailsUpdate {
4371
pub id: PaymentId,
4472
pub payment_update: Option<PaymentDetailsUpdate>,
4573
pub conflicting_txids: Option<Vec<Txid>>,
74+
pub funding_details: Option<Option<FundingDetails>>,
4675
}
4776

4877
impl StorableObject for PendingPaymentDetails {
@@ -68,6 +97,13 @@ impl StorableObject for PendingPaymentDetails {
6897
}
6998
}
7099

100+
if let Some(new_funding_details) = update.funding_details {
101+
if self.funding_details != new_funding_details {
102+
self.funding_details = new_funding_details;
103+
updated = true;
104+
}
105+
}
106+
71107
updated
72108
}
73109

@@ -89,6 +125,11 @@ impl From<&PendingPaymentDetails> for PendingPaymentDetailsUpdate {
89125
} else {
90126
Some(value.conflicting_txids.clone())
91127
};
92-
Self { id: value.id(), payment_update: Some(value.details.to_update()), conflicting_txids }
128+
Self {
129+
id: value.id(),
130+
payment_update: Some(value.details.to_update()),
131+
conflicting_txids,
132+
funding_details: Some(value.funding_details.clone()),
133+
}
93134
}
94135
}

src/tx_broadcaster.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
// accordance with one or both of these licenses.
77

88
use std::ops::Deref;
9+
use std::sync::{Mutex as StdMutex, Weak};
910

1011
use bitcoin::Transaction;
1112
use lightning::chain::chaininterface::{BroadcasterInterface, TransactionType};
1213
use tokio::sync::{mpsc, Mutex, MutexGuard};
1314

1415
use crate::logger::{log_error, LdkLogger};
16+
use crate::types::Wallet;
1517

1618
const BCAST_PACKAGE_QUEUE_SIZE: usize = 50;
1719

@@ -21,6 +23,12 @@ where
2123
{
2224
queue_sender: mpsc::Sender<Vec<Transaction>>,
2325
queue_receiver: Mutex<mpsc::Receiver<Vec<Transaction>>>,
26+
/// Weak handle to the [`Wallet`] that performs classification of funding broadcasts
27+
/// (channel opens and splices) into payment records. Remains `None` while the
28+
/// builder is wiring the node up, during which broadcasts are still forwarded to
29+
/// the queue but no payment record is written. [`Self::set_wallet`] installs the
30+
/// handle once the [`Wallet`] exists.
31+
wallet: StdMutex<Option<Weak<Wallet>>>,
2432
logger: L,
2533
}
2634

@@ -30,7 +38,19 @@ where
3038
{
3139
pub(crate) fn new(logger: L) -> Self {
3240
let (queue_sender, queue_receiver) = mpsc::channel(BCAST_PACKAGE_QUEUE_SIZE);
33-
Self { queue_sender, queue_receiver: Mutex::new(queue_receiver), logger }
41+
Self {
42+
queue_sender,
43+
queue_receiver: Mutex::new(queue_receiver),
44+
wallet: StdMutex::new(None),
45+
logger,
46+
}
47+
}
48+
49+
/// Installs the [`Wallet`] handle used to classify funding broadcasts (channel
50+
/// opens and splices) into payment records. Called once the builder has constructed
51+
/// both the broadcaster and the wallet.
52+
pub(crate) fn set_wallet(&self, wallet: Weak<Wallet>) {
53+
*self.wallet.lock().expect("lock") = Some(wallet);
3454
}
3555

3656
pub(crate) async fn get_broadcast_queue(
@@ -45,6 +65,19 @@ where
4565
L::Target: LdkLogger,
4666
{
4767
fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) {
68+
let wallet = self.wallet.lock().expect("lock").as_ref().and_then(Weak::upgrade);
69+
if let Some(wallet) = wallet {
70+
for (tx, tx_type) in txs {
71+
if let Err(e) = wallet.classify_broadcast(tx, tx_type) {
72+
log_error!(
73+
self.logger,
74+
"Failed to classify broadcast tx {}: {:?}",
75+
tx.compute_txid(),
76+
e,
77+
);
78+
}
79+
}
80+
}
4881
let package = txs.iter().map(|(t, _)| (*t).clone()).collect::<Vec<Transaction>>();
4982
self.queue_sender.try_send(package).unwrap_or_else(|e| {
5083
log_error!(self.logger, "Failed to broadcast transactions: {}", e);

0 commit comments

Comments
 (0)