mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-03-13 14:52:21 +01:00
Merge pull request #3177 from shaavan/padding
Introduce Padding for Payment and Message Blinded Tlvs
This commit is contained in:
commit
43de15ed8a
6 changed files with 210 additions and 30 deletions
|
@ -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<W: Writer>(&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<T: secp256k1::Signing + secp256k1::Verification>(
|
||||
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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)?,
|
||||
|
@ -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),
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(&<Vec<u8>>::from_hex(hex).unwrap()).unwrap()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: 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, short_channel_id, option),
|
||||
(4, next_node_id, option),
|
||||
(8, next_blinding_override, option),
|
||||
(65537, context, option),
|
||||
});
|
||||
let _padding: Option<Padding> = _padding;
|
||||
|
||||
let next_hop = match (short_channel_id, next_node_id) {
|
||||
(Some(_), Some(_)) => return Err(DecodeError::InvalidValue),
|
||||
|
|
Loading…
Add table
Reference in a new issue