From df32f6ea9b83e1329360b0941b21694f26da9025 Mon Sep 17 00:00:00 2001 From: Jainakin Date: Mon, 10 Aug 2026 15:32:37 +0530 Subject: [PATCH] Treat monitor-held spontaneous payments as in flight --- lightning/src/ln/outbound_payment.rs | 81 +++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index db8fcf78a..307056ec1 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -947,8 +947,29 @@ impl OutboundPayments { payment_id, None, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path ); - log_info!(logger, "Sending spontaneous payment with id {} and hash {} returned {:?}", - payment_id, payment_hash, res); + match &res { + Ok(()) => { + log_info!(logger, + "Sending spontaneous payment with id {} and hash {} succeeded", + payment_id, payment_hash); + }, + Err(PaymentSendFailure::PartialFailure { .. }) => { + log_info!(logger, + "Sending spontaneous payment with id {} and hash {} is awaiting durable channel state", + payment_id, payment_hash); + }, + Err(error) => { + log_error!(logger, + "Sending spontaneous payment with id {} and hash {} failed: {:?}", + payment_id, payment_hash, error); + }, + } + if matches!(res, Err(PaymentSendFailure::PartialFailure { .. })) { + // At least one path was accepted by the channel state machine. In particular, + // MonitorUpdateInProgress means the HTLC is held until its monitor update is durable. + // Reporting a retryable error could make the caller submit the payment twice. + return Ok(()); + } if let Err(ref e) = res { self.remove_outbound_if_all_failed(payment_id, e); if let PaymentSendFailure::AllFailedResendSafe(_) = e { @@ -2963,6 +2984,62 @@ mod tests { assert!(onion_fields.with_custom_tlvs(good_tlvs).is_ok()); } + #[test] + #[rustfmt::skip] + fn spontaneous_route_accepts_monitor_update_in_progress_as_in_flight() { + let logger = test_utils::TestLogger::new(); + let logger_ref = &logger; + let log = WithContext::from(&logger_ref, None, None, Some(PaymentHash([1; 32]))); + let outbound_payments = OutboundPayments::new( + new_hash_map(), + Arc::new(test_utils::TestStore::new(false)), + ); + let keys_manager = test_utils::TestKeysInterface::new(&[2; 32], Network::Testnet); + let secp_ctx = Secp256k1::new(); + let receiver_pk = PublicKey::from_secret_key( + &secp_ctx, &SecretKey::from_slice(&[3; 32]).unwrap(), + ); + let payment_preimage = PaymentPreimage([4; 32]); + let payment_hash = payment_preimage.into(); + let payment_id = PaymentId([5; 32]); + let route = Route { + paths: vec![Path { + hops: vec![RouteHop { + payment_amount: 1_000, + rgb_payment: None, + pubkey: receiver_pk, + node_features: NodeFeatures::empty(), + short_channel_id: 42, + channel_features: ChannelFeatures::empty(), + fee_msat: 1_000, + cltv_expiry_delta: 18, + maybe_announced_channel: false, + }], + blinded_tail: None, + }], + route_params: None, + }; + let pending_events = Mutex::new(VecDeque::new()); + + let result = outbound_payments.send_spontaneous_payment_with_route( + route, + payment_hash, + payment_preimage, + RecipientOnionFields::spontaneous_empty(), + payment_id, + &&keys_manager, + &&keys_manager, + 0, + &pending_events, + |_| Err(APIError::MonitorUpdateInProgress), + &log, + ); + + assert!(result.is_ok()); + assert!(pending_events.lock().unwrap().is_empty()); + assert!(outbound_payments.pending_outbound_payments.lock().unwrap().contains_key(&payment_id)); + } + #[test] #[cfg(feature = "std")] fn fails_paying_after_expiration() {