Skip to content

Commit e6b04c5

Browse files
committed
Introduce CurrencyConversion in InvoiceRequest builder
To support currency-denominated Offers, the InvoiceRequest builder needs to resolve the Offer amount at multiple points during construction. This occurs when explicitly setting `amount_msats` and again when the InvoiceRequest is finalized via `build()`. To avoid repeatedly passing a `CurrencyConversion` implementation into these checks, the builder now stores a reference to it at creation time. This allows the builder to resolve currency-denominated Offer amounts whenever validation requires it. As part of this change, `InvoiceRequest::amount_msats()` is updated to use the provided `CurrencyConversion` to resolve the underlying Offer amount when necessary.
1 parent c9ccdab commit e6b04c5

11 files changed

Lines changed: 412 additions & 224 deletions

File tree

fuzz/src/offer_deser.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ use bitcoin::secp256k1::Secp256k1;
1212
use core::convert::TryFrom;
1313
use lightning::ln::channelmanager::PaymentId;
1414
use lightning::ln::inbound_payment::ExpandedKey;
15+
use lightning::offers::currency::DefaultCurrencyConversion;
1516
use lightning::offers::invoice_request::InvoiceRequest;
1617
use lightning::offers::nonce::Nonce;
17-
use lightning::offers::offer::{Amount, Offer, Quantity};
18+
use lightning::offers::offer::{Offer, Quantity};
1819
use lightning::offers::parse::Bolt12SemanticError;
1920
use lightning::sign::EntropySource;
2021
use lightning::util::ser::Writeable;
@@ -48,13 +49,13 @@ fn build_request(offer: &Offer) -> Result<InvoiceRequest, Bolt12SemanticError> {
4849
let nonce = Nonce::from_entropy_source(&entropy);
4950
let secp_ctx = Secp256k1::new();
5051
let payment_id = PaymentId([1; 32]);
52+
let conversion = DefaultCurrencyConversion;
5153

5254
let mut builder = offer.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)?;
5355

5456
builder = match offer.amount() {
5557
None => builder.amount_msats(1000).unwrap(),
56-
Some(Amount::Bitcoin { amount_msats }) => builder.amount_msats(amount_msats + 1)?,
57-
Some(Amount::Currency { .. }) => return Err(Bolt12SemanticError::UnsupportedCurrency),
58+
Some(amount) => builder.amount_msats(amount.into_msats(&conversion)?)?,
5859
};
5960

6061
builder = match offer.supported_quantity() {

lightning/src/ln/async_payments_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ use crate::types::features::Bolt12InvoiceFeatures;
6060
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
6161
use crate::util::config::{HTLCInterceptionFlags, UserConfig};
6262
use crate::util::ser::Writeable;
63+
use crate::util::test_utils::TestCurrencyConversion;
6364
use bitcoin::constants::ChainHash;
6465
use bitcoin::network::Network;
6566
use bitcoin::secp256k1;
@@ -1448,6 +1449,8 @@ fn amount_doesnt_match_invreq() {
14481449

14491450
let amt_msat = 5000;
14501451
let payment_id = PaymentId([1; 32]);
1452+
let conversion = TestCurrencyConversion;
1453+
14511454
nodes[0].node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap();
14521455
let release_held_htlc_om_3_0 = pass_async_payments_oms(
14531456
static_invoice,
@@ -1471,6 +1474,7 @@ fn amount_doesnt_match_invreq() {
14711474
Nonce::from_entropy_source(nodes[0].keys_manager),
14721475
&secp_ctx,
14731476
payment_id,
1477+
&conversion,
14741478
)
14751479
.unwrap()
14761480
.amount_msats(amt_msat + 1)

lightning/src/ln/channelmanager.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8538,12 +8538,26 @@ impl<
85388538
});
85398539
let verified_invreq = match verify_opt {
85408540
Some(verified_invreq) => {
8541-
if let Some(invreq_amt_msat) =
8542-
verified_invreq.amount_msats()
8541+
match verified_invreq
8542+
.amount_msats(&self.flow.currency_conversion)
85438543
{
8544-
if payment_data.total_msat < invreq_amt_msat {
8544+
Ok(invreq_amt_msat) => {
8545+
if payment_data.total_msat < invreq_amt_msat {
8546+
fail_htlc!(claimable_htlc, payment_hash);
8547+
}
8548+
},
8549+
Err(_) => {
8550+
// `amount_msats()` can only fail if the invoice request does not specify an amount
8551+
// and the underlying offer's amount cannot be resolved.
8552+
//
8553+
// This invoice request corresponds to an offer we constructed, and we only allow
8554+
// creating offers with currency amounts that the node explicitly supports.
8555+
//
8556+
// Therefore, amount resolution must succeed here. Reaching this branch indicates
8557+
// an internal logic error.
8558+
debug_assert!(false);
85458559
fail_htlc!(claimable_htlc, payment_hash);
8546-
}
8560+
},
85478561
}
85488562
verified_invreq
85498563
},
@@ -14679,10 +14693,12 @@ impl<
1467914693
None => builder,
1468014694
Some(quantity) => builder.quantity(quantity)?,
1468114695
};
14696+
1468214697
let builder = match amount_msats {
1468314698
None => builder,
1468414699
Some(amount_msats) => builder.amount_msats(amount_msats)?,
1468514700
};
14701+
1468614702
let builder = match payer_note {
1468714703
None => builder,
1468814704
Some(payer_note) => builder.payer_note(payer_note),

lightning/src/ln/offers_tests.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ use crate::util::ser::Writeable;
7373
const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24);
7474

7575
use crate::prelude::*;
76+
use crate::util::test_utils::TestCurrencyConversion;
7677

7778
macro_rules! expect_recent_payment {
7879
($node: expr, $payment_state: path, $payment_id: expr) => {{
@@ -517,12 +518,14 @@ fn check_dummy_hop_pattern_in_offer() {
517518
}
518519

519520
let payment_id = PaymentId([1; 32]);
521+
let conversion = TestCurrencyConversion;
522+
520523
bob.node.pay_for_offer(&compact_offer, None, payment_id, Default::default()).unwrap();
521524

522525
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
523526
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
524527

525-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
528+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
526529
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
527530
assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH));
528531

@@ -544,7 +547,7 @@ fn check_dummy_hop_pattern_in_offer() {
544547
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
545548
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
546549

547-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
550+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
548551
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
549552
assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH));
550553
}
@@ -706,6 +709,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
706709
}
707710

708711
let payment_id = PaymentId([1; 32]);
712+
let conversion = TestCurrencyConversion;
713+
709714
david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
710715
expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id);
711716

@@ -729,7 +734,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
729734
human_readable_name: None,
730735
},
731736
});
732-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
737+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
733738
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
734739
assert!(check_dummy_hopped_path_length(&reply_path, bob, charlie_id, DUMMY_HOPS_PATH_LENGTH));
735740

@@ -871,6 +876,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
871876
}
872877

873878
let payment_id = PaymentId([1; 32]);
879+
let conversion = TestCurrencyConversion;
874880
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
875881
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
876882

@@ -887,7 +893,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
887893
human_readable_name: None,
888894
},
889895
});
890-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
896+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
891897
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
892898
assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH));
893899

@@ -1253,6 +1259,7 @@ fn creates_and_pays_for_offer_with_retry() {
12531259
assert!(check_compact_path_introduction_node(&path, bob, alice_id));
12541260
}
12551261
let payment_id = PaymentId([1; 32]);
1262+
let conversion = TestCurrencyConversion;
12561263
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
12571264
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
12581265

@@ -1276,7 +1283,7 @@ fn creates_and_pays_for_offer_with_retry() {
12761283
human_readable_name: None,
12771284
},
12781285
});
1279-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
1286+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
12801287
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
12811288
assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH));
12821289
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
@@ -1576,6 +1583,8 @@ fn fails_authentication_when_handling_invoice_request() {
15761583

15771584
// Send the invoice request directly to Alice instead of using a blinded path.
15781585
let payment_id = PaymentId([1; 32]);
1586+
let conversion = TestCurrencyConversion;
1587+
15791588
david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
15801589
expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id);
15811590

@@ -1590,7 +1599,7 @@ fn fails_authentication_when_handling_invoice_request() {
15901599
alice.onion_messenger.handle_onion_message(david_id, &onion_message);
15911600

15921601
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
1593-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
1602+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
15941603
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
15951604
assert!(check_dummy_hopped_path_length(&reply_path, david, charlie_id, DUMMY_HOPS_PATH_LENGTH));
15961605

@@ -1619,7 +1628,7 @@ fn fails_authentication_when_handling_invoice_request() {
16191628
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
16201629

16211630
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
1622-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
1631+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
16231632
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
16241633
assert!(check_dummy_hopped_path_length(&reply_path, david, charlie_id, DUMMY_HOPS_PATH_LENGTH));
16251634

@@ -1693,6 +1702,8 @@ fn fails_authentication_when_handling_invoice_for_offer() {
16931702
};
16941703

16951704
let payment_id = PaymentId([2; 32]);
1705+
let conversion = TestCurrencyConversion;
1706+
16961707
david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
16971708
expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id);
16981709

@@ -1719,7 +1730,7 @@ fn fails_authentication_when_handling_invoice_for_offer() {
17191730
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
17201731

17211732
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
1722-
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
1733+
assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000));
17231734
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
17241735
assert!(check_dummy_hopped_path_length(&reply_path, david, charlie_id, DUMMY_HOPS_PATH_LENGTH));
17251736

@@ -1973,6 +1984,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() {
19731984
.create_offer_builder().unwrap()
19741985
.clear_chains()
19751986
.chain(Network::Signet)
1987+
.amount_msats(1_000).unwrap()
19761988
.build();
19771989

19781990
match bob.node.pay_for_offer(&offer, None, PaymentId([1; 32]), Default::default()) {

lightning/src/ln/outbound_payment.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,7 +2932,7 @@ mod tests {
29322932
use crate::util::errors::APIError;
29332933
use crate::util::hash_tables::new_hash_map;
29342934
use crate::util::logger::WithContext;
2935-
use crate::util::test_utils;
2935+
use crate::util::test_utils::{self, TestCurrencyConversion};
29362936

29372937
use alloc::collections::VecDeque;
29382938

@@ -3295,6 +3295,7 @@ mod tests {
32953295
let pending_events = Mutex::new(VecDeque::new());
32963296
let outbound_payments = OutboundPayments::new(new_hash_map());
32973297
let payment_id = PaymentId([0; 32]);
3298+
let conversion = TestCurrencyConversion;
32983299
let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100));
32993300

33003301
assert!(
@@ -3308,7 +3309,7 @@ mod tests {
33083309
let invoice = OfferBuilder::new(recipient_pubkey())
33093310
.amount_msats(1000).unwrap()
33103311
.build()
3311-
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
3312+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap()
33123313
.build_and_sign().unwrap()
33133314
.respond_with_no_std(payment_paths(), payment_hash(), created_at).unwrap()
33143315
.build().unwrap()
@@ -3352,12 +3353,13 @@ mod tests {
33523353
let expanded_key = ExpandedKey::new([42; 32]);
33533354
let nonce = Nonce([0; 16]);
33543355
let payment_id = PaymentId([0; 32]);
3356+
let conversion = TestCurrencyConversion;
33553357
let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100));
33563358

33573359
let invoice = OfferBuilder::new(recipient_pubkey())
33583360
.amount_msats(1000).unwrap()
33593361
.build()
3360-
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
3362+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap()
33613363
.build_and_sign().unwrap()
33623364
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
33633365
.build().unwrap()
@@ -3417,12 +3419,13 @@ mod tests {
34173419
let expanded_key = ExpandedKey::new([42; 32]);
34183420
let nonce = Nonce([0; 16]);
34193421
let payment_id = PaymentId([0; 32]);
3422+
let conversion = TestCurrencyConversion;
34203423
let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100));
34213424

34223425
let invoice = OfferBuilder::new(recipient_pubkey())
34233426
.amount_msats(1000).unwrap()
34243427
.build()
3425-
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
3428+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap()
34263429
.build_and_sign().unwrap()
34273430
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
34283431
.build().unwrap()
@@ -3507,11 +3510,12 @@ mod tests {
35073510
let nonce = Nonce::from_entropy_source(&entropy);
35083511
let secp_ctx = Secp256k1::new();
35093512
let payment_id = PaymentId([1; 32]);
3513+
let conversion = TestCurrencyConversion;
35103514

35113515
OfferBuilder::new(recipient_pubkey())
35123516
.amount_msats(1000).unwrap()
35133517
.build()
3514-
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
3518+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion)
35153519
.unwrap()
35163520
.build_and_sign()
35173521
.unwrap()

lightning/src/offers/flow.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -833,12 +833,19 @@ impl<MR: MessageRouter, CC: CurrencyConversion, L: Logger> OffersMessageFlow<MR,
833833
/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
834834
pub fn create_invoice_request_builder<'a>(
835835
&'a self, offer: &'a Offer, nonce: Nonce, payment_id: PaymentId,
836-
) -> Result<InvoiceRequestBuilder<'a, 'a, secp256k1::All>, Bolt12SemanticError> {
836+
) -> Result<InvoiceRequestBuilder<'a, 'a, secp256k1::All, CC>, Bolt12SemanticError> {
837837
let expanded_key = &self.inbound_payment_key;
838838
let secp_ctx = &self.secp_ctx;
839+
let conversion = &self.currency_conversion;
840+
841+
let builder: InvoiceRequestBuilder<'a, 'a, secp256k1::All, CC> =
842+
offer.request_invoice(expanded_key, nonce, secp_ctx, payment_id, conversion)?.into();
843+
844+
let builder = match offer.resolve_offer_amount(conversion)? {
845+
None => builder,
846+
Some(amount_msats) => builder.amount_msats(amount_msats)?,
847+
};
839848

840-
let builder: InvoiceRequestBuilder<secp256k1::All> =
841-
offer.request_invoice(expanded_key, nonce, secp_ctx, payment_id)?.into();
842849
let builder = builder.chain_hash(self.chain_hash)?;
843850

844851
Ok(builder)

0 commit comments

Comments
 (0)