@@ -37,9 +37,15 @@ use crate::config::{BackgroundSyncConfig, Config, WALLET_SYNC_INTERVAL_MINIMUM_S
3737use crate :: fee_estimator:: OnchainFeeEstimator ;
3838use crate :: logger:: { log_debug, log_error, log_info, log_trace, LdkLogger , Logger } ;
3939use crate :: runtime:: Runtime ;
40+ use crate :: tx_broadcaster:: BroadcastPackage ;
4041use crate :: types:: { Broadcaster , ChainMonitor , ChannelManager , DynStore , Sweeper , Wallet } ;
4142use crate :: { Error , PersistedNodeMetrics } ;
4243
44+ /// How long to wait before re-classifying a package whose classification failed. Long enough to
45+ /// give a struggling store room to recover, short against the ~minutes until the transaction
46+ /// could confirm.
47+ const FAILED_CLASSIFY_RETRY_DELAY : Duration = Duration :: from_secs ( 2 ) ;
48+
4349/// We use this parent-child TRUC package to make sure the configured chain source supports
4450/// broadcasting packages via the `submitpackage` Bitcoin Core RPC.
4551const PARENT_TXID : & str = "9a015f93fac6cb203c2b994e18b85176eb0354a22a468255516f3c6002d3f696" ;
@@ -562,12 +568,53 @@ impl ChainSource {
562568 }
563569 }
564570
571+ /// Classifies the package's funding broadcasts into payment records, then broadcasts it.
572+ /// Returns the package back on classification failure so the caller can retry it after a
573+ /// delay: broadcasting a tx we failed to record would leave it on-chain without a payment,
574+ /// while dropping the package would not keep an interactively funded tx off-chain (the
575+ /// counterparty broadcasts it regardless), only leave it confirming without a recorded
576+ /// candidate.
577+ async fn classify_and_broadcast (
578+ & self , package : BroadcastPackage ,
579+ ) -> Result < ( ) , BroadcastPackage > {
580+ if let Err ( e) = self . tx_broadcaster . classify_package ( & package) . await {
581+ log_error ! (
582+ self . logger,
583+ "Delaying broadcast: failed to persist payment records, will retry: {:?}" ,
584+ e,
585+ ) ;
586+ return Err ( package) ;
587+ }
588+ let package = package. into_sorted_transactions ( ) ;
589+ match & self . kind {
590+ #[ cfg( feature = "chain-esplora" ) ]
591+ ChainSourceKind :: Esplora ( esplora_chain_source) => {
592+ esplora_chain_source. process_transaction_broadcast ( package) . await
593+ } ,
594+ #[ cfg( feature = "chain-electrum" ) ]
595+ ChainSourceKind :: Electrum ( electrum_chain_source) => {
596+ electrum_chain_source. process_transaction_broadcast ( package) . await
597+ } ,
598+ #[ cfg( feature = "chain-bitcoind" ) ]
599+ ChainSourceKind :: Bitcoind ( bitcoind_chain_source) => {
600+ bitcoind_chain_source. process_transaction_broadcast ( package) . await
601+ } ,
602+ }
603+ Ok ( ( ) )
604+ }
605+
565606 pub ( crate ) async fn continuously_process_broadcast_queue (
566607 & self , mut stop_tx_bcast_receiver : tokio:: sync:: watch:: Receiver < ( ) > ,
567608 ) {
568609 let mut receiver = self . tx_broadcaster . get_broadcast_queue ( ) . await ;
610+ // Packages whose classification failed, each waiting out FAILED_CLASSIFY_RETRY_DELAY
611+ // before its next attempt. New packages keep flowing while these wait, and pending
612+ // retries die with the loop on shutdown rather than resurfacing after a later start.
613+ let mut parked: Vec < ( tokio:: time:: Instant , BroadcastPackage ) > = Vec :: new ( ) ;
569614 loop {
570615 let tx_bcast_logger = Arc :: clone ( & self . logger ) ;
616+ // Entries are appended with a fixed delay, so the first is always the next due.
617+ let next_retry_at = parked. first ( ) . map ( |( deadline, _) | * deadline) ;
571618 tokio:: select! {
572619 _ = stop_tx_bcast_receiver. changed( ) => {
573620 log_debug!(
@@ -577,35 +624,18 @@ impl ChainSource {
577624 return ;
578625 }
579626 Some ( next_package) = receiver. recv( ) => {
580- // Classify funding broadcasts into payment records before sending. If
581- // classification fails we delay the broadcast and retry, since broadcasting
582- // a tx we failed to record would leave it on-chain without a payment —
583- // while dropping the package would not keep an interactively funded tx
584- // off-chain (the counterparty broadcasts it regardless), only leave it
585- // confirming without a recorded candidate.
586- if let Err ( e) = self . tx_broadcaster. classify_package( & next_package) . await {
587- log_error!(
588- tx_bcast_logger,
589- "Delaying broadcast: failed to persist payment records, will retry: {:?}" ,
590- e,
591- ) ;
592- self . tx_broadcaster. requeue_failed_classify( next_package) ;
593- continue ;
627+ if let Err ( package) = self . classify_and_broadcast( next_package) . await {
628+ let retry_at = tokio:: time:: Instant :: now( ) + FAILED_CLASSIFY_RETRY_DELAY ;
629+ parked. push( ( retry_at, package) ) ;
594630 }
595- let package = next_package. into_sorted_transactions( ) ;
596- match & self . kind {
597- #[ cfg( feature = "chain-esplora" ) ]
598- ChainSourceKind :: Esplora ( esplora_chain_source) => {
599- esplora_chain_source. process_transaction_broadcast( package) . await
600- } ,
601- #[ cfg( feature = "chain-electrum" ) ]
602- ChainSourceKind :: Electrum ( electrum_chain_source) => {
603- electrum_chain_source. process_transaction_broadcast( package) . await
604- } ,
605- #[ cfg( feature = "chain-bitcoind" ) ]
606- ChainSourceKind :: Bitcoind ( bitcoind_chain_source) => {
607- bitcoind_chain_source. process_transaction_broadcast( package) . await
608- } ,
631+ }
632+ _ = tokio:: time:: sleep_until(
633+ next_retry_at. unwrap_or_else( tokio:: time:: Instant :: now)
634+ ) , if next_retry_at. is_some( ) => {
635+ let ( _, package) = parked. remove( 0 ) ;
636+ if let Err ( package) = self . classify_and_broadcast( package) . await {
637+ let retry_at = tokio:: time:: Instant :: now( ) + FAILED_CLASSIFY_RETRY_DELAY ;
638+ parked. push( ( retry_at, package) ) ;
609639 }
610640 }
611641 }
0 commit comments