From cd8952d94deb8731a68aadc3d299f056ed54e73c Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 20 Jun 2023 20:27:57 -0400 Subject: [PATCH] Compute aggregated BlindedPayInfo in path construction --- lightning/src/blinded_path/mod.rs | 13 ++- lightning/src/blinded_path/payment.rs | 127 +++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 6 deletions(-) diff --git a/lightning/src/blinded_path/mod.rs b/lightning/src/blinded_path/mod.rs index c0a9036a31a..d2a8c8acdbe 100644 --- a/lightning/src/blinded_path/mod.rs +++ b/lightning/src/blinded_path/mod.rs @@ -16,8 +16,9 @@ pub(crate) mod utils; use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use crate::blinded_path::payment::BlindedPaymentTlvs; -use crate::sign::EntropySource; +use crate::offers::invoice::BlindedPayInfo; use crate::ln::msgs::DecodeError; +use crate::sign::EntropySource; use crate::util::ser::{Readable, Writeable, Writer}; use crate::io; @@ -79,20 +80,22 @@ impl BlindedPath { /// Create a blinded path for a payment, to be forwarded along `path`. The last node /// in `path` will be the destination node. /// - /// Errors if `path` is empty or a node id in `path` is invalid. + /// Errors if `path` is empty, a node id in `path` is invalid, or [`BlindedPayInfo`] calculation + /// results in an integer overflow. // TODO: make all payloads the same size with padding + add dummy hops pub fn new_for_payment( path: &[(PublicKey, BlindedPaymentTlvs)], entropy_source: &ES, secp_ctx: &Secp256k1 - ) -> Result { + ) -> Result<(BlindedPayInfo, Self), ()> { if path.len() < 1 { return Err(()) } let blinding_secret_bytes = entropy_source.get_secure_random_bytes(); let blinding_secret = SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted"); - Ok(BlindedPath { + let blinded_payinfo = payment::compute_payinfo(path)?; + Ok((blinded_payinfo, BlindedPath { introduction_node_id: path[0].0, blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret), blinded_hops: payment::blinded_hops(secp_ctx, path, &blinding_secret).map_err(|_| ())?, - }) + })) } } diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 3e8e26b472a..fb22a1026bd 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -3,13 +3,14 @@ //! [`BlindedPath`]: crate::blinded_path::BlindedPath use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; - +use core::convert::TryFrom; use crate::blinded_path::BlindedHop; use crate::blinded_path::utils; use crate::io; use crate::ln::PaymentSecret; use crate::ln::features::BlindedHopFeatures; use crate::ln::msgs::DecodeError; +use crate::offers::invoice::BlindedPayInfo; use crate::prelude::*; use crate::util::ser::{Readable, Writeable, Writer}; @@ -45,6 +46,33 @@ pub enum BlindedPaymentTlvs { }, } +impl BlindedPaymentTlvs { + fn fee_base_msat(&self) -> u32 { + match self { + Self::Forward { payment_relay, .. } => payment_relay.fee_base_msat, + _ => 0, + } + } + fn fee_proportional_millionths(&self) -> u32 { + match self { + Self::Forward { payment_relay, .. } => payment_relay.fee_proportional_millionths, + _ => 0, + } + } + fn cltv_expiry_delta(&self) -> u16 { + match self { + Self::Forward { payment_relay, .. } => payment_relay.cltv_expiry_delta, + _ => 0, + } + } + fn htlc_minimum_msat(&self) -> u64 { + match self { + Self::Forward { payment_constraints, .. } | Self::Receive { payment_constraints, .. } => + payment_constraints.htlc_minimum_msat, + } + } +} + /// Parameters for relaying over a given [`BlindedHop`]. /// /// [`BlindedHop`]: crate::blinded_path::BlindedHop @@ -148,6 +176,34 @@ pub(super) fn blinded_hops( Ok(blinded_hops) } +pub(super) fn compute_payinfo( + path: &[(PublicKey, BlindedPaymentTlvs)] +) -> Result { + let mut curr_base_fee: u128 = 0; + let mut curr_prop_mil: u128 = 0; + for (_, payment_tlvs) in path.iter().rev().skip(1) { + let next_base_fee = payment_tlvs.fee_base_msat() as u128; + let next_prop_mil = payment_tlvs.fee_proportional_millionths() as u128; + curr_base_fee = + ((next_base_fee * 1_000_000 + (curr_base_fee * (1_000_000 + next_prop_mil))) + 1_000_000 - 1) + / 1_000_000; + curr_prop_mil = + (((curr_prop_mil + next_prop_mil) * 1_000_000 + curr_prop_mil * next_prop_mil) + 1_000_000 - 1) + / 1_000_000; + } + Ok(BlindedPayInfo { + fee_base_msat: u32::try_from(curr_base_fee).map_err(|_| ())?, + fee_proportional_millionths: u32::try_from(curr_prop_mil).map_err(|_| ())?, + cltv_expiry_delta: path.iter().map(|(_, tlvs)| tlvs.cltv_expiry_delta()) + .try_fold(0u16, |acc, delta| acc.checked_add(delta)).ok_or(())?, + htlc_minimum_msat: path.iter().map(|(_, tlvs)| tlvs.htlc_minimum_msat()).max().unwrap_or(0), + // TODO: this field isn't present in route blinding encrypted data + htlc_maximum_msat: 21_000_000 * 1_0000_0000, // Total bitcoin supply + // TODO: when there are blinded hop features, take the subset of them here + features: BlindedHopFeatures::empty(), + }) +} + impl_writeable_msg!(PaymentRelay, { cltv_expiry_delta, fee_proportional_millionths, @@ -158,3 +214,72 @@ impl_writeable_msg!(PaymentConstraints, { max_cltv_expiry, htlc_minimum_msat }, {}); + +#[cfg(test)] +mod tests { + use bitcoin::secp256k1::PublicKey; + use crate::blinded_path::payment::{BlindedPaymentTlvs, PaymentConstraints, PaymentRelay}; + use crate::ln::PaymentSecret; + use crate::ln::features::BlindedHopFeatures; + + #[test] + fn compute_payinfo() { + // Taken from the spec example for aggregating blinded payment info. + let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap(); + let path = vec![(dummy_pk, BlindedPaymentTlvs::Forward { + 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, + }, + features: BlindedHopFeatures::empty(), + }), (dummy_pk, BlindedPaymentTlvs::Forward { + 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, + }, + features: BlindedHopFeatures::empty(), + }), (dummy_pk, BlindedPaymentTlvs::Receive { + payment_secret: PaymentSecret([0; 32]), + payment_constraints: PaymentConstraints { + max_cltv_expiry: 0, + htlc_minimum_msat: 1, + }, + features: BlindedHopFeatures::empty(), + })]; + let blinded_payinfo = super::compute_payinfo(&path[..]).unwrap(); + assert_eq!(blinded_payinfo.fee_base_msat, 201); + assert_eq!(blinded_payinfo.fee_proportional_millionths, 1001); + assert_eq!(blinded_payinfo.cltv_expiry_delta, 288); + assert_eq!(blinded_payinfo.htlc_minimum_msat, 1_000); + } + + #[test] + fn compute_payinfo_1_hop() { + let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap(); + let path = vec![(dummy_pk, BlindedPaymentTlvs::Receive { + payment_secret: PaymentSecret([0; 32]), + payment_constraints: PaymentConstraints { + max_cltv_expiry: 0, + htlc_minimum_msat: 1, + }, + features: BlindedHopFeatures::empty(), + })]; + let blinded_payinfo = super::compute_payinfo(&path[..]).unwrap(); + assert_eq!(blinded_payinfo.fee_base_msat, 0); + assert_eq!(blinded_payinfo.fee_proportional_millionths, 0); + assert_eq!(blinded_payinfo.cltv_expiry_delta, 0); + assert_eq!(blinded_payinfo.htlc_minimum_msat, 1); + } +}