Test recipient failing an HTLC received to a multi-hop blinded path
[rust-lightning] / lightning / src / ln / onion_payment.rs
index c10cdc9ab10ea61e3dbd2d3063835553d57acf2b..5ea07cfab30db4325bc71415f1b3f331ca1a37dc 100644 (file)
@@ -1,17 +1,22 @@
-//! Utilities for channelmanager.rs
+//! Utilities to decode payment onions and do contextless validation of incoming payments.
 //!
-//! Includes a public [`peel_payment_onion`] function for use by external projects or libraries.
+//! Primarily features [`peel_payment_onion`], which allows the decoding of an onion statelessly
+//! and can be used to predict whether we'd accept a payment.
 
-use bitcoin::hashes::Hash;
+use bitcoin::hashes::{Hash, HashEngine};
+use bitcoin::hashes::hmac::{Hmac, HmacEngine};
 use bitcoin::hashes::sha256::Hash as Sha256;
-use bitcoin::secp256k1::{self, Secp256k1, PublicKey};
+use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1};
 
+use crate::blinded_path;
+use crate::blinded_path::payment::{PaymentConstraints, PaymentRelay};
 use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS};
 use crate::ln::PaymentHash;
-use crate::ln::channelmanager::{CLTV_FAR_FAR_AWAY, HTLCFailureMsg, MIN_CLTV_EXPIRY_DELTA, PendingHTLCInfo, PendingHTLCRouting};
+use crate::ln::channelmanager::{BlindedForward, CLTV_FAR_FAR_AWAY, HTLCFailureMsg, MIN_CLTV_EXPIRY_DELTA, PendingHTLCInfo, PendingHTLCRouting};
+use crate::ln::features::BlindedHopFeatures;
 use crate::ln::msgs;
 use crate::ln::onion_utils;
-use crate::ln::onion_utils::HTLCFailReason;
+use crate::ln::onion_utils::{HTLCFailReason, INVALID_ONION_BLINDING};
 use crate::sign::{NodeSigner, Recipient};
 use crate::util::logger::Logger;
 
@@ -28,6 +33,23 @@ pub struct InboundOnionErr {
        pub msg: &'static str,
 }
 
+fn check_blinded_forward(
+       inbound_amt_msat: u64, inbound_cltv_expiry: u32, payment_relay: &PaymentRelay,
+       payment_constraints: &PaymentConstraints, features: &BlindedHopFeatures
+) -> Result<(u64, u32), ()> {
+       let amt_to_forward = blinded_path::payment::amt_to_forward_msat(
+               inbound_amt_msat, payment_relay
+       ).ok_or(())?;
+       let outgoing_cltv_value = inbound_cltv_expiry.checked_sub(
+               payment_relay.cltv_expiry_delta as u32
+       ).ok_or(())?;
+       if inbound_amt_msat < payment_constraints.htlc_minimum_msat ||
+               outgoing_cltv_value > payment_constraints.max_cltv_expiry
+               { return Err(()) }
+       if features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) { return Err(()) }
+       Ok((amt_to_forward, outgoing_cltv_value))
+}
+
 pub(super) fn create_fwd_pending_htlc_info(
        msg: &msgs::UpdateAddHTLC, hop_data: msgs::InboundOnionPayload, hop_hmac: [u8; 32],
        new_packet_bytes: [u8; onion_utils::ONION_DATA_LEN], shared_secret: [u8; 32],
@@ -41,9 +63,27 @@ pub(super) fn create_fwd_pending_htlc_info(
                hmac: hop_hmac,
        };
 
-       let (short_channel_id, amt_to_forward, outgoing_cltv_value) = match hop_data {
+       let (
+               short_channel_id, amt_to_forward, outgoing_cltv_value, inbound_blinding_point
+       ) = match hop_data {
                msgs::InboundOnionPayload::Forward { short_channel_id, amt_to_forward, outgoing_cltv_value } =>
-                       (short_channel_id, amt_to_forward, outgoing_cltv_value),
+                       (short_channel_id, amt_to_forward, outgoing_cltv_value, None),
+               msgs::InboundOnionPayload::BlindedForward {
+                       short_channel_id, payment_relay, payment_constraints, intro_node_blinding_point, features,
+               } => {
+                       let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward(
+                               msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features
+                       ).map_err(|()| {
+                               // We should be returning malformed here if `msg.blinding_point` is set, but this is
+                               // unreachable right now since we checked it in `decode_update_add_htlc_onion`.
+                               InboundOnionErr {
+                                       msg: "Underflow calculating outbound amount or cltv value for blinded forward",
+                                       err_code: INVALID_ONION_BLINDING,
+                                       err_data: vec![0; 32],
+                               }
+                       })?;
+                       (short_channel_id, amt_to_forward, outgoing_cltv_value, Some(intro_node_blinding_point))
+               },
                msgs::InboundOnionPayload::Receive { .. } | msgs::InboundOnionPayload::BlindedReceive { .. } =>
                        return Err(InboundOnionErr {
                                msg: "Final Node OnionHopData provided for us as an intermediary node",
@@ -56,7 +96,7 @@ pub(super) fn create_fwd_pending_htlc_info(
                routing: PendingHTLCRouting::Forward {
                        onion_packet: outgoing_packet,
                        short_channel_id,
-                       blinded: None,
+                       blinded: inbound_blinding_point.map(|bp| BlindedForward { inbound_blinding_point: bp }),
                },
                payment_hash: msg.payment_hash,
                incoming_shared_secret: shared_secret,
@@ -72,16 +112,21 @@ pub(super) fn create_recv_pending_htlc_info(
        amt_msat: u64, cltv_expiry: u32, phantom_shared_secret: Option<[u8; 32]>, allow_underpay: bool,
        counterparty_skimmed_fee_msat: Option<u64>, current_height: u32, accept_mpp_keysend: bool,
 ) -> Result<PendingHTLCInfo, InboundOnionErr> {
-       let (payment_data, keysend_preimage, custom_tlvs, onion_amt_msat, outgoing_cltv_value, payment_metadata) = match hop_data {
+       let (
+               payment_data, keysend_preimage, custom_tlvs, onion_amt_msat, outgoing_cltv_value,
+               payment_metadata, requires_blinded_error
+       ) = match hop_data {
                msgs::InboundOnionPayload::Receive {
                        payment_data, keysend_preimage, custom_tlvs, amt_msat, outgoing_cltv_value, payment_metadata, ..
                } =>
-                       (payment_data, keysend_preimage, custom_tlvs, amt_msat, outgoing_cltv_value, payment_metadata),
+                       (payment_data, keysend_preimage, custom_tlvs, amt_msat, outgoing_cltv_value, payment_metadata,
+                        false),
                msgs::InboundOnionPayload::BlindedReceive {
-                       amt_msat, total_msat, outgoing_cltv_value, payment_secret, ..
+                       amt_msat, total_msat, outgoing_cltv_value, payment_secret, intro_node_blinding_point, ..
                } => {
                        let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat };
-                       (Some(payment_data), None, Vec::new(), amt_msat, outgoing_cltv_value, None)
+                       (Some(payment_data), None, Vec::new(), amt_msat, outgoing_cltv_value, None,
+                        intro_node_blinding_point.is_none())
                }
                msgs::InboundOnionPayload::Forward { .. } => {
                        return Err(InboundOnionErr {
@@ -90,6 +135,13 @@ pub(super) fn create_recv_pending_htlc_info(
                                msg: "Got non final data with an HMAC of 0",
                        })
                },
+               msgs::InboundOnionPayload::BlindedForward { .. } => {
+                       return Err(InboundOnionErr {
+                               err_code: INVALID_ONION_BLINDING,
+                               err_data: vec![0; 32],
+                               msg: "Got blinded non final data with an HMAC of 0",
+                       })
+               }
        };
        // final_incorrect_cltv_expiry
        if outgoing_cltv_value > cltv_expiry {
@@ -161,6 +213,7 @@ pub(super) fn create_recv_pending_htlc_info(
                        incoming_cltv_expiry: outgoing_cltv_value,
                        phantom_shared_secret,
                        custom_tlvs,
+                       requires_blinded_error,
                }
        } else {
                return Err(InboundOnionErr {
@@ -180,7 +233,9 @@ pub(super) fn create_recv_pending_htlc_info(
        })
 }
 
-/// Peel one layer off an incoming onion, returning [`PendingHTLCInfo`] (either Forward or Receive).
+/// Peel one layer off an incoming onion, returning a [`PendingHTLCInfo`] that contains information
+/// about the intended next-hop for the HTLC.
+///
 /// This does all the relevant context-free checks that LDK requires for payment relay or
 /// acceptance. If the payment is to be received, and the amount matches the expected amount for
 /// a given invoice, this indicates the [`msgs::UpdateAddHTLC`], once fully committed in the
@@ -189,7 +244,7 @@ pub(super) fn create_recv_pending_htlc_info(
 /// [`Event::PaymentClaimable`]: crate::events::Event::PaymentClaimable
 pub fn peel_payment_onion<NS: Deref, L: Deref, T: secp256k1::Verification>(
        msg: &msgs::UpdateAddHTLC, node_signer: &NS, logger: &L, secp_ctx: &Secp256k1<T>,
-       cur_height: u32, accept_mpp_keysend: bool,
+       cur_height: u32, accept_mpp_keysend: bool, allow_skimmed_fees: bool,
 ) -> Result<PendingHTLCInfo, InboundOnionErr>
 where
        NS::Target: NodeSigner,
@@ -228,6 +283,10 @@ where
                                        err_data: Vec::new(),
                                });
                        }
+
+                       // TODO: If this is potentially a phantom payment we should decode the phantom payment
+                       // onion here and check it.
+
                        create_fwd_pending_htlc_info(
                                msg, next_hop_data, next_hop_hmac, new_packet_bytes, shared_secret,
                                Some(next_packet_pubkey)
@@ -236,7 +295,7 @@ where
                onion_utils::Hop::Receive(received_data) => {
                        create_recv_pending_htlc_info(
                                received_data, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry,
-                               None, false, msg.skimmed_fee_msat, cur_height, accept_mpp_keysend,
+                               None, allow_skimmed_fees, msg.skimmed_fee_msat, cur_height, accept_mpp_keysend,
                        )?
                }
        })
@@ -274,8 +333,14 @@ where
                return_malformed_err!("invalid ephemeral pubkey", 0x8000 | 0x4000 | 6);
        }
 
+       let blinded_node_id_tweak = msg.blinding_point.map(|bp| {
+               let blinded_tlvs_ss = node_signer.ecdh(Recipient::Node, &bp, None).unwrap().secret_bytes();
+               let mut hmac = HmacEngine::<Sha256>::new(b"blinded_node_id");
+               hmac.input(blinded_tlvs_ss.as_ref());
+               Scalar::from_be_bytes(Hmac::from_engine(hmac).to_byte_array()).unwrap()
+       });
        let shared_secret = node_signer.ecdh(
-               Recipient::Node, &msg.onion_routing_packet.public_key.unwrap(), None
+               Recipient::Node, &msg.onion_routing_packet.public_key.unwrap(), blinded_node_id_tweak.as_ref()
        ).unwrap().secret_bytes();
 
        if msg.onion_routing_packet.version != 0 {
@@ -303,7 +368,7 @@ where
 
        let next_hop = match onion_utils::decode_next_payment_hop(
                shared_secret, &msg.onion_routing_packet.hop_data[..], msg.onion_routing_packet.hmac,
-               msg.payment_hash, node_signer
+               msg.payment_hash, msg.blinding_point, node_signer
        ) {
                Ok(res) => res,
                Err(onion_utils::OnionDecodeErr::Malformed { err_msg, err_code }) => {
@@ -327,6 +392,27 @@ where
                                outgoing_amt_msat: amt_to_forward, outgoing_cltv_value
                        }
                },
+               onion_utils::Hop::Forward {
+                       next_hop_data: msgs::InboundOnionPayload::BlindedForward {
+                               short_channel_id, ref payment_relay, ref payment_constraints, ref features, ..
+                       }, ..
+               } => {
+                       let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward(
+                               msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features
+                       ) {
+                               Ok((amt, cltv)) => (amt, cltv),
+                               Err(()) => {
+                                       return_err!("Underflow calculating outbound amount or cltv value for blinded forward",
+                                               INVALID_ONION_BLINDING, &[0; 32]);
+                               }
+                       };
+                       let next_packet_pubkey = onion_utils::next_hop_pubkey(&secp_ctx,
+                               msg.onion_routing_packet.public_key.unwrap(), &shared_secret);
+                       NextPacketDetails {
+                               next_packet_pubkey, outgoing_scid: short_channel_id, outgoing_amt_msat: amt_to_forward,
+                               outgoing_cltv_value
+                       }
+               },
                onion_utils::Hop::Receive { .. } => return Ok((next_hop, shared_secret, None)),
                onion_utils::Hop::Forward { next_hop_data: msgs::InboundOnionPayload::Receive { .. }, .. } |
                        onion_utils::Hop::Forward { next_hop_data: msgs::InboundOnionPayload::BlindedReceive { .. }, .. } =>
@@ -411,7 +497,7 @@ mod tests {
                let msg = make_update_add_msg(amount_msat, cltv_expiry, payment_hash, onion);
                let logger = test_utils::TestLogger::with_id("bob".to_string());
 
-               let peeled = peel_payment_onion(&msg, &&bob, &&logger, &secp_ctx, cur_height, true)
+               let peeled = peel_payment_onion(&msg, &&bob, &&logger, &secp_ctx, cur_height, true, false)
                        .map_err(|e| e.msg).unwrap();
 
                let next_onion = match peeled.routing {
@@ -422,7 +508,7 @@ mod tests {
                };
 
                let msg2 = make_update_add_msg(amount_msat, cltv_expiry, payment_hash, next_onion);
-               let peeled2 = peel_payment_onion(&msg2, &&charlie, &&logger, &secp_ctx, cur_height, true)
+               let peeled2 = peel_payment_onion(&msg2, &&charlie, &&logger, &secp_ctx, cur_height, true, false)
                        .map_err(|e| e.msg).unwrap();
 
                match peeled2.routing {