Skip to content

Commit fbf1093

Browse files
shaavancodex
andcommitted
[test] Add coverage for currency-denominated offer flows
Add end-to-end tests for offers whose amounts are denominated in fiat currencies. Cover offer creation, invoice request amount resolution, and invoice construction across the currency-denominated flow. Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent 86f718f commit fbf1093

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

lightning/src/ln/offers_tests.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use crate::blinded_path::message::OffersContext;
5252
use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose};
5353
use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, self};
5454
use crate::ln::outbound_payment::{Bolt12PaymentError, RecipientOnionFields, Retry};
55+
use crate::offers::offer::{Amount, CurrencyCode};
5556
use crate::types::features::Bolt12InvoiceFeatures;
5657
use crate::ln::functional_test_utils::*;
5758
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement};
@@ -73,6 +74,7 @@ use crate::util::ser::Writeable;
7374
const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24);
7475

7576
use crate::prelude::*;
77+
use crate::offers::test_utils::{payment_hash, payment_paths};
7678
use crate::util::test_utils::TestCurrencyConversion;
7779

7880
macro_rules! expect_recent_payment {
@@ -916,6 +918,78 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
916918
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
917919
}
918920

921+
/// Checks that an offer can be paid through a one-hop blinded path and that ephemeral pubkeys are
922+
/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the
923+
/// introduction node of the blinded path.
924+
#[test]
925+
fn creates_and_pays_for_offer_with_fiat_amount_using_one_hop_blinded_path() {
926+
let chanmon_cfgs = create_chanmon_cfgs(2);
927+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
928+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
929+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
930+
931+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
932+
933+
let alice = &nodes[0];
934+
let alice_id = alice.node.get_our_node_id();
935+
let bob = &nodes[1];
936+
let bob_id = bob.node.get_our_node_id();
937+
938+
let amount = Amount::Currency {
939+
iso4217_code: CurrencyCode::new(*b"USD").unwrap(),
940+
amount: 1000,
941+
};
942+
943+
let offer = alice.node
944+
.create_offer_builder().unwrap()
945+
.amount(amount, alice.node.currency_conversion).unwrap()
946+
.build();
947+
assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id));
948+
assert!(!offer.paths().is_empty());
949+
for path in offer.paths() {
950+
assert!(check_compact_path_introduction_node(&path, bob, alice_id));
951+
}
952+
953+
let payment_id = PaymentId([1; 32]);
954+
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
955+
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
956+
957+
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
958+
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
959+
960+
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
961+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
962+
offer_id: offer.id(),
963+
invoice_request: InvoiceRequestFields {
964+
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
965+
quantity: None,
966+
payer_note_truncated: None,
967+
human_readable_name: None,
968+
},
969+
});
970+
assert_eq!(invoice_request.amount_msats(alice.node.currency_conversion), Ok(1_000_000));
971+
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
972+
assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH));
973+
974+
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
975+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
976+
977+
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
978+
assert_eq!(invoice.amount_msats(), 1_000_000);
979+
assert_ne!(invoice.signing_pubkey(), alice_id);
980+
assert!(!invoice.payment_paths().is_empty());
981+
for path in invoice.payment_paths() {
982+
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
983+
}
984+
assert!(check_dummy_hopped_path_length(&reply_path, bob, alice_id, DUMMY_HOPS_PATH_LENGTH));
985+
986+
route_bolt12_payment(bob, &[alice], &invoice);
987+
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
988+
989+
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
990+
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
991+
}
992+
919993
/// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are
920994
/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the
921995
/// introduction node of the blinded path.
@@ -1400,6 +1474,90 @@ fn pays_bolt12_invoice_asynchronously() {
14001474
);
14011475
}
14021476

1477+
/// Checks that a deferred fiat-denominated invoice is rejected if its quoted msat amount does not
1478+
/// match the payer's local conversion result.
1479+
#[test]
1480+
fn rejects_unexpected_fiat_bolt12_invoice_amount_asynchronously() {
1481+
let mut manually_pay_cfg = test_default_channel_config();
1482+
manually_pay_cfg.manually_handle_bolt12_invoices = true;
1483+
1484+
let chanmon_cfgs = create_chanmon_cfgs(2);
1485+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1486+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_pay_cfg)]);
1487+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1488+
1489+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
1490+
1491+
let alice = &nodes[0];
1492+
let alice_id = alice.node.get_our_node_id();
1493+
let bob = &nodes[1];
1494+
let bob_id = bob.node.get_our_node_id();
1495+
let conversion = TestCurrencyConversion;
1496+
1497+
let offer = alice.node
1498+
.create_offer_builder().unwrap()
1499+
.amount(
1500+
Amount::Currency {
1501+
iso4217_code: CurrencyCode::new(*b"USD").unwrap(),
1502+
amount: 1000,
1503+
},
1504+
&conversion,
1505+
)
1506+
.unwrap()
1507+
.build();
1508+
1509+
let payment_id = PaymentId([1; 32]);
1510+
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
1511+
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
1512+
1513+
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
1514+
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
1515+
1516+
let (invoice_request, _) = extract_invoice_request(alice, &onion_message);
1517+
let nonce = extract_offer_nonce(alice, &onion_message);
1518+
1519+
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
1520+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
1521+
1522+
let mut events = bob.node.get_and_clear_pending_events();
1523+
assert_eq!(events.len(), 1);
1524+
1525+
let (invoice, context) = match events.pop().unwrap() {
1526+
Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => {
1527+
assert_eq!(actual_payment_id, payment_id);
1528+
(invoice, context)
1529+
},
1530+
_ => panic!("No Event::InvoiceReceived"),
1531+
};
1532+
1533+
let expanded_key = alice.keys_manager.get_expanded_key();
1534+
let secp_ctx = Secp256k1::new();
1535+
let verified_invoice_request =
1536+
invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap();
1537+
1538+
let bad_invoice = match verified_invoice_request {
1539+
InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => request
1540+
.respond_using_derived_keys_no_std(
1541+
&conversion,
1542+
payment_paths(),
1543+
payment_hash(),
1544+
Duration::from_secs(1000),
1545+
)
1546+
.unwrap()
1547+
.amount_msats_unchecked(invoice.amount_msats() + 1)
1548+
.build_and_sign(&secp_ctx)
1549+
.unwrap(),
1550+
InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => {
1551+
panic!("Expected invoice request with derived keys");
1552+
},
1553+
};
1554+
1555+
assert_eq!(
1556+
bob.node.send_payment_for_bolt12_invoice(&bad_invoice, context.as_ref()),
1557+
Err(Bolt12PaymentError::UnexpectedInvoice),
1558+
);
1559+
}
1560+
14031561
/// Checks that an offer can be created using an unannounced node as a blinded path's introduction
14041562
/// node. This is only preferred if there are no other options which may indicated either the offer
14051563
/// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but

0 commit comments

Comments
 (0)