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
2 changes: 2 additions & 0 deletions lightning-dns-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ mod test {
recipient,
local_node_receive_key,
context,
false,
&keys,
secp_ctx,
)])
Expand Down Expand Up @@ -345,6 +346,7 @@ mod test {
payer_id,
receive_key,
query_context,
false,
&*payer_keys,
&secp_ctx,
);
Expand Down
131 changes: 99 additions & 32 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,38 @@ impl Readable for BlindedMessagePath {

impl BlindedMessagePath {
/// Create a one-hop blinded path for a message.
///
/// `compact_padding` selects between space-inefficient padding that better hides contents and
/// a space-constrained padding that does very little to hide the contents, especially for the
/// last hop. It should only be set when the blinded path needs to be as compact as possible.
pub fn one_hop<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
recipient_node_id: PublicKey, local_node_receive_key: ReceiveAuthKey,
context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1<T>,
context: MessageContext, compact_padding: bool, entropy_source: ES,
secp_ctx: &Secp256k1<T>,
) -> Self
where
ES::Target: EntropySource,
{
Self::new(&[], recipient_node_id, local_node_receive_key, context, entropy_source, secp_ctx)
Self::new(
&[],
recipient_node_id,
local_node_receive_key,
context,
compact_padding,
entropy_source,
secp_ctx,
)
}

/// Create a path for an onion message, to be forwarded along `node_pks`.
///
/// `compact_padding` selects between space-inefficient padding that better hides contents and
/// a space-constrained padding that does very little to hide the contents, especially for the
/// last hop. It should only be set when the blinded path needs to be as compact as possible.
pub fn new<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
local_node_receive_key: ReceiveAuthKey, context: MessageContext, entropy_source: ES,
secp_ctx: &Secp256k1<T>,
local_node_receive_key: ReceiveAuthKey, context: MessageContext, compact_padding: bool,
entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Self
where
ES::Target: EntropySource,
Expand All @@ -79,19 +96,24 @@ impl BlindedMessagePath {
0,
local_node_receive_key,
context,
compact_padding,
entropy_source,
secp_ctx,
)
}

/// Same as [`BlindedMessagePath::new`], but allows specifying a number of dummy hops.
///
/// Note:
/// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path.
///
/// `compact_padding` selects between space-inefficient padding that better hides contents and
/// a space-constrained padding that does very little to hide the contents, especially for the
/// last hop. It should only be set when the blinded path needs to be as compact as possible.
///
/// Note: At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path.
pub fn new_with_dummy_hops<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, context: MessageContext,
entropy_source: ES, secp_ctx: &Secp256k1<T>,
compact_padding: bool, entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Self
where
ES::Target: EntropySource,
Expand All @@ -114,6 +136,7 @@ impl BlindedMessagePath {
context,
&blinding_secret,
local_node_receive_key,
compact_padding,
),
})
}
Expand Down Expand Up @@ -416,28 +439,45 @@ pub enum OffersContext {
/// Useful to timeout async recipients that are no longer supported as clients.
path_absolute_expiry: Duration,
},
/// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an
/// [`InvoiceRequest`].
/// Context used by a [`BlindedMessagePath`] within a [`Refund`].
///
/// This variant is intended to be received when handling a [`Bolt12Invoice`] or an
/// [`InvoiceError`].
///
/// [`Refund`]: crate::offers::refund::Refund
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
OutboundPayment {
/// Payment ID used when creating a [`Refund`] or [`InvoiceRequest`].
OutboundPaymentInRefund {
/// Payment ID used when creating a [`Refund`].
///
/// [`Refund`]: crate::offers::refund::Refund
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
payment_id: PaymentId,

/// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid [`Refund`] or
/// [`InvoiceRequest`] and for deriving their signing keys.
/// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid [`Refund`] and
/// for deriving its signing keys.
///
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// [`Refund`]: crate::offers::refund::Refund
nonce: Nonce,
},
/// Context used by a [`BlindedMessagePath`] as a reply path for an [`InvoiceRequest`].
///
/// This variant is intended to be received when handling a [`Bolt12Invoice`] or an
/// [`InvoiceError`].
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
OutboundPaymentInInvReq {
/// Payment ID used when creating an [`InvoiceRequest`].
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
payment_id: PaymentId,

/// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid
/// [`InvoiceRequest`] and for deriving its signing keys.
///
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
nonce: Nonce,
},
Expand Down Expand Up @@ -619,7 +659,7 @@ impl_writeable_tlv_based_enum!(OffersContext,
(0, InvoiceRequest) => {
(0, nonce, required),
},
(1, OutboundPayment) => {
(1, OutboundPaymentInRefund) => {
(0, payment_id, required),
(1, nonce, required),
},
Expand All @@ -631,6 +671,10 @@ impl_writeable_tlv_based_enum!(OffersContext,
(2, invoice_slot, required),
(4, path_absolute_expiry, required),
},
(4, OutboundPaymentInInvReq) => {
(0, payment_id, required),
(1, nonce, required),
},
);

impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
Expand Down Expand Up @@ -693,7 +737,7 @@ pub const MAX_DUMMY_HOPS_COUNT: usize = 10;
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
recipient_node_id: PublicKey, dummy_hop_count: usize, context: MessageContext,
session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey,
session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, compact_padding: bool,
) -> Vec<BlindedHop> {
let dummy_count = cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT);
let pks = intermediate_nodes
Expand All @@ -703,9 +747,8 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
core::iter::repeat((recipient_node_id, Some(local_node_receive_key))).take(dummy_count),
)
.chain(core::iter::once((recipient_node_id, Some(local_node_receive_key))));
let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some());

let tlvs = pks
let intermediate_tlvs = pks
.clone()
.skip(1) // The first node's TLVs contains the next node's pubkey
.zip(intermediate_nodes.iter().map(|node| node.short_channel_id))
Expand All @@ -716,18 +759,42 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
.map(|next_hop| {
ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None })
})
.chain((0..dummy_count).map(|_| ControlTlvs::Dummy))
.chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) })));

if is_compact {
let path = pks.zip(tlvs);
utils::construct_blinded_hops(secp_ctx, path, session_priv)
.chain((0..dummy_count).map(|_| ControlTlvs::Dummy));

let max_intermediate_len =
intermediate_tlvs.clone().map(|tlvs| tlvs.serialized_length()).max().unwrap_or(0);
let have_intermediate_one_byte_smaller =
intermediate_tlvs.clone().any(|tlvs| tlvs.serialized_length() == max_intermediate_len - 1);

let round_off = if compact_padding {
// We can only pad by a minimum of two bytes. Thus, if there are any intermediate hops that
// need to be padded by exactly one byte, we have to instead pad everything by two.
if have_intermediate_one_byte_smaller {
max_intermediate_len + 2
} else {
max_intermediate_len
}
} else {
let path =
pks.zip(tlvs.map(|tlv| BlindedPathWithPadding {
tlvs: tlv,
round_off: MESSAGE_PADDING_ROUND_OFF,
}));
utils::construct_blinded_hops(secp_ctx, path, session_priv)
}
MESSAGE_PADDING_ROUND_OFF
};

let tlvs = intermediate_tlvs
.map(|tlvs| {
let res = BlindedPathWithPadding { tlvs, round_off };
if compact_padding {
debug_assert_eq!(res.serialized_length(), max_intermediate_len);
} else {
// We don't currently ever push extra stuff to intermediate hops, so simply assert that
// the fully-padded hops are always `MESSAGE_PADDING_ROUND_OFF` long.
debug_assert_eq!(res.serialized_length(), MESSAGE_PADDING_ROUND_OFF);
}
res
})
.chain(core::iter::once(BlindedPathWithPadding {
tlvs: ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }),
round_off: if compact_padding { 0 } else { MESSAGE_PADDING_ROUND_OFF },
}));

let path = pks.zip(tlvs);
utils::construct_blinded_hops(secp_ctx, path, session_priv)
}
9 changes: 6 additions & 3 deletions lightning/src/blinded_path/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,12 @@ impl<T: Writeable> Writeable for BlindedPathWithPadding<T> {
let tlv_length = self.tlvs.serialized_length();
let total_length = tlv_length + TLV_OVERHEAD;

let padding_length = total_length.div_ceil(self.round_off) * self.round_off - total_length;

let padding = Some(BlindedPathPadding::new(padding_length));
let padding = if self.round_off == 0 || tlv_length % self.round_off == 0 {
None
} else {
let length = total_length.div_ceil(self.round_off) * self.round_off - total_length;
Some(BlindedPathPadding::new(length))
};

encode_tlv_stream!(writer, {
(1, padding, option),
Expand Down
24 changes: 4 additions & 20 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5593,29 +5593,12 @@ where
pub fn send_payment_for_bolt12_invoice(
&self, invoice: &Bolt12Invoice, context: Option<&OffersContext>,
) -> Result<(), Bolt12PaymentError> {
match self.verify_bolt12_invoice(invoice, context) {
match self.flow.verify_bolt12_invoice(invoice, context) {
Ok(payment_id) => self.send_payment_for_verified_bolt12_invoice(invoice, payment_id),
Err(()) => Err(Bolt12PaymentError::UnexpectedInvoice),
}
}

fn verify_bolt12_invoice(
&self, invoice: &Bolt12Invoice, context: Option<&OffersContext>,
) -> Result<PaymentId, ()> {
let secp_ctx = &self.secp_ctx;
let expanded_key = &self.inbound_payment_key;

match context {
None if invoice.is_for_refund_without_paths() => {
invoice.verify_using_metadata(expanded_key, secp_ctx)
},
Some(&OffersContext::OutboundPayment { payment_id, nonce, .. }) => {
invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx)
},
_ => Err(()),
}
}

fn send_payment_for_verified_bolt12_invoice(
&self, invoice: &Bolt12Invoice, payment_id: PaymentId,
) -> Result<(), Bolt12PaymentError> {
Expand Down Expand Up @@ -15366,7 +15349,7 @@ where
},
OffersMessage::StaticInvoice(invoice) => {
let payment_id = match context {
Some(OffersContext::OutboundPayment { payment_id, .. }) => payment_id,
Some(OffersContext::OutboundPaymentInInvReq { payment_id, .. }) => payment_id,
_ => return None
};
let res = self.initiate_async_payment(&invoice, payment_id);
Expand All @@ -15382,7 +15365,8 @@ where
log_trace!(logger, "Received invoice_error: {}", invoice_error);

match context {
Some(OffersContext::OutboundPayment { payment_id, .. }) => {
Some(OffersContext::OutboundPaymentInInvReq { payment_id, .. })
|Some(OffersContext::OutboundPaymentInRefund { payment_id, .. }) => {
self.abandon_payment_with_reason(
payment_id, PaymentFailureReason::InvoiceRequestRejected,
);
Expand Down
Loading