Skip to content

Commit 866cedf

Browse files
authored
Merge pull request #3313 from valentinewallace/2024-09-fix-offer-double-pay
Don't pay a duplicate BOLT 12 invoice if `ChannelManager` is stale
2 parents 429cbe1 + fbb3ab2 commit 866cedf

File tree

7 files changed

+159
-36
lines changed

7 files changed

+159
-36
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ use crate::ln::onion_utils::{HTLCFailReason, INVALID_ONION_BLINDING};
6161
use crate::ln::msgs::{ChannelMessageHandler, DecodeError, LightningError};
6262
#[cfg(test)]
6363
use crate::ln::outbound_payment;
64-
use crate::ln::outbound_payment::{OutboundPayments, PaymentAttempts, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration};
64+
use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration};
6565
use crate::ln::wire::Encode;
6666
use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice};
6767
use crate::offers::invoice_error::InvoiceError;
@@ -12569,37 +12569,11 @@ where
1256912569
return Err(DecodeError::InvalidValue);
1257012570
}
1257112571

12572-
let path_amt = path.final_value_msat();
1257312572
let mut session_priv_bytes = [0; 32];
1257412573
session_priv_bytes[..].copy_from_slice(&session_priv[..]);
12575-
match pending_outbounds.pending_outbound_payments.lock().unwrap().entry(payment_id) {
12576-
hash_map::Entry::Occupied(mut entry) => {
12577-
let newly_added = entry.get_mut().insert(session_priv_bytes, &path);
12578-
log_info!(logger, "{} a pending payment path for {} msat for session priv {} on an existing pending payment with payment hash {}",
12579-
if newly_added { "Added" } else { "Had" }, path_amt, log_bytes!(session_priv_bytes), htlc.payment_hash);
12580-
},
12581-
hash_map::Entry::Vacant(entry) => {
12582-
let path_fee = path.fee_msat();
12583-
entry.insert(PendingOutboundPayment::Retryable {
12584-
retry_strategy: None,
12585-
attempts: PaymentAttempts::new(),
12586-
payment_params: None,
12587-
session_privs: hash_set_from_iter([session_priv_bytes]),
12588-
payment_hash: htlc.payment_hash,
12589-
payment_secret: None, // only used for retries, and we'll never retry on startup
12590-
payment_metadata: None, // only used for retries, and we'll never retry on startup
12591-
keysend_preimage: None, // only used for retries, and we'll never retry on startup
12592-
custom_tlvs: Vec::new(), // only used for retries, and we'll never retry on startup
12593-
pending_amt_msat: path_amt,
12594-
pending_fee_msat: Some(path_fee),
12595-
total_msat: path_amt,
12596-
starting_block_height: best_block_height,
12597-
remaining_max_total_routing_fee_msat: None, // only used for retries, and we'll never retry on startup
12598-
});
12599-
log_info!(logger, "Added a pending payment for {} msat with payment hash {} for path with session priv {}",
12600-
path_amt, &htlc.payment_hash, log_bytes!(session_priv_bytes));
12601-
}
12602-
}
12574+
pending_outbounds.insert_from_monitor_on_startup(
12575+
payment_id, htlc.payment_hash, session_priv_bytes, &path, best_block_height, logger
12576+
);
1260312577
}
1260412578
}
1260512579
for (htlc_source, (htlc, preimage_opt)) in monitor.get_all_current_outbound_htlcs() {

lightning/src/ln/functional_test_utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,8 +1144,8 @@ macro_rules! reload_node {
11441144
($node: expr, $new_config: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => {
11451145
let chanman_encoded = $chanman_encoded;
11461146

1147-
$persister = test_utils::TestPersister::new();
1148-
$new_chain_monitor = test_utils::TestChainMonitor::new(Some($node.chain_source), $node.tx_broadcaster.clone(), $node.logger, $node.fee_estimator, &$persister, &$node.keys_manager);
1147+
$persister = $crate::util::test_utils::TestPersister::new();
1148+
$new_chain_monitor = $crate::util::test_utils::TestChainMonitor::new(Some($node.chain_source), $node.tx_broadcaster.clone(), $node.logger, $node.fee_estimator, &$persister, &$node.keys_manager);
11491149
$node.chain_monitor = &$new_chain_monitor;
11501150

11511151
$new_channelmanager = _reload_node(&$node, $new_config, &chanman_encoded, $monitors_encoded);

lightning/src/ln/monitor_tests.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ use crate::ln::msgs::ChannelMessageHandler;
2323
use crate::crypto::utils::sign;
2424
use crate::util::ser::Writeable;
2525
use crate::util::scid_utils::block_from_scid;
26-
use crate::util::test_utils;
2726

2827
use bitcoin::{Amount, PublicKey, ScriptBuf, Transaction, TxIn, TxOut, Witness};
2928
use bitcoin::locktime::absolute::LockTime;

lightning/src/ln/offers_tests.rs

Lines changed: 91 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,92 @@ 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 ChannelMonitors
2263+
// - The monitors are successfully persisted, but the ChannelManager fails to persist, so the
2264+
// payment remains AwaitingInvoice
2265+
// - We restart, causing the channels 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 monitors 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_0 = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000).2;
2279+
let chan_id_1 = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000).2;
2280+
2281+
let alice_id = nodes[0].node.get_our_node_id();
2282+
let bob_id = nodes[1].node.get_our_node_id();
2283+
2284+
let amt_msat = nodes[0].node.list_usable_channels()[0].next_outbound_htlc_limit_msat + 1; // Force MPP
2285+
let offer = nodes[1].node
2286+
.create_offer_builder(None).unwrap()
2287+
.clear_paths()
2288+
.amount_msats(amt_msat)
2289+
.build().unwrap();
2290+
assert_eq!(offer.signing_pubkey(), Some(bob_id));
2291+
assert!(offer.paths().is_empty());
2292+
2293+
let payment_id = PaymentId([1; 32]);
2294+
nodes[0].node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap();
2295+
expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id);
2296+
2297+
let invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
2298+
nodes[1].onion_messenger.handle_onion_message(alice_id, &invreq_om);
2299+
2300+
// Save the manager while the payment is in state AwaitingInvoice so we can reload it later.
2301+
let alice_chan_manager_serialized = nodes[0].node.encode();
2302+
2303+
let invoice_om = nodes[1].onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
2304+
nodes[0].onion_messenger.handle_onion_message(bob_id, &invoice_om);
2305+
let payment_hash = extract_invoice(&nodes[0], &invoice_om).0.payment_hash();
2306+
2307+
let expected_route: &[&[&Node]] = &[&[&nodes[1]], &[&nodes[1]]];
2308+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
2309+
assert_eq!(events.len(), 2);
2310+
check_added_monitors!(nodes[0], 2);
2311+
2312+
let ev = remove_first_msg_event_to_node(&bob_id, &mut events);
2313+
let args = PassAlongPathArgs::new(&nodes[0], expected_route[0], amt_msat, payment_hash, ev)
2314+
.without_clearing_recipient_events();
2315+
do_pass_along_path(args);
2316+
2317+
let ev = remove_first_msg_event_to_node(&bob_id, &mut events);
2318+
let args = PassAlongPathArgs::new(&nodes[0], expected_route[0], amt_msat, payment_hash, ev)
2319+
.without_clearing_recipient_events();
2320+
do_pass_along_path(args);
2321+
2322+
expect_recent_payment!(nodes[0], RecentPaymentDetails::Pending, payment_id);
2323+
match get_event!(nodes[1], Event::PaymentClaimable) {
2324+
Event::PaymentClaimable { .. } => {},
2325+
_ => panic!("No Event::PaymentClaimable"),
2326+
}
2327+
2328+
// Reload with the stale manager and check that receiving the invoice again won't result in a
2329+
// duplicate payment attempt.
2330+
let monitor_0 = get_monitor!(nodes[0], chan_id_0).encode();
2331+
let monitor_1 = get_monitor!(nodes[0], chan_id_1).encode();
2332+
reload_node!(nodes[0], &alice_chan_manager_serialized, &[&monitor_0, &monitor_1], persister, chain_monitor, alice_deserialized);
2333+
// The stale manager results in closing the channels.
2334+
check_closed_event!(nodes[0], 2, ClosureReason::OutdatedChannelManager, [bob_id, bob_id], 10_000_000);
2335+
check_added_monitors!(nodes[0], 2);
2336+
2337+
// Alice receives a duplicate invoice, but the payment should be transitioned to Retryable by now.
2338+
nodes[0].onion_messenger.handle_onion_message(bob_id, &invoice_om);
2339+
// Previously, Alice would've attempted to pay the invoice a 2nd time. In this test case, this 2nd
2340+
// attempt would have resulted in a PaymentFailed event here, since the only channels between
2341+
// Alice and Bob is closed. Since no 2nd attempt should be made, check that no events are
2342+
// generated in response to the duplicate invoice.
2343+
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
2344+
}
2345+

lightning/src/ln/outbound_payment.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2121,6 +2121,68 @@ impl OutboundPayments {
21212121
self.awaiting_invoice.store(false, Ordering::Release);
21222122
invoice_requests
21232123
}
2124+
2125+
pub(super) fn insert_from_monitor_on_startup<L: Logger>(
2126+
&self, payment_id: PaymentId, payment_hash: PaymentHash, session_priv_bytes: [u8; 32],
2127+
path: &Path, best_block_height: u32, logger: L
2128+
) {
2129+
let path_amt = path.final_value_msat();
2130+
let path_fee = path.fee_msat();
2131+
2132+
macro_rules! new_retryable {
2133+
() => {
2134+
PendingOutboundPayment::Retryable {
2135+
retry_strategy: None,
2136+
attempts: PaymentAttempts::new(),
2137+
payment_params: None,
2138+
session_privs: hash_set_from_iter([session_priv_bytes]),
2139+
payment_hash,
2140+
payment_secret: None, // only used for retries, and we'll never retry on startup
2141+
payment_metadata: None, // only used for retries, and we'll never retry on startup
2142+
keysend_preimage: None, // only used for retries, and we'll never retry on startup
2143+
custom_tlvs: Vec::new(), // only used for retries, and we'll never retry on startup
2144+
pending_amt_msat: path_amt,
2145+
pending_fee_msat: Some(path_fee),
2146+
total_msat: path_amt,
2147+
starting_block_height: best_block_height,
2148+
remaining_max_total_routing_fee_msat: None, // only used for retries, and we'll never retry on startup
2149+
}
2150+
}
2151+
}
2152+
2153+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
2154+
hash_map::Entry::Occupied(mut entry) => {
2155+
let newly_added = match entry.get() {
2156+
PendingOutboundPayment::AwaitingInvoice { .. } |
2157+
PendingOutboundPayment::InvoiceReceived { .. } |
2158+
PendingOutboundPayment::StaticInvoiceReceived { .. } =>
2159+
{
2160+
// If we've reached this point, it means we initiated a payment to a BOLT 12 invoice and
2161+
// locked the htlc(s) into the `ChannelMonitor`(s), but failed to persist the
2162+
// `ChannelManager` after transitioning from this state to `Retryable` prior to shutdown.
2163+
// Therefore, we need to move this payment to `Retryable` now to avoid double-paying if
2164+
// the recipient sends a duplicate invoice or release_held_htlc onion message.
2165+
*entry.get_mut() = new_retryable!();
2166+
true
2167+
},
2168+
PendingOutboundPayment::Legacy { .. } |
2169+
PendingOutboundPayment::Retryable { .. } |
2170+
PendingOutboundPayment::Fulfilled { .. } |
2171+
PendingOutboundPayment::Abandoned { .. } =>
2172+
{
2173+
entry.get_mut().insert(session_priv_bytes, &path)
2174+
}
2175+
};
2176+
log_info!(logger, "{} a pending payment path for {} msat for session priv {} on an existing pending payment with payment hash {}",
2177+
if newly_added { "Added" } else { "Had" }, path_amt, log_bytes!(session_priv_bytes), payment_hash);
2178+
},
2179+
hash_map::Entry::Vacant(entry) => {
2180+
entry.insert(new_retryable!());
2181+
log_info!(logger, "Added a pending payment for {} msat with payment hash {} for path with session priv {}",
2182+
path_amt, payment_hash, log_bytes!(session_priv_bytes));
2183+
}
2184+
}
2185+
}
21242186
}
21252187

21262188
/// Returns whether a payment with the given [`PaymentHash`] and [`PaymentId`] is, in fact, a

lightning/src/ln/priv_short_conf_tests.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ use crate::ln::msgs::{ChannelMessageHandler, RoutingMessageHandler, ChannelUpdat
2424
use crate::ln::wire::Encode;
2525
use crate::util::config::{UserConfig, MaxDustHTLCExposure};
2626
use crate::util::ser::Writeable;
27-
use crate::util::test_utils;
2827

2928
use crate::prelude::*;
3029

lightning/src/ln/reorg_tests.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use crate::events::{Event, MessageSendEventsProvider, ClosureReason, HTLCDestina
1717
use crate::ln::msgs::{ChannelMessageHandler, Init};
1818
use crate::ln::types::ChannelId;
1919
use crate::sign::OutputSpender;
20-
use crate::util::test_utils;
2120
use crate::util::ser::Writeable;
2221
use crate::util::string::UntrustedString;
2322

0 commit comments

Comments
 (0)