Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,8 @@ pub enum Event {
/// If the recipient or an intermediate node misbehaves and gives us free money, this may
/// overstate the amount paid, though this is unlikely.
///
/// This is only `None` for payments initiated on LDK versions prior to 0.0.103.
/// This is only `None` for payments abandoned but ultimately claimed when using LDK versions
/// prior to 0.3, 0.2.3, or 0.1.10.
///
/// [`Route::get_total_fees`]: crate::routing::router::Route::get_total_fees
fee_paid_msat: Option<u64>,
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8579,7 +8579,7 @@ pub fn test_inconsistent_mpp_params() {
pass_along_path(&nodes[0], path_b, real_amt, hash, Some(payment_secret), event, true, None);

do_claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path_a, path_b], preimage));
expect_payment_sent(&nodes[0], preimage, Some(None), true, true);
expect_payment_sent(&nodes[0], preimage, Some(Some(2000)), true, true);
}

#[xtest(feature = "_externalize_tests")]
Expand Down
7 changes: 7 additions & 0 deletions lightning/src/ln/outbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ pub(crate) enum PendingOutboundPayment {
/// The total payment amount across all paths, used to be able to issue `PaymentSent` if
/// an HTLC still happens to succeed after we marked the payment as abandoned.
total_msat: Option<u64>,
/// Preserved from `Retryable` so we can still report `fee_paid_msat` if an HTLC succeeds after
/// the payment was abandoned. Added in 0.3.
pending_fee_msat: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: remove() (line 335) only decrements pending_fee_msat for the Retryable variant, not for Abandoned. This means if an MPP payment is manually abandoned with multiple paths in-flight, and then some paths fail back while others succeed, the fee for the failed paths won't be subtracted — causing PaymentSent::fee_paid_msat to be overstated.

Concrete scenario:

  1. Two-path MPP, path A (fee=100) and path B (fee=200) in-flight → pending_fee_msat = Some(300)
  2. abandon_payment()Abandoned { pending_fee_msat: Some(300) }
  3. Path A fails (intermediate node timeout) → remove() removes session_priv but doesn't decrement fee → pending_fee_msat still Some(300)
  4. Path B succeeds (recipient claims) → PaymentSent { fee_paid_msat: Some(300) } — should be Some(200)

The remove() function at line 348-363 needs an additional branch for Abandoned:

if let PendingOutboundPayment::Abandoned {
    ref mut pending_fee_msat, ..
} = self {
    let path = path.expect("Removing a failed payment should always come with a path");
    if let Some(fee_msat) = pending_fee_msat.as_mut() {
        *fee_msat -= path.fee_msat();
    }
}

Note: for the auto-abandon path (in handle_update_fail_htlc), remove() is called before mark_abandoned(), so the fee is correctly captured. This bug only affects the manual-abandon path and any future failures arriving after auto-abandonment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

},
}

Expand Down Expand Up @@ -252,6 +255,7 @@ impl PendingOutboundPayment {
fn get_pending_fee_msat(&self) -> Option<u64> {
match self {
PendingOutboundPayment::Retryable { pending_fee_msat, .. } => pending_fee_msat.clone(),
PendingOutboundPayment::Abandoned { pending_fee_msat, .. } => pending_fee_msat.clone(),
_ => None,
}
}
Expand Down Expand Up @@ -308,6 +312,7 @@ impl PendingOutboundPayment {
_ => new_hash_set(),
};
let total_msat = self.total_msat();
let pending_fee_msat = self.get_pending_fee_msat();
match self {
Self::Retryable { payment_hash, .. } |
Self::InvoiceReceived { payment_hash, .. } |
Expand All @@ -318,6 +323,7 @@ impl PendingOutboundPayment {
payment_hash: *payment_hash,
reason: Some(reason),
total_msat,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious here if we should be setting this to the sum of the current paths currently in-flight, and not the fixed total msat ?

pending_fee_msat,
};
},
_ => {}
Expand Down Expand Up @@ -2778,6 +2784,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
(1, reason, upgradable_option),
(2, payment_hash, required),
(3, total_msat, option),
(5, pending_fee_msat, option),
},
(5, AwaitingInvoice) => {
(0, expiration, required),
Expand Down
30 changes: 30 additions & 0 deletions lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,36 @@ fn abandoned_send_payment_idempotent() {
claim_payment(&nodes[0], &[&nodes[1]], second_payment_preimage);
}

#[test]
fn abandoned_payment_fulfilled_preserves_fee_paid_msat() {
// Previously, if we abandoned a payment with HTLCs in-flight and the payment eventually
// succeeded, we would set the `Event::PaymentSent::fee_paid_msat` to None, even though we had
// docs guaranteeing that it would always be Some after 0.0.103.
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);

create_announced_chan_between_nodes(&nodes, 0, 1);
create_announced_chan_between_nodes(&nodes, 1, 2);

let amt_msat = 10_000_000;
let (route, payment_hash, payment_preimage, payment_secret) =
get_route_and_payment_hash!(&nodes[0], nodes[2], amt_msat);
let payment_id = PaymentId(payment_hash.0);
let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat);
nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap();
check_added_monitors(&nodes[0], 1);

let path: &[&Node] = &[&nodes[1], &nodes[2]];
pass_along_route(&nodes[0], &[path], amt_msat, payment_hash, payment_secret);

nodes[0].node.abandon_payment(payment_id);
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());

claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], payment_preimage));
}

#[derive(PartialEq)]
enum InterceptTest {
Forward,
Expand Down