-//! 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::sha256::Hash as Sha256;
use bitcoin::secp256k1::{self, Secp256k1, PublicKey};
+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;
use core::ops::Deref;
/// Invalid inbound onion payment.
+#[derive(Debug)]
pub struct InboundOnionErr {
/// BOLT 4 error code.
pub err_code: u16,
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],
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",
routing: PendingHTLCRouting::Forward {
onion_packet: outgoing_packet,
short_channel_id,
+ blinded: inbound_blinding_point.map(|bp| BlindedForward { inbound_blinding_point: bp }),
},
payment_hash: msg.payment_hash,
incoming_shared_secret: shared_secret,
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 {
})
}
-/// 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
/// [`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,
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)
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,
)?
}
})
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 { .. }, .. } =>
mod tests {
use bitcoin::hashes::Hash;
use bitcoin::hashes::sha256::Hash as Sha256;
- use bitcoin::secp256k1::{PublicKey, SecretKey};
+ use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use crate::ln::{PaymentPreimage, PaymentHash, PaymentSecret};
use crate::ln::ChannelId;
use crate::ln::channelmanager::RecipientOnionFields;
use crate::routing::router::{Path, RouteHop};
use crate::util::test_utils;
+ #[test]
+ fn fail_construct_onion_on_too_big_payloads() {
+ // Ensure that if we call `construct_onion_packet` and friends where payloads are too large for
+ // the allotted packet length, we'll fail to construct. Previously, senders would happily
+ // construct invalid packets by array-shifting the final node's HMAC out of the packet when
+ // adding an intermediate onion layer, causing the receiver to error with "final payload
+ // provided for us as an intermediate node."
+ let secp_ctx = Secp256k1::new();
+ let bob = crate::sign::KeysManager::new(&[2; 32], 42, 42);
+ let bob_pk = PublicKey::from_secret_key(&secp_ctx, &bob.get_node_secret_key());
+ let charlie = crate::sign::KeysManager::new(&[3; 32], 42, 42);
+ let charlie_pk = PublicKey::from_secret_key(&secp_ctx, &charlie.get_node_secret_key());
+
+ let (
+ session_priv, total_amt_msat, cur_height, mut recipient_onion, keysend_preimage, payment_hash,
+ prng_seed, hops, ..
+ ) = payment_onion_args(bob_pk, charlie_pk);
+
+ // Ensure the onion will not fit all the payloads by adding a large custom TLV.
+ recipient_onion.custom_tlvs.push((13377331, vec![0; 1156]));
+
+ let path = Path { hops, blinded_tail: None, };
+ let onion_keys = super::onion_utils::construct_onion_keys(&secp_ctx, &path, &session_priv).unwrap();
+ let (onion_payloads, ..) = super::onion_utils::build_onion_payloads(
+ &path, total_amt_msat, recipient_onion, cur_height + 1, &Some(keysend_preimage)
+ ).unwrap();
+
+ assert!(super::onion_utils::construct_onion_packet(
+ onion_payloads, onion_keys, prng_seed, &payment_hash
+ ).is_err());
+ }
+
#[test]
fn test_peel_payment_onion() {
use super::*;
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 {
- PendingHTLCRouting::Forward { onion_packet, short_channel_id: _ } => {
+ PendingHTLCRouting::Forward { onion_packet, .. } => {
onion_packet
},
_ => panic!("expected a forwarded onion"),
};
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 {
payment_hash,
onion_routing_packet,
skimmed_fee_msat: None,
+ blinding_point: None,
}
}