Skip to content

Authenticate InvoiceError messages #3192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Aug 14, 2024
Merged
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
10 changes: 5 additions & 5 deletions lightning-types/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ mod sealed {

#[cfg(any(test, feature = "_test_utils"))]
define_feature!(
123456789,
12345,
UnknownFeature,
[
NodeContext,
Expand Down Expand Up @@ -1117,7 +1117,7 @@ mod tests {
features.set_unknown_feature_required();
assert!(features.requires_unknown_bits());
assert!(features.supports_unknown_bits());
assert_eq!(features.required_unknown_bits_from(&ChannelFeatures::empty()), vec![123456788]);
assert_eq!(features.required_unknown_bits_from(&ChannelFeatures::empty()), vec![12344]);

let mut features = ChannelFeatures::empty();
features.set_unknown_feature_optional();
Expand All @@ -1127,17 +1127,17 @@ mod tests {

let mut features = ChannelFeatures::empty();
features.set_unknown_feature_required();
features.set_custom_bit(123456786).unwrap();
features.set_custom_bit(12346).unwrap();
assert!(features.requires_unknown_bits());
assert!(features.supports_unknown_bits());
assert_eq!(
features.required_unknown_bits_from(&ChannelFeatures::empty()),
vec![123456786, 123456788]
vec![12344, 12346]
);

let mut limiter = ChannelFeatures::empty();
limiter.set_unknown_feature_optional();
assert_eq!(features.required_unknown_bits_from(&limiter), vec![123456786]);
assert_eq!(features.required_unknown_bits_from(&limiter), vec![12346]);
}

#[test]
Expand Down
9 changes: 9 additions & 0 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
#[allow(unused_imports)]
use crate::prelude::*;

use bitcoin::hashes::hmac::Hmac;
use bitcoin::hashes::sha256::Hash as Sha256;
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NextMessageHop, NodeIdLookUp};
use crate::blinded_path::utils;
use crate::io;
Expand Down Expand Up @@ -146,6 +148,12 @@ pub enum OffersContext {
/// [`Refund`]: crate::offers::refund::Refund
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
nonce: Nonce,

/// Authentication code for the [`PaymentId`], which should be checked when the context is
/// used with an [`InvoiceError`].
///
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
hmac: Option<Hmac<Sha256>>,
},
/// Context used by a [`BlindedPath`] as a reply path for a [`Bolt12Invoice`].
///
Expand Down Expand Up @@ -173,6 +181,7 @@ impl_writeable_tlv_based_enum!(OffersContext,
(1, OutboundPayment) => {
(0, payment_id, required),
(1, nonce, required),
(2, hmac, option),
},
(2, InboundPayment) => {
(0, payment_hash, required),
Expand Down
97 changes: 67 additions & 30 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,12 @@ impl_writeable_tlv_based_enum!(InterceptNextHop,
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PaymentFailureReason {
/// The intended recipient rejected our payment.
///
/// Also used for [`UnknownRequiredFeatures`] and [`InvoiceRequestRejected`] when downgrading to
/// version prior to 0.0.124.
///
/// [`UnknownRequiredFeatures`]: Self::UnknownRequiredFeatures
/// [`InvoiceRequestRejected`]: Self::InvoiceRequestRejected
RecipientRejected,
/// The user chose to abandon this payment by calling [`ChannelManager::abandon_payment`].
///
Expand All @@ -517,7 +523,10 @@ pub enum PaymentFailureReason {
/// The payment expired while retrying, based on the provided
/// [`PaymentParameters::expiry_time`].
///
/// Also used for [`InvoiceRequestExpired`] when downgrading to version prior to 0.0.124.
///
/// [`PaymentParameters::expiry_time`]: crate::routing::router::PaymentParameters::expiry_time
/// [`InvoiceRequestExpired`]: Self::InvoiceRequestExpired
PaymentExpired,
/// We failed to find a route while retrying the payment.
///
Expand All @@ -528,12 +537,23 @@ pub enum PaymentFailureReason {
/// This error should generally never happen. This likely means that there is a problem with
/// your router.
UnexpectedError,
/// An invoice was received that required unknown features.
UnknownRequiredFeatures,
/// A [`Bolt12Invoice`] was not received in a reasonable amount of time.
InvoiceRequestExpired,
/// An [`InvoiceRequest`] for the payment was rejected by the recipient.
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
InvoiceRequestRejected,
Comment on lines +541 to +547
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm pretty meh on the idea that users might upgrade, use bolt12, and suddenly be unable to downgrade even one version. Not entirely sure what to do about it, though - we could delay this part of the patch for a release, or we could write the PaymentFailureReason twice, once with these versions in a new TLV and once without, mapping them to UnexpectedError or whatever.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Went ahead and wrote the reason twice. UnexpectedError would be a misleading, IMO, though. Used None instead, but let me know if there was some other alternative you were thinking about.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, I suppose None is fine, but its a bit annoying to suddenly have no reason when the docs on 123 say that its "only None for events generated by versions prior to 0.0.115". I guess for InvoiceRequestRejected we could easily just say RecipientRejected, but InvoiceRequestExpired is tougher...maybe RetriesExhausted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... UnknownRequiredFeatures also needs to map to something.

Copy link
Collaborator

Choose a reason for hiding this comment

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

RecipientRejected, I assume - they would have rejected a payment that has unknown features in the onion so...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here it is us -- the payer -- rejecting an invoice for an outbound payment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea but the principle is the same - we have some issue causing us to be unable to talk to the intended recipient so cannot pay. RecipientRejected makes sense imo as it's also the most final - "don't bother trying this again cause they already said they won't accept it"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha. Updated now.

}

impl_writeable_tlv_based_enum!(PaymentFailureReason,
impl_writeable_tlv_based_enum_upgradable!(PaymentFailureReason,
(0, RecipientRejected) => {},
(1, UnknownRequiredFeatures) => {},
(2, UserAbandoned) => {},
(3, InvoiceRequestExpired) => {},
(4, RetriesExhausted) => {},
(5, InvoiceRequestRejected) => {},
(6, PaymentExpired) => {},
(8, RouteNotFound) => {},
(10, UnexpectedError) => {},
Expand Down Expand Up @@ -769,22 +789,6 @@ pub enum Event {
/// Sockets for connecting to the node.
addresses: Vec<msgs::SocketAddress>,
},
/// Indicates a request for an invoice failed to yield a response in a reasonable amount of time
/// or was explicitly abandoned by [`ChannelManager::abandon_payment`]. This may be for an
/// [`InvoiceRequest`] sent for an [`Offer`] or for a [`Refund`] that hasn't been redeemed.
///
/// # Failure Behavior and Persistence
/// This event will eventually be replayed after failures-to-handle (i.e., the event handler
/// returning `Err(ReplayEvent ())`) and will be persisted across restarts.
///
/// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`Offer`]: crate::offers::offer::Offer
/// [`Refund`]: crate::offers::refund::Refund
InvoiceRequestFailed {
/// The `payment_id` to have been associated with payment for the requested invoice.
payment_id: PaymentId,
},
/// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was
/// received.
///
Expand Down Expand Up @@ -876,12 +880,15 @@ pub enum Event {
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment
payment_id: PaymentId,
/// The hash that was given to [`ChannelManager::send_payment`].
/// The hash that was given to [`ChannelManager::send_payment`]. `None` if the payment failed
/// before receiving an invoice when paying a BOLT12 [`Offer`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment
payment_hash: PaymentHash,
/// [`Offer`]: crate::offers::offer::Offer
payment_hash: Option<PaymentHash>,
/// The reason the payment failed. This is only `None` for events generated or serialized
/// by versions prior to 0.0.115.
/// by versions prior to 0.0.115, or when downgrading to a version with a reason that was
/// added after.
reason: Option<PaymentFailureReason>,
},
/// Indicates that a path for an outbound payment was successful.
Expand Down Expand Up @@ -1552,10 +1559,34 @@ impl Writeable for Event {
},
&Event::PaymentFailed { ref payment_id, ref payment_hash, ref reason } => {
15u8.write(writer)?;
let (payment_hash, invoice_received) = match payment_hash {
Some(payment_hash) => (payment_hash, true),
None => (&PaymentHash([0; 32]), false),
};
let legacy_reason = match reason {
None => &None,
// Variants available prior to version 0.0.124.
Some(PaymentFailureReason::RecipientRejected)
| Some(PaymentFailureReason::UserAbandoned)
| Some(PaymentFailureReason::RetriesExhausted)
| Some(PaymentFailureReason::PaymentExpired)
| Some(PaymentFailureReason::RouteNotFound)
| Some(PaymentFailureReason::UnexpectedError) => reason,
// Variants introduced at version 0.0.124 or later. Prior versions fail to parse
// unknown variants, while versions 0.0.124 or later will use None.
Some(PaymentFailureReason::UnknownRequiredFeatures) =>
&Some(PaymentFailureReason::RecipientRejected),
Some(PaymentFailureReason::InvoiceRequestExpired) =>
&Some(PaymentFailureReason::RetriesExhausted),
Some(PaymentFailureReason::InvoiceRequestRejected) =>
&Some(PaymentFailureReason::RecipientRejected),
};
write_tlv_fields!(writer, {
(0, payment_id, required),
(1, reason, option),
(1, legacy_reason, option),
(2, payment_hash, required),
(3, invoice_received, required),
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like if someone downgraded after writing a zeroed out payment hash, then upgraded again, their event would then contain a payment_hash: Some(PaymentHash([0; 32])), IIUC. I think we could prevent that by omitting this invoice_received field and explicitly detecting a zeroed out payment hash instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, not sure how concerning that is but went ahead with your alternative.

@TheBlueMatt I'm not checking in ChannelManager for such a payment hash when sending. Seems in such a rare scenario it would be better to check the reason to determine whether the 0-payment_hash should be included in the event when reading. Maybe there some obscure upgrade-downgrade-upgrade scenario I'm not thinking about. But I'm not sure if it's worth adding some complex checking if the worse case is dropping the 0-payment_hash.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we keep the old code writing the extra flag but then drop 0 payment hash if its missing? While people shouldn't be paying 0 payment hashes, its a bit weird that the payment hash will dissapear on you if you try to pay one because your counterparty made one up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated accordingly, IIUC.

(5, reason, option),
})
},
&Event::OpenChannelRequest { .. } => {
Expand Down Expand Up @@ -1634,12 +1665,6 @@ impl Writeable for Event {
(8, funding_txo, required),
});
},
&Event::InvoiceRequestFailed { ref payment_id } => {
33u8.write(writer)?;
write_tlv_fields!(writer, {
(0, payment_id, required),
})
},
&Event::ConnectionNeeded { .. } => {
35u8.write(writer)?;
// Never write ConnectionNeeded events as buffered onion messages aren't serialized.
Expand Down Expand Up @@ -1929,15 +1954,24 @@ impl MaybeReadable for Event {
let mut payment_hash = PaymentHash([0; 32]);
let mut payment_id = PaymentId([0; 32]);
let mut reason = None;
let mut legacy_reason = None;
let mut invoice_received: Option<bool> = None;
read_tlv_fields!(reader, {
(0, payment_id, required),
(1, reason, upgradable_option),
(1, legacy_reason, upgradable_option),
(2, payment_hash, required),
(3, invoice_received, option),
(5, reason, upgradable_option),
});
let payment_hash = match invoice_received {
Some(invoice_received) => invoice_received.then(|| payment_hash),
None => (payment_hash != PaymentHash([0; 32])).then(|| payment_hash),
};
let reason = reason.or(legacy_reason);
Ok(Some(Event::PaymentFailed {
payment_id,
payment_hash,
reason,
reason: _init_tlv_based_struct_field!(reason, upgradable_option),
}))
};
f()
Expand Down Expand Up @@ -2076,13 +2110,16 @@ impl MaybeReadable for Event {
};
f()
},
// This was Event::InvoiceRequestFailed prior to version 0.0.124.
33u8 => {
let mut f = || {
_init_and_read_len_prefixed_tlv_fields!(reader, {
(0, payment_id, required),
});
Ok(Some(Event::InvoiceRequestFailed {
Ok(Some(Event::PaymentFailed {
payment_id: payment_id.0.unwrap(),
payment_hash: None,
reason: Some(PaymentFailureReason::InvoiceRequestExpired),
}))
};
f()
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/blinded_payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1068,7 +1068,7 @@ fn blinded_path_retries() {
assert_eq!(evs.len(), 1);
match evs[0] {
Event::PaymentFailed { payment_hash: ev_payment_hash, reason, .. } => {
assert_eq!(ev_payment_hash, payment_hash);
assert_eq!(ev_payment_hash, Some(payment_hash));
// We have 1 retry attempt remaining, but we're out of blinded paths to try.
assert_eq!(reason, Some(PaymentFailureReason::RouteNotFound));
},
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/chanmon_update_fail_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1776,7 +1776,7 @@ fn test_monitor_update_on_pending_forwards() {
} else { panic!("Unexpected event!"); }
match events[2] {
Event::PaymentFailed { payment_hash, .. } => {
assert_eq!(payment_hash, payment_hash_1);
assert_eq!(payment_hash, Some(payment_hash_1));
},
_ => panic!("Unexpected event"),
}
Expand Down
Loading
Loading