From 5e4d49be196d2a86ad818c02794e5c9cd2f20306 Mon Sep 17 00:00:00 2001 From: Evan Feenstra Date: Sat, 28 Oct 2023 15:10:00 -0700 Subject: [PATCH] public onion utils, create/peel payment --- lightning/src/ln/mod.rs | 1 + lightning/src/ln/msgs.rs | 14 +- lightning/src/ln/onion_utils.rs | 292 ++++++++++++++++++++++++++++- lightning/src/onion_message/mod.rs | 1 + 4 files changed, 301 insertions(+), 7 deletions(-) diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index beefd2d463b..445cf68f139 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -39,6 +39,7 @@ pub(crate) mod onion_utils; mod outbound_payment; pub mod wire; +pub use onion_utils::{ForwardedPayment, PeeledPayment, ReceivedPayment, create_payment_onion, peel_payment_onion}; // Older rustc (which we support) refuses to let us call the get_payment_preimage_hash!() macro // without the node parameter being mut. This is incorrect, and thus newer rustcs will complain // about an unnecessary mut. Thus, we silence the unused_mut warning in two test modules below. diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 6043bf35b99..fac1b68d9a3 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -1674,17 +1674,21 @@ pub use self::fuzzy_internal_msgs::*; #[cfg(not(fuzzing))] pub(crate) use self::fuzzy_internal_msgs::*; +/// Bolt04 OnionPacket including hop data for the next peer #[derive(Clone)] -pub(crate) struct OnionPacket { - pub(crate) version: u8, +pub struct OnionPacket { + /// Bolt 04 version number + pub version: u8, /// In order to ensure we always return an error on onion decode in compliance with [BOLT /// #4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md), we have to /// deserialize `OnionPacket`s contained in [`UpdateAddHTLC`] messages even if the ephemeral /// public key (here) is bogus, so we hold a [`Result`] instead of a [`PublicKey`] as we'd /// like. - pub(crate) public_key: Result, - pub(crate) hop_data: [u8; 20*65], - pub(crate) hmac: [u8; 32], + pub public_key: Result, + /// 1300 bytes encrypted payload for the next hop + pub hop_data: [u8; 20*65], + /// HMAC to verify the integrity of hop_data + pub hmac: [u8; 32], } impl onion_utils::Packet for OnionPacket { diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 9af3de07ff4..39d9793212f 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -13,7 +13,7 @@ use crate::ln::msgs; use crate::ln::wire::Encode; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop}; -use crate::sign::NodeSigner; +use crate::sign::{NodeSigner, Recipient}; use crate::util::chacha20::{ChaCha20, ChaChaReader}; use crate::util::errors::{self, APIError}; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer, LengthCalculatingWriter}; @@ -935,6 +935,180 @@ pub(crate) fn decode_next_payment_hop( } } +/// Build a payment onion, returning the first hop msat and cltv values as well. +pub fn create_payment_onion( + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, total_msat: u64, + recipient_onion: RecipientOnionFields, best_block_height: u32, payment_hash: PaymentHash, + keysend_preimage: Option, prng_seed: [u8; 32] +) -> Result<(u64, u32, msgs::OnionPacket), ()> +where + T: secp256k1::Signing +{ + let onion_keys = construct_onion_keys(&secp_ctx, &path, &session_priv).map_err(|_| ())?; + let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( + &path, + total_msat, + recipient_onion, + best_block_height + 1, + &keysend_preimage, + ).map_err(|_| ())?; + let onion_packet = construct_onion_packet( + onion_payloads, onion_keys, prng_seed, &payment_hash + )?; + Ok((htlc_msat, htlc_cltv, onion_packet)) +} + +/// Forwarded Payment, including next hop details. +pub struct ForwardedPayment { + /// Short channel id of the next hop. + pub short_channel_id: u64, + /// The value, in msat, of the payment after this hop's fee is deducted. + pub amt_to_forward: u64, + /// Outgoing CLTV for the next hop. + pub outgoing_cltv_value: u32, + /// Onion packet for the next hop. + pub onion_packet: msgs::OnionPacket, +} + +/// Received payment, of either regular or blinded type. +pub enum ReceivedPayment { + /// Regular (unblinded) payment. + Unblinded { + /// Payment_secret to authenticate sender to the receiver. + payment_secret: Option<[u8; 32]>, + /// The total value, in msat, of the payment as received by the ultimate recipient. + total_msat: Option, + /// Custom payment metadata included in the payment. + payment_metadata: Option>, + /// Preimage used in spontaneous payment. + keysend_preimage: Option<[u8; 32]>, + /// Custom TLV records included in the payment. + custom_tlvs: Vec<(u64, Vec)>, + /// Amount received. + amt_msat: u64, + /// CLTV expiration. + cltv_expiry: u32, + }, + /// Blinded payment + Blinded { + /// Amount received. + amt_msat: u64, + /// Amount received plus fees paid. + total_msat: u64, + /// CLTV expiration. + cltv_expiry: u32, + /// Payment secret. + payment_secret: [u8; 32], + /// The maximum total CLTV that is acceptable when relaying a payment over this hop. + max_cltv_expiry: u32, + /// The minimum value, in msat, that may be accepted by the node corresponding to this hop. + htlc_minimum_msat: u64, + /// Blinding point from intro node. + intro_node_blinding_point: PublicKey, + } +} + +impl core::convert::TryFrom for ReceivedPayment { + type Error = (); + fn try_from(pld: msgs::InboundOnionPayload) -> Result { + match pld { + msgs::InboundOnionPayload::Forward { + short_channel_id: _, amt_to_forward: _, outgoing_cltv_value: _ + } => { + Err(()) + }, + msgs::InboundOnionPayload::Receive { + payment_data, payment_metadata, keysend_preimage, custom_tlvs, amt_msat, + outgoing_cltv_value + } => { + let (payment_secret, total_msat) = match payment_data { + Some(p) => (Some(p.payment_secret.0), Some(p.total_msat)), + None => (None, None), + }; + let keysend_preimage = keysend_preimage.map(|p| p.0); + Ok(Self::Unblinded { + payment_secret, total_msat, payment_metadata, keysend_preimage, custom_tlvs, + amt_msat, cltv_expiry: outgoing_cltv_value + }) + }, + msgs::InboundOnionPayload::BlindedReceive { + amt_msat, total_msat, outgoing_cltv_value, payment_secret, payment_constraints, + intro_node_blinding_point + } => { + let payment_secret = payment_secret.0; + let max_cltv_expiry = payment_constraints.max_cltv_expiry; + let htlc_minimum_msat = payment_constraints.htlc_minimum_msat; + Ok(Self::Blinded { + amt_msat, total_msat, cltv_expiry: outgoing_cltv_value, payment_secret, + max_cltv_expiry, htlc_minimum_msat, intro_node_blinding_point + }) + } + } + } +} + +/// Received and decrypted onion payment, either of type Receive (for us), or Forward. +pub enum PeeledPayment { + /// This onion payload was for us, not for forwarding to a next-hop. + Receive(ReceivedPayment), + /// This onion payload to be forwarded to next peer. + Forward(ForwardedPayment), +} + +/// Unwrap one layer of an incoming HTLC, returning either or a received payment, or a another +/// onion to forward. +pub fn peel_payment_onion( + onion: &msgs::OnionPacket, payment_hash: PaymentHash, node_signer: &NS, + secp_ctx: &Secp256k1 +) -> Result +where + NS::Target: NodeSigner, +{ + if onion.public_key.is_err() { + return Err(()); + } + let shared_secret = node_signer + .ecdh(Recipient::Node, &onion.public_key.unwrap(), None) + .unwrap() + .secret_bytes(); + + let hop = decode_next_payment_hop( + shared_secret, &onion.hop_data[..], onion.hmac, payment_hash, node_signer + ).map_err(|_| ())?; + let peeled = match hop { + Hop::Forward { next_hop_data, next_hop_hmac, new_packet_bytes } => { + if let msgs::InboundOnionPayload::Forward{ + short_channel_id, amt_to_forward, outgoing_cltv_value + } = next_hop_data { + + let next_packet_pk = next_hop_pubkey( + secp_ctx, onion.public_key.unwrap(), &shared_secret + ); + + let onion_packet = msgs::OnionPacket { + version: 0, + public_key: next_packet_pk, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + PeeledPayment::Forward(ForwardedPayment{ + short_channel_id, + amt_to_forward, + outgoing_cltv_value, + onion_packet, + }) + } else { + return Err(()); + } + }, + Hop::Receive(inbound) => { + let payload: ReceivedPayment = inbound.try_into().map_err(|_| ())?; + PeeledPayment::Receive(payload) + }, + }; + Ok(peeled) +} + pub(crate) fn decode_next_untagged_hop, N: NextPacketBytes>(shared_secret: [u8; 32], hop_data: &[u8], hmac_bytes: [u8; 32], read_args: T) -> Result<(R, Option<([u8; 32], N)>), OnionDecodeErr> { decode_next_hop(shared_secret, hop_data, hmac_bytes, None, read_args) } @@ -1019,7 +1193,8 @@ fn decode_next_hop, N: NextPacketBytes>(shared_secret: [u8 #[cfg(test)] mod tests { use crate::io; - use crate::prelude::*; + use crate::ln::{PaymentPreimage, PaymentSecret}; +use crate::prelude::*; use crate::ln::PaymentHash; use crate::ln::features::{ChannelFeatures, NodeFeatures}; use crate::routing::router::{Path, Route, RouteHop}; @@ -1235,4 +1410,117 @@ mod tests { writer.write_all(&self.data[..]) } } + + use crate::ln::channelmanager::RecipientOnionFields; + use crate::sign::KeysManager; + use super::{create_payment_onion, peel_payment_onion}; + use super::{PeeledPayment, ReceivedPayment}; + + fn payment_onion_args(hop_pk: PublicKey, recipient_pk: PublicKey) -> ( + SecretKey, u64, u32, RecipientOnionFields, PaymentPreimage, PaymentHash, [u8; 32], + Vec, u64, PaymentSecret, + ) { + use core::convert::TryInto; + use super::{Sha256, Hash}; + + let session_priv_bytes = [42; 32]; + let session_priv = SecretKey::from_slice(&session_priv_bytes).unwrap(); + let total_amt_msat = 1000; + let cur_height = 1000; + let pay_secret = PaymentSecret([99; 32]); + let recipient_onion = RecipientOnionFields::secret_only(pay_secret); + let preimage_bytes = [43; 32]; + let preimage = PaymentPreimage(preimage_bytes); + let rhash = Sha256::hash(&preimage_bytes).to_vec(); + let rhash_bytes: [u8; 32] = rhash.try_into().unwrap(); + let payment_hash = PaymentHash(rhash_bytes); + let prng_seed = [44; 32]; + + // make a route alice -> bob -> charlie + let hop_fee = 1; + let recipient_amount = total_amt_msat - hop_fee; + let hops = vec![ + RouteHop { + pubkey: hop_pk, + fee_msat: hop_fee, + cltv_expiry_delta: 0, + short_channel_id: 1, + node_features: NodeFeatures::empty(), + channel_features: ChannelFeatures::empty(), + maybe_announced_channel: false, + }, + RouteHop { + pubkey: recipient_pk, + fee_msat: recipient_amount, + cltv_expiry_delta: 0, + short_channel_id: 2, + node_features: NodeFeatures::empty(), + channel_features: ChannelFeatures::empty(), + maybe_announced_channel: false, + } + ]; + + (session_priv, total_amt_msat, cur_height, recipient_onion, preimage, payment_hash, + prng_seed, hops, recipient_amount, pay_secret) + } + + #[test] + fn create_and_peel_unblinded_payment() { + let secp_ctx = Secp256k1::new(); + + // let alice = make_keys_manager(&[1; 32]); + let bob = make_keys_manager(&[2; 32]); + let bob_pk = PublicKey::from_secret_key(&secp_ctx, &bob.get_node_secret_key()); + let charlie = make_keys_manager(&[3; 32]); + let charlie_pk = PublicKey::from_secret_key(&secp_ctx, &charlie.get_node_secret_key()); + + let (session_priv, total_amt_msat, cur_height, recipient_onion, preimage, payment_hash, + prng_seed, hops, recipient_amount, pay_secret) = payment_onion_args(bob_pk, charlie_pk); + + let path = Path { + hops: hops, + blinded_tail: None, + }; + + let (_htlc_msat, _htlc_cltv, onion) = create_payment_onion( + &secp_ctx, &path, &session_priv, total_amt_msat, recipient_onion, cur_height, + payment_hash, Some(preimage), prng_seed + ).unwrap(); + + // bob peels to find another onion + let next_onion = match peel_payment_onion(&onion, payment_hash, &&bob, &secp_ctx).unwrap() { + PeeledPayment::Receive(_) => panic!("should not be Receive"), + PeeledPayment::Forward(forwarded) => forwarded.onion_packet, + }; + + // charlie peels to find a received payment + let received = match peel_payment_onion( + &next_onion, payment_hash, &&charlie, &secp_ctx + ).unwrap() { + PeeledPayment::Receive(received) => received, + PeeledPayment::Forward(_) => panic!("should not be a Forward"), + }; + + match received { + ReceivedPayment::Unblinded{ + amt_msat, keysend_preimage, payment_secret, total_msat, payment_metadata, + custom_tlvs, cltv_expiry + } => { + assert_eq!(amt_msat, recipient_amount); + assert_eq!(keysend_preimage, Some(preimage.0)); + assert_eq!(payment_secret, Some(pay_secret.0)); + assert_eq!(total_msat, Some(total_amt_msat)); + assert_eq!(payment_metadata, None); + assert_eq!(custom_tlvs, Vec::new()); + assert_eq!(cltv_expiry, cur_height + 1); + }, + ReceivedPayment::Blinded { .. } => panic!("Should not be Blinded"), + } + + } + + fn make_keys_manager(seed: &[u8; 32]) -> KeysManager { + KeysManager::new(seed, 42, 42) + } + } diff --git a/lightning/src/onion_message/mod.rs b/lightning/src/onion_message/mod.rs index ff6e0cd8e5d..d106a542fd3 100644 --- a/lightning/src/onion_message/mod.rs +++ b/lightning/src/onion_message/mod.rs @@ -28,6 +28,7 @@ mod functional_tests; // Re-export structs so they can be imported with just the `onion_message::` module prefix. pub use self::messenger::{CustomOnionMessageHandler, DefaultMessageRouter, Destination, MessageRouter, OnionMessageContents, OnionMessagePath, OnionMessenger, PeeledOnion, PendingOnionMessage, SendError}; +pub use self::messenger::{create_onion_message, peel_onion_message}; #[cfg(not(c_bindings))] pub use self::messenger::{SimpleArcOnionMessenger, SimpleRefOnionMessenger}; pub use self::offers::{OffersMessage, OffersMessageHandler};