Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static invoice creation utils to ChannelManager #3408

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion fuzz/src/chanmon_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ impl Router for FuzzRouter {

fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>(
&self, _recipient: PublicKey, _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs,
_amount_msats: u64, _secp_ctx: &Secp256k1<T>,
_amount_msats: Option<u64>, _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPaymentPath>, ()> {
unreachable!()
}
Expand Down
2 changes: 1 addition & 1 deletion fuzz/src/full_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ impl Router for FuzzRouter {

fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>(
&self, _recipient: PublicKey, _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs,
_amount_msats: u64, _secp_ctx: &Secp256k1<T>,
_amount_msats: Option<u64>, _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPaymentPath>, ()> {
unreachable!()
}
Expand Down
28 changes: 28 additions & 0 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::ln::msgs::DecodeError;
use crate::ln::onion_utils;
use crate::types::payment::PaymentHash;
use crate::offers::nonce::Nonce;
use crate::offers::offer::OfferId;
use crate::onion_message::packet::ControlTlvs;
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
use crate::sign::{EntropySource, NodeSigner, Recipient};
Expand Down Expand Up @@ -402,6 +403,28 @@ pub enum AsyncPaymentsContext {
/// containing the expected [`PaymentId`].
hmac: Hmac<Sha256>,
},
/// Context contained within the [`BlindedMessagePath`]s we put in static invoices, provided back
/// to us in corresponding [`HeldHtlcAvailable`] messages.
///
/// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable
InboundPayment {
/// The ID of the [`Offer`] that this [`BlindedMessagePath`]'s static invoice corresponds to.
/// Useful to authenticate that this blinded path was created by us for asynchronously paying
/// one of our offers.
///
/// [`Offer`]: crate::offers::offer::Offer
offer_id: OfferId,
/// A nonce used for authenticating that a [`HeldHtlcAvailable`] message is valid for a
/// preceding static invoice.
///
/// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable
nonce: Nonce,
/// Authentication code for the [`OfferId`].
///
/// Prevents the recipient from being able to deanonymize us by creating a blinded path to us
/// containing the expected [`OfferId`].
hmac: Hmac<Sha256>,
},
}

impl_writeable_tlv_based_enum!(MessageContext,
Expand Down Expand Up @@ -433,6 +456,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
(2, nonce, required),
(4, hmac, required),
},
(1, InboundPayment) => {
(0, offer_id, required),
(2, nonce, required),
(4, hmac, required),
},
);

/// Contains a simple nonce for use in a blinded path's context.
Expand Down
28 changes: 28 additions & 0 deletions lightning/src/blinded_path/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::types::features::BlindedHopFeatures;
use crate::ln::msgs::DecodeError;
use crate::ln::onion_utils;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::nonce::Nonce;
use crate::offers::offer::OfferId;
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
use crate::sign::{EntropySource, NodeSigner, Recipient};
Expand Down Expand Up @@ -318,6 +319,11 @@ pub enum PaymentContext {
/// [`Offer`]: crate::offers::offer::Offer
Bolt12Offer(Bolt12OfferContext),

/// The payment was made for a static invoice requested from a BOLT 12 [`Offer`].
///
/// [`Offer`]: crate::offers::offer::Offer
AsyncBolt12Offer(AsyncBolt12OfferContext),

/// The payment was made for an invoice sent for a BOLT 12 [`Refund`].
///
/// [`Refund`]: crate::offers::refund::Refund
Expand Down Expand Up @@ -351,6 +357,22 @@ pub struct Bolt12OfferContext {
pub invoice_request: InvoiceRequestFields,
}

/// The context of a payment made for a static invoice requested from a BOLT 12 [`Offer`].
///
/// [`Offer`]: crate::offers::offer::Offer
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AsyncBolt12OfferContext {
/// The identifier of the [`Offer`].
///
/// [`Offer`]: crate::offers::offer::Offer
pub offer_id: OfferId,
/// The [`Nonce`] used to verify that an inbound [`InvoiceRequest`] corresponds to this static
/// invoice's offer.
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
pub offer_nonce: Nonce,
}

/// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`].
///
/// [`Refund`]: crate::offers::refund::Refund
Expand Down Expand Up @@ -590,6 +612,7 @@ impl_writeable_tlv_based_enum_legacy!(PaymentContext,
(0, Unknown),
(1, Bolt12Offer),
(2, Bolt12Refund),
(3, AsyncBolt12Offer),
);

impl<'a> Writeable for PaymentContextRef<'a> {
Expand Down Expand Up @@ -626,6 +649,11 @@ impl_writeable_tlv_based!(Bolt12OfferContext, {
(2, invoice_request, required),
});

impl_writeable_tlv_based!(AsyncBolt12OfferContext, {
(0, offer_id, required),
(2, offer_nonce, required),
});

impl_writeable_tlv_based!(Bolt12RefundContext, {});

#[cfg(test)]
Expand Down
9 changes: 9 additions & 0 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ impl PaymentPurpose {
payment_context: context,
}
},
Some(PaymentContext::AsyncBolt12Offer(_context)) => {
debug_assert!(false, "Receiving async payments is not yet supported");
// This code will change to return Self::Bolt12OfferPayment when we add support for async
// receive.
PaymentPurpose::Bolt11InvoicePayment {
payment_preimage,
payment_secret,
}
},
}
}
}
Expand Down
211 changes: 211 additions & 0 deletions lightning/src/ln/blinded_payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ use crate::util::ser::WithoutLength;
use crate::util::test_utils;
use lightning_invoice::RawBolt11Invoice;
#[cfg(async_payments)] use {
crate::blinded_path::BlindedHop,
crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, OffersContext},
crate::ln::channelmanager::Verification,
crate::ln::inbound_payment,
crate::ln::msgs::OnionMessageHandler,
crate::offers::nonce::Nonce,
crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler, ReleaseHeldHtlc},
crate::onion_message::offers::{OffersMessage, OffersMessageHandler},
crate::types::features::Bolt12InvoiceFeatures,
crate::types::payment::PaymentPreimage,
};

Expand Down Expand Up @@ -1416,6 +1424,209 @@ fn custom_tlvs_to_blinded_path() {
);
}

#[test]
#[cfg(async_payments)]
fn static_invoice_unknown_required_features() {
// Test that we will fail to pay a static invoice with unsupported required features.
let secp_ctx = Secp256k1::new();
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);

// Use a dummy blinded path because we don't support retrieving the static invoice from the
// recipient's LSP yet.
let dummy_blinded_path_to_always_online_node = BlindedMessagePath::from_raw(
nodes[1].node.get_our_node_id(), test_utils::pubkey(42),
vec![BlindedHop { blinded_node_id: test_utils::pubkey(42), encrypted_payload: vec![42; 32] }]
);
let (offer_builder, nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node]).unwrap();
let offer = offer_builder.build().unwrap();
let static_invoice_unknown_req_features = nodes[2].node.create_static_invoice_builder_for_async_receive_offer(
&offer, nonce, None
)
.unwrap()
.features_unchecked(Bolt12InvoiceFeatures::unknown())
.build_and_sign(&secp_ctx).unwrap();

let amt_msat = 5000;
let payment_id = PaymentId([1; 32]);
// Set the random bytes so we can predict the offer outbound payment context nonce.
*nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some([42; 32]);
nodes[0].node.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None).unwrap();

// Don't forward the invreq since we don't support retrieving the static invoice from the
// recipient's LSP yet, instead just provide the invoice directly to the payer.
let _invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap();

let inbound_payment_key = inbound_payment::ExpandedKey::new(
&nodes[0].keys_manager.get_inbound_payment_key_material()
);
let offer_outbound_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager);
let hmac = payment_id.hmac_for_offer_payment(offer_outbound_context_nonce, &inbound_payment_key);
if nodes[0].node.handle_message(
OffersMessage::StaticInvoice(static_invoice_unknown_req_features),
Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None
).is_some() { panic!() }

let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
match events[0] {
Event::PaymentFailed { payment_hash, payment_id: ev_payment_id, reason } => {
assert_eq!(payment_hash, None);
assert_eq!(payment_id, ev_payment_id);
assert_eq!(reason, Some(PaymentFailureReason::UnknownRequiredFeatures));
},
_ => panic!()
}
}

#[test]
#[cfg(async_payments)]
fn ignore_unexpected_static_invoice() {
// Test that we'll ignore unexpected static invoices, invoices that don't match our invoice
// request, and duplicate invoices.
let secp_ctx = Secp256k1::new();
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);

// Initiate payment to the sender's intended offer.
let dummy_blinded_path_to_always_online_node = BlindedMessagePath::from_raw(
nodes[1].node.get_our_node_id(), test_utils::pubkey(42),
vec![BlindedHop { blinded_node_id: test_utils::pubkey(42), encrypted_payload: vec![42; 32] }]
);
let (offer_builder, offer_nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node.clone()]).unwrap();
let offer = offer_builder.build().unwrap();
let amt_msat = 5000;
let payment_id = PaymentId([1; 32]);
// Set the random bytes so we can predict the offer outbound payment context nonce.
*nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some([42; 32]);
nodes[0].node.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None).unwrap();

// Don't forward the invreq since we don't support retrieving the static invoice from the
// recipient's LSP yet, instead just provide the invoice directly to the payer.
let _invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap();

// Create a static invoice with the same payment_id but corresponding to a different offer.
let unexpected_static_invoice = {
let (offer_builder, nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node]).unwrap();
let sender_unintended_offer = offer_builder.build().unwrap();

nodes[2].node.create_static_invoice_builder_for_async_receive_offer(
&sender_unintended_offer, nonce, None
).unwrap().build_and_sign(&secp_ctx).unwrap()
};

// Check that we'll ignore the unexpected static invoice.
let inbound_payment_key = inbound_payment::ExpandedKey::new(
&nodes[0].keys_manager.get_inbound_payment_key_material()
);
let offer_outbound_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager);
let hmac = payment_id.hmac_for_offer_payment(offer_outbound_context_nonce, &inbound_payment_key);
assert!(nodes[0].node.handle_message(
OffersMessage::StaticInvoice(unexpected_static_invoice),
Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None
).is_none());
let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node);
assert!(async_pmts_msgs.is_empty());
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());

// A valid static invoice corresponding to the correct offer will succeed and cause us to send a
// held_htlc_available onion message.
let valid_static_invoice = nodes[2].node.create_static_invoice_builder_for_async_receive_offer(
&offer, offer_nonce, None
).unwrap().build_and_sign(&secp_ctx).unwrap();

assert!(nodes[0].node.handle_message(
OffersMessage::StaticInvoice(valid_static_invoice.clone()),
Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None
).is_none());
let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node);
assert!(!async_pmts_msgs.is_empty());
assert!(async_pmts_msgs.into_iter().all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_))));

// Receiving a duplicate invoice will have no effect.
assert!(nodes[0].node.handle_message(
OffersMessage::StaticInvoice(valid_static_invoice),
Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None
).is_none());
let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node);
assert!(async_pmts_msgs.is_empty());
}

#[test]
#[cfg(async_payments)]
fn pays_static_invoice() {
// Test that we support the async payments flow up to and including sending the actual payment.
// Async receive is not yet supported so we don't complete the payment yet.
let secp_ctx = Secp256k1::new();
let chanmon_cfgs = create_chanmon_cfgs(3);
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);

let dummy_blinded_path_to_always_online_node = BlindedMessagePath::from_raw(
nodes[1].node.get_our_node_id(), test_utils::pubkey(42),
vec![BlindedHop { blinded_node_id: test_utils::pubkey(42), encrypted_payload: vec![42; 32] }]
);
let (offer_builder, offer_nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node.clone()]).unwrap();
let offer = offer_builder.build().unwrap();
let amt_msat = 5000;
let payment_id = PaymentId([1; 32]);
// Set the random bytes so we can predict the offer outbound payment context nonce.
*nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some([42; 32]);
nodes[0].node.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None).unwrap();

// Don't forward the invreq since we don't support retrieving the static invoice from the
// recipient's LSP yet, instead just provide the invoice directly to the payer.
let _invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap();

let inbound_payment_key = inbound_payment::ExpandedKey::new(
&nodes[0].keys_manager.get_inbound_payment_key_material()
);
let offer_outbound_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager);
let hmac = payment_id.hmac_for_offer_payment(offer_outbound_context_nonce, &inbound_payment_key);

let static_invoice = nodes[2].node.create_static_invoice_builder_for_async_receive_offer(
&offer, offer_nonce, None
).unwrap().build_and_sign(&secp_ctx).unwrap();

assert!(nodes[0].node.handle_message(
OffersMessage::StaticInvoice(static_invoice),
Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None
).is_none());
let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node);
assert!(!async_pmts_msgs.is_empty());
assert!(async_pmts_msgs.into_iter().all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_))));

// Manually create the message and context releasing the HTLC since the recipient doesn't support
// responding themselves yet.
let outbound_async_payment_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager);
let outbound_async_payment_context = AsyncPaymentsContext::OutboundPayment {
payment_id,
nonce: outbound_async_payment_context_nonce,
hmac: payment_id.hmac_for_async_payment(outbound_async_payment_context_nonce, &inbound_payment_key),
};
nodes[0].node.handle_release_held_htlc(ReleaseHeldHtlc {}, outbound_async_payment_context.clone());

// Check that we've queued the HTLCs of the async keysend payment.
let htlc_updates = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id());
assert_eq!(htlc_updates.update_add_htlcs.len(), 1);
check_added_monitors!(nodes[0], 1);

// Receiving a duplicate release_htlc message doesn't result in duplicate payment.
nodes[0].node.handle_release_held_htlc(ReleaseHeldHtlc {}, outbound_async_payment_context.clone());
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
}

fn secret_from_hex(hex: &str) -> SecretKey {
SecretKey::from_slice(&<Vec<u8>>::from_hex(hex).unwrap()).unwrap()
}
Expand Down
Loading
Loading