Skip to content

Commit 8367e02

Browse files
committed
Include InvoiceRequest fields in PaymentContext
When receiving a payment, it's useful to know information about the InvoiceRequest. Include this data in PaymentContext::Bolt12Offer so users can display information about an inbound payment (e.g., the payer note).
1 parent b06b2c7 commit 8367e02

File tree

4 files changed

+176
-9
lines changed

4 files changed

+176
-9
lines changed

lightning/src/blinded_path/payment.rs

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::ln::channelmanager::CounterpartyForwardingInfo;
1212
use crate::ln::features::BlindedHopFeatures;
1313
use crate::ln::msgs::DecodeError;
1414
use crate::offers::invoice::BlindedPayInfo;
15+
use crate::offers::invoice_request::InvoiceRequestFields;
1516
use crate::offers::offer::OfferId;
1617
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};
1718

@@ -140,6 +141,12 @@ pub struct Bolt12OfferContext {
140141
///
141142
/// [`Offer`]: crate::offers::offer::Offer
142143
pub offer_id: OfferId,
144+
145+
/// Fields from an [`InvoiceRequest`] sent for a [`Bolt12Invoice`].
146+
///
147+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
148+
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
149+
pub invoice_request: InvoiceRequestFields,
143150
}
144151

145152
/// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`].
@@ -409,6 +416,7 @@ impl Readable for UnknownPaymentContext {
409416

410417
impl_writeable_tlv_based!(Bolt12OfferContext, {
411418
(0, offer_id, required),
419+
(2, invoice_request, required),
412420
});
413421

414422
impl_writeable_tlv_based!(Bolt12RefundContext, {});

lightning/src/ln/channelmanager.rs

+1
Original file line numberDiff line numberDiff line change
@@ -10367,6 +10367,7 @@ where
1036710367

1036810368
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
1036910369
offer_id: invoice_request.offer_id,
10370+
invoice_request: invoice_request.fields(),
1037010371
});
1037110372
let payment_paths = match self.create_blinded_payment_paths(
1037210373
amount_msats, payment_secret, payment_context

lightning/src/ln/offers_tests.rs

+34-4
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ use crate::blinded_path::{BlindedPath, IntroductionNode};
4646
use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext};
4747
use crate::events::{Event, MessageSendEventsProvider, PaymentPurpose};
4848
use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, Retry, self};
49+
use crate::ln::features::InvoiceRequestFeatures;
4950
use crate::ln::functional_test_utils::*;
5051
use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement};
5152
use crate::offers::invoice::Bolt12Invoice;
5253
use crate::offers::invoice_error::InvoiceError;
53-
use crate::offers::invoice_request::InvoiceRequest;
54+
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields};
5455
use crate::offers::parse::Bolt12SemanticError;
5556
use crate::onion_message::messenger::PeeledOnion;
5657
use crate::onion_message::offers::OffersMessage;
@@ -385,7 +386,6 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
385386
.unwrap()
386387
.amount_msats(10_000_000)
387388
.build().unwrap();
388-
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() });
389389
assert_ne!(offer.signing_pubkey(), alice_id);
390390
assert!(!offer.paths().is_empty());
391391
for path in offer.paths() {
@@ -408,6 +408,16 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
408408
alice.onion_messenger.handle_onion_message(&bob_id, &onion_message);
409409

410410
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
411+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
412+
offer_id: offer.id(),
413+
invoice_request: InvoiceRequestFields {
414+
payer_id: invoice_request.payer_id(),
415+
amount_msats: None,
416+
features: InvoiceRequestFeatures::empty(),
417+
quantity: None,
418+
payer_note_truncated: None,
419+
},
420+
});
411421
assert_eq!(invoice_request.amount_msats(), None);
412422
assert_ne!(invoice_request.payer_id(), david_id);
413423
assert_eq!(reply_path.unwrap().introduction_node, IntroductionNode::NodeId(charlie_id));
@@ -537,7 +547,6 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
537547
.create_offer_builder("coffee".to_string()).unwrap()
538548
.amount_msats(10_000_000)
539549
.build().unwrap();
540-
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() });
541550
assert_ne!(offer.signing_pubkey(), alice_id);
542551
assert!(!offer.paths().is_empty());
543552
for path in offer.paths() {
@@ -552,6 +561,16 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
552561
alice.onion_messenger.handle_onion_message(&bob_id, &onion_message);
553562

554563
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
564+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
565+
offer_id: offer.id(),
566+
invoice_request: InvoiceRequestFields {
567+
payer_id: invoice_request.payer_id(),
568+
amount_msats: None,
569+
features: InvoiceRequestFeatures::empty(),
570+
quantity: None,
571+
payer_note_truncated: None,
572+
},
573+
});
555574
assert_eq!(invoice_request.amount_msats(), None);
556575
assert_ne!(invoice_request.payer_id(), bob_id);
557576
assert_eq!(reply_path.unwrap().introduction_node, IntroductionNode::NodeId(bob_id));
@@ -653,7 +672,6 @@ fn pays_for_offer_without_blinded_paths() {
653672
.clear_paths()
654673
.amount_msats(10_000_000)
655674
.build().unwrap();
656-
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() });
657675
assert_eq!(offer.signing_pubkey(), alice_id);
658676
assert!(offer.paths().is_empty());
659677

@@ -664,6 +682,18 @@ fn pays_for_offer_without_blinded_paths() {
664682
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
665683
alice.onion_messenger.handle_onion_message(&bob_id, &onion_message);
666684

685+
let (invoice_request, _) = extract_invoice_request(alice, &onion_message);
686+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
687+
offer_id: offer.id(),
688+
invoice_request: InvoiceRequestFields {
689+
payer_id: invoice_request.payer_id(),
690+
amount_msats: None,
691+
features: InvoiceRequestFeatures::empty(),
692+
quantity: None,
693+
payer_note_truncated: None,
694+
},
695+
});
696+
667697
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
668698
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);
669699

lightning/src/offers/invoice_request.rs

+133-5
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferT
7676
use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError};
7777
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
7878
use crate::offers::signer::{Metadata, MetadataMaterial};
79-
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
80-
use crate::util::string::PrintableString;
79+
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, SeekReadable, WithoutLength, Writeable, Writer};
80+
use crate::util::string::{PrintableString, UntrustedString};
8181

8282
#[cfg(not(c_bindings))]
8383
use {
@@ -872,6 +872,24 @@ impl VerifiedInvoiceRequest {
872872
invoice_request_respond_with_derived_signing_pubkey_methods!(self, self.inner, InvoiceBuilder<DerivedSigningPubkey>);
873873
#[cfg(c_bindings)]
874874
invoice_request_respond_with_derived_signing_pubkey_methods!(self, self.inner, InvoiceWithDerivedSigningPubkeyBuilder);
875+
876+
pub(crate) fn fields(&self) -> InvoiceRequestFields {
877+
let InvoiceRequestContents {
878+
payer_id,
879+
inner: InvoiceRequestContentsWithoutPayerId {
880+
payer: _, offer: _, chain: _, amount_msats, features, quantity, payer_note
881+
},
882+
} = &self.inner.contents;
883+
884+
InvoiceRequestFields {
885+
payer_id: *payer_id,
886+
amount_msats: *amount_msats,
887+
features: features.clone(),
888+
quantity: *quantity,
889+
payer_note_truncated: payer_note.clone()
890+
.map(|mut s| { s.truncate(PAYER_NOTE_LIMIT); UntrustedString(s) }),
891+
}
892+
}
875893
}
876894

877895
impl InvoiceRequestContents {
@@ -1100,9 +1118,68 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
11001118
}
11011119
}
11021120

1121+
/// Fields sent in an [`InvoiceRequest`] message to include in [`PaymentContext::Bolt12Offer`].
1122+
///
1123+
/// [`PaymentContext::Bolt12Offer`]: crate::blinded_path::payment::PaymentContext::Bolt12Offer
1124+
#[derive(Clone, Debug, Eq, PartialEq)]
1125+
pub struct InvoiceRequestFields {
1126+
/// A possibly transient pubkey used to sign the invoice request.
1127+
pub payer_id: PublicKey,
1128+
1129+
/// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which
1130+
/// must be greater than or equal to [`Offer::amount`], converted if necessary.
1131+
///
1132+
/// [`chain`]: InvoiceRequest::chain
1133+
pub amount_msats: Option<u64>,
1134+
1135+
/// Features pertaining to requesting an invoice.
1136+
pub features: InvoiceRequestFeatures,
1137+
1138+
/// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`].
1139+
pub quantity: Option<u64>,
1140+
1141+
/// A payer-provided note which will be seen by the recipient and reflected back in the invoice
1142+
/// response. Truncated to [`PAYER_NOTE_LIMIT`] characters.
1143+
pub payer_note_truncated: Option<UntrustedString>,
1144+
}
1145+
1146+
/// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`].
1147+
pub const PAYER_NOTE_LIMIT: usize = 512;
1148+
1149+
impl Writeable for InvoiceRequestFields {
1150+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
1151+
write_tlv_fields!(writer, {
1152+
(0, self.payer_id, required),
1153+
(2, self.amount_msats.map(|v| HighZeroBytesDroppedBigSize(v)), option),
1154+
(4, WithoutLength(&self.features), required),
1155+
(6, self.quantity.map(|v| HighZeroBytesDroppedBigSize(v)), option),
1156+
(8, self.payer_note_truncated.as_ref().map(|s| WithoutLength(&s.0)), option),
1157+
});
1158+
Ok(())
1159+
}
1160+
}
1161+
1162+
impl Readable for InvoiceRequestFields {
1163+
fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
1164+
_init_and_read_len_prefixed_tlv_fields!(reader, {
1165+
(0, payer_id, required),
1166+
(2, amount_msats, (option, encoding: (u64, HighZeroBytesDroppedBigSize))),
1167+
(4, features, (option, encoding: (InvoiceRequestFeatures, WithoutLength))),
1168+
(6, quantity, (option, encoding: (u64, HighZeroBytesDroppedBigSize))),
1169+
(8, payer_note_truncated, (option, encoding: (String, WithoutLength))),
1170+
});
1171+
let features = features.unwrap_or(InvoiceRequestFeatures::empty());
1172+
1173+
Ok(InvoiceRequestFields {
1174+
payer_id: payer_id.0.unwrap(), amount_msats, features, quantity,
1175+
payer_note_truncated: payer_note_truncated.map(|s| UntrustedString(s)),
1176+
})
1177+
}
1178+
}
1179+
11031180
#[cfg(test)]
11041181
mod tests {
1105-
use super::{InvoiceRequest, InvoiceRequestTlvStreamRef, SIGNATURE_TAG, UnsignedInvoiceRequest};
1182+
use super::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest};
11061183

11071184
use bitcoin::blockdata::constants::ChainHash;
11081185
use bitcoin::network::constants::Network;
@@ -1129,8 +1206,8 @@ mod tests {
11291206
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
11301207
use crate::offers::payer::PayerTlvStreamRef;
11311208
use crate::offers::test_utils::*;
1132-
use crate::util::ser::{BigSize, Writeable};
1133-
use crate::util::string::PrintableString;
1209+
use crate::util::ser::{BigSize, Readable, Writeable};
1210+
use crate::util::string::{PrintableString, UntrustedString};
11341211

11351212
#[test]
11361213
fn builds_invoice_request_with_defaults() {
@@ -2166,4 +2243,55 @@ mod tests {
21662243
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
21672244
}
21682245
}
2246+
2247+
#[test]
2248+
fn copies_verified_invoice_request_fields() {
2249+
let desc = "foo".to_string();
2250+
let node_id = recipient_pubkey();
2251+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
2252+
let entropy = FixedEntropy {};
2253+
let secp_ctx = Secp256k1::new();
2254+
2255+
#[cfg(c_bindings)]
2256+
use crate::offers::offer::OfferWithDerivedMetadataBuilder as OfferBuilder;
2257+
let offer = OfferBuilder
2258+
::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
2259+
.chain(Network::Testnet)
2260+
.amount_msats(1000)
2261+
.supported_quantity(Quantity::Unbounded)
2262+
.build().unwrap();
2263+
assert_eq!(offer.signing_pubkey(), node_id);
2264+
2265+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
2266+
.chain(Network::Testnet).unwrap()
2267+
.amount_msats(1001).unwrap()
2268+
.quantity(1).unwrap()
2269+
.payer_note("0".repeat(PAYER_NOTE_LIMIT * 2))
2270+
.build().unwrap()
2271+
.sign(payer_sign).unwrap();
2272+
match invoice_request.verify(&expanded_key, &secp_ctx) {
2273+
Ok(invoice_request) => {
2274+
let fields = invoice_request.fields();
2275+
assert_eq!(invoice_request.offer_id, offer.id());
2276+
assert_eq!(
2277+
fields,
2278+
InvoiceRequestFields {
2279+
payer_id: payer_pubkey(),
2280+
amount_msats: Some(1001),
2281+
features: InvoiceRequestFeatures::empty(),
2282+
quantity: Some(1),
2283+
payer_note_truncated: Some(UntrustedString("0".repeat(PAYER_NOTE_LIMIT))),
2284+
}
2285+
);
2286+
2287+
let mut buffer = Vec::new();
2288+
fields.write(&mut buffer).unwrap();
2289+
2290+
let deserialized_fields: InvoiceRequestFields =
2291+
Readable::read(&mut buffer.as_slice()).unwrap();
2292+
assert_eq!(deserialized_fields, fields);
2293+
},
2294+
Err(_) => panic!("unexpected error"),
2295+
}
2296+
}
21692297
}

0 commit comments

Comments
 (0)