Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions fuzz/src/invoice_request_deser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ use lightning::blinded_path::payment::{
use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA;
use lightning::ln::inbound_payment::ExpandedKey;
use lightning::offers::invoice::UnsignedBolt12Invoice;
use lightning::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields};
use lightning::offers::offer::OfferId;
use lightning::offers::invoice_request::{
CurrencyConversion, InvoiceRequest, InvoiceRequestFields,
};
use lightning::offers::offer::{CurrencyCode, OfferId};
use lightning::offers::parse::Bolt12SemanticError;
use lightning::sign::{EntropySource, ReceiveAuthKey};
use lightning::types::features::BlindedHopFeatures;
Expand Down Expand Up @@ -78,6 +80,14 @@ fn privkey(byte: u8) -> SecretKey {
SecretKey::from_slice(&[byte; 32]).unwrap()
}

struct FuzzCurrencyConversion;

impl CurrencyConversion for FuzzCurrencyConversion {
fn fiat_to_msats(&self, _iso4217_code: CurrencyCode) -> Result<u64, ()> {
unreachable!()
}
}

fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
invoice_request: &InvoiceRequest, secp_ctx: &Secp256k1<T>,
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
Expand Down Expand Up @@ -144,7 +154,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
.unwrap();

let payment_hash = PaymentHash([42; 32]);
invoice_request.respond_with(vec![payment_path], payment_hash)?.build()
invoice_request.respond_with(&FuzzCurrencyConversion, vec![payment_path], payment_hash)?.build()
}

pub fn invoice_request_deser_test<Out: test_logger::Output>(data: &[u8], out: Out) {
Expand Down
61 changes: 52 additions & 9 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
use crate::offers::flow::{HeldHtlcReplyPath, InvreqResponseInstructions, OffersMessageFlow};
use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice};
use crate::offers::invoice_error::InvoiceError;
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestVerifiedFromOffer};
use crate::offers::invoice_request::{
DefaultCurrencyConversion, InvoiceRequest, InvoiceRequestVerifiedFromOffer,
};
use crate::offers::nonce::Nonce;
use crate::offers::offer::{Offer, OfferFromHrn};
use crate::offers::offer::{Amount, Offer, OfferFromHrn};
use crate::offers::parse::Bolt12SemanticError;
use crate::offers::refund::Refund;
use crate::offers::static_invoice::StaticInvoice;
Expand Down Expand Up @@ -2664,6 +2666,9 @@ pub struct ChannelManager<
fee_estimator: LowerBoundedFeeEstimator<F>,
chain_monitor: M,
tx_broadcaster: T,
#[cfg(test)]
pub(super) router: R,
#[cfg(not(test))]
router: R,

#[cfg(test)]
Expand Down Expand Up @@ -2890,6 +2895,9 @@ pub struct ChannelManager<
pub(super) entropy_source: ES,
#[cfg(not(test))]
entropy_source: ES,
#[cfg(test)]
pub(super) node_signer: NS,
#[cfg(not(test))]
node_signer: NS,
#[cfg(test)]
pub(super) signer_provider: SP,
Expand Down Expand Up @@ -3945,7 +3953,8 @@ where
let flow = OffersMessageFlow::new(
ChainHash::using_genesis_block(params.network), params.best_block,
our_network_pubkey, current_timestamp, expanded_inbound_key,
node_signer.get_receive_auth_key(), secp_ctx.clone(), message_router, logger.clone(),
node_signer.get_receive_auth_key(), secp_ctx.clone(), message_router, false,
logger.clone(),
);

ChannelManager {
Expand Down Expand Up @@ -5666,6 +5675,7 @@ where
let features = self.bolt12_invoice_features();
let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received(
invoice,
&DefaultCurrencyConversion,
payment_id,
features,
best_block_height,
Expand Down Expand Up @@ -12935,12 +12945,21 @@ where
/// Uses [`InvoiceRequestBuilder`] such that the [`InvoiceRequest`] it builds is recognized by
/// the [`ChannelManager`] when handling a [`Bolt12Invoice`] message in response to the request.
///
/// `amount_msats` allows you to overpay what is required to satisfy the offer, or may be
/// required if the offer does not require a specific amount.
///
/// If the [`Offer`] was built from a human readable name resolved using BIP 353, you *must*
/// instead call [`Self::pay_for_offer_from_hrn`].
///
/// # Amount
/// `amount_msats` allows you to overpay what is required to satisfy the offer, or may be
/// required if the offer does not require a specific amount.
///
/// # Currency
///
/// If the [`Offer`] specifies its amount in a currency (that is, [`Amount::Currency`]), callers
/// must provide the `amount_msats`. [`ChannelManager`] enforces only that an amount is present
/// in this case. It does not verify here that the provided `amount_msats` is sufficient once
/// converted from the currency amount. The recipient may reject the resulting [`InvoiceRequest`]
/// if the amount is insufficient after conversion.
///
/// # Payment
///
/// The provided `payment_id` is used to ensure that only one invoice is paid for the request
Expand Down Expand Up @@ -13078,6 +13097,13 @@ where
let entropy = &*self.entropy_source;
let nonce = Nonce::from_entropy_source(entropy);

// If the offer is for a specific currency, ensure the amount is provided.
if let Some(Amount::Currency { iso4217_code: _, amount: _ }) = offer.amount() {
if amount_msats.is_none() {
return Err(Bolt12SemanticError::MissingAmount);
}
}

let builder = self.flow.create_invoice_request_builder(
offer, nonce, payment_id,
)?;
Expand Down Expand Up @@ -13160,7 +13186,20 @@ where

let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?;

self.flow.enqueue_invoice(invoice.clone(), refund, self.get_peers_for_blinded_path())?;
if refund.paths().is_empty() {
self.flow.enqueue_invoice_using_node_id(
invoice.clone(),
refund.payer_signing_pubkey(),
self.get_peers_for_blinded_path(),
)?;
} else {
self.flow.enqueue_invoice_using_reply_paths(
invoice.clone(),
refund.paths(),
self.get_peers_for_blinded_path(),
)?;
}

Ok(invoice)
}

Expand Down Expand Up @@ -13383,7 +13422,7 @@ where
now
}

fn get_peers_for_blinded_path(&self) -> Vec<MessageForwardNode> {
pub(crate) fn get_peers_for_blinded_path(&self) -> Vec<MessageForwardNode> {
let per_peer_state = self.per_peer_state.read().unwrap();
per_peer_state
.iter()
Expand Down Expand Up @@ -15296,7 +15335,7 @@ where
None => return None,
};

let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) {
let invoice_request = match self.flow.verify_invoice_request(invoice_request, context, responder.clone()) {
Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request,
Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, invoice_request }) => {
self.pending_events.lock().unwrap().push_back((Event::StaticInvoiceRequested {
Expand All @@ -15305,6 +15344,7 @@ where

return None
},
Ok(InvreqResponseInstructions::AsynchronouslyHandleResponse) => return None,
Err(_) => return None,
};

Expand All @@ -15320,6 +15360,7 @@ where
InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => {
let result = self.flow.create_invoice_builder_from_invoice_request_with_keys(
&self.router,
&DefaultCurrencyConversion,
&request,
self.list_usable_channels(),
get_payment_info,
Expand All @@ -15344,6 +15385,7 @@ where
InvoiceRequestVerifiedFromOffer::ExplicitKeys(request) => {
let result = self.flow.create_invoice_builder_from_invoice_request_without_keys(
&self.router,
&DefaultCurrencyConversion,
&request,
self.list_usable_channels(),
get_payment_info,
Expand Down Expand Up @@ -18101,6 +18143,7 @@ where
args.node_signer.get_receive_auth_key(),
secp_ctx.clone(),
args.message_router,
false,
args.logger.clone(),
)
.with_async_payments_offers_cache(async_receive_offer_cache);
Expand Down
118 changes: 116 additions & 2 deletions lightning/src/ln/offers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,14 @@ use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, Paym
use crate::blinded_path::message::OffersContext;
use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose};
use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self};
use crate::offers::flow::OfferMessageFlowEvent;
use crate::types::features::Bolt12InvoiceFeatures;
use crate::ln::functional_test_utils::*;
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement};
use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS;
use crate::offers::invoice::Bolt12Invoice;
use crate::offers::invoice_error::InvoiceError;
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer};
use crate::offers::invoice_request::{DefaultCurrencyConversion, InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer};
use crate::offers::nonce::Nonce;
use crate::offers::parse::Bolt12SemanticError;
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH};
Expand Down Expand Up @@ -866,6 +867,119 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

/// Checks that an offer can be paid through a one-hop blinded path and that ephemeral pubkeys are
/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the
/// introduction node of the blinded path.
#[test]
fn creates_and_manually_respond_to_ir_then_pays_for_offer_using_one_hop_blinded_path() {
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let mut node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
node_chanmgrs[0].flow.enable_events();
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);

create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);

let alice = &nodes[0];
let alice_id = alice.node.get_our_node_id();
let bob = &nodes[1];
let bob_id = bob.node.get_our_node_id();

let offer = alice.node
.create_offer_builder().unwrap()
.amount_msats(10_000_000)
.build().unwrap();
assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id));
assert!(!offer.paths().is_empty());
for path in offer.paths() {
assert!(check_compact_path_introduction_node(&path, bob, alice_id));
}

let payment_id = PaymentId([1; 32]);
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);

let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);

let flow_events = alice.node.flow.release_pending_flow_events();
assert_eq!(flow_events.len(), 1, "expected exactly one flow event");

let (invoice_request, reply_path) = match flow_events.into_iter().next().unwrap() {
OfferMessageFlowEvent::InvoiceRequestReceived {
invoice_request: InvoiceRequestVerifiedFromOffer::DerivedKeys(req),
reply_path
} => (req, reply_path),
_ => panic!("Unexpected flow event"),
};

let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
offer_id: offer.id(),
invoice_request: InvoiceRequestFields {
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
},
});
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id));

// Create response for invoice request manually.
let get_payment_info = |amount_msats, relative_expiry| {
alice
.node
.create_inbound_payment(Some(amount_msats), relative_expiry, None)
.map_err(|_| Bolt12SemanticError::InvalidAmount)
};

let router = &alice.node.router;
let (builder, _) = alice
.node
.flow
.create_invoice_builder_from_invoice_request_with_keys(
router,
&DefaultCurrencyConversion {},
&invoice_request,
alice.node.list_usable_channels(),
get_payment_info,
)
.expect("failed to create builder with derived keys");

let invoice = builder
.build_and_sign(&alice.node.secp_ctx)
.expect("failed to build and sign invoice");

alice
.node
.flow
.enqueue_invoice_using_reply_paths(
invoice,
&[reply_path],
alice.node.get_peers_for_blinded_path(),
)
.expect("failed to enqueue invoice");

let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);

let (invoice, reply_path) = extract_invoice(bob, &onion_message);
assert_eq!(invoice.amount_msats(), 10_000_000);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
for path in invoice.payment_paths() {
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
}
assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id));

route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

/// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are
/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the
/// introduction node of the blinded path.
Expand Down Expand Up @@ -2331,7 +2445,7 @@ fn fails_paying_invoice_with_unknown_required_features() {

let invoice = match verified_invoice_request {
InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => {
request.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at).unwrap()
request.respond_using_derived_keys_no_std(&DefaultCurrencyConversion, payment_paths, payment_hash, created_at).unwrap()
.features_unchecked(Bolt12InvoiceFeatures::unknown())
.build_and_sign(&secp_ctx).unwrap()
},
Expand Down
Loading
Loading