Skip to content

Commit

Permalink
public onion utils, create/peel payment
Browse files Browse the repository at this point in the history
  • Loading branch information
Evanfeenstra committed Oct 29, 2023
1 parent d2242f6 commit 5e4d49b
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 7 deletions.
1 change: 1 addition & 0 deletions lightning/src/ln/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 9 additions & 5 deletions lightning/src/ln/msgs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PublicKey, secp256k1::Error>,
pub(crate) hop_data: [u8; 20*65],
pub(crate) hmac: [u8; 32],
pub public_key: Result<PublicKey, secp256k1::Error>,
/// 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 {
Expand Down
292 changes: 290 additions & 2 deletions lightning/src/ln/onion_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -935,6 +935,180 @@ pub(crate) fn decode_next_payment_hop<NS: Deref>(
}
}

/// Build a payment onion, returning the first hop msat and cltv values as well.
pub fn create_payment_onion<T>(
secp_ctx: &Secp256k1<T>, path: &Path, session_priv: &SecretKey, total_msat: u64,
recipient_onion: RecipientOnionFields, best_block_height: u32, payment_hash: PaymentHash,
keysend_preimage: Option<PaymentPreimage>, 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<u64>,
/// Custom payment metadata included in the payment.
payment_metadata: Option<Vec<u8>>,
/// Preimage used in spontaneous payment.
keysend_preimage: Option<[u8; 32]>,
/// Custom TLV records included in the payment.
custom_tlvs: Vec<(u64, Vec<u8>)>,
/// 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<msgs::InboundOnionPayload> for ReceivedPayment {
type Error = ();
fn try_from(pld: msgs::InboundOnionPayload) -> Result<Self, Self::Error> {
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<NS: Deref>(
onion: &msgs::OnionPacket, payment_hash: PaymentHash, node_signer: &NS,
secp_ctx: &Secp256k1<secp256k1::All>
) -> Result<PeeledPayment, ()>
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<T, R: ReadableArgs<T>, 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)
}
Expand Down Expand Up @@ -1019,7 +1193,8 @@ fn decode_next_hop<T, R: ReadableArgs<T>, 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};
Expand Down Expand Up @@ -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<RouteHop>, 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)
}

}
1 change: 1 addition & 0 deletions lightning/src/onion_message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down

0 comments on commit 5e4d49b

Please sign in to comment.