From d282ea8167086ea87ff996179f7ffb19419a356b Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 5 Mar 2025 09:14:12 +0100 Subject: [PATCH 1/2] Attributable failures --- lightning/src/ln/channel.rs | 93 +++++- lightning/src/ln/channelmanager.rs | 21 +- lightning/src/ln/functional_tests.rs | 2 + lightning/src/ln/msgs.rs | 22 +- lightning/src/ln/onion_payment.rs | 1 + lightning/src/ln/onion_route_tests.rs | 45 ++- lightning/src/ln/onion_utils.rs | 461 ++++++++++++++++++++++---- lightning/src/ln/outbound_payment.rs | 4 +- lightning/src/util/ser.rs | 3 + 9 files changed, 566 insertions(+), 86 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 8625eb6914a..7e33c63b1e6 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -50,7 +50,7 @@ use crate::ln::chan_utils::{ #[cfg(splicing)] use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use crate::ln::chan_utils; -use crate::ln::onion_utils::{HTLCFailReason}; +use crate::ln::onion_utils::{HTLCFailReason, ATTRIBUTION_DATA_LEN}; use crate::chain::BestBlock; use crate::chain::chaininterface::{FeeEstimator, ConfirmationTarget, LowerBoundedFeeEstimator, fee_for_weight}; use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, LATENCY_GRACE_PERIOD_BLOCKS}; @@ -68,8 +68,11 @@ use crate::util::scid_utils::scid_from_parts; use crate::io; use crate::prelude::*; +use core::time::Duration; use core::{cmp,mem,fmt}; use core::ops::Deref; +#[cfg(feature = "std")] +use std::time::SystemTime; #[cfg(any(test, fuzzing, debug_assertions))] use crate::sync::Mutex; use crate::sign::type_resolver::ChannelSignerType; @@ -323,6 +326,7 @@ struct OutboundHTLCOutput { source: HTLCSource, blinding_point: Option, skimmed_fee_msat: Option, + timestamp: Option, } /// See AwaitingRemoteRevoke ChannelState for more info @@ -4933,7 +4937,7 @@ trait FailHTLCContents { impl FailHTLCContents for msgs::OnionErrorPacket { type Message = msgs::UpdateFailHTLC; fn to_message(self, htlc_id: u64, channel_id: ChannelId) -> Self::Message { - msgs::UpdateFailHTLC { htlc_id, channel_id, reason: self.data } + msgs::UpdateFailHTLC { htlc_id, channel_id, reason: self.data, attribution_data: self.attribution_data } } fn to_inbound_htlc_state(self) -> InboundHTLCState { InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::FailRelay(self)) @@ -6100,10 +6104,16 @@ impl FundedChannel where false } else { true } }); + let now = duration_since_epoch(); pending_outbound_htlcs.retain(|htlc| { if let &OutboundHTLCState::AwaitingRemovedRemoteRevoke(ref outcome) = &htlc.state { log_trace!(logger, " ...removing outbound AwaitingRemovedRemoteRevoke {}", &htlc.payment_hash); - if let OutboundHTLCOutcome::Failure(reason) = outcome.clone() { // We really want take() here, but, again, non-mut ref :( + if let OutboundHTLCOutcome::Failure(mut reason) = outcome.clone() { // We really want take() here, but, again, non-mut ref :( + if let (Some(timestamp), Some(now)) = (htlc.timestamp, now) { + let hold_time = u32::try_from((now - timestamp).as_millis()).unwrap_or(u32::MAX); + reason.set_hold_time(hold_time); + } + revoked_htlcs.push((htlc.source.clone(), htlc.payment_hash, reason)); } else { finalized_claimed_htlcs.push(htlc.source.clone()); @@ -6845,6 +6855,7 @@ impl FundedChannel where channel_id: self.context.channel_id(), htlc_id: htlc.htlc_id, reason: err_packet.data.clone(), + attribution_data: err_packet.attribution_data, }); }, &InboundHTLCRemovalReason::FailMalformed((ref sha256_of_onion, ref failure_code)) => { @@ -8671,6 +8682,7 @@ impl FundedChannel where return Ok(None); } + let timestamp = duration_since_epoch(); self.context.pending_outbound_htlcs.push(OutboundHTLCOutput { htlc_id: self.context.next_holder_htlc_id, amount_msat, @@ -8680,6 +8692,7 @@ impl FundedChannel where source, blinding_point, skimmed_fee_msat, + timestamp, }); let res = msgs::UpdateAddHTLC { @@ -10247,6 +10260,7 @@ impl Writeable for FundedChannel where SP::Target: SignerProvider dropped_inbound_htlcs += 1; } } + let mut removed_htlc_failure_attribution_data: Vec<&Option<[u8; ATTRIBUTION_DATA_LEN]>> = Vec::new(); (self.context.pending_inbound_htlcs.len() as u64 - dropped_inbound_htlcs).write(writer)?; for htlc in self.context.pending_inbound_htlcs.iter() { if let &InboundHTLCState::RemoteAnnounced(_) = &htlc.state { @@ -10272,9 +10286,10 @@ impl Writeable for FundedChannel where SP::Target: SignerProvider &InboundHTLCState::LocalRemoved(ref removal_reason) => { 4u8.write(writer)?; match removal_reason { - InboundHTLCRemovalReason::FailRelay(msgs::OnionErrorPacket { data }) => { + InboundHTLCRemovalReason::FailRelay(msgs::OnionErrorPacket { data, attribution_data }) => { 0u8.write(writer)?; data.write(writer)?; + removed_htlc_failure_attribution_data.push(&attribution_data); }, InboundHTLCRemovalReason::FailMalformed((hash, code)) => { 1u8.write(writer)?; @@ -10336,10 +10351,11 @@ impl Writeable for FundedChannel where SP::Target: SignerProvider let mut holding_cell_skimmed_fees: Vec> = Vec::new(); let mut holding_cell_blinding_points: Vec> = Vec::new(); + let mut holding_cell_failure_attribution_data: Vec<(u32, [u8; ATTRIBUTION_DATA_LEN])> = Vec::new(); // Vec of (htlc_id, failure_code, sha256_of_onion) let mut malformed_htlcs: Vec<(u64, u16, [u8; 32])> = Vec::new(); (self.context.holding_cell_htlc_updates.len() as u64).write(writer)?; - for update in self.context.holding_cell_htlc_updates.iter() { + for (i, update) in self.context.holding_cell_htlc_updates.iter().enumerate() { match update { &HTLCUpdateAwaitingACK::AddHTLC { ref amount_msat, ref cltv_expiry, ref payment_hash, ref source, ref onion_routing_packet, @@ -10364,6 +10380,13 @@ impl Writeable for FundedChannel where SP::Target: SignerProvider 2u8.write(writer)?; htlc_id.write(writer)?; err_packet.data.write(writer)?; + + // Store the attribution data for later writing. Include the holding cell htlc update index because + // FailMalformedHTLC is stored with the same type 2 and we wouldn't be able to distinguish the two + // when reading back in. + if let Some(attribution_data ) = err_packet.attribution_data { + holding_cell_failure_attribution_data.push((i as u32, attribution_data)); + } } &HTLCUpdateAwaitingACK::FailMalformedHTLC { htlc_id, failure_code, sha256_of_onion @@ -10547,6 +10570,8 @@ impl Writeable for FundedChannel where SP::Target: SignerProvider (49, self.context.local_initiated_shutdown, option), // Added in 0.0.122 (51, is_manual_broadcast, option), // Added in 0.0.124 (53, funding_tx_broadcast_safe_event_emitted, option), // Added in 0.0.124 + (55, removed_htlc_failure_attribution_data, optional_vec), // Added in 0.2 + (57, holding_cell_failure_attribution_data, optional_vec), // Added in 0.2 }); Ok(()) @@ -10624,6 +10649,7 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel let reason = match ::read(reader)? { 0 => InboundHTLCRemovalReason::FailRelay(msgs::OnionErrorPacket { data: Readable::read(reader)?, + attribution_data: None, }), 1 => InboundHTLCRemovalReason::FailMalformed(Readable::read(reader)?), 2 => InboundHTLCRemovalReason::Fulfill(Readable::read(reader)?), @@ -10664,6 +10690,7 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel }, skimmed_fee_msat: None, blinding_point: None, + timestamp: None, }); } @@ -10688,6 +10715,7 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel htlc_id: Readable::read(reader)?, err_packet: OnionErrorPacket { data: Readable::read(reader)?, + attribution_data: None, }, }, _ => return Err(DecodeError::InvalidValue), @@ -10831,6 +10859,9 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel let mut pending_outbound_blinding_points_opt: Option>> = None; let mut holding_cell_blinding_points_opt: Option>> = None; + let mut removed_htlc_failure_attribution_data: Option>> = None; + let mut holding_cell_failure_attribution_data: Option> = None; + let mut malformed_htlcs: Option> = None; let mut monitor_pending_update_adds: Option> = None; @@ -10873,6 +10904,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel (49, local_initiated_shutdown, option), (51, is_manual_broadcast, option), (53, funding_tx_broadcast_safe_event_emitted, option), + (55, removed_htlc_failure_attribution_data, optional_vec), + (57, holding_cell_failure_attribution_data, optional_vec), }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -10954,6 +10987,38 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel if iter.next().is_some() { return Err(DecodeError::InvalidValue) } } + if let Some(attribution_datas) = removed_htlc_failure_attribution_data { + let mut removed_htlc_relay_failures = + pending_inbound_htlcs.iter_mut().filter_map(|status| + if let InboundHTLCState::LocalRemoved(ref mut reason) = &mut status.state { + if let InboundHTLCRemovalReason::FailRelay(ref mut packet) = reason { + Some(&mut packet.attribution_data) + } else { + None + } + } else { + None + } + ); + + for attribution_data in attribution_datas { + *removed_htlc_relay_failures.next().ok_or(DecodeError::InvalidValue)? = attribution_data; + } + if removed_htlc_relay_failures.next().is_some() { return Err(DecodeError::InvalidValue); } + } + + if let Some(attribution_datas) = holding_cell_failure_attribution_data { + for (i, attribution_data) in attribution_datas { + let update = holding_cell_htlc_updates.get_mut(i as usize).ok_or(DecodeError::InvalidValue)?; + + if let HTLCUpdateAwaitingACK::FailHTLC { htlc_id: _, ref mut err_packet } = update { + err_packet.attribution_data = Some(attribution_data); + } else { + return Err(DecodeError::InvalidValue); + } + } + } + if let Some(malformed_htlcs) = malformed_htlcs { for (malformed_htlc_id, failure_code, sha256_of_onion) in malformed_htlcs { let htlc_idx = holding_cell_htlc_updates.iter().position(|htlc| { @@ -11144,6 +11209,18 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel } } +fn duration_since_epoch() -> Option { + #[cfg(not(feature = "std"))] + let now = None; + + #[cfg(feature = "std")] + let now = Some(std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH")); + + now +} + #[cfg(test)] mod tests { use std::cmp; @@ -11157,7 +11234,7 @@ mod tests { use bitcoin::network::Network; #[cfg(splicing)] use bitcoin::Weight; - use crate::ln::onion_utils::INVALID_ONION_BLINDING; + use crate::ln::onion_utils::{ATTRIBUTION_DATA_LEN, INVALID_ONION_BLINDING}; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::ln::channel_keys::{RevocationKey, RevocationBasepoint}; use crate::ln::channelmanager::{self, HTLCSource, PaymentId}; @@ -11378,6 +11455,7 @@ mod tests { }, skimmed_fee_msat: None, blinding_point: None, + timestamp: None, }); // Make sure when Node A calculates their local commitment transaction, none of the HTLCs pass @@ -11762,6 +11840,7 @@ mod tests { source: dummy_htlc_source.clone(), skimmed_fee_msat: None, blinding_point: None, + timestamp: None, }; let mut pending_outbound_htlcs = vec![dummy_outbound_output.clone(); 10]; for (idx, htlc) in pending_outbound_htlcs.iter_mut().enumerate() { @@ -11793,7 +11872,7 @@ mod tests { htlc_id: 0, }; let dummy_holding_cell_failed_htlc = |htlc_id| HTLCUpdateAwaitingACK::FailHTLC { - htlc_id, err_packet: msgs::OnionErrorPacket { data: vec![42] } + htlc_id, err_packet: msgs::OnionErrorPacket { data: vec![42], attribution_data: Some([1; ATTRIBUTION_DATA_LEN]) } }; let dummy_holding_cell_malformed_htlc = |htlc_id| HTLCUpdateAwaitingACK::FailMalformedHTLC { htlc_id, failure_code: INVALID_ONION_BLINDING, sha256_of_onion: [0; 32], diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 4574b2e7f8a..57ccdb6136c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -59,7 +59,7 @@ use crate::types::features::Bolt11InvoiceFeatures; use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, Payee, PaymentParameters, RouteParameters, RouteParametersConfig, Router, FixedRouter, Route}; use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, HopConnector, InboundHTLCErr, NextPacketDetails}; use crate::ln::msgs; -use crate::ln::onion_utils; +use crate::ln::onion_utils::{self, ATTRIBUTION_DATA_LEN}; use crate::ln::onion_utils::{HTLCFailReason, INVALID_ONION_BLINDING}; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, DecodeError, LightningError, MessageSendEvent}; #[cfg(test)] @@ -4476,6 +4476,7 @@ where channel_id: msg.channel_id, htlc_id: msg.htlc_id, reason: failure.data, + attribution_data: failure.attribution_data, }) } @@ -4501,10 +4502,12 @@ where } let failure = HTLCFailReason::reason($err_code, $data.to_vec()) .get_encrypted_failure_packet(&shared_secret, &None); + return PendingHTLCStatus::Fail(HTLCFailureMsg::Relay(msgs::UpdateFailHTLC { channel_id: msg.channel_id, htlc_id: msg.htlc_id, reason: failure.data, + attribution_data: failure.attribution_data, })); } } @@ -12968,11 +12971,15 @@ impl_writeable_tlv_based!(PendingHTLCInfo, { impl Writeable for HTLCFailureMsg { fn write(&self, writer: &mut W) -> Result<(), io::Error> { match self { - HTLCFailureMsg::Relay(msgs::UpdateFailHTLC { channel_id, htlc_id, reason }) => { + HTLCFailureMsg::Relay(msgs::UpdateFailHTLC { channel_id, htlc_id, reason, attribution_data }) => { 0u8.write(writer)?; channel_id.write(writer)?; htlc_id.write(writer)?; reason.write(writer)?; + + // This code will only ever be hit for legacy data that is re-serialized. It isn't necessary to try + // writing out attribution data, because it can never be present. + debug_assert!(attribution_data.is_none()); }, HTLCFailureMsg::Malformed(msgs::UpdateFailMalformedHTLC { channel_id, htlc_id, sha256_of_onion, failure_code @@ -12997,6 +13004,7 @@ impl Readable for HTLCFailureMsg { channel_id: Readable::read(reader)?, htlc_id: Readable::read(reader)?, reason: Readable::read(reader)?, + attribution_data: None, })) }, 1 => { @@ -13227,6 +13235,7 @@ impl Writeable for HTLCForwardInfo { write_tlv_fields!(w, { (0, htlc_id, required), (2, err_packet.data, required), + (5, err_packet.attribution_data, option), }); }, Self::FailMalformedHTLC { htlc_id, failure_code, sha256_of_onion } => { @@ -13257,8 +13266,12 @@ impl Readable for HTLCForwardInfo { (1, malformed_htlc_failure_code, option), (2, err_packet, required), (3, sha256_of_onion, option), + (5, attribution_data, option), }); if let Some(failure_code) = malformed_htlc_failure_code { + if attribution_data.is_some() { + return Err(DecodeError::InvalidValue); + } Self::FailMalformedHTLC { htlc_id: _init_tlv_based_struct_field!(htlc_id, required), failure_code, @@ -13269,6 +13282,7 @@ impl Readable for HTLCForwardInfo { htlc_id: _init_tlv_based_struct_field!(htlc_id, required), err_packet: crate::ln::msgs::OnionErrorPacket { data: _init_tlv_based_struct_field!(err_packet, required), + attribution_data: _init_tlv_based_struct_field!(attribution_data, option), }, } } @@ -14954,6 +14968,7 @@ mod tests { use bitcoin::secp256k1::ecdh::SharedSecret; use core::sync::atomic::Ordering; use crate::events::{Event, HTLCDestination, ClosureReason}; + use crate::ln::onion_utils::ATTRIBUTION_DATA_LEN; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; use crate::ln::channelmanager::{create_recv_pending_htlc_info, inbound_payment, ChannelConfigOverrides, HTLCForwardInfo, InterceptId, PaymentId, RecipientOnionFields}; @@ -16276,7 +16291,7 @@ mod tests { let mut nodes = create_network(1, &node_cfg, &chanmgrs); let dummy_failed_htlc = |htlc_id| { - HTLCForwardInfo::FailHTLC { htlc_id, err_packet: msgs::OnionErrorPacket { data: vec![42] } } + HTLCForwardInfo::FailHTLC { htlc_id, err_packet: msgs::OnionErrorPacket { data: vec![42], attribution_data: Some([0; ATTRIBUTION_DATA_LEN]) } } }; let dummy_malformed_htlc = |htlc_id| { HTLCForwardInfo::FailMalformedHTLC { htlc_id, failure_code: 0x4000, sha256_of_onion: [0; 32] } diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index a65646e9b61..4620ffbac16 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -17,6 +17,7 @@ use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::channelmonitor; use crate::chain::channelmonitor::{Balance, ChannelMonitorUpdateStep, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY, COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE}; use crate::chain::transaction::OutPoint; +use crate::ln::onion_utils::ATTRIBUTION_DATA_LEN; use crate::sign::{ecdsa::EcdsaChannelSigner, EntropySource, OutputSpender, SignerProvider}; use crate::events::bump_transaction::WalletSource; use crate::events::{Event, FundingInfo, PathFailure, PaymentPurpose, ClosureReason, HTLCDestination, PaymentFailureReason}; @@ -7058,6 +7059,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fail_htlc_before_commitment() { channel_id: chan.2, htlc_id: 0, reason: Vec::new(), + attribution_data: Some([0; ATTRIBUTION_DATA_LEN]) }; nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &update_msg); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 4d2102171eb..3870d934e72 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -767,8 +767,10 @@ pub struct UpdateFailHTLC { /// The HTLC ID pub htlc_id: u64, pub(crate) reason: Vec, -} + /// Optional field for the attribution data that allows the sender to pinpoint the failing node under all conditions + pub attribution_data: Option<[u8; ATTRIBUTION_DATA_LEN]> +} /// An [`update_fail_malformed_htlc`] message to be sent to or received from a peer. /// /// [`update_fail_malformed_htlc`]: https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#removing-an-htlc-update_fulfill_htlc-update_fail_htlc-and-update_fail_malformed_htlc @@ -2247,6 +2249,8 @@ pub use self::fuzzy_internal_msgs::*; #[cfg(not(fuzzing))] pub(crate) use self::fuzzy_internal_msgs::*; +use super::onion_utils::ATTRIBUTION_DATA_LEN; + /// BOLT 4 onion packet including hop data for the next peer. #[derive(Clone, Hash, PartialEq, Eq)] pub struct OnionPacket { @@ -2353,12 +2357,14 @@ pub(crate) struct OnionErrorPacket { // This really should be a constant size slice, but the spec lets these things be up to 128KB? // (TODO) We limit it in decode to much lower... pub(crate) data: Vec, + pub(crate) attribution_data: Option<[u8; ATTRIBUTION_DATA_LEN]>, } impl From for OnionErrorPacket { fn from(msg: UpdateFailHTLC) -> Self { OnionErrorPacket { data: msg.reason, + attribution_data: msg.attribution_data, } } } @@ -2699,6 +2705,7 @@ impl_writeable!(DecodedOnionErrorPacket, { pad }); + #[cfg(not(taproot))] impl_writeable_msg!(FundingCreated, { temporary_channel_id, @@ -2981,7 +2988,9 @@ impl_writeable_msg!(UpdateFailHTLC, { channel_id, htlc_id, reason -}, {}); +}, { + (1, attribution_data, option) +}); impl_writeable_msg!(UpdateFailMalformedHTLC, { channel_id, @@ -3931,7 +3940,8 @@ impl_writeable_msg!(GossipTimestampFilter, { mod tests { use bitcoin::{Amount, Transaction, TxIn, ScriptBuf, Sequence, Witness, TxOut}; use bitcoin::hex::DisplayHex; - use crate::ln::types::ChannelId; + use crate::ln::onion_utils::ATTRIBUTION_DATA_LEN; +use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; use crate::ln::msgs::{self, FinalOnionHopData, CommonOpenChannelFields, CommonAcceptChannelFields, OutboundTrampolinePayload, TrampolineOnionPacket, InboundOnionForwardPayload, InboundOnionReceivePayload}; @@ -4936,10 +4946,12 @@ mod tests { let update_fail_htlc = msgs::UpdateFailHTLC { channel_id: ChannelId::from_bytes([2; 32]), htlc_id: 2316138423780173, - reason: [1; 32].to_vec() + reason: [1; 32].to_vec(), + attribution_data: Some([3; ATTRIBUTION_DATA_LEN]) }; let encoded_value = update_fail_htlc.encode(); - let target_value = >::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d00200101010101010101010101010101010101010101010101010101010101010101").unwrap(); + + let target_value = >::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d0020010101010101010101010101010101010101010101010101010101010101010101fd03980303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303").unwrap(); assert_eq!(encoded_value, target_value); } diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 46661df6807..7c816e1a4af 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -518,6 +518,7 @@ where channel_id: msg.channel_id, htlc_id: msg.htlc_id, reason: failure.data, + attribution_data: failure.attribution_data, })); }; diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index 127d4a45588..0441aae9ac7 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -50,6 +50,7 @@ use crate::ln::functional_test_utils::*; use crate::ln::onion_utils::{construct_trampoline_onion_keys, construct_trampoline_onion_packet}; use super::msgs::OnionErrorPacket; +use super::onion_utils::{add_hmacs, ATTRIBUTION_DATA_LEN}; fn run_onion_failure_test(_name: &str, test_case: u8, nodes: &Vec, route: &Route, payment_hash: &PaymentHash, payment_secret: &PaymentSecret, callback_msg: F1, callback_node: F2, expected_retryable: bool, expected_error_code: Option, expected_channel_update: Option, expected_short_channel_id: Option, expected_htlc_destination: Option) where F1: for <'a> FnMut(&'a mut msgs::UpdateAddHTLC), @@ -166,6 +167,7 @@ fn run_onion_failure_test_with_fail_intercept( // backward fail on 1 let update_1_0 = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); assert!(update_1_0.update_fail_htlcs.len() == 1); + update_1_0 }, _ => unreachable!(), @@ -174,6 +176,7 @@ fn run_onion_failure_test_with_fail_intercept( // 1 => 0 commitment_signed_dance if update_1_0.update_fail_htlcs.len() > 0 { let mut fail_msg = update_1_0.update_fail_htlcs[0].clone(); + if test_case == 100 { callback_fail(&mut fail_msg); } @@ -411,8 +414,9 @@ fn test_onion_failure() { // and tamper returning error message let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), NODE|2, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), NODE|2, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{}, true, Some(NODE|2), Some(NetworkUpdate::NodeFailure{node_id: route.paths[0].hops[0].pubkey, is_permanent: false}), Some(route.paths[0].hops[0].short_channel_id), Some(next_hop_failure.clone())); // final node failure @@ -420,8 +424,9 @@ fn test_onion_failure() { // and tamper returning error message let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), NODE|2, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), NODE|2, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash); }, true, Some(NODE|2), Some(NetworkUpdate::NodeFailure{node_id: route.paths[0].hops[1].pubkey, is_permanent: false}), Some(route.paths[0].hops[1].short_channel_id), None); @@ -433,16 +438,18 @@ fn test_onion_failure() { }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|NODE|2, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|NODE|2, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{}, true, Some(PERM|NODE|2), Some(NetworkUpdate::NodeFailure{node_id: route.paths[0].hops[0].pubkey, is_permanent: true}), Some(route.paths[0].hops[0].short_channel_id), Some(next_hop_failure.clone())); // final node failure run_onion_failure_test_with_fail_intercept("permanent_node_failure", 200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), PERM|NODE|2, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), PERM|NODE|2, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash); }, false, Some(PERM|NODE|2), Some(NetworkUpdate::NodeFailure{node_id: route.paths[0].hops[1].pubkey, is_permanent: true}), Some(route.paths[0].hops[1].short_channel_id), None); @@ -454,8 +461,9 @@ fn test_onion_failure() { }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|NODE|3, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|NODE|3, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash); }, true, Some(PERM|NODE|3), Some(NetworkUpdate::NodeFailure{node_id: route.paths[0].hops[0].pubkey, is_permanent: true}), Some(route.paths[0].hops[0].short_channel_id), Some(next_hop_failure.clone())); @@ -464,8 +472,9 @@ fn test_onion_failure() { run_onion_failure_test_with_fail_intercept("required_node_feature_missing", 200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), PERM|NODE|3, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), PERM|NODE|3, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash); }, false, Some(PERM|NODE|3), Some(NetworkUpdate::NodeFailure{node_id: route.paths[0].hops[1].pubkey, is_permanent: true}), Some(route.paths[0].hops[1].short_channel_id), None); @@ -495,8 +504,9 @@ fn test_onion_failure() { }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), UPDATE|7, &err_data); + let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), UPDATE|7, &err_data, &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{}, true, Some(UPDATE|7), Some(NetworkUpdate::ChannelFailure { short_channel_id, is_permanent: false }), Some(short_channel_id), Some(next_hop_failure.clone())); @@ -508,8 +518,9 @@ fn test_onion_failure() { }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), UPDATE|7, &err_data_without_type); + let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), UPDATE|7, &err_data_without_type, &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{}, true, Some(UPDATE|7), Some(NetworkUpdate::ChannelFailure { short_channel_id, is_permanent: false }), Some(short_channel_id), Some(next_hop_failure.clone())); @@ -520,8 +531,9 @@ fn test_onion_failure() { }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|8, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|8, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; // short_channel_id from the processing node }, ||{}, true, Some(PERM|8), Some(NetworkUpdate::ChannelFailure{short_channel_id, is_permanent: true}), Some(short_channel_id), Some(next_hop_failure.clone())); @@ -531,8 +543,9 @@ fn test_onion_failure() { }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|9, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[0].shared_secret.as_ref(), PERM|9, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; // short_channel_id from the processing node }, ||{}, true, Some(PERM|9), Some(NetworkUpdate::ChannelFailure{short_channel_id, is_permanent: true}), Some(short_channel_id), Some(next_hop_failure.clone())); @@ -664,8 +677,9 @@ fn test_onion_failure() { // Tamper returning error message let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); - let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), 23, &[0;0]); + let failure = onion_utils::build_failure_packet(onion_keys[1].shared_secret.as_ref(), 23, &[0;0], &[0; 4]); msg.reason = failure.data; + msg.attribution_data = failure.attribution_data; }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash); }, true, Some(23), None, None, None); @@ -685,10 +699,13 @@ fn test_onion_failure() { decoded_err_packet.hmac = Hmac::from_engine(hmac).to_byte_array(); let mut onion_error = OnionErrorPacket { data: decoded_err_packet.encode(), + attribution_data: Some([0; ATTRIBUTION_DATA_LEN]), }; + add_hmacs(&onion_keys[1].shared_secret.as_ref(), &onion_error.data, onion_error.attribution_data.as_mut().unwrap()); onion_utils::test_crypt_failure_packet( &onion_keys[1].shared_secret.as_ref(), &mut onion_error); msg.reason = onion_error.data; + msg.attribution_data = onion_error.attribution_data; }, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None, Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }), Some(channels[1].0.contents.short_channel_id), None); @@ -712,10 +729,13 @@ fn test_onion_failure() { decoded_err_packet.hmac = Hmac::from_engine(hmac).to_byte_array(); let mut onion_error = OnionErrorPacket{ data: decoded_err_packet.encode(), + attribution_data: Some([0; ATTRIBUTION_DATA_LEN]), }; + add_hmacs(&onion_keys[0].shared_secret.as_ref(), &onion_error.data, onion_error.attribution_data.as_mut().unwrap()); onion_utils::test_crypt_failure_packet( &onion_keys[0].shared_secret.as_ref(), &mut onion_error); msg.reason = onion_error.data; + msg.attribution_data = onion_error.attribution_data; }, || {}, true, Some(0x1000|7), Some(NetworkUpdate::ChannelFailure { short_channel_id: channels[1].0.contents.short_channel_id, @@ -740,10 +760,13 @@ fn test_onion_failure() { decoded_err_packet.hmac = Hmac::from_engine(hmac).to_byte_array(); let mut onion_error = OnionErrorPacket{ data: decoded_err_packet.encode(), + attribution_data: Some([0; ATTRIBUTION_DATA_LEN]), }; + add_hmacs(&onion_keys[1].shared_secret.as_ref(), &onion_error.data, onion_error.attribution_data.as_mut().unwrap()); onion_utils::test_crypt_failure_packet( &onion_keys[1].shared_secret.as_ref(), &mut onion_error); msg.reason = onion_error.data; + msg.attribution_data = onion_error.attribution_data; }, || nodes[2].node.fail_htlc_backwards(&payment_hash), true, Some(0x1000|7), Some(NetworkUpdate::ChannelFailure { short_channel_id: channels[1].0.contents.short_channel_id, diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 41701b1ade3..1940b1c67db 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -22,7 +22,9 @@ use crate::types::features::{ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::errors::{self, APIError}; use crate::util::logger::Logger; -use crate::util::ser::{LengthCalculatingWriter, Readable, ReadableArgs, Writeable, Writer}; +use crate::util::ser::{ + LengthCalculatingWriter, Readable, ReadableArgs, VecWriter, Writeable, Writer, +}; use bitcoin::hashes::cmp::fixed_time_eq; use bitcoin::hashes::hmac::{Hmac, HmacEngine}; @@ -87,6 +89,14 @@ pub(super) fn gen_ammag_from_shared_secret(shared_secret: &[u8]) -> [u8; 32] { Hmac::from_engine(hmac).to_byte_array() } +#[inline] +pub(super) fn gen_ammagext_from_shared_secret(shared_secret: &[u8]) -> [u8; 32] { + assert_eq!(shared_secret.len(), 32); + let mut hmac = HmacEngine::::new(&[0x61, 0x6d, 0x6d, 0x61, 0x67, 0x65, 0x78, 0x74]); // ammagext + hmac.input(&shared_secret); + Hmac::from_engine(hmac).to_byte_array() +} + #[cfg(test)] #[inline] pub(super) fn gen_pad_from_shared_secret(shared_secret: &[u8]) -> [u8; 32] { @@ -874,6 +884,11 @@ fn construct_onion_packet_with_init_noise( fn crypt_failure_packet(shared_secret: &[u8], packet: &mut OnionErrorPacket) { let ammag = gen_ammag_from_shared_secret(&shared_secret); process_chacha(&ammag, &mut packet.data); + + if let Some(ref mut attribution_data) = packet.attribution_data { + let ammagext = gen_ammagext_from_shared_secret(&shared_secret); + process_chacha(&ammagext, attribution_data); + } } #[cfg(test)] @@ -887,10 +902,9 @@ fn process_chacha(key: &[u8; 32], packet: &mut [u8]) { } fn build_unencrypted_failure_packet( - shared_secret: &[u8], failure_type: u16, failure_data: &[u8], + shared_secret: &[u8], failure_type: u16, failure_data: &[u8], payload: &[u8; 4], ) -> OnionErrorPacket { assert_eq!(shared_secret.len(), 32); - assert!(failure_data.len() <= 256 - 2); let um = gen_um_from_shared_secret(&shared_secret); @@ -901,25 +915,40 @@ fn build_unencrypted_failure_packet( res.extend_from_slice(&failure_data[..]); res }; + let pad_len = if failure_data.len() > 1024 - 2 { 0 } else { 1024 - 2 - failure_data.len() }; let pad = { - let mut res = Vec::with_capacity(256 - 2 - failure_data.len()); - res.resize(256 - 2 - failure_data.len(), 0); + let mut res = Vec::with_capacity(pad_len); + res.resize(pad_len, 0); res }; - let mut packet = msgs::DecodedOnionErrorPacket { hmac: [0; 32], failuremsg, pad }; + + let mut writer = VecWriter(Vec::new()); + failuremsg.write(&mut writer).unwrap(); + pad.write(&mut writer).unwrap(); + let encoded_msg = writer.0; let mut hmac = HmacEngine::::new(&um); - hmac.input(&packet.encode()[32..]); - packet.hmac = Hmac::from_engine(hmac).to_byte_array(); + hmac.input(encoded_msg.as_slice()); + let hmac = Hmac::from_engine(hmac).to_byte_array(); - OnionErrorPacket { data: packet.encode() } + let mut data = Vec::new(); + data.extend_from_slice(&hmac); + data.extend_from_slice(&encoded_msg); + + // Prepare attribution data. + let mut attribution_data = [0; ATTRIBUTION_DATA_LEN]; + attribution_data[..PAYLOAD_LEN].copy_from_slice(payload); + + add_hmacs(shared_secret, &data, &mut attribution_data); + + OnionErrorPacket { data, attribution_data: Some(attribution_data) } } pub(super) fn build_failure_packet( - shared_secret: &[u8], failure_type: u16, failure_data: &[u8], + shared_secret: &[u8], failure_type: u16, failure_data: &[u8], payload: &[u8; 4], ) -> OnionErrorPacket { let mut onion_error_packet = - build_unencrypted_failure_packet(shared_secret, failure_type, failure_data); + build_unencrypted_failure_packet(shared_secret, failure_type, failure_data, payload); crypt_failure_packet(shared_secret, &mut onion_error_packet); @@ -931,6 +960,7 @@ pub(crate) struct DecodedOnionFailure { pub(crate) short_channel_id: Option, pub(crate) payment_failed_permanently: bool, pub(crate) failed_within_blinded_path: bool, + pub(crate) hold_times: Vec, #[cfg(any(test, feature = "_test_utils"))] pub(crate) onion_error_code: Option, #[cfg(any(test, feature = "_test_utils"))] @@ -947,6 +977,8 @@ pub(super) fn process_onion_failure( where L::Target: Logger, { + // log_info!(logger, "ATTRIBUTION DATA: {:?}", encrypted_packet.attribution_data); + let (path, session_priv, first_hop_htlc_msat) = match htlc_source { HTLCSource::OutboundRoute { ref path, ref session_priv, ref first_hop_htlc_msat, .. @@ -974,6 +1006,8 @@ where const NODE: u16 = 0x2000; const UPDATE: u16 = 0x1000; + let mut hold_times: Vec = Vec::new(); + // Handle packed channel/node updates for passing back for the route handler let callback = |shared_secret: SharedSecret, _, @@ -984,6 +1018,8 @@ where return; } + // log_info!(logger, "Processing index {} of {}", route_hop_idx, path.hops.len()); + let route_hop = match route_hop_opt { Some(hop) => hop, None => { @@ -1049,6 +1085,73 @@ where crypt_failure_packet(shared_secret.as_ref(), &mut encrypted_packet); let um = gen_um_from_shared_secret(shared_secret.as_ref()); + + // Check attr error hmacs if present. + if let Some(ref attribution_data) = encrypted_packet.attribution_data { + let message = &encrypted_packet.data; + let payloads = &attribution_data[..MAX_HOPS * PAYLOAD_LEN]; + let hmacs = &attribution_data[MAX_HOPS * PAYLOAD_LEN..]; + + let um = gen_um_from_shared_secret(shared_secret.as_ref()); + let mut hmac = HmacEngine::::new(&um); + + hmac.input(&message); + hmac.input(&payloads[..(MAX_HOPS - route_hop_idx) * PAYLOAD_LEN]); + + let position: usize = MAX_HOPS - route_hop_idx - 1; + write_downstream_hmacs(position, MAX_HOPS, hmacs, &mut hmac); + + let actual_hmac = &hmacs[route_hop_idx * HMAC_LEN..route_hop_idx * HMAC_LEN + HMAC_LEN]; + let expected_hmac = &Hmac::from_engine(hmac).to_byte_array()[..HMAC_LEN]; + + if !fixed_time_eq(expected_hmac, actual_hmac) { + res = Some(FailureLearnings { + network_update: None, + short_channel_id: Some(route_hop.short_channel_id), + payment_failed_permanently: false, + failed_within_blinded_path: false, + }); + + log_debug!( + logger, + "Invalid HMAC in attributable data for node at pos {}", + route_hop_idx + ); + + return; + } + + // Record hold time. + let hold_time: u32 = u32::from_be_bytes(payloads[..PAYLOAD_LEN].try_into().unwrap()); + hold_times.push(hold_time); + + log_debug!(logger, "Htlc hold time at pos {}: {} ms", route_hop_idx, hold_time); + + // Shift payloads left. + let payloads = + &mut encrypted_packet.attribution_data.as_mut().unwrap()[..MAX_HOPS * PAYLOAD_LEN]; + payloads.copy_within(PAYLOAD_LEN.., 0); + + // Shift hmacs left. + let hmacs = + &mut encrypted_packet.attribution_data.as_mut().unwrap()[MAX_HOPS * PAYLOAD_LEN..]; + let mut src_idx = MAX_HOPS; + let mut dest_idx = 1; + let mut copy_len = MAX_HOPS - 1; + + for i in 0..MAX_HOPS - 1 { + hmacs.copy_within( + src_idx * HMAC_LEN..(src_idx + copy_len) * HMAC_LEN, + dest_idx * HMAC_LEN, + ); + + src_idx += copy_len; + dest_idx += copy_len + 1; + copy_len -= 1; + } + } + + // Check legacy hmac let mut hmac = HmacEngine::::new(&um); hmac.input(&encrypted_packet.data[32..]); @@ -1235,6 +1338,7 @@ where short_channel_id, payment_failed_permanently, failed_within_blinded_path, + hold_times, #[cfg(any(test, feature = "_test_utils"))] onion_error_code: error_code_ret, #[cfg(any(test, feature = "_test_utils"))] @@ -1254,6 +1358,7 @@ where short_channel_id: None, payment_failed_permanently: is_from_final_node, failed_within_blinded_path: false, + hold_times, #[cfg(any(test, feature = "_test_utils"))] onion_error_code: None, #[cfg(any(test, feature = "_test_utils"))] @@ -1269,10 +1374,21 @@ pub(super) struct HTLCFailReason(HTLCFailReasonRepr); #[derive(Clone)] // See Channel::revoke_and_ack for why, tl;dr: Rust bug #[cfg_attr(test, derive(PartialEq))] enum HTLCFailReasonRepr { - LightningError { err: msgs::OnionErrorPacket }, + LightningError { err: msgs::OnionErrorPacket, hold_time: Option }, Reason { failure_code: u16, data: Vec }, } +impl HTLCFailReason { + pub fn set_hold_time(&mut self, hold_time: u32) { + match self.0 { + HTLCFailReasonRepr::LightningError { hold_time: ref mut current_hold_time, .. } => { + *current_hold_time = Some(hold_time); + }, + _ => {}, + } + } +} + impl core::fmt::Debug for HTLCFailReason { fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { match self.0 { @@ -1300,13 +1416,21 @@ impl Readable for HTLCFailReason { impl_writeable_tlv_based_enum!(HTLCFailReasonRepr, (0, LightningError) => { (0, data, (legacy, Vec, |us| - if let &HTLCFailReasonRepr::LightningError { err: msgs::OnionErrorPacket { ref data, .. } } = us { + if let &HTLCFailReasonRepr::LightningError { err: msgs::OnionErrorPacket { ref data, .. }, .. } = us { Some(data) } else { None }) ), - (_unused, err, (static_value, msgs::OnionErrorPacket { data: data.ok_or(DecodeError::InvalidValue)? })), + (1, attribution_data, (legacy, [u8; ATTRIBUTION_DATA_LEN], |us| + if let &HTLCFailReasonRepr::LightningError { err: msgs::OnionErrorPacket { ref attribution_data, .. }, .. } = us { + *attribution_data + } else { + None + }) + ), + (2, hold_time, option), + (_unused, err, (static_value, msgs::OnionErrorPacket { data: data.ok_or(DecodeError::InvalidValue)?, attribution_data })), }, (1, Reason) => { (0, failure_code, required), @@ -1364,7 +1488,11 @@ impl HTLCFailReason { pub(super) fn from_msg(msg: &msgs::UpdateFailHTLC) -> Self { Self(HTLCFailReasonRepr::LightningError { - err: OnionErrorPacket { data: msg.reason.clone() }, + err: OnionErrorPacket { + data: msg.reason.clone(), + attribution_data: msg.attribution_data, + }, + hold_time: None, }) } @@ -1378,20 +1506,38 @@ impl HTLCFailReason { ) -> msgs::OnionErrorPacket { match self.0 { HTLCFailReasonRepr::Reason { ref failure_code, ref data } => { + let hold_time: u32 = 100; + let payload: [u8; 4] = hold_time.to_be_bytes(); + if let Some(secondary_shared_secret) = secondary_shared_secret { - let mut packet = - build_failure_packet(secondary_shared_secret, *failure_code, &data[..]); + // Phantom hop always has zero hold time. + let phantom_payload = [0u8; 4]; + + let mut packet = build_failure_packet( + secondary_shared_secret, + *failure_code, + &data[..], + &phantom_payload, + ); + process_failure_packet(&mut packet, incoming_packet_shared_secret, &payload); crypt_failure_packet(incoming_packet_shared_secret, &mut packet); packet } else { - build_failure_packet(incoming_packet_shared_secret, *failure_code, &data[..]) + build_failure_packet( + incoming_packet_shared_secret, + *failure_code, + &data[..], + &payload, + ) } }, - HTLCFailReasonRepr::LightningError { ref err } => { + HTLCFailReasonRepr::LightningError { ref err, hold_time } => { let mut err = err.clone(); + let payload: [u8; 4] = hold_time.unwrap_or(0).to_be_bytes(); + process_failure_packet(&mut err, incoming_packet_shared_secret, &payload); crypt_failure_packet(incoming_packet_shared_secret, &mut err); err @@ -1406,7 +1552,7 @@ impl HTLCFailReason { L::Target: Logger, { match self.0 { - HTLCFailReasonRepr::LightningError { ref err } => { + HTLCFailReasonRepr::LightningError { ref err, .. } => { process_onion_failure(secp_ctx, logger, &htlc_source, err.clone()) }, #[allow(unused)] @@ -1422,6 +1568,7 @@ impl HTLCFailReason { payment_failed_permanently: false, short_channel_id: Some(path.hops[0].short_channel_id), failed_within_blinded_path: false, + hold_times: Vec::new(), #[cfg(any(test, feature = "_test_utils"))] onion_error_code: Some(*failure_code), #[cfg(any(test, feature = "_test_utils"))] @@ -1959,8 +2106,110 @@ fn decode_next_hop, N: NextPacketBytes>( } } +const PAYLOAD_LEN: usize = 4; +const MAX_HOPS: usize = 20; +const HMAC_LEN: usize = 4; +const HMAC_COUNT: usize = MAX_HOPS * (MAX_HOPS + 1) / 2; + +pub const ATTRIBUTION_DATA_LEN: usize = MAX_HOPS * PAYLOAD_LEN + HMAC_COUNT * HMAC_LEN; + +// Adds the current node's hmacs for all possible positions to this packet. +pub(crate) fn add_hmacs( + shared_secret: &[u8], message: &[u8], packet: &mut [u8; ATTRIBUTION_DATA_LEN], +) { + let payloads = &packet[..MAX_HOPS * PAYLOAD_LEN]; + let hmacs: &[u8] = &packet[MAX_HOPS * PAYLOAD_LEN..]; + + let mut new_hmacs = vec![0u8; MAX_HOPS * HMAC_LEN]; + let um: [u8; 32] = gen_um_from_shared_secret(&shared_secret); + + for i in 0..MAX_HOPS { + let position: usize = MAX_HOPS - i - 1; + let mut hmac_engine = HmacEngine::::new(&um); + + hmac_engine.input(&message); + hmac_engine.input(&payloads[..(position + 1) * PAYLOAD_LEN]); + write_downstream_hmacs(position, MAX_HOPS, hmacs, &mut hmac_engine); + + let full_hmac = Hmac::from_engine(hmac_engine).to_byte_array(); + let hmac = &full_hmac[..HMAC_LEN]; + + new_hmacs[i * HMAC_LEN..(i + 1) * HMAC_LEN].copy_from_slice(hmac); + } + + let hmacs = &mut packet[MAX_HOPS * PAYLOAD_LEN..]; + hmacs[..new_hmacs.len()].copy_from_slice(&new_hmacs); +} + +pub fn write_downstream_hmacs( + position: usize, max_hops: usize, hmacs: &[u8], w: &mut HmacEngine, +) { + let mut hmac_idx = max_hops + (max_hops - position - 1); + + for j in 0..position { + w.input(&hmacs[hmac_idx * HMAC_LEN..(hmac_idx + 1) * HMAC_LEN]); + + let block_size = max_hops - j - 1; + hmac_idx += block_size; + } +} + +fn process_failure_packet( + onion_error: &mut OnionErrorPacket, shared_secret: &[u8], payload: &[u8; 4], +) { + // Create new packet. + let mut processed_packet = [0; ATTRIBUTION_DATA_LEN]; + + // Process received attribution data. If there's none, we'll still add our payload and hmacs to potentially give the + // sender attribution data for the partial path. In order for this to work, all upstream nodes need to support + // attributable failures. + if let Some(ref attribution_data) = onion_error.attribution_data { + // Shift payloads right. + { + let payloads = &attribution_data[..MAX_HOPS * PAYLOAD_LEN]; + processed_packet[PAYLOAD_LEN..MAX_HOPS * PAYLOAD_LEN] + .copy_from_slice(&payloads[..payloads.len() - PAYLOAD_LEN]); + } + + // Shift hmacs right. + { + let hmacs = &attribution_data[MAX_HOPS * PAYLOAD_LEN..]; + let processed_hmacs = &mut processed_packet[MAX_HOPS * PAYLOAD_LEN..]; + + let mut src_idx = HMAC_COUNT - 2; + let mut dest_idx = HMAC_COUNT - 1; + let mut copy_len = 1; + + for i in 0..MAX_HOPS - 1 { + processed_hmacs[dest_idx * HMAC_LEN..(dest_idx + copy_len) * HMAC_LEN] + .copy_from_slice(&hmacs[src_idx * HMAC_LEN..(src_idx + copy_len) * HMAC_LEN]); + + // Break at last iteration to prevent underflow when updating indices. + if i == MAX_HOPS - 2 { + break; + } + + copy_len += 1; + src_idx -= copy_len + 1; + dest_idx -= copy_len; + } + } + } + + // Add this node's payload. + processed_packet[0..PAYLOAD_LEN].copy_from_slice(payload); + + // Add this node's hmacs. + add_hmacs(&shared_secret, &onion_error.data, &mut processed_packet); + + onion_error.attribution_data = Some(processed_packet); +} + #[cfg(test)] mod tests { + use core::array; + use std::fs::File; + use std::io::BufReader; use std::sync::Arc; use crate::io; @@ -1975,7 +2224,7 @@ mod tests { use crate::prelude::*; use crate::util::test_utils::TestLogger; - use bitcoin::hex::FromHex; + use bitcoin::hex::{DisplayHex, FromHex}; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; @@ -2291,40 +2540,129 @@ mod tests { } #[test] - fn test_failure_packet_onion() { - // Returning Errors test vectors from BOLT 4 + fn test_attributable_failure_packet_onion_mutations() { + for mutating_node in 0..5 { + for mutated_index in 0..1060 + 920 { + let mutation = Mutation { node: mutating_node, index: mutated_index }; + let decrypted_failure = + test_attributable_failure_packet_onion_with_mutation(Some(mutation)); + + if decrypted_failure.onion_error_code == Some(16399) { + continue; + } + + assert!(decrypted_failure.short_channel_id == Some(mutating_node as u64)); + assert_eq!(decrypted_failure.hold_times, [5, 4, 3, 2, 1][..mutating_node]); + } + } + } + + #[test] + fn test_attributable_failure_packet_onion_happy() { + let decrypted_failure = test_attributable_failure_packet_onion_with_mutation(None); + assert_eq!(decrypted_failure.onion_error_code, Some(16399)); + assert_eq!(decrypted_failure.hold_times, [5, 4, 3, 2, 1]); + } + + struct Mutation { + node: usize, + index: usize, + } + + fn test_attributable_failure_packet_onion_with_mutation( + mutation: Option, + ) -> DecodedOnionFailure { + struct ExpectedMessage<'a> { + message: &'a str, + attribution_data: &'a str, + } + + const FAILURE_DATA: &str = "0000000000000064000c3500fd84d1fd012c808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080"; + const EXPECTED_MESSAGES: [ExpectedMessage; 5] = [ + ExpectedMessage { + message: "146e94a9086dbbed6a0ab6932d00c118a7195dbf69b7d7a12b0e6956fc54b5e0a989f165b5f12fd45edd73a5b0c48630ff5be69500d3d82a29c0803f0a0679a6a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e332a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e63a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f12909cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b38392a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c531eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e70d04a0a57afe4af42e99912fbb4f8883a5ecb9cb29b883cb6bfa0f4db2279ff8c6d2b56a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9f2a007229a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f", + attribution_data: "d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252" + }, + ExpectedMessage { + message: "7512354d6a26781d25e65539772ba049b7ed7c530bf75ab7ef80cf974b978a07a1c3dabc61940011585323f70fa98cfa1d4c868da30b1f751e44a72d9b3f79809c8c51c9f0843daa8fe83587844fedeacb7348362003b31922cbb4d6169b2087b6f8d192d9cfe5363254cd1fde24641bde9e422f170c3eb146f194c48a459ae2889d706dc654235fa9dd20307ea54091d09970bf956c067a3bcc05af03c41e01af949a131533778bf6ee3b546caf2eabe9d53d0fb2e8cc952b7e0f5326a69ed2e58e088729a1d85971c6b2e129a5643f3ac43da031e655b27081f10543262cf9d72d6f64d5d96387ac0d43da3e3a03da0c309af121dcf3e99192efa754eab6960c256ffd4c546208e292e0ab9894e3605db098dc16b40f17c320aa4a0e42fc8b105c22f08c9bc6537182c24e32062c6cd6d7ec7062a0c2c2ecdae1588c82185cdc61d874ee916a7873ac54cddf929354f307e870011704a0e9fbc5c7802d6140134028aca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be6403edc599c220e03ca917db765980ac781e758179cd93983e9c1e769e4241d47c", + attribution_data: "1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb" + }, + ExpectedMessage { + message: "145bc1c63058f7204abbd2320d422e69fb1b3801a14312f81e5e29e6b5f4774cfed8a25241d3dfb7466e749c1b3261559e49090853612e07bd669dfb5f4c54162fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0eef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a921a2c931500ab08e686cffb2d87258d254fb17843959cccd265a57ba26c740f0f231bb76df932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb31820064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a150776bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db121a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec8768848333156dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2caf8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b948d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee694ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b446c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab0946534e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef4341b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b761d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb89574a6bdca1185b40f8aa54026d5926725f99ef028da1be892e3586361efe15f4a148ff1bc9", + attribution_data: "34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a" + }, + ExpectedMessage { + message: "1b4b09a935ce7af95b336baae307f2b400e3a7e808d9b4cf421cc4b3955620acb69dcdb656128dae8857adbd4e6b37fbb1be9c1f2f02e61e9e59a630c4c77cf383cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd2111e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f360d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b03eba82b5d7cb78e34dc961948d6161eadd7cf5d95d9c56df2ff5faa6ccf85eacdc9ff2fc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ffe31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9fe79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c9762de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bce49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2ff45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b142444aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f35c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641cddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d25b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaaeb3bd7fc02ec02066b324cb72a09b6b43dec1097f49d69d3c138ce6f1a6402898baf7568c", + attribution_data: "74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1" + }, + ExpectedMessage { + message: "2dd2f49c1f5af0fcad371d96e8cddbdcd5096dc309c1d4e110f955926506b3c03b44c192896f45610741c85ed4074212537e0c118d472ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822669861dafcd29de74eac48b64411987285491f98d78584d0c2a163b7221ea796f9e8671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4", + attribution_data: "84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad" + } + ]; + + let failure_data = >::from_hex(FAILURE_DATA).unwrap(); + let payload = [0, 0, 0, 1]; let onion_keys = build_test_onion_keys(); let mut onion_error = super::build_unencrypted_failure_packet( onion_keys[4].shared_secret.as_ref(), - 0x2002, - &[0; 0], + 0x400f, + &failure_data, + &payload, ); - let hex = "4c2fc8bc08510334b6833ad9c3e79cd1b52ae59dfe5c2a4b23ead50f09f7ee0b0002200200fe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - assert_eq!(onion_error.data, >::from_hex(hex).unwrap()); + + let logger: Arc = Arc::new(TestLogger::new()); super::crypt_failure_packet(onion_keys[4].shared_secret.as_ref(), &mut onion_error); - let hex = "a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4"; - assert_eq!(onion_error.data, >::from_hex(hex).unwrap()); + assert_eq!(onion_error.data.to_lower_hex_string(), EXPECTED_MESSAGES[0].message); + assert_eq!( + onion_error.attribution_data.unwrap().to_lower_hex_string(), + EXPECTED_MESSAGES[0].attribution_data + ); - super::crypt_failure_packet(onion_keys[3].shared_secret.as_ref(), &mut onion_error); - let hex = "c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270"; - assert_eq!(onion_error.data, >::from_hex(hex).unwrap()); + let mut mutated = false; + let mutate_packet = |packet: &mut OnionErrorPacket, mutated_index| { + let data_len = packet.data.len(); + if mutated_index < data_len { + packet.data[mutated_index] ^= 1; + } else { + packet.attribution_data.as_mut().unwrap()[mutated_index - data_len] ^= 1; + } + }; - super::crypt_failure_packet(onion_keys[2].shared_secret.as_ref(), &mut onion_error); - let hex = "a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3"; - assert_eq!(onion_error.data, >::from_hex(hex).unwrap()); + if let Some(Mutation { node, index }) = mutation { + if node == 4 { + mutate_packet(&mut onion_error, index); + mutated = true; + } + } - super::crypt_failure_packet(onion_keys[1].shared_secret.as_ref(), &mut onion_error); - let hex = "aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921"; - assert_eq!(onion_error.data, >::from_hex(hex).unwrap()); + for idx in (0..4).rev() { + let shared_secret = onion_keys[idx].shared_secret.as_ref(); + let payload = [0, 0, 0, 5 - idx as u8]; + process_failure_packet(&mut onion_error, shared_secret, &payload); + super::crypt_failure_packet(shared_secret, &mut onion_error); - super::crypt_failure_packet(onion_keys[0].shared_secret.as_ref(), &mut onion_error); - let hex = "9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d"; - assert_eq!(onion_error.data, >::from_hex(hex).unwrap()); + if let Some(Mutation { node, index }) = mutation { + if node == idx { + mutate_packet(&mut onion_error, index); + mutated = true; + } + } + + if !mutated { + let expected_messages = &EXPECTED_MESSAGES[4 - idx]; + assert_eq!(onion_error.data.to_lower_hex_string(), expected_messages.message); + assert_eq!( + onion_error.attribution_data.unwrap().to_lower_hex_string(), + expected_messages.attribution_data + ); + } + } - let logger: Arc = Arc::new(TestLogger::new()); let ctx_full = Secp256k1::new(); + let path = build_test_path(); let htlc_source = HTLCSource::OutboundRoute { path, @@ -2333,29 +2671,20 @@ mod tests { payment_id: PaymentId([1; 32]), }; - // Assert that the original failure can be retrieved and that all hmacs check out. - let decrypted_failure = - process_onion_failure(&ctx_full, &logger, &htlc_source, onion_error); - - assert_eq!(decrypted_failure.onion_error_code, Some(0x2002)); + process_onion_failure(&ctx_full, &logger, &htlc_source, onion_error) } #[test] fn test_non_attributable_failure_packet_onion() { // Create a failure packet with bogus data. let packet = vec![1u8; 292]; - let onion_error_packet = OnionErrorPacket { data: packet }; + let onion_error_packet = + OnionErrorPacket { data: packet, attribution_data: Some([0; ATTRIBUTION_DATA_LEN]) }; - // In the current protocol, it is unfortunately not possible to identify the failure source. + // With attributable failures, it should still be possible to identify the failing node. let logger: TestLogger = TestLogger::new(); let decrypted_failure = test_failure_attribution(&logger, onion_error_packet); - assert_eq!(decrypted_failure.short_channel_id, None); - - logger.assert_log_contains( - "lightning::ln::onion_utils", - "Non-attributable failure encountered", - 1, - ); + assert_eq!(decrypted_failure.short_channel_id, Some(0)); } #[test] @@ -2373,7 +2702,15 @@ mod tests { let hmac = Hmac::from_engine(hmac).to_byte_array(); packet[..32].copy_from_slice(&hmac); - let mut onion_error_packet = OnionErrorPacket { data: packet.to_vec() }; + let mut onion_error_packet = OnionErrorPacket { + data: packet.to_vec(), + attribution_data: Some([0; ATTRIBUTION_DATA_LEN]), + }; + add_hmacs( + shared_secret, + &onion_error_packet.data, + onion_error_packet.attribution_data.as_mut().unwrap(), + ); crypt_failure_packet(shared_secret, &mut onion_error_packet); // For the unreadable failure, it is still expected that the failing channel can be identified. @@ -2399,7 +2736,15 @@ mod tests { hmac.input(&packet.encode()[32..]); packet.hmac = Hmac::from_engine(hmac).to_byte_array(); - let mut onion_error_packet = OnionErrorPacket { data: packet.encode() }; + let mut onion_error_packet = OnionErrorPacket { + data: packet.encode(), + attribution_data: Some([0; ATTRIBUTION_DATA_LEN]), + }; + add_hmacs( + shared_secret, + &onion_error_packet.data, + onion_error_packet.attribution_data.as_mut().unwrap(), + ); crypt_failure_packet(shared_secret, &mut onion_error_packet); let logger = TestLogger::new(); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 3033ffd3a2d..b31f236ae18 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2163,11 +2163,11 @@ impl OutboundPayments { #[cfg(any(test, feature = "_test_utils"))] let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently, onion_error_code, - onion_error_data, failed_within_blinded_path + onion_error_data, failed_within_blinded_path, hold_times, } = onion_error.decode_onion_failure(secp_ctx, logger, &source); #[cfg(not(any(test, feature = "_test_utils")))] let DecodedOnionFailure { - network_update, short_channel_id, payment_failed_permanently, failed_within_blinded_path + network_update, short_channel_id, payment_failed_permanently, failed_within_blinded_path, hold_times, } = onion_error.decode_onion_failure(secp_ctx, logger, &source); let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 21997c09c1a..a9edd59f782 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -712,6 +712,8 @@ impl_array!(1300, u8); // for OnionPacket.hop_data impl_array!(8, u16); impl_array!(32, u16); +impl_array!(920, u8); + /// A type for variable-length values within TLV record where the length is encoded as part of the record. /// Used to prevent encoding the length twice. /// @@ -1061,6 +1063,7 @@ impl_for_vec!((A, B), A, B); impl_writeable_for_vec!(&crate::routing::router::BlindedTail); impl_readable_for_vec!(crate::routing::router::BlindedTail); impl_for_vec!(crate::routing::router::TrampolineHop); +impl_for_vec!(Option<[u8; 920]>); impl_for_vec_with_element_length_prefix!(crate::ln::msgs::UpdateAddHTLC); impl_writeable_for_vec_with_element_length_prefix!(&crate::ln::msgs::UpdateAddHTLC); From e7923e992f81e6f958c880e4522b60b9eff73ec2 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 10 Mar 2025 14:10:30 +0100 Subject: [PATCH 2/2] Attributable failures optional feature bit Signal to senders that the node will return attributable failures. When the sender makes sure all path nodes supports this, they will be able to attribute every failure that may occur. --- lightning-invoice/src/lib.rs | 8 +++++++- lightning-types/src/features.rs | 18 ++++++++++++++---- lightning/src/offers/invoice.rs | 24 ++++++++++++++++++------ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 17cc41f9502..880f00f6e36 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -597,12 +597,18 @@ impl InvoiceBuilder Self { + let mut features = Bolt11InvoiceFeatures::empty(); + features.set_attributable_failures_optional(); + + let mut tagged_fields = Vec::with_capacity(8); + tagged_fields.push(TaggedField::Features(features)); + InvoiceBuilder { currency, amount: None, si_prefix: None, timestamp: None, - tagged_fields: Vec::with_capacity(8), + tagged_fields, error: None, phantom_d: core::marker::PhantomData, diff --git a/lightning-types/src/features.rs b/lightning-types/src/features.rs index ea1b9b5b522..28493d84d19 100644 --- a/lightning-types/src/features.rs +++ b/lightning-types/src/features.rs @@ -154,7 +154,7 @@ mod sealed { // Byte 3 RouteBlinding | ShutdownAnySegwit | DualFund | Taproot, // Byte 4 - Quiescence | OnionMessages, + Quiescence | OnionMessages | AttributableFailures, // Byte 5 ProvideStorage | ChannelType | SCIDPrivacy, // Byte 6 @@ -175,7 +175,7 @@ mod sealed { // Byte 3 RouteBlinding | ShutdownAnySegwit | DualFund | Taproot, // Byte 4 - Quiescence | OnionMessages, + Quiescence | OnionMessages | AttributableFailures, // Byte 5 ProvideStorage | ChannelType | SCIDPrivacy, // Byte 6 @@ -199,7 +199,7 @@ mod sealed { // Byte 3 , // Byte 4 - , + AttributableFailures, // Byte 5 , // Byte 6 @@ -219,7 +219,7 @@ mod sealed { // Byte 3 , // Byte 4 - , + AttributableFailures, // Byte 5 , // Byte 6 @@ -548,6 +548,16 @@ mod sealed { supports_quiescence, requires_quiescence ); + define_feature!( + 37, + AttributableFailures, + [InitContext, NodeContext, Bolt11InvoiceContext, Bolt12InvoiceContext], + "Feature flags for `option_attributable_failures`.", + set_attributable_failures_optional, + set_attributable_failures_required, + supports_attributable_failures, + requires_attributable_failures + ); define_feature!( 39, OnionMessages, diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index be3886346c5..5d84df94dec 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -413,6 +413,9 @@ macro_rules! invoice_builder_methods { payment_paths: Vec, created_at: Duration, payment_hash: PaymentHash, amount_msats: u64, signing_pubkey: PublicKey, ) -> InvoiceFields { + let mut features = Bolt12InvoiceFeatures::empty(); + features.set_attributable_failures_optional(); + InvoiceFields { payment_paths, created_at, @@ -420,7 +423,7 @@ macro_rules! invoice_builder_methods { payment_hash, amount_msats, fallbacks: None, - features: Bolt12InvoiceFeatures::empty(), + features, signing_pubkey, #[cfg(test)] experimental_baz: None, @@ -1777,6 +1780,7 @@ mod tests { use bitcoin::secp256k1::{self, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey}; use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion}; + use core::f32::consts::E; use core::time::Duration; use crate::blinded_path::message::BlindedMessagePath; @@ -1878,7 +1882,10 @@ mod tests { assert!(!unsigned_invoice.is_expired()); assert_eq!(unsigned_invoice.payment_hash(), payment_hash); assert!(unsigned_invoice.fallbacks().is_empty()); - assert_eq!(unsigned_invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); + + let mut expected_features = Bolt12InvoiceFeatures::empty(); + expected_features.set_attributable_failures_optional(); + assert_eq!(unsigned_invoice.invoice_features(), &expected_features); match UnsignedBolt12Invoice::try_from(buffer) { Err(e) => panic!("error parsing unsigned invoice: {:?}", e), @@ -1926,7 +1933,7 @@ mod tests { assert!(!invoice.is_expired()); assert_eq!(invoice.payment_hash(), payment_hash); assert!(invoice.fallbacks().is_empty()); - assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); + assert_eq!(invoice.invoice_features(), &expected_features); assert!(!invoice.is_for_refund_without_paths()); let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); @@ -1974,7 +1981,7 @@ mod tests { payment_hash: Some(&payment_hash), amount: Some(1000), fallbacks: None, - features: None, + features: Some(&expected_features), node_id: Some(&recipient_pubkey()), message_paths: None, }, @@ -2009,6 +2016,9 @@ mod tests { let mut buffer = Vec::new(); invoice.write(&mut buffer).unwrap(); + let mut expected_features = Bolt12InvoiceFeatures::empty(); + expected_features.set_attributable_failures_optional(); + assert_eq!(invoice.bytes, buffer.as_slice()); assert_eq!(invoice.payer_metadata(), &[1; 32]); assert_eq!(invoice.offer_chains(), None); @@ -2034,7 +2044,7 @@ mod tests { assert!(!invoice.is_expired()); assert_eq!(invoice.payment_hash(), payment_hash); assert!(invoice.fallbacks().is_empty()); - assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); + assert_eq!(invoice.invoice_features(), &expected_features); assert!(invoice.is_for_refund_without_paths()); let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); @@ -2077,7 +2087,7 @@ mod tests { payment_hash: Some(&payment_hash), amount: Some(1000), fallbacks: None, - features: None, + features: Some(&expected_features), node_id: Some(&recipient_pubkey()), message_paths: None, }, @@ -2501,6 +2511,7 @@ mod tests { let mut features = Bolt12InvoiceFeatures::empty(); features.set_basic_mpp_optional(); + features.set_attributable_failures_optional(); let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) @@ -2838,6 +2849,7 @@ mod tests { Ok(invoice) => { let mut features = Bolt12InvoiceFeatures::empty(); features.set_basic_mpp_optional(); + features.set_attributable_failures_optional(); assert_eq!(invoice.invoice_features(), &features); }, Err(e) => panic!("error parsing invoice: {:?}", e),