use bitcoin::hashes::hmac::Hmac;
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
use crate::blinded_path::utils::{self, BlindedPathWithPadding};
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp};
use crate::crypto::streams::ChaChaPolyReadAdapter;
use crate::io;
use crate::io::Cursor;
use crate::ln::channel_state::CounterpartyForwardingInfo;
use crate::ln::channelmanager::Verification;
use crate::ln::inbound_payment::ExpandedKey;
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};
use crate::types::features::BlindedHopFeatures;
use crate::types::payment::PaymentSecret;
use crate::types::routing::RoutingFees;
use crate::util::ser::{
FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable, WithoutLength,
Writeable, Writer,
};
use core::mem;
use core::ops::Deref;
#[allow(unused_imports)]
use crate::prelude::*;
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct BlindedPayInfo {
pub fee_base_msat: u32,
pub fee_proportional_millionths: u32,
pub cltv_expiry_delta: u16,
pub htlc_minimum_msat: u64,
pub htlc_maximum_msat: u64,
pub features: BlindedHopFeatures,
}
impl_writeable!(BlindedPayInfo, {
fee_base_msat,
fee_proportional_millionths,
cltv_expiry_delta,
htlc_minimum_msat,
htlc_maximum_msat,
features
});
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BlindedPaymentPath {
pub(super) inner_path: BlindedPath,
pub payinfo: BlindedPayInfo,
}
impl BlindedPaymentPath {
pub fn one_hop<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
payee_node_id: PublicKey, payee_tlvs: ReceiveTlvs, min_final_cltv_expiry_delta: u16,
entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()>
where
ES::Target: EntropySource,
{
let htlc_maximum_msat = u64::max_value();
Self::new(
&[],
payee_node_id,
payee_tlvs,
htlc_maximum_msat,
min_final_cltv_expiry_delta,
entropy_source,
secp_ctx,
)
}
pub fn new<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()>
where
ES::Target: EntropySource,
{
let introduction_node = IntroductionNode::NodeId(
intermediate_nodes.first().map_or(payee_node_id, |n| n.node_id),
);
let blinding_secret_bytes = entropy_source.get_secure_random_bytes();
let blinding_secret =
SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted");
let blinded_payinfo = compute_payinfo(
intermediate_nodes,
&payee_tlvs.tlvs,
htlc_maximum_msat,
min_final_cltv_expiry_delta,
)?;
Ok(Self {
inner_path: BlindedPath {
introduction_node,
blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret),
blinded_hops: blinded_hops(
secp_ctx,
intermediate_nodes,
payee_node_id,
payee_tlvs,
&blinding_secret,
),
},
payinfo: blinded_payinfo,
})
}
pub fn public_introduction_node_id<'a>(
&self, network_graph: &'a ReadOnlyNetworkGraph,
) -> Option<&'a NodeId> {
self.inner_path.public_introduction_node_id(network_graph)
}
pub fn introduction_node(&self) -> &IntroductionNode {
&self.inner_path.introduction_node
}
pub fn blinding_point(&self) -> PublicKey {
self.inner_path.blinding_point
}
pub fn blinded_hops(&self) -> &[BlindedHop] {
&self.inner_path.blinded_hops
}
pub fn advance_path_by_one<NS: Deref, NL: Deref, T>(
&mut self, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1<T>,
) -> Result<(), ()>
where
NS::Target: NodeSigner,
NL::Target: NodeIdLookUp,
T: secp256k1::Signing + secp256k1::Verification,
{
match self.decrypt_intro_payload::<NS>(node_signer) {
Ok((
BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }),
control_tlvs_ss,
)) => {
let next_node_id = match node_id_lookup.next_node_id(short_channel_id) {
Some(node_id) => node_id,
None => return Err(()),
};
let mut new_blinding_point = onion_utils::next_hop_pubkey(
secp_ctx,
self.inner_path.blinding_point,
control_tlvs_ss.as_ref(),
)
.map_err(|_| ())?;
mem::swap(&mut self.inner_path.blinding_point, &mut new_blinding_point);
self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id);
self.inner_path.blinded_hops.remove(0);
Ok(())
},
_ => Err(()),
}
}
pub(crate) fn decrypt_intro_payload<NS: Deref>(
&self, node_signer: &NS,
) -> Result<(BlindedPaymentTlvs, SharedSecret), ()>
where
NS::Target: NodeSigner,
{
let control_tlvs_ss =
node_signer.ecdh(Recipient::Node, &self.inner_path.blinding_point, None)?;
let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes());
let encrypted_control_tlvs =
&self.inner_path.blinded_hops.get(0).ok_or(())?.encrypted_payload;
let mut s = Cursor::new(encrypted_control_tlvs);
let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64);
match ChaChaPolyReadAdapter::read(&mut reader, rho) {
Ok(ChaChaPolyReadAdapter { readable, .. }) => Ok((readable, control_tlvs_ss)),
_ => Err(()),
}
}
pub(crate) fn inner_blinded_path(&self) -> &BlindedPath {
&self.inner_path
}
pub(crate) fn from_parts(inner_path: BlindedPath, payinfo: BlindedPayInfo) -> Self {
Self { inner_path, payinfo }
}
pub fn from_blinded_path_and_payinfo(
introduction_node_id: PublicKey, blinding_point: PublicKey, blinded_hops: Vec<BlindedHop>,
payinfo: BlindedPayInfo,
) -> Self {
Self::from_parts(
BlindedPath {
introduction_node: IntroductionNode::NodeId(introduction_node_id),
blinding_point,
blinded_hops,
},
payinfo,
)
}
#[cfg(test)]
pub fn clear_blinded_hops(&mut self) {
self.inner_path.blinded_hops.clear()
}
}
#[derive(Clone, Debug)]
pub struct PaymentForwardNode {
pub tlvs: ForwardTlvs,
pub node_id: PublicKey,
pub htlc_maximum_msat: u64,
}
#[derive(Clone, Debug)]
pub struct ForwardTlvs {
pub short_channel_id: u64,
pub payment_relay: PaymentRelay,
pub payment_constraints: PaymentConstraints,
pub features: BlindedHopFeatures,
pub next_blinding_override: Option<PublicKey>,
}
#[derive(Clone, Debug)]
pub struct TrampolineForwardTlvs {
pub next_trampoline: PublicKey,
pub payment_relay: PaymentRelay,
pub payment_constraints: PaymentConstraints,
pub features: BlindedHopFeatures,
pub next_blinding_override: Option<PublicKey>,
}
#[derive(Clone, Debug)]
pub struct ReceiveTlvs {
pub(crate) tlvs: UnauthenticatedReceiveTlvs,
pub(crate) authentication: (Hmac<Sha256>, Nonce),
}
impl ReceiveTlvs {
pub fn tlvs(&self) -> &UnauthenticatedReceiveTlvs {
&self.tlvs
}
}
#[derive(Clone, Debug)]
pub struct UnauthenticatedReceiveTlvs {
pub payment_secret: PaymentSecret,
pub payment_constraints: PaymentConstraints,
pub payment_context: PaymentContext,
}
impl UnauthenticatedReceiveTlvs {
pub fn authenticate(self, nonce: Nonce, expanded_key: &ExpandedKey) -> ReceiveTlvs {
ReceiveTlvs {
authentication: (self.hmac_for_offer_payment(nonce, expanded_key), nonce),
tlvs: self,
}
}
}
pub(crate) enum BlindedPaymentTlvs {
Forward(ForwardTlvs),
Receive(ReceiveTlvs),
}
pub(crate) enum BlindedTrampolineTlvs {
Forward(TrampolineForwardTlvs),
Receive(ReceiveTlvs),
}
enum BlindedPaymentTlvsRef<'a> {
Forward(&'a ForwardTlvs),
Receive(&'a ReceiveTlvs),
}
#[derive(Clone, Debug, PartialEq)]
pub struct PaymentRelay {
pub cltv_expiry_delta: u16,
pub fee_proportional_millionths: u32,
pub fee_base_msat: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PaymentConstraints {
pub max_cltv_expiry: u32,
pub htlc_minimum_msat: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PaymentContext {
Bolt12Offer(Bolt12OfferContext),
AsyncBolt12Offer(AsyncBolt12OfferContext),
Bolt12Refund(Bolt12RefundContext),
}
pub(crate) enum PaymentContextRef<'a> {
Bolt12Offer(&'a Bolt12OfferContext),
Bolt12Refund(&'a Bolt12RefundContext),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Bolt12OfferContext {
pub offer_id: OfferId,
pub invoice_request: InvoiceRequestFields,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AsyncBolt12OfferContext {
pub offer_nonce: Nonce,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Bolt12RefundContext {}
impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();
fn try_from(info: CounterpartyForwardingInfo) -> Result<Self, ()> {
let CounterpartyForwardingInfo {
fee_base_msat,
fee_proportional_millionths,
cltv_expiry_delta,
} = info;
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
}
}
impl Writeable for ForwardTlvs {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
let features_opt = if self.features == BlindedHopFeatures::empty() {
None
} else {
Some(WithoutLength(&self.features))
};
encode_tlv_stream!(w, {
(2, self.short_channel_id, required),
(10, self.payment_relay, required),
(12, self.payment_constraints, required),
(14, features_opt, option)
});
Ok(())
}
}
impl Writeable for TrampolineForwardTlvs {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
let features_opt = if self.features == BlindedHopFeatures::empty() {
None
} else {
Some(WithoutLength(&self.features))
};
encode_tlv_stream!(w, {
(4, self.next_trampoline, required),
(10, self.payment_relay, required),
(12, self.payment_constraints, required),
(14, features_opt, option)
});
Ok(())
}
}
impl Writeable for ReceiveTlvs {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
encode_tlv_stream!(w, {
(12, self.tlvs.payment_constraints, required),
(65536, self.tlvs.payment_secret, required),
(65537, self.tlvs.payment_context, required),
(65539, self.authentication, required),
});
Ok(())
}
}
impl Writeable for UnauthenticatedReceiveTlvs {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
encode_tlv_stream!(w, {
(12, self.payment_constraints, required),
(65536, self.payment_secret, required),
(65537, self.payment_context, required),
});
Ok(())
}
}
impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
match self {
Self::Forward(tlvs) => tlvs.write(w)?,
Self::Receive(tlvs) => tlvs.write(w)?,
}
Ok(())
}
}
impl Readable for BlindedPaymentTlvs {
fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
_init_and_read_tlv_stream!(r, {
(2, scid, option),
(8, next_blinding_override, option),
(10, payment_relay, option),
(12, payment_constraints, required),
(14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))),
(65536, payment_secret, option),
(65537, payment_context, option),
(65539, authentication, option),
});
if let Some(short_channel_id) = scid {
if payment_secret.is_some() {
return Err(DecodeError::InvalidValue);
}
Ok(BlindedPaymentTlvs::Forward(ForwardTlvs {
short_channel_id,
payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?,
payment_constraints: payment_constraints.0.unwrap(),
next_blinding_override,
features: features.unwrap_or_else(BlindedHopFeatures::empty),
}))
} else {
if payment_relay.is_some() || features.is_some() {
return Err(DecodeError::InvalidValue);
}
Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs {
tlvs: UnauthenticatedReceiveTlvs {
payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?,
payment_constraints: payment_constraints.0.unwrap(),
payment_context: payment_context.ok_or(DecodeError::InvalidValue)?,
},
authentication: authentication.ok_or(DecodeError::InvalidValue)?,
}))
}
}
}
impl Readable for BlindedTrampolineTlvs {
fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
_init_and_read_tlv_stream!(r, {
(4, next_trampoline, option),
(8, next_blinding_override, option),
(10, payment_relay, option),
(12, payment_constraints, required),
(14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))),
(65536, payment_secret, option),
(65537, payment_context, option),
(65539, authentication, option),
});
if let Some(next_trampoline) = next_trampoline {
if payment_secret.is_some() {
return Err(DecodeError::InvalidValue);
}
Ok(BlindedTrampolineTlvs::Forward(TrampolineForwardTlvs {
next_trampoline,
payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?,
payment_constraints: payment_constraints.0.unwrap(),
next_blinding_override,
features: features.unwrap_or_else(BlindedHopFeatures::empty),
}))
} else {
if payment_relay.is_some() || features.is_some() {
return Err(DecodeError::InvalidValue);
}
Ok(BlindedTrampolineTlvs::Receive(ReceiveTlvs {
tlvs: UnauthenticatedReceiveTlvs {
payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?,
payment_constraints: payment_constraints.0.unwrap(),
payment_context: payment_context.ok_or(DecodeError::InvalidValue)?,
},
authentication: authentication.ok_or(DecodeError::InvalidValue)?,
}))
}
}
}
pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30;
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
payee_tlvs: ReceiveTlvs, session_priv: &SecretKey,
) -> Vec<BlindedHop> {
let pks = intermediate_nodes
.iter()
.map(|node| (node.node_id, None))
.chain(core::iter::once((payee_node_id, None)));
let tlvs = intermediate_nodes
.iter()
.map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs))
.chain(core::iter::once(BlindedPaymentTlvsRef::Receive(&payee_tlvs)));
let path = pks.zip(
tlvs.map(|tlv| BlindedPathWithPadding { tlvs: tlv, round_off: PAYMENT_PADDING_ROUND_OFF }),
);
utils::construct_blinded_hops(secp_ctx, path, session_priv)
}
pub(crate) fn amt_to_forward_msat(
inbound_amt_msat: u64, payment_relay: &PaymentRelay,
) -> Option<u64> {
let inbound_amt = inbound_amt_msat as u128;
let base = payment_relay.fee_base_msat as u128;
let prop = payment_relay.fee_proportional_millionths as u128;
let post_base_fee_inbound_amt =
if let Some(amt) = inbound_amt.checked_sub(base) { amt } else { return None };
let mut amt_to_forward =
(post_base_fee_inbound_amt * 1_000_000 + 1_000_000 + prop - 1) / (prop + 1_000_000);
let fee = ((amt_to_forward * prop) / 1_000_000) + base;
if inbound_amt - fee < amt_to_forward {
amt_to_forward -= 1;
}
debug_assert_eq!(amt_to_forward + fee, inbound_amt);
u64::try_from(amt_to_forward).ok()
}
pub(crate) fn compute_aggregated_base_prop_fee<I>(hops_fees: I) -> Result<(u64, u64), ()>
where
I: DoubleEndedIterator<Item = RoutingFees>,
{
let mut curr_base_fee: u64 = 0;
let mut curr_prop_mil: u64 = 0;
for fees in hops_fees.rev() {
let next_base_fee = fees.base_msat as u64;
let next_prop_mil = fees.proportional_millionths as u64;
curr_base_fee = curr_base_fee
.checked_mul(1_000_000 + next_prop_mil)
.and_then(|f| f.checked_add(1_000_000 - 1))
.map(|f| f / 1_000_000)
.and_then(|f| f.checked_add(next_base_fee))
.ok_or(())?;
curr_prop_mil = curr_prop_mil
.checked_add(1_000_000)
.and_then(|f1| next_prop_mil.checked_add(1_000_000).and_then(|f2| f2.checked_mul(f1)))
.and_then(|f| f.checked_add(1_000_000 - 1))
.map(|f| f / 1_000_000)
.and_then(|f| f.checked_sub(1_000_000))
.ok_or(())?;
}
Ok((curr_base_fee, curr_prop_mil))
}
pub(super) fn compute_payinfo(
intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &UnauthenticatedReceiveTlvs,
payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
) -> Result<BlindedPayInfo, ()> {
let (aggregated_base_fee, aggregated_prop_fee) =
compute_aggregated_base_prop_fee(intermediate_nodes.iter().map(|node| RoutingFees {
base_msat: node.tlvs.payment_relay.fee_base_msat,
proportional_millionths: node.tlvs.payment_relay.fee_proportional_millionths,
}))?;
let mut htlc_minimum_msat: u64 = 1;
let mut htlc_maximum_msat: u64 = 21_000_000 * 100_000_000 * 1_000; let mut cltv_expiry_delta: u16 = min_final_cltv_expiry_delta;
for node in intermediate_nodes.iter() {
if node.tlvs.features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) {
return Err(());
}
cltv_expiry_delta =
cltv_expiry_delta.checked_add(node.tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
htlc_minimum_msat = amt_to_forward_msat(
core::cmp::max(node.tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat),
&node.tlvs.payment_relay,
)
.unwrap_or(1); htlc_maximum_msat = amt_to_forward_msat(
core::cmp::min(node.htlc_maximum_msat, htlc_maximum_msat),
&node.tlvs.payment_relay,
)
.ok_or(())?; }
htlc_minimum_msat =
core::cmp::max(payee_tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat);
htlc_maximum_msat = core::cmp::min(payee_htlc_maximum_msat, htlc_maximum_msat);
if htlc_maximum_msat < htlc_minimum_msat {
return Err(());
}
Ok(BlindedPayInfo {
fee_base_msat: u32::try_from(aggregated_base_fee).map_err(|_| ())?,
fee_proportional_millionths: u32::try_from(aggregated_prop_fee).map_err(|_| ())?,
cltv_expiry_delta,
htlc_minimum_msat,
htlc_maximum_msat,
features: BlindedHopFeatures::empty(),
})
}
impl Writeable for PaymentRelay {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
self.cltv_expiry_delta.write(w)?;
self.fee_proportional_millionths.write(w)?;
HighZeroBytesDroppedBigSize(self.fee_base_msat).write(w)
}
}
impl Readable for PaymentRelay {
fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
let cltv_expiry_delta: u16 = Readable::read(r)?;
let fee_proportional_millionths: u32 = Readable::read(r)?;
let fee_base_msat: HighZeroBytesDroppedBigSize<u32> = Readable::read(r)?;
Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat: fee_base_msat.0 })
}
}
impl Writeable for PaymentConstraints {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
self.max_cltv_expiry.write(w)?;
HighZeroBytesDroppedBigSize(self.htlc_minimum_msat).write(w)
}
}
impl Readable for PaymentConstraints {
fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
let max_cltv_expiry: u32 = Readable::read(r)?;
let htlc_minimum_msat: HighZeroBytesDroppedBigSize<u64> = Readable::read(r)?;
Ok(Self { max_cltv_expiry, htlc_minimum_msat: htlc_minimum_msat.0 })
}
}
impl_writeable_tlv_based_enum_legacy!(PaymentContext,
;
(1, Bolt12Offer),
(2, Bolt12Refund),
(3, AsyncBolt12Offer),
);
impl<'a> Writeable for PaymentContextRef<'a> {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
match self {
PaymentContextRef::Bolt12Offer(context) => {
1u8.write(w)?;
context.write(w)?;
},
PaymentContextRef::Bolt12Refund(context) => {
2u8.write(w)?;
context.write(w)?;
},
}
Ok(())
}
}
impl_writeable_tlv_based!(Bolt12OfferContext, {
(0, offer_id, required),
(2, invoice_request, required),
});
impl_writeable_tlv_based!(AsyncBolt12OfferContext, {
(0, offer_nonce, required),
});
impl_writeable_tlv_based!(Bolt12RefundContext, {});
#[cfg(test)]
mod tests {
use crate::blinded_path::payment::{
Bolt12RefundContext, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentForwardNode,
PaymentRelay, UnauthenticatedReceiveTlvs,
};
use crate::ln::functional_test_utils::TEST_FINAL_CLTV;
use crate::types::features::BlindedHopFeatures;
use crate::types::payment::PaymentSecret;
use bitcoin::secp256k1::PublicKey;
#[test]
fn compute_payinfo() {
let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
let intermediate_nodes = [
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 144,
fee_proportional_millionths: 500,
fee_base_msat: 100,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 100,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: u64::max_value(),
},
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 144,
fee_proportional_millionths: 500,
fee_base_msat: 100,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 1_000,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: u64::max_value(),
},
];
let recv_tlvs = UnauthenticatedReceiveTlvs {
payment_secret: PaymentSecret([0; 32]),
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
};
let htlc_maximum_msat = 100_000;
let blinded_payinfo =
super::compute_payinfo(&intermediate_nodes[..], &recv_tlvs, htlc_maximum_msat, 12)
.unwrap();
assert_eq!(blinded_payinfo.fee_base_msat, 201);
assert_eq!(blinded_payinfo.fee_proportional_millionths, 1001);
assert_eq!(blinded_payinfo.cltv_expiry_delta, 300);
assert_eq!(blinded_payinfo.htlc_minimum_msat, 900);
assert_eq!(blinded_payinfo.htlc_maximum_msat, htlc_maximum_msat);
}
#[test]
fn compute_payinfo_1_hop() {
let recv_tlvs = UnauthenticatedReceiveTlvs {
payment_secret: PaymentSecret([0; 32]),
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
};
let blinded_payinfo =
super::compute_payinfo(&[], &recv_tlvs, 4242, TEST_FINAL_CLTV as u16).unwrap();
assert_eq!(blinded_payinfo.fee_base_msat, 0);
assert_eq!(blinded_payinfo.fee_proportional_millionths, 0);
assert_eq!(blinded_payinfo.cltv_expiry_delta, TEST_FINAL_CLTV as u16);
assert_eq!(blinded_payinfo.htlc_minimum_msat, 1);
assert_eq!(blinded_payinfo.htlc_maximum_msat, 4242);
}
#[test]
fn simple_aggregated_htlc_min() {
let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
let intermediate_nodes = [
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 0,
fee_proportional_millionths: 0,
fee_base_msat: 0,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 1,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: u64::max_value(),
},
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 0,
fee_proportional_millionths: 0,
fee_base_msat: 0,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 2_000,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: u64::max_value(),
},
];
let recv_tlvs = UnauthenticatedReceiveTlvs {
payment_secret: PaymentSecret([0; 32]),
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 3 },
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
};
let htlc_maximum_msat = 100_000;
let blinded_payinfo = super::compute_payinfo(
&intermediate_nodes[..],
&recv_tlvs,
htlc_maximum_msat,
TEST_FINAL_CLTV as u16,
)
.unwrap();
assert_eq!(blinded_payinfo.htlc_minimum_msat, 2_000);
}
#[test]
fn aggregated_htlc_min() {
let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
let intermediate_nodes = [
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 0,
fee_proportional_millionths: 500,
fee_base_msat: 1_000,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 5_000,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: u64::max_value(),
},
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 0,
fee_proportional_millionths: 500,
fee_base_msat: 200,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 2_000,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: u64::max_value(),
},
];
let recv_tlvs = UnauthenticatedReceiveTlvs {
payment_secret: PaymentSecret([0; 32]),
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
};
let htlc_minimum_msat = 3798;
assert!(super::compute_payinfo(
&intermediate_nodes[..],
&recv_tlvs,
htlc_minimum_msat - 1,
TEST_FINAL_CLTV as u16
)
.is_err());
let htlc_maximum_msat = htlc_minimum_msat + 1;
let blinded_payinfo = super::compute_payinfo(
&intermediate_nodes[..],
&recv_tlvs,
htlc_maximum_msat,
TEST_FINAL_CLTV as u16,
)
.unwrap();
assert_eq!(blinded_payinfo.htlc_minimum_msat, htlc_minimum_msat);
assert_eq!(blinded_payinfo.htlc_maximum_msat, htlc_maximum_msat);
}
#[test]
fn aggregated_htlc_max() {
let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
let intermediate_nodes = [
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 0,
fee_proportional_millionths: 500,
fee_base_msat: 1_000,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 1,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: 5_000,
},
PaymentForwardNode {
node_id: dummy_pk,
tlvs: ForwardTlvs {
short_channel_id: 0,
payment_relay: PaymentRelay {
cltv_expiry_delta: 0,
fee_proportional_millionths: 500,
fee_base_msat: 1,
},
payment_constraints: PaymentConstraints {
max_cltv_expiry: 0,
htlc_minimum_msat: 1,
},
next_blinding_override: None,
features: BlindedHopFeatures::empty(),
},
htlc_maximum_msat: 10_000,
},
];
let recv_tlvs = UnauthenticatedReceiveTlvs {
payment_secret: PaymentSecret([0; 32]),
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
};
let blinded_payinfo = super::compute_payinfo(
&intermediate_nodes[..],
&recv_tlvs,
10_000,
TEST_FINAL_CLTV as u16,
)
.unwrap();
assert_eq!(blinded_payinfo.htlc_maximum_msat, 3997);
}
}