Skip to content
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

BOLT 12 Offers message flow #2039

Merged
merged 19 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
140 changes: 137 additions & 3 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ use crate::ln::onion_utils::HTLCFailReason;
use crate::ln::msgs::{ChannelMessageHandler, DecodeError, LightningError};
#[cfg(test)]
use crate::ln::outbound_payment;
use crate::ln::outbound_payment::{OutboundPayments, PaymentAttempts, PendingOutboundPayment, SendAlongPathArgs, StaleExpiration};
use crate::ln::outbound_payment::{Bolt12PaymentError, OutboundPayments, PaymentAttempts, PendingOutboundPayment, SendAlongPathArgs, StaleExpiration};
use crate::ln::wire::Encode;
use crate::offers::invoice::{BlindedPayInfo, DEFAULT_RELATIVE_EXPIRY};
use crate::offers::invoice::{BlindedPayInfo, Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, InvoiceBuilder};
use crate::offers::invoice_error::InvoiceError;
use crate::offers::merkle::SignError;
use crate::offers::offer::{DerivedMetadata, Offer, OfferBuilder};
use crate::offers::parse::Bolt12SemanticError;
use crate::offers::refund::{Refund, RefundBuilder};
use crate::onion_message::{Destination, OffersMessage, PendingOnionMessage};
use crate::onion_message::{Destination, OffersMessage, OffersMessageHandler, PendingOnionMessage};
use crate::sign::{EntropySource, KeysManager, NodeSigner, Recipient, SignerProvider, WriteableEcdsaChannelSigner};
use crate::util::config::{UserConfig, ChannelConfig, ChannelConfigUpdate};
use crate::util::wakers::{Future, Notifier};
Expand Down Expand Up @@ -3579,6 +3581,17 @@ where
self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata);
}

pub(super) fn send_payment_for_bolt12_invoice(&self, invoice: &Bolt12Invoice, payment_id: PaymentId) -> Result<(), Bolt12PaymentError> {
let best_block_height = self.best_block.read().unwrap().height();
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
self.pending_outbound_payments
.send_payment_for_bolt12_invoice(
invoice, payment_id, &self.router, self.list_usable_channels(),
|| self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer,
best_block_height, &self.logger, &self.pending_events,
|args| self.send_payment_along_path(args)
)
}

/// Signals that no further attempts for the given payment should occur. Useful if you have a
/// pending outbound payment with retries remaining, but wish to stop retrying the payment before
Expand Down Expand Up @@ -8809,6 +8822,127 @@ where
}
}

impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
OffersMessageHandler for ChannelManager<M, T, ES, NS, SP, F, R, L>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
T::Target: BroadcasterInterface,
ES::Target: EntropySource,
NS::Target: NodeSigner,
SP::Target: SignerProvider,
F::Target: FeeEstimator,
R::Target: Router,
L::Target: Logger,
{
fn handle_message(&self, message: OffersMessage) -> Option<OffersMessage> {
let secp_ctx = &self.secp_ctx;
let expanded_key = &self.inbound_payment_key;

match message {
OffersMessage::InvoiceRequest(invoice_request) => {
let amount_msats = match InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(
&invoice_request
) {
Ok(amount_msats) => Some(amount_msats),
Err(error) => return Some(OffersMessage::InvoiceError(error.into())),
};
let invoice_request = match invoice_request.verify(expanded_key, secp_ctx) {
Ok(invoice_request) => invoice_request,
Err(()) => {
let error = Bolt12SemanticError::InvalidMetadata;
return Some(OffersMessage::InvoiceError(error.into()));
},
};
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;

match self.create_inbound_payment(amount_msats, relative_expiry, None) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not important for right now, but in the future are we going to want to support user-provided payment preimages for BOLT 12 invoices? 🤔

Ok((payment_hash, payment_secret)) if invoice_request.keys.is_some() => {
let payment_paths = vec![
self.create_one_hop_blinded_payment_path(payment_secret),
];
#[cfg(not(feature = "no-std"))]
let builder = invoice_request.respond_using_derived_keys(
payment_paths, payment_hash
);
#[cfg(feature = "no-std")]
let created_at = Duration::from_secs(
self.highest_seen_timestamp.load(Ordering::Acquire) as u64
);
#[cfg(feature = "no-std")]
let builder = invoice_request.respond_using_derived_keys_no_std(
payment_paths, payment_hash, created_at
);
match builder.and_then(|b| b.allow_mpp().build_and_sign(secp_ctx)) {
Ok(invoice) => Some(OffersMessage::Invoice(invoice)),
Err(error) => Some(OffersMessage::InvoiceError(error.into())),
}
},
Ok((payment_hash, payment_secret)) => {
let payment_paths = vec![
self.create_one_hop_blinded_payment_path(payment_secret),
];
#[cfg(not(feature = "no-std"))]
let builder = invoice_request.respond_with(payment_paths, payment_hash);
#[cfg(feature = "no-std")]
let created_at = Duration::from_secs(
self.highest_seen_timestamp.load(Ordering::Acquire) as u64
);
#[cfg(feature = "no-std")]
let builder = invoice_request.respond_with_no_std(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we be checking for an expired offer based on highest_seen_timestamp here? Sorry if I missed a discussion about this. Seems we won't check for expired offer otherwise in no-std?

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, good catch. I've add a no-std check in the builder using created_at, so we'll refuse to build the invoice entirely.

payment_paths, payment_hash, created_at
);
let response = builder.and_then(|builder| builder.allow_mpp().build())
.map_err(|e| OffersMessage::InvoiceError(e.into()))
.and_then(|invoice|
match invoice.sign(|invoice| self.node_signer.sign_bolt12_invoice(invoice)) {
Ok(invoice) => Ok(OffersMessage::Invoice(invoice)),
Err(SignError::Signing(())) => Err(OffersMessage::InvoiceError(
InvoiceError::from_str("Failed signing invoice")
)),
Err(SignError::Verification(_)) => Err(OffersMessage::InvoiceError(
InvoiceError::from_str("Failed invoice signature verification")
)),
});
match response {
Ok(invoice) => Some(invoice),
Err(error) => Some(error),
}
},
Err(()) => {
Some(OffersMessage::InvoiceError(Bolt12SemanticError::InvalidAmount.into()))
},
}
},
OffersMessage::Invoice(invoice) => {
match invoice.verify(expanded_key, secp_ctx) {
Err(()) => {
Some(OffersMessage::InvoiceError(InvoiceError::from_str("Unrecognized invoice")))
},
Ok(_) if invoice.invoice_features().requires_unknown_bits_from(&self.bolt12_invoice_features()) => {
Some(OffersMessage::InvoiceError(Bolt12SemanticError::UnknownRequiredFeatures.into()))
},
Ok(payment_id) => {
if let Err(e) = self.send_payment_for_bolt12_invoice(&invoice, payment_id) {
log_trace!(self.logger, "Failed paying invoice: {:?}", e);
Some(OffersMessage::InvoiceError(InvoiceError::from_str(&format!("{:?}", e))))
} else {
None
}
},
}
},
OffersMessage::InvoiceError(invoice_error) => {
log_trace!(self.logger, "Received invoice_error: {}", invoice_error);
None
},
}
}

fn release_pending_messages(&self) -> Vec<PendingOnionMessage<OffersMessage>> {
core::mem::take(&mut self.pending_offers_messages.lock().unwrap())
}
}

/// Fetches the set of [`NodeFeatures`] flags that are provided by or required by
/// [`ChannelManager`].
pub(crate) fn provided_node_features(config: &UserConfig) -> NodeFeatures {
Expand Down
8 changes: 5 additions & 3 deletions lightning/src/offers/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> {
invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
created_at: Duration, payment_hash: PaymentHash
) -> Result<Self, Bolt12SemanticError> {
let amount_msats = Self::check_amount_msats(invoice_request)?;
let amount_msats = Self::amount_msats(invoice_request)?;
let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey();
let contents = InvoiceContents::ForOffer {
invoice_request: invoice_request.contents.clone(),
Expand Down Expand Up @@ -207,7 +207,7 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
created_at: Duration, payment_hash: PaymentHash, keys: KeyPair
) -> Result<Self, Bolt12SemanticError> {
let amount_msats = Self::check_amount_msats(invoice_request)?;
let amount_msats = Self::amount_msats(invoice_request)?;
let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey();
let contents = InvoiceContents::ForOffer {
invoice_request: invoice_request.contents.clone(),
Expand Down Expand Up @@ -237,7 +237,9 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
}

impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> {
fn check_amount_msats(invoice_request: &InvoiceRequest) -> Result<u64, Bolt12SemanticError> {
pub(crate) fn amount_msats(
invoice_request: &InvoiceRequest
) -> Result<u64, Bolt12SemanticError> {
match invoice_request.amount_msats() {
Some(amount_msats) => Ok(amount_msats),
None => match invoice_request.contents.inner.offer.amount() {
Expand Down
10 changes: 10 additions & 0 deletions lightning/src/offers/invoice_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ pub struct ErroneousField {
pub suggested_value: Option<Vec<u8>>,
}

impl InvoiceError {
/// Creates an [`InvoiceError`] with the given message.
pub fn from_str(s: &str) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should just take the String - in one call we format!() create a String, then pass by reference here, and end up cloning it.

Self {
erroneous_field: None,
message: UntrustedString(s.to_string()),
}
}
}

impl core::fmt::Display for InvoiceError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
self.message.fmt(f)
Expand Down