Skip to content

Commit 29c9055

Browse files
shaavancodex
andcommitted
[test] cover currency-denominated invoice verification paths
Add payer-side tests for currency-denominated offers and invoices. Cover the standard payment flow, rejection of excessive invoices, handling of unverifiable fiat invoices, quantity-scaled invoice requests, and async static-invoice payments. Together these tests exercise the invoice amount verification paths introduced for currency-denominated offers. AI-assisted: Planned and wrote focused test coverage Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent b6cdd9b commit 29c9055

3 files changed

Lines changed: 298 additions & 2 deletions

File tree

lightning/src/ln/async_payments_tests.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use crate::offers::flow::{
4444
};
4545
use crate::offers::invoice_request::InvoiceRequest;
4646
use crate::offers::nonce::Nonce;
47-
use crate::offers::offer::{Amount, Offer};
47+
use crate::offers::offer::{Amount, CurrencyCode, Offer};
4848
use crate::offers::static_invoice::{
4949
StaticInvoice, StaticInvoiceBuilder,
5050
DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY,
@@ -63,6 +63,7 @@ use crate::types::features::Bolt12InvoiceFeatures;
6363
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
6464
use crate::util::config::{HTLCInterceptionFlags, UserConfig};
6565
use crate::util::ser::Writeable;
66+
use crate::util::test_utils::TestCurrencyConversion;
6667
use bitcoin::constants::ChainHash;
6768
use bitcoin::network::Network;
6869
use bitcoin::secp256k1;
@@ -1168,6 +1169,127 @@ fn async_receive_flow_success() {
11681169
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
11691170
}
11701171

1172+
#[test]
1173+
fn async_receive_flow_uses_currency_conversion() {
1174+
// Test that an often-offline recipient's currency-denominated async offer is paid using the
1175+
// sender's configured currency conversion when the sender receives the static invoice.
1176+
let secp_ctx = Secp256k1::new();
1177+
let chanmon_cfgs = create_chanmon_cfgs(3);
1178+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
1179+
1180+
let mut allow_priv_chan_fwds_cfg = test_default_channel_config();
1181+
allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true;
1182+
let node_chanmgrs =
1183+
create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]);
1184+
1185+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
1186+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
1187+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
1188+
1189+
let sender = &nodes[0];
1190+
let always_online_node = &nodes[1];
1191+
let async_recipient = &nodes[2];
1192+
let sender_id = sender.node.get_our_node_id();
1193+
let always_online_node_id = always_online_node.node.get_our_node_id();
1194+
let async_recipient_id = async_recipient.node.get_our_node_id();
1195+
1196+
let blinded_paths_to_always_online_node = always_online_node
1197+
.message_router
1198+
.create_blinded_paths(
1199+
always_online_node_id,
1200+
always_online_node.keys_manager.get_receive_auth_key(),
1201+
MessageContext::Offers(OffersContext::InvoiceRequest {
1202+
nonce: Nonce([42; 16]),
1203+
payment_metadata: None,
1204+
}),
1205+
Vec::new(),
1206+
&secp_ctx,
1207+
)
1208+
.unwrap();
1209+
let (offer_builder, nonce) = async_recipient
1210+
.node
1211+
.flow
1212+
.create_async_receive_offer_builder(
1213+
async_recipient.keys_manager,
1214+
&TestCurrencyConversion {},
1215+
blinded_paths_to_always_online_node,
1216+
)
1217+
.unwrap();
1218+
let offer = offer_builder
1219+
.amount(Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 })
1220+
.build()
1221+
.unwrap();
1222+
let static_invoice = create_static_invoice_builder(async_recipient, &offer, nonce, None)
1223+
.build_and_sign(&secp_ctx)
1224+
.unwrap();
1225+
assert!(static_invoice.invoice_features().supports_basic_mpp());
1226+
1227+
let converted_amt_msat = 10_000;
1228+
let expected_first_hop_amt_msat = converted_amt_msat + 1_000;
1229+
let payment_id = PaymentId([1; 32]);
1230+
sender.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
1231+
1232+
let invreq_om =
1233+
sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap();
1234+
let (invoice_request, invreq_reply_path) =
1235+
offers_tests::extract_invoice_request(always_online_node, &invreq_om);
1236+
assert_eq!(invoice_request.amount_msats(), None);
1237+
1238+
always_online_node
1239+
.onion_messenger
1240+
.send_onion_message(
1241+
ParsedOnionMessageContents::<Infallible>::Offers(OffersMessage::StaticInvoice(
1242+
static_invoice.clone(),
1243+
)),
1244+
MessageSendInstructions::WithoutReplyPath {
1245+
destination: Destination::BlindedPath(invreq_reply_path),
1246+
},
1247+
)
1248+
.unwrap();
1249+
1250+
let static_invoice_om =
1251+
always_online_node.onion_messenger.next_onion_message_for_peer(sender_id).unwrap();
1252+
sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om);
1253+
sender.node.process_pending_htlc_forwards();
1254+
assert!(sender.node.get_and_clear_pending_msg_events().is_empty());
1255+
1256+
let held_htlc_available_om_0_1 =
1257+
sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap();
1258+
always_online_node.onion_messenger.handle_onion_message(sender_id, &held_htlc_available_om_0_1);
1259+
let held_htlc_available_om_1_2 =
1260+
always_online_node.onion_messenger.next_onion_message_for_peer(async_recipient_id).unwrap();
1261+
async_recipient
1262+
.onion_messenger
1263+
.handle_onion_message(always_online_node_id, &held_htlc_available_om_1_2);
1264+
1265+
let release_held_htlc_om =
1266+
async_recipient.onion_messenger.next_onion_message_for_peer(sender_id).unwrap();
1267+
sender.onion_messenger.handle_onion_message(async_recipient_id, &release_held_htlc_om);
1268+
1269+
let mut events = sender.node.get_and_clear_pending_msg_events();
1270+
assert_eq!(events.len(), 1);
1271+
let ev = remove_first_msg_event_to_node(&always_online_node_id, &mut events);
1272+
let payment_hash = match ev {
1273+
MessageSendEvent::UpdateHTLCs { ref updates, .. } => {
1274+
// The first-hop HTLC includes the routing fee, while the final payment
1275+
// amount remains the currency-converted offer amount.
1276+
assert_eq!(updates.update_add_htlcs[0].amount_msat, expected_first_hop_amt_msat);
1277+
updates.update_add_htlcs[0].payment_hash
1278+
},
1279+
_ => panic!(),
1280+
};
1281+
check_added_monitors(sender, 1);
1282+
1283+
let route: &[&[&Node]] = &[&[always_online_node, async_recipient]];
1284+
let args = PassAlongPathArgs::new(sender, route[0], converted_amt_msat, payment_hash, ev)
1285+
.with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]);
1286+
let claimable_ev = do_pass_along_path(args).unwrap();
1287+
let keysend_preimage = extract_payment_preimage(&claimable_ev);
1288+
let (res, _) =
1289+
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
1290+
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
1291+
}
1292+
11711293
#[test]
11721294
fn async_payment_delivers_payment_metadata() {
11731295
// Test that `payment_metadata` set in the `AsyncBolt12OfferContext` of a static invoice's

lightning/src/ln/offers_tests.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ use crate::offers::invoice::Bolt12Invoice;
6363
use crate::offers::invoice_error::InvoiceError;
6464
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer};
6565
use crate::offers::nonce::Nonce;
66-
use crate::offers::offer::OfferBuilder;
66+
use crate::offers::offer::{Amount, CurrencyCode, OfferBuilder};
6767
use crate::offers::parse::Bolt12SemanticError;
6868
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageRouter, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH};
6969
use crate::onion_message::offers::OffersMessage;
@@ -1401,6 +1401,64 @@ fn pays_bolt12_invoice_asynchronously() {
14011401
);
14021402
}
14031403

1404+
#[test]
1405+
fn creates_and_pays_bolt12_invoice_for_currency_denominated_offer() {
1406+
let chanmon_cfgs = create_chanmon_cfgs(2);
1407+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1408+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1409+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1410+
1411+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
1412+
1413+
let alice = &nodes[0];
1414+
let alice_id = alice.node.get_our_node_id();
1415+
let bob = &nodes[1];
1416+
let bob_id = bob.node.get_our_node_id();
1417+
1418+
let offer = alice.node
1419+
.create_offer_builder()
1420+
.unwrap()
1421+
.amount(Amount::Currency {
1422+
iso4217_code: CurrencyCode::new(*b"USD").unwrap(),
1423+
amount: 10,
1424+
})
1425+
.build()
1426+
.unwrap();
1427+
1428+
let payment_id = PaymentId([1; 32]);
1429+
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
1430+
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
1431+
1432+
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
1433+
let (invoice_request, _) = extract_invoice_request(alice, &onion_message);
1434+
assert_eq!(invoice_request.amount_msats(), None);
1435+
1436+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
1437+
offer_id: offer.id(),
1438+
invoice_request: InvoiceRequestFields {
1439+
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
1440+
quantity: None,
1441+
payer_note_truncated: None,
1442+
human_readable_name: None,
1443+
},
1444+
payment_metadata: None,
1445+
});
1446+
1447+
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
1448+
1449+
let invoice_onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
1450+
let (invoice, _) = extract_invoice(bob, &invoice_onion_message);
1451+
assert_eq!(invoice.amount_msats(), 10_000);
1452+
1453+
bob.onion_messenger.handle_onion_message(alice_id, &invoice_onion_message);
1454+
1455+
route_bolt12_payment(bob, &[alice], &invoice);
1456+
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
1457+
1458+
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
1459+
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
1460+
}
1461+
14041462
/// Checks that an offer can be created using an unannounced node as a blinded path's introduction
14051463
/// node. This is only preferred if there are no other options which may indicated either the offer
14061464
/// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but

lightning/src/offers/invoice.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,7 @@ mod tests {
19011901
use crate::types::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures};
19021902
use crate::types::string::PrintableString;
19031903
use crate::util::ser::{BigSize, Iterable, Writeable};
1904+
use crate::util::test_utils::TestCurrencyConversion;
19041905
#[cfg(not(c_bindings))]
19051906
use {crate::offers::offer::OfferBuilder, crate::offers::refund::RefundBuilder};
19061907
#[cfg(c_bindings)]
@@ -2540,6 +2541,121 @@ mod tests {
25402541
}
25412542
}
25422543

2544+
#[test]
2545+
fn rejects_excessive_amount_for_currency_offer() {
2546+
let expanded_key = ExpandedKey::new([42; 32]);
2547+
let entropy = FixedEntropy {};
2548+
let nonce = Nonce::from_entropy_source(&entropy);
2549+
let secp_ctx = Secp256k1::new();
2550+
let payment_id = PaymentId([1; 32]);
2551+
let converter = TestCurrencyConversion {};
2552+
2553+
let invoice = OfferBuilder::new(recipient_pubkey(), &converter)
2554+
.amount(Amount::Currency {
2555+
iso4217_code: crate::offers::offer::CurrencyCode::new(*b"USD").unwrap(),
2556+
amount: 10,
2557+
})
2558+
.build()
2559+
.unwrap()
2560+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
2561+
.unwrap()
2562+
.build_and_sign()
2563+
.unwrap()
2564+
.respond_with_no_std(&converter, payment_paths(), payment_hash(), now())
2565+
.unwrap()
2566+
.amount_msats_unchecked(10_001)
2567+
.build()
2568+
.unwrap()
2569+
.sign(recipient_sign)
2570+
.unwrap();
2571+
2572+
assert_eq!(
2573+
invoice.verify_amount_acceptable_for_payment(&converter),
2574+
Err(Bolt12SemanticError::ExcessiveAmount),
2575+
);
2576+
}
2577+
2578+
#[test]
2579+
fn fails_verifying_currency_offer_invoice_without_conversion_support() {
2580+
let expanded_key = ExpandedKey::new([42; 32]);
2581+
let entropy = FixedEntropy {};
2582+
let nonce = Nonce::from_entropy_source(&entropy);
2583+
let secp_ctx = Secp256k1::new();
2584+
let payment_id = PaymentId([1; 32]);
2585+
let converter = TestCurrencyConversion {};
2586+
2587+
let invoice = OfferBuilder::new(recipient_pubkey(), &converter)
2588+
.amount(Amount::Currency {
2589+
iso4217_code: crate::offers::offer::CurrencyCode::new(*b"USD").unwrap(),
2590+
amount: 10,
2591+
})
2592+
.build()
2593+
.unwrap()
2594+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
2595+
.unwrap()
2596+
.build_and_sign()
2597+
.unwrap()
2598+
.respond_with_no_std(&converter, payment_paths(), payment_hash(), now())
2599+
.unwrap()
2600+
.build()
2601+
.unwrap()
2602+
.sign(recipient_sign)
2603+
.unwrap();
2604+
2605+
assert_eq!(
2606+
invoice.verify_amount_acceptable_for_payment(&NullCurrencyConversion),
2607+
Err(Bolt12SemanticError::UnsupportedCurrency),
2608+
);
2609+
}
2610+
2611+
#[test]
2612+
fn verifies_currency_offer_invoice_amount_with_quantity() {
2613+
let expanded_key = ExpandedKey::new([42; 32]);
2614+
let entropy = FixedEntropy {};
2615+
let nonce = Nonce::from_entropy_source(&entropy);
2616+
let secp_ctx = Secp256k1::new();
2617+
let payment_id = PaymentId([1; 32]);
2618+
let converter = TestCurrencyConversion {};
2619+
2620+
let invoice_request = OfferBuilder::new(recipient_pubkey(), &converter)
2621+
.amount(Amount::Currency {
2622+
iso4217_code: crate::offers::offer::CurrencyCode::new(*b"USD").unwrap(),
2623+
amount: 10,
2624+
})
2625+
.supported_quantity(Quantity::Unbounded)
2626+
.build()
2627+
.unwrap()
2628+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
2629+
.unwrap()
2630+
.quantity(2)
2631+
.unwrap()
2632+
.build_and_sign()
2633+
.unwrap();
2634+
2635+
let invoice = invoice_request
2636+
.respond_with_no_std(&converter, payment_paths(), payment_hash(), now())
2637+
.unwrap()
2638+
.build()
2639+
.unwrap()
2640+
.sign(recipient_sign)
2641+
.unwrap();
2642+
assert_eq!(invoice.amount_msats(), 20_000);
2643+
assert_eq!(invoice.verify_amount_acceptable_for_payment(&converter), Ok(()));
2644+
2645+
let invoice = invoice_request
2646+
.respond_with_no_std(&converter, payment_paths(), payment_hash(), now())
2647+
.unwrap()
2648+
.amount_msats_unchecked(20_001)
2649+
.build()
2650+
.unwrap()
2651+
.sign(recipient_sign)
2652+
.unwrap();
2653+
assert_eq!(
2654+
invoice.verify_amount_acceptable_for_payment(&converter),
2655+
Err(Bolt12SemanticError::ExcessiveAmount),
2656+
);
2657+
}
2658+
25432659
#[test]
25442660
fn builds_invoice_with_fallback_address() {
25452661
let expanded_key = ExpandedKey::new([42; 32]);

0 commit comments

Comments
 (0)