Skip to content

Commit f4b1c1b

Browse files
Fix bug where we double-pay an offer due to stale manager.
This fixes the following bug: - An outbound payment is AwaitingInvoice - We receive an invoice and lock the HTLCs into the relevant ChannelMonitors - The monitor is successfully persisted, but the ChannelManager fails to persist, so the outbound payment remains AwaitingInvoice - We restart, causing the channel to close due to a stale ChannelManager - We receive a duplicate invoice, and attempt to pay it again due to the payment still being AwaitingInvoice in the stale ChannelManager After the fix for this, we will notice that the payment is already locked into the monitor on startup and transition the incorrectly-AwaitingInvoice payment to Retryable, which prevents double-paying on duplicate invoice receipt.
1 parent 500aaae commit f4b1c1b

File tree

3 files changed

+111
-2
lines changed

3 files changed

+111
-2
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12425,7 +12425,11 @@ where
1242512425
session_priv_bytes[..].copy_from_slice(&session_priv[..]);
1242612426
match pending_outbounds.pending_outbound_payments.lock().unwrap().entry(payment_id) {
1242712427
hash_map::Entry::Occupied(mut entry) => {
12428-
let newly_added = entry.get_mut().insert(session_priv_bytes, &path);
12428+
let newly_added = entry
12429+
.get_mut()
12430+
.insert_from_monitor_on_startup(
12431+
session_priv_bytes, &path, htlc.payment_hash, best_block_height
12432+
);
1242912433
log_info!(logger, "{} a pending payment path for {} msat for session priv {} on an existing pending payment with payment hash {}",
1243012434
if newly_added { "Added" } else { "Had" }, path_amt, log_bytes!(session_priv_bytes), htlc.payment_hash);
1243112435
},

lightning/src/ln/offers_tests.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use crate::blinded_path::IntroductionNode;
4747
use crate::blinded_path::message::BlindedMessagePath;
4848
use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext};
4949
use crate::blinded_path::message::{MessageContext, OffersContext};
50-
use crate::events::{Event, MessageSendEventsProvider, PaymentFailureReason, PaymentPurpose};
50+
use crate::events::{ClosureReason, Event, MessageSendEventsProvider, PaymentFailureReason, PaymentPurpose};
5151
use crate::ln::channelmanager::{Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, Retry, self};
5252
use crate::ln::features::Bolt12InvoiceFeatures;
5353
use crate::ln::functional_test_utils::*;
@@ -64,6 +64,7 @@ use crate::onion_message::offers::OffersMessage;
6464
use crate::onion_message::packet::ParsedOnionMessageContents;
6565
use crate::routing::gossip::{NodeAlias, NodeId};
6666
use crate::sign::{NodeSigner, Recipient};
67+
use crate::util::ser::Writeable;
6768

6869
use crate::prelude::*;
6970

@@ -2253,3 +2254,73 @@ fn fails_paying_invoice_with_unknown_required_features() {
22532254
_ => panic!("Expected Event::PaymentFailed with reason"),
22542255
}
22552256
}
2257+
2258+
#[test]
2259+
fn no_double_pay_with_stale_channelmanager() {
2260+
// This tests the following bug:
2261+
// - An outbound payment is AwaitingInvoice
2262+
// - We receive an invoice and lock the HTLCs into the relevant ChannelMonitor
2263+
// - The monitor is successfully persisted, but the ChannelManager fails to persist, so the
2264+
// payment remains AwaitingInvoice
2265+
// - We restart, causing the channel to close due to a stale ChannelManager
2266+
// - We receive a duplicate invoice, and attempt to pay it again due to the payment still being
2267+
// AwaitingInvoice in the stale ChannelManager
2268+
// After the fix for this, we will notice that the payment is already locked into the monitor on
2269+
// startup and transition the incorrectly-AwaitingInvoice payment to Retryable, which prevents
2270+
// double-paying on duplicate invoice receipt.
2271+
let chanmon_cfgs = create_chanmon_cfgs(2);
2272+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2273+
let persister;
2274+
let chain_monitor;
2275+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
2276+
let alice_deserialized;
2277+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2278+
let chan_id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000).2;
2279+
2280+
let alice_id = nodes[0].node.get_our_node_id();
2281+
let bob_id = nodes[1].node.get_our_node_id();
2282+
2283+
let offer = nodes[1].node
2284+
.create_offer_builder(None).unwrap()
2285+
.clear_paths()
2286+
.amount_msats(10_000_000)
2287+
.build().unwrap();
2288+
assert_eq!(offer.signing_pubkey(), Some(bob_id));
2289+
assert!(offer.paths().is_empty());
2290+
2291+
let payment_id = PaymentId([1; 32]);
2292+
nodes[0].node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap();
2293+
expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id);
2294+
2295+
let invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
2296+
nodes[1].onion_messenger.handle_onion_message(alice_id, &invreq_om);
2297+
2298+
// Save the manager while the payment is in state AwaitingInvoice so we can reload it later.
2299+
let alice_chan_manager_serialized = nodes[0].node.encode();
2300+
2301+
let invoice_om = nodes[1].onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
2302+
nodes[0].onion_messenger.handle_onion_message(bob_id, &invoice_om);
2303+
2304+
let (invoice, _) = extract_invoice(&nodes[0], &invoice_om);
2305+
route_bolt12_payment(&nodes[0], &[&nodes[1]], &invoice);
2306+
expect_recent_payment!(nodes[0], RecentPaymentDetails::Pending, payment_id);
2307+
match get_event!(nodes[1], Event::PaymentClaimable) {
2308+
Event::PaymentClaimable { .. } => {},
2309+
_ => panic!("No Event::PaymentClaimable"),
2310+
}
2311+
2312+
// Reload with the stale manager and check that receiving the invoice again won't result in a
2313+
// duplicate payment attempt.
2314+
let monitor = get_monitor!(nodes[0], chan_id).encode();
2315+
reload_node!(nodes[0], &alice_chan_manager_serialized, &[&monitor], persister, chain_monitor, alice_deserialized);
2316+
// The stale manager results in closing the channel.
2317+
check_closed_event!(nodes[0], 1, ClosureReason::OutdatedChannelManager, [bob_id], 10_000_000);
2318+
check_added_monitors!(nodes[0], 1);
2319+
2320+
// Alice receives a duplicate invoice, but the payment should be transitioned to Retryable by now.
2321+
nodes[0].onion_messenger.handle_onion_message(bob_id, &invoice_om);
2322+
// Previously we would've attempted to pay the invoice a 2nd time, resulting in a PaymentFailed
2323+
// event here.
2324+
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
2325+
}
2326+

lightning/src/ln/outbound_payment.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,40 @@ impl PendingOutboundPayment {
282282
insert_res
283283
}
284284

285+
pub(super) fn insert_from_monitor_on_startup(
286+
&mut self, session_priv: [u8; 32], path: &Path, payment_hash: PaymentHash,
287+
best_block_height: u32
288+
) -> bool {
289+
match self {
290+
PendingOutboundPayment::AwaitingInvoice { .. } |
291+
PendingOutboundPayment::InvoiceReceived { .. } => {
292+
// If we've reached this point, it means we initiated a payment to a BOLT 12 invoice and
293+
// locked the htlc(s) into the `ChannelMonitor`(s), but failed to persist the
294+
// `ChannelManager` after transitioning from this state to `Retryable` prior to shutdown.
295+
// Therefore, we need to move this payment to `Retryable` now to avoid double-paying if
296+
// the recipient sends a duplicate invoice.
297+
*self = Self::Retryable {
298+
retry_strategy: None,
299+
attempts: PaymentAttempts::new(),
300+
payment_params: None,
301+
session_privs: hash_set_from_iter([session_priv]),
302+
payment_hash,
303+
payment_secret: None,
304+
payment_metadata: None,
305+
keysend_preimage: None,
306+
custom_tlvs: Vec::new(),
307+
pending_amt_msat: path.final_value_msat(),
308+
pending_fee_msat: Some(path.fee_msat()),
309+
total_msat: path.final_value_msat(),
310+
starting_block_height: best_block_height,
311+
remaining_max_total_routing_fee_msat: None,
312+
};
313+
}
314+
_ => {}
315+
}
316+
self.insert(session_priv, path)
317+
}
318+
285319
pub(super) fn remaining_parts(&self) -> usize {
286320
match self {
287321
PendingOutboundPayment::Legacy { session_privs } |

0 commit comments

Comments
 (0)