Skip to content

Commit ab8ba39

Browse files
shaavancodex
andcommitted
[test] Cover currency-denominated invoice handling
Add targeted tests for the currency-denominated offer and invoice handling paths introduced by the new conversion flow. Cover end-to-end offer flows, parsed fiat request amounts below an offer minimum, deferred invoice replay identity, and async amount resolution failures. This keeps the invoice-quoting change pinned across its success and rejection paths without spreading closely related test updates across multiple commits. Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent 000060e commit ab8ba39

3 files changed

Lines changed: 438 additions & 6 deletions

File tree

lightning/src/ln/offers_tests.rs

Lines changed: 234 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};
@@ -66,13 +67,15 @@ use crate::onion_message::offers::OffersMessage;
6667
use crate::routing::gossip::{NodeAlias, NodeId};
6768
use crate::routing::router::{DEFAULT_PAYMENT_DUMMY_HOPS, PaymentParameters, RouteParameters, RouteParametersConfig};
6869
use crate::sign::{NodeSigner, Recipient};
70+
use crate::types::payment::PaymentHash;
6971
use crate::util::ser::Writeable;
7072

7173
/// This used to determine whether we built a compact path or not, but now its just a random
7274
/// constant we apply to blinded path expiry in these tests.
7375
const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24);
7476

7577
use crate::prelude::*;
78+
use crate::offers::test_utils::{payment_hash, payment_paths};
7679
use crate::util::test_utils::TestCurrencyConversion;
7780

7881
macro_rules! expect_recent_payment {
@@ -916,6 +919,78 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
916919
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
917920
}
918921

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

1478+
/// Checks that a deferred fiat-denominated invoice is rejected if its quoted msat amount does not
1479+
/// match the payer's local conversion result.
1480+
#[test]
1481+
fn rejects_unexpected_fiat_bolt12_invoice_amount_asynchronously() {
1482+
let mut manually_pay_cfg = test_default_channel_config();
1483+
manually_pay_cfg.manually_handle_bolt12_invoices = true;
1484+
1485+
let chanmon_cfgs = create_chanmon_cfgs(2);
1486+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1487+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_pay_cfg)]);
1488+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1489+
1490+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
1491+
1492+
let alice = &nodes[0];
1493+
let alice_id = alice.node.get_our_node_id();
1494+
let bob = &nodes[1];
1495+
let bob_id = bob.node.get_our_node_id();
1496+
let conversion = TestCurrencyConversion;
1497+
1498+
let offer = alice.node
1499+
.create_offer_builder().unwrap()
1500+
.amount(
1501+
Amount::Currency {
1502+
iso4217_code: CurrencyCode::new(*b"USD").unwrap(),
1503+
amount: 1000,
1504+
},
1505+
&conversion,
1506+
)
1507+
.unwrap()
1508+
.build();
1509+
1510+
let payment_id = PaymentId([1; 32]);
1511+
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
1512+
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
1513+
1514+
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
1515+
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
1516+
1517+
let (invoice_request, _) = extract_invoice_request(alice, &onion_message);
1518+
let nonce = extract_offer_nonce(alice, &onion_message);
1519+
1520+
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
1521+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
1522+
1523+
let mut events = bob.node.get_and_clear_pending_events();
1524+
assert_eq!(events.len(), 1);
1525+
1526+
let (invoice, context) = match events.pop().unwrap() {
1527+
Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => {
1528+
assert_eq!(actual_payment_id, payment_id);
1529+
(invoice, context)
1530+
},
1531+
_ => panic!("No Event::InvoiceReceived"),
1532+
};
1533+
1534+
let expanded_key = alice.keys_manager.get_expanded_key();
1535+
let secp_ctx = Secp256k1::new();
1536+
let verified_invoice_request =
1537+
invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap();
1538+
1539+
let bad_invoice = match verified_invoice_request {
1540+
InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => request
1541+
.respond_using_derived_keys_no_std(
1542+
&conversion,
1543+
payment_paths(),
1544+
payment_hash(),
1545+
Duration::from_secs(1000),
1546+
)
1547+
.unwrap()
1548+
.amount_msats_unchecked(invoice.amount_msats() + 1)
1549+
.build_and_sign(&secp_ctx)
1550+
.unwrap(),
1551+
InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => {
1552+
panic!("Expected invoice request with derived keys");
1553+
},
1554+
};
1555+
1556+
assert_eq!(
1557+
bob.node.send_payment_for_bolt12_invoice(&bad_invoice, context.as_ref()),
1558+
Err(Bolt12PaymentError::UnexpectedInvoice),
1559+
);
1560+
}
1561+
1562+
/// Checks that deferred manual invoice handling rejects a different invoice replayed for the same
1563+
/// payment id after the first invoice has already been recorded.
1564+
#[test]
1565+
fn rejects_different_bolt12_invoice_replayed_asynchronously() {
1566+
let mut manually_pay_cfg = test_default_channel_config();
1567+
manually_pay_cfg.manually_handle_bolt12_invoices = true;
1568+
1569+
let chanmon_cfgs = create_chanmon_cfgs(2);
1570+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1571+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_pay_cfg)]);
1572+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1573+
1574+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
1575+
1576+
let alice = &nodes[0];
1577+
let alice_id = alice.node.get_our_node_id();
1578+
let bob = &nodes[1];
1579+
let bob_id = bob.node.get_our_node_id();
1580+
1581+
let offer = alice.node
1582+
.create_offer_builder().unwrap()
1583+
.amount_msats(10_000_000).unwrap()
1584+
.build();
1585+
1586+
let payment_id = PaymentId([1; 32]);
1587+
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
1588+
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
1589+
1590+
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
1591+
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
1592+
1593+
let (invoice_request, _) = extract_invoice_request(alice, &onion_message);
1594+
let nonce = extract_offer_nonce(alice, &onion_message);
1595+
1596+
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
1597+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
1598+
1599+
let mut events = bob.node.get_and_clear_pending_events();
1600+
assert_eq!(events.len(), 1);
1601+
1602+
let (invoice, context) = match events.pop().unwrap() {
1603+
Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => {
1604+
assert_eq!(actual_payment_id, payment_id);
1605+
(invoice, context)
1606+
},
1607+
_ => panic!("No Event::InvoiceReceived"),
1608+
};
1609+
1610+
let expanded_key = alice.keys_manager.get_expanded_key();
1611+
let secp_ctx = Secp256k1::new();
1612+
let verified_invoice_request =
1613+
invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap();
1614+
1615+
let replayed_invoice = match verified_invoice_request {
1616+
InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => request
1617+
.respond_using_derived_keys_no_std(
1618+
alice.node.currency_conversion,
1619+
payment_paths(),
1620+
PaymentHash([43; 32]),
1621+
Duration::from_secs(1000),
1622+
)
1623+
.unwrap()
1624+
.build_and_sign(&secp_ctx)
1625+
.unwrap(),
1626+
InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => {
1627+
panic!("Expected invoice request with derived keys");
1628+
},
1629+
};
1630+
1631+
assert_ne!(replayed_invoice.payment_hash(), invoice.payment_hash());
1632+
assert_eq!(
1633+
bob.node.send_payment_for_bolt12_invoice(&replayed_invoice, context.as_ref()),
1634+
Err(Bolt12PaymentError::DuplicateInvoice),
1635+
);
1636+
}
14031637
/// Checks that an offer can be created using an unannounced node as a blinded path's introduction
14041638
/// node. This is only preferred if there are no other options which may indicated either the offer
14051639
/// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but

0 commit comments

Comments
 (0)