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
c7e8123
Only adopt a funding payment's own transactions from wallet sync
jkczyz Aug 18, 2026
970eb1a
Retry funding-broadcast classification instead of dropping it
jkczyz Aug 18, 2026
3b64639
f - Wait for a failed write before letting the retry test recover
jkczyz Aug 26, 2026
1f406a6
f - Keep classification retries inside the broadcast loop
jkczyz Aug 26, 2026
3a8eb12
f - Keep stale classification retries from reverting the record
jkczyz Sep 2, 2026
6cf748a
f - Bound and deduplicate pending classification retries
jkczyz Sep 2, 2026
238ccc8
f - Never drop a queued cooperative close for the retry bound
jkczyz Sep 4, 2026
a55bf73
Fail funding payments lost to a confirmed conflict
jkczyz Sep 2, 2026
3f75c44
Record splice funding payments when signing
jkczyz Sep 5, 2026
3cdc5a1
f - Record splice funding payments when signing
jkczyz Sep 6, 2026
b24cb5a
Resolve funding payments when LDK discards a splice round
jkczyz Sep 7, 2026
d7b6808
Assign random PaymentIds to funding records
jkczyz Aug 18, 2026
0d0866b
Model pending payments as an enum for pre-broadcast splices
jkczyz Aug 3, 2026
4799639
Adopt the splice-time PaymentId when classifying a splice
jkczyz Aug 3, 2026
019bfcb
Merge sync-created duplicates when recording a funding round
jkczyz Aug 18, 2026
1a1eaaf
Persist splice intents until the splice locks
jkczyz Sep 2, 2026
377de3c
Abort a splice when funding signing fails
jkczyz Sep 5, 2026
b6205da
Add reason and splice parameters to splice failure events
jkczyz Sep 2, 2026
3da91d4
Unlock lost splice inputs at startup
jkczyz Sep 7, 2026
d961644
Test splice failure surfacing and recovery
jkczyz Sep 2, 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
9 changes: 9 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ use lightning_dns_resolver::OMDomainResolver;
use vss_client::headers::VssHeaderProvider;

use crate::chain::ChainSource;
use crate::channel::SpliceTracker;
#[cfg(feature = "chain-bitcoind")]
use crate::config::BitcoindRestClientConfig;
use crate::config::{
Expand Down Expand Up @@ -2451,6 +2452,13 @@ fn build_with_store_internal(
})
});

let splice_tracker = Arc::new(SpliceTracker::new(
Arc::clone(&channel_manager),
Arc::clone(&wallet),
Arc::clone(&pending_payment_store),
Arc::clone(&logger),
));

#[cfg(cycle_tests)]
let mut _leak_checker = crate::LeakChecker(Vec::new());
#[cfg(cycle_tests)]
Expand Down Expand Up @@ -2490,6 +2498,7 @@ fn build_with_store_internal(
scorer,
peer_store,
payment_store,
splice_tracker,
lnurl_auth,
is_running,
node_metrics,
Expand Down
113 changes: 80 additions & 33 deletions src/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,15 @@ use crate::config::{BackgroundSyncConfig, Config, WALLET_SYNC_INTERVAL_MINIMUM_S
use crate::fee_estimator::OnchainFeeEstimator;
use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
use crate::runtime::Runtime;
use crate::tx_broadcaster::{BroadcastPackage, RetryQueue, ScheduleOutcome};
use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
use crate::{Error, PersistedNodeMetrics};

/// How long to wait before re-classifying a package whose classification failed. Long enough to
/// give a struggling store room to recover, short against the ~minutes until the transaction
/// could confirm.
const FAILED_CLASSIFY_RETRY_DELAY: Duration = Duration::from_secs(2);

/// We use this parent-child TRUC package to make sure the configured chain source supports
/// broadcasting packages via the `submitpackage` Bitcoin Core RPC.
const PARENT_TXID: &str = "9a015f93fac6cb203c2b994e18b85176eb0354a22a468255516f3c6002d3f696";
Expand Down Expand Up @@ -562,50 +568,91 @@ impl ChainSource {
}
}

/// Classifies the package's funding broadcasts into payment records, then broadcasts it.
/// Returns the package back on classification failure so the caller can retry it after a
/// delay: broadcasting a tx we failed to record would leave it on-chain without a payment,
/// while dropping the package would not keep an interactively funded tx off-chain (the
/// counterparty broadcasts it regardless), only leave it confirming without a recorded
/// candidate.
async fn classify_and_broadcast(
&self, package: BroadcastPackage,
) -> Result<(), BroadcastPackage> {
if let Err(e) = self.tx_broadcaster.classify_package(&package).await {
log_error!(
self.logger,
"Delaying broadcast: failed to persist payment records, will retry: {:?}",
e,
);
return Err(package);
}
let package = package.into_sorted_transactions();
match &self.kind {
#[cfg(feature = "chain-esplora")]
ChainSourceKind::Esplora(esplora_chain_source) => {
esplora_chain_source.process_transaction_broadcast(package).await
},
#[cfg(feature = "chain-electrum")]
ChainSourceKind::Electrum(electrum_chain_source) => {
electrum_chain_source.process_transaction_broadcast(package).await
},
#[cfg(feature = "chain-bitcoind")]
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
bitcoind_chain_source.process_transaction_broadcast(package).await
},
}
Ok(())
}

pub(crate) async fn continuously_process_broadcast_queue(
&self, mut stop_tx_bcast_receiver: tokio::sync::watch::Receiver<()>,
) {
let mut receiver = self.tx_broadcaster.get_broadcast_queue().await;
// Packages whose classification failed, each waiting out FAILED_CLASSIFY_RETRY_DELAY
// before its next attempt. New packages keep flowing while these wait, and pending
// retries die with the loop on shutdown rather than resurfacing after a later start.
let mut retries = RetryQueue::new();
loop {
let tx_bcast_logger = Arc::clone(&self.logger);
tokio::select! {
let next_retry_at = retries.next_retry_at();
let package = tokio::select! {
_ = stop_tx_bcast_receiver.changed() => {
log_debug!(
tx_bcast_logger,
self.logger,
"Stopping broadcasting transactions.",
);
return;
}
Some(next_package) = receiver.recv() => {
// Classify funding broadcasts into payment records before sending. If
// classification fails we skip the broadcast, since broadcasting a tx we
// failed to record would leave it on-chain without a payment.
let package = match self.tx_broadcaster.classify_package(next_package).await {
Ok(package) => package,
Err(e) => {
log_error!(
tx_bcast_logger,
"Skipping broadcast: failed to persist payment records: {:?}",
e,
);
continue;
},
};
let package = package.into_sorted_transactions();
match &self.kind {
#[cfg(feature = "chain-esplora")]
ChainSourceKind::Esplora(esplora_chain_source) => {
esplora_chain_source.process_transaction_broadcast(package).await
},
#[cfg(feature = "chain-electrum")]
ChainSourceKind::Electrum(electrum_chain_source) => {
electrum_chain_source.process_transaction_broadcast(package).await
},
#[cfg(feature = "chain-bitcoind")]
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
bitcoind_chain_source.process_transaction_broadcast(package).await
},
}
Some(next_package) = receiver.recv() => next_package,
_ = tokio::time::sleep_until(
next_retry_at.unwrap_or_else(tokio::time::Instant::now)
), if next_retry_at.is_some() => {
retries.pop_next().expect("a retry is queued")
}
};
if let Err(package) = self.classify_and_broadcast(package).await {
let retry_at = tokio::time::Instant::now() + FAILED_CLASSIFY_RETRY_DELAY;
match retries.schedule(package, retry_at) {
ScheduleOutcome::Scheduled { dropped: None } => {},
ScheduleOutcome::Scheduled { dropped: Some(dropped) } => {
log_error!(
self.logger,
"Dropped the oldest package awaiting a classification retry; LDK re-broadcasts its transactions periodically: {:?}",
dropped.sorted_txids(),
);
},
ScheduleOutcome::AlreadyQueued(duplicate) => {
log_debug!(
self.logger,
"Dropped a re-broadcast package; an identical one already awaits a classification retry: {:?}",
duplicate.sorted_txids(),
);
},
ScheduleOutcome::Refused(package) => {
log_error!(
self.logger,
"Dropped a package failing classification; too many await retries: {:?}",
package.sorted_txids(),
);
},
}
}
}
Expand Down
Loading