Skip to content

Introduce Padding for Payment and Message Blinded Tlvs #3177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -277,7 +276,6 @@ impl Writeable for ForwardTlvs {

impl Writeable for ReceiveTlvs {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
// TODO: write padding
encode_tlv_stream!(writer, {
(65537, self.context, option),
});
Expand Down Expand Up @@ -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<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
Expand All @@ -504,6 +506,8 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
.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
Expand All @@ -517,7 +521,15 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
})
.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)
}
}
17 changes: 12 additions & 5 deletions lightning/src/blinded_path/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -508,7 +508,6 @@ impl Writeable for UnauthenticatedReceiveTlvs {

impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
// TODO: write padding
match self {
Self::Forward(tlvs) => tlvs.write(w)?,
Self::Receive(tlvs) => tlvs.write(w)?,
Expand All @@ -520,7 +519,10 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
impl Readable for BlindedPaymentTlvs {
fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
_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),
Expand All @@ -530,7 +532,6 @@ impl Readable for BlindedPaymentTlvs {
(65537, payment_context, option),
(65539, authentication, option),
});
let _padding: Option<utils::Padding> = _padding;

if let Some(short_channel_id) = scid {
if payment_secret.is_some() {
Expand Down Expand Up @@ -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<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
Expand All @@ -571,7 +576,9 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
.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)
}
Expand Down
87 changes: 74 additions & 13 deletions lightning/src/blinded_path/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -196,19 +194,82 @@ fn encrypt_payload<P: Writeable>(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<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
/// 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<W: Writer>(&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<T: Writeable> {
pub(crate) tlvs: T,
pub(crate) round_off: usize,
}

impl<T: Writeable> Writeable for BlindedPathWithPadding<T> {
fn write<W: Writer>(&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
})
}
43 changes: 41 additions & 2 deletions lightning/src/ln/blinded_payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -353,7 +354,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()));
Expand Down Expand Up @@ -878,6 +879,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.
Expand Down Expand Up @@ -1426,6 +1429,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(&<Vec<u8>>::from_hex(hex).unwrap()).unwrap()
}
Expand Down
62 changes: 61 additions & 1 deletion lightning/src/onion_message/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading