diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 441a2c2a625..5a5821043c8 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -14,7 +14,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; #[allow(unused_imports)] use crate::prelude::*; -use crate::blinded_path::utils; +use crate::blinded_path::utils::{self, BlindedPathWithPadding}; use crate::blinded_path::{BlindedHop, BlindedPath, Direction, IntroductionNode, NodeIdLookUp}; use crate::crypto::streams::ChaChaPolyReadAdapter; use crate::io; @@ -265,7 +265,6 @@ impl Writeable for ForwardTlvs { NextMessageHop::NodeId(pubkey) => (Some(pubkey), None), NextMessageHop::ShortChannelId(scid) => (None, Some(scid)), }; - // TODO: write padding encode_tlv_stream!(writer, { (2, short_channel_id, option), (4, next_node_id, option), @@ -277,7 +276,6 @@ impl Writeable for ForwardTlvs { impl Writeable for ReceiveTlvs { fn write(&self, writer: &mut W) -> Result<(), io::Error> { - // TODO: write padding encode_tlv_stream!(writer, { (65537, self.context, option), }); @@ -495,6 +493,10 @@ impl_writeable_tlv_based!(DNSResolverContext, { (0, nonce, required), }); +/// Represents the padding round off size (in bytes) that is used +/// to pad message blinded path's [`BlindedHop`] +pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100; + /// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`. pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[MessageForwardNode], @@ -504,6 +506,8 @@ pub(super) fn blinded_hops( .iter() .map(|node| node.node_id) .chain(core::iter::once(recipient_node_id)); + let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some()); + let tlvs = pks .clone() .skip(1) // The first node's TLVs contains the next node's pubkey @@ -517,7 +521,15 @@ pub(super) fn blinded_hops( }) .chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); - let path = pks.zip(tlvs); - - utils::construct_blinded_hops(secp_ctx, path, session_priv) + if is_compact { + let path = pks.zip(tlvs); + utils::construct_blinded_hops(secp_ctx, path, session_priv) + } else { + let path = + pks.zip(tlvs.map(|tlv| BlindedPathWithPadding { + tlvs: tlv, + round_off: MESSAGE_PADDING_ROUND_OFF, + })); + utils::construct_blinded_hops(secp_ctx, path, session_priv) + } } diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index df7c29909f8..7c205ff8c48 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -14,7 +14,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; -use crate::blinded_path::utils; +use crate::blinded_path::utils::{self, BlindedPathWithPadding}; use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp}; use crate::crypto::streams::ChaChaPolyReadAdapter; use crate::io; @@ -508,7 +508,6 @@ impl Writeable for UnauthenticatedReceiveTlvs { impl<'a> Writeable for BlindedPaymentTlvsRef<'a> { fn write(&self, w: &mut W) -> Result<(), io::Error> { - // TODO: write padding match self { Self::Forward(tlvs) => tlvs.write(w)?, Self::Receive(tlvs) => tlvs.write(w)?, @@ -520,7 +519,10 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> { impl Readable for BlindedPaymentTlvs { fn read(r: &mut R) -> Result { _init_and_read_tlv_stream!(r, { - (1, _padding, option), + // Reasoning: Padding refers to filler data added to a packet to increase + // its size and obscure its actual length. Since padding contains no meaningful + // information, we can safely omit reading it here. + // (1, _padding, option), (2, scid, option), (8, next_blinding_override, option), (10, payment_relay, option), @@ -530,7 +532,6 @@ impl Readable for BlindedPaymentTlvs { (65537, payment_context, option), (65539, authentication, option), }); - let _padding: Option = _padding; if let Some(short_channel_id) = scid { if payment_secret.is_some() { @@ -559,6 +560,10 @@ impl Readable for BlindedPaymentTlvs { } } +/// Represents the padding round off size (in bytes) that +/// is used to pad payment bilnded path's [`BlindedHop`] +pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30; + /// Construct blinded payment hops for the given `intermediate_nodes` and payee info. pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, @@ -571,7 +576,9 @@ pub(super) fn blinded_hops( .map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs)) .chain(core::iter::once(BlindedPaymentTlvsRef::Receive(&payee_tlvs))); - let path = pks.zip(tlvs); + let path = pks.zip( + tlvs.map(|tlv| BlindedPathWithPadding { tlvs: tlv, round_off: PAYMENT_PADDING_ROUND_OFF }), + ); utils::construct_blinded_hops(secp_ctx, path, session_priv) } diff --git a/lightning/src/blinded_path/utils.rs b/lightning/src/blinded_path/utils.rs index 25000e84650..b17fa01bbcf 100644 --- a/lightning/src/blinded_path/utils.rs +++ b/lightning/src/blinded_path/utils.rs @@ -18,12 +18,10 @@ use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1, SecretKey}; use super::message::BlindedMessagePath; use super::{BlindedHop, BlindedPath}; use crate::crypto::streams::ChaChaPolyWriteAdapter; -use crate::ln::msgs::DecodeError; +use crate::io; use crate::ln::onion_utils; use crate::onion_message::messenger::Destination; -use crate::util::ser::{Readable, Writeable}; - -use crate::io; +use crate::util::ser::{Writeable, Writer}; use core::borrow::Borrow; @@ -196,19 +194,82 @@ fn encrypt_payload(payload: P, encrypted_tlvs_rho: [u8; 32]) -> Ve write_adapter.encode() } -/// Blinded path encrypted payloads may be padded to ensure they are equal length. +/// A data structure used exclusively to pad blinded path payloads, ensuring they are of +/// equal length. Padding is written at Type 1 for compatibility with the lightning specification. /// -/// Reads padding to the end, ignoring what's read. -pub(crate) struct Padding {} -impl Readable for Padding { - #[inline] - fn read(reader: &mut R) -> Result { +/// For more details, see the [BOLTs Specification - Encrypted Recipient Data](https://github.com/lightning/bolts/blob/8707471dbc23245fb4d84c5f5babac1197f1583e/04-onion-routing.md#inside-encrypted_recipient_data-encrypted_data_tlv). +pub(crate) struct BlindedPathPadding { + length: usize, +} + +impl BlindedPathPadding { + /// Creates a new [`BlindedPathPadding`] instance with a specified size. + /// Use this method when defining the padding size before writing + /// an encrypted payload. + pub fn new(length: usize) -> Self { + Self { length } + } +} + +impl Writeable for BlindedPathPadding { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + const BUFFER_SIZE: usize = 1024; + let buffer = [0u8; BUFFER_SIZE]; + + let mut remaining = self.length; loop { - let mut buf = [0; 8192]; - if reader.read(&mut buf[..])? == 0 { + let to_write = core::cmp::min(remaining, BUFFER_SIZE); + writer.write_all(&buffer[..to_write])?; + remaining -= to_write; + if remaining == 0 { break; } } - Ok(Self {}) + Ok(()) + } +} + +/// Padding storage requires two extra bytes: +/// - One byte for the type. +/// - One byte for the padding length. +/// This constant accounts for that overhead. +const TLV_OVERHEAD: usize = 2; + +/// A generic struct that applies padding to blinded path TLVs, rounding their size off to `round_off` +pub(crate) struct BlindedPathWithPadding { + pub(crate) tlvs: T, + pub(crate) round_off: usize, +} + +impl Writeable for BlindedPathWithPadding { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + let tlv_length = self.tlvs.serialized_length(); + let total_length = tlv_length + TLV_OVERHEAD; + + let padding_length = + (total_length + self.round_off - 1) / self.round_off * self.round_off - total_length; + + let padding = Some(BlindedPathPadding::new(padding_length)); + + encode_tlv_stream!(writer, { + (1, padding, option), + }); + + self.tlvs.write(writer) } } + +#[cfg(test)] +/// Checks if all the packets in the blinded path are properly padded. +pub fn is_padded(hops: &[BlindedHop], padding_round_off: usize) -> bool { + let first_hop = hops.first().expect("BlindedPath must have at least one hop"); + let first_payload_size = first_hop.encrypted_payload.len(); + + // The unencrypted payload data is padded before getting encrypted. + // Assuming the first payload is padded properly, get the extra data length. + let extra_length = first_payload_size % padding_round_off; + hops.iter().all(|hop| { + // Check that every packet is padded to the round off length subtracting the extra length. + (hop.encrypted_payload.len() - extra_length) % padding_round_off == 0 + }) +} diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 5c52ecc2f4d..24f0985b107 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -13,7 +13,8 @@ use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, schnorr}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use crate::blinded_path; -use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12RefundContext, PaymentForwardNode, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentRelay, UnauthenticatedReceiveTlvs}; +use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12RefundContext, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentForwardNode, PaymentRelay, UnauthenticatedReceiveTlvs, PAYMENT_PADDING_ROUND_OFF}; +use crate::blinded_path::utils::is_padded; use crate::events::{Event, HTLCDestination, PaymentFailureReason}; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentHash, PaymentSecret}; @@ -367,7 +368,7 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { let mut route_params = get_blinded_route_parameters(amt_msat, payment_secret, 1, 1_0000_0000, nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2, &chan_upd_2_3], &chanmon_cfgs[3].keys_manager); - route_params.payment_params.max_path_length = 17; + route_params.payment_params.max_path_length = 16; let route = get_route(&nodes[0], &route_params).unwrap(); node_cfgs[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -892,6 +893,8 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); + route_params.payment_params.max_path_length = 17; + let route = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck { let mut route = get_route(&nodes[0], &route_params).unwrap(); // Set the final CLTV expiry too low to trigger the failure in process_pending_htlc_forwards. @@ -1446,6 +1449,42 @@ fn fails_receive_tlvs_authentication() { ); } +#[test] +fn blinded_payment_path_padding() { + // Make sure that for a blinded payment path, all encrypted payloads are padded to equal lengths. + let chanmon_cfgs = create_chanmon_cfgs(5); + let node_cfgs = create_node_cfgs(5, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(5, &node_cfgs, &[None, None, None, None, None]); + let mut nodes = create_network(5, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let chan_upd_2_3 = create_announced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0).0.contents; + let chan_upd_3_4 = create_announced_chan_between_nodes_with_value(&nodes, 3, 4, 1_000_000, 0).0.contents; + + // Get all our nodes onto the same height so payments don't fail for CLTV violations. + connect_blocks(&nodes[0], nodes[4].best_block_info().1 - nodes[0].best_block_info().1); + connect_blocks(&nodes[1], nodes[4].best_block_info().1 - nodes[1].best_block_info().1); + connect_blocks(&nodes[2], nodes[4].best_block_info().1 - nodes[2].best_block_info().1); + assert_eq!(nodes[4].best_block_info().1, nodes[3].best_block_info().1); + + let amt_msat = 5000; + let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[4], Some(amt_msat), None); + + let blinded_path = blinded_payment_path(payment_secret, 1, 1_0000_0000, + nodes.iter().skip(2).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_2_3, &chan_upd_3_4], + &chanmon_cfgs[4].keys_manager + ); + + assert!(is_padded(&blinded_path.blinded_hops(), PAYMENT_PADDING_ROUND_OFF)); + + let route_params = RouteParameters::from_payment_params_and_value(PaymentParameters::blinded(vec![blinded_path]), amt_msat); + + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + check_added_monitors(&nodes[0], 1); + pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3], &nodes[4]]], amt_msat, payment_hash, payment_secret); + claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3], &nodes[4]], payment_preimage); +} + fn secret_from_hex(hex: &str) -> SecretKey { SecretKey::from_slice(&>::from_hex(hex).unwrap()).unwrap() } diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index e007bb616d2..b28819ee692 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -21,8 +21,9 @@ use super::offers::{OffersMessage, OffersMessageHandler}; use super::packet::{OnionMessageContents, Packet}; use crate::blinded_path::message::{ AsyncPaymentsContext, BlindedMessagePath, DNSResolverContext, MessageContext, - MessageForwardNode, OffersContext, + MessageForwardNode, OffersContext, MESSAGE_PADDING_ROUND_OFF, }; +use crate::blinded_path::utils::is_padded; use crate::blinded_path::EmptyNodeIdLookUp; use crate::events::{Event, EventsProvider}; use crate::ln::msgs::{self, BaseMessageHandler, DecodeError, OnionMessageHandler}; @@ -596,6 +597,65 @@ fn too_big_packet_error() { assert_eq!(err, SendError::TooBigPacket); } +#[test] +fn test_blinded_path_padding_for_full_length_path() { + // Check that for a full blinded path, all encrypted payload are padded to rounded-off length. + let nodes = create_nodes(4); + let test_msg = TestCustomMessage::Pong; + + let secp_ctx = Secp256k1::new(); + let intermediate_nodes = [ + MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, + MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, + ]; + // Update the context to create a larger final receive TLVs, ensuring that + // the hop sizes vary before padding. + let context = MessageContext::Custom(vec![0u8; 42]); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[3].node_id, + context, + &*nodes[3].entropy_source, + &secp_ctx, + ) + .unwrap(); + + assert!(is_padded(&blinded_path.blinded_hops(), MESSAGE_PADDING_ROUND_OFF)); + + let destination = Destination::BlindedPath(blinded_path); + let instructions = MessageSendInstructions::WithoutReplyPath { destination }; + + nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); + nodes[3].custom_message_handler.expect_message(TestCustomMessage::Pong); + pass_along_path(&nodes); +} + +#[test] +fn test_blinded_path_no_padding_for_compact_path() { + // Check that for a compact blinded path, no padding is applied. + let nodes = create_nodes(4); + let secp_ctx = Secp256k1::new(); + + // Include some short_channel_id, so that MessageRouter uses this to create compact blinded paths. + let intermediate_nodes = [ + MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: Some(24) }, + MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: Some(25) }, + ]; + // Update the context to create a larger final receive TLVs, ensuring that + // the hop sizes vary before padding. + let context = MessageContext::Custom(vec![0u8; 42]); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[3].node_id, + context, + &*nodes[3].entropy_source, + &secp_ctx, + ) + .unwrap(); + + assert!(!is_padded(&blinded_path.blinded_hops(), MESSAGE_PADDING_ROUND_OFF)); +} + #[test] fn we_are_intro_node() { // If we are sending straight to a blinded path and we are the introduction node, we need to diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 961ae71a2d9..8cc95461b73 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -18,7 +18,6 @@ use super::dns_resolution::DNSResolverMessage; use super::messenger::CustomOnionMessageHandler; use super::offers::OffersMessage; use crate::blinded_path::message::{BlindedMessagePath, ForwardTlvs, NextMessageHop, ReceiveTlvs}; -use crate::blinded_path::utils::Padding; use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter}; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; @@ -336,13 +335,15 @@ pub(crate) enum ControlTlvs { impl Readable for ControlTlvs { fn read(r: &mut R) -> Result { _init_and_read_tlv_stream!(r, { - (1, _padding, option), + // Reasoning: Padding refers to filler data added to a packet to increase + // its size and obscure its actual length. Since padding contains no meaningful + // information, we can safely omit reading it here. + // (1, _padding, option), (2, short_channel_id, option), (4, next_node_id, option), (8, next_blinding_override, option), (65537, context, option), }); - let _padding: Option = _padding; let next_hop = match (short_channel_id, next_node_id) { (Some(_), Some(_)) => return Err(DecodeError::InvalidValue),