From 64c26c8a793f35c55076a2188912e92106900bab Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Apr 2023 12:06:35 -0400 Subject: [PATCH] Add blinded path {metadata} fields to Path, but disallow paying blinded paths for now --- fuzz/src/chanmon_consistency.rs | 4 +- lightning-background-processor/src/lib.rs | 2 +- lightning/src/events/mod.rs | 8 +- lightning/src/ln/channel.rs | 2 +- lightning/src/ln/channelmanager.rs | 4 +- lightning/src/ln/functional_tests.rs | 4 +- lightning/src/ln/onion_utils.rs | 2 +- lightning/src/ln/outbound_payment.rs | 6 +- lightning/src/ln/payment_tests.rs | 28 +++---- lightning/src/routing/router.rs | 93 +++++++++++++++++------ lightning/src/routing/scoring.rs | 6 +- lightning/src/util/macro_logger.rs | 1 + 12 files changed, 106 insertions(+), 54 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 76305303..eca4c4c8 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -359,7 +359,7 @@ fn send_payment(source: &ChanMan, dest: &ChanMan, dest_chan_id: u64, amt: u64, p channel_features: dest.channel_features(), fee_msat: amt, cltv_expiry_delta: 200, - }]}], + }], blinded_tail: None }], payment_params: None, }, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_id)) { check_payment_err(err); @@ -388,7 +388,7 @@ fn send_hop_payment(source: &ChanMan, middle: &ChanMan, middle_chan_id: u64, des channel_features: dest.channel_features(), fee_msat: amt, cltv_expiry_delta: 200, - }]}], + }], blinded_tail: None }], payment_params: None, }, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_id)) { check_payment_err(err); diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 18ed6da0..627f0db1 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -1512,7 +1512,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 0, cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA as u32, - }]}; + }], blinded_tail: None }; $nodes[0].scorer.lock().unwrap().expect(TestResult::PaymentFailure { path: path.clone(), short_channel_id: scored_scid }); $nodes[0].node.push_pending_event(Event::PaymentPathFailed { diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index dc066b1e..b5f682e1 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1141,7 +1141,7 @@ impl MaybeReadable for Event { payment_hash, payment_failed_permanently, failure, - path: Path { hops: path.unwrap() }, + path: Path { hops: path.unwrap(), blinded_tail: None }, short_channel_id, #[cfg(test)] error_code, @@ -1255,7 +1255,7 @@ impl MaybeReadable for Event { Ok(Some(Event::PaymentPathSuccessful { payment_id, payment_hash, - path: Path { hops: path.unwrap() }, + path: Path { hops: path.unwrap(), blinded_tail: None }, })) }; f() @@ -1316,7 +1316,7 @@ impl MaybeReadable for Event { Ok(Some(Event::ProbeSuccessful { payment_id, payment_hash, - path: Path { hops: path.unwrap() }, + path: Path { hops: path.unwrap(), blinded_tail: None }, })) }; f() @@ -1336,7 +1336,7 @@ impl MaybeReadable for Event { Ok(Some(Event::ProbeFailed { payment_id, payment_hash, - path: Path { hops: path.unwrap() }, + path: Path { hops: path.unwrap(), blinded_tail: None }, short_channel_id, })) }; diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 79711080..cc3f7352 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -7202,7 +7202,7 @@ mod tests { cltv_expiry: 200000000, state: OutboundHTLCState::Committed, source: HTLCSource::OutboundRoute { - path: Path { hops: Vec::new() }, + path: Path { hops: Vec::new(), blinded_tail: None }, session_priv: SecretKey::from_slice(&hex::decode("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(), first_hop_htlc_msat: 548, payment_id: PaymentId([42; 32]), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5f77c43c..60de5a3d 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7019,13 +7019,13 @@ impl Readable for HTLCSource { // instead. payment_id = Some(PaymentId(*session_priv.0.unwrap().as_ref())); } - let path = Path { hops: path_hops.ok_or(DecodeError::InvalidValue)? }; + let path = Path { hops: path_hops.ok_or(DecodeError::InvalidValue)?, blinded_tail: None }; if path.hops.len() == 0 { return Err(DecodeError::InvalidValue); } if let Some(params) = payment_params.as_mut() { if params.final_cltv_expiry_delta == 0 { - params.final_cltv_expiry_delta = path.final_cltv_expiry_delta(); + params.final_cltv_expiry_delta = path.final_cltv_expiry_delta().ok_or(DecodeError::InvalidValue)?; } } Ok(HTLCSource::OutboundRoute { diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 65ad811d..d56f1bd3 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1044,7 +1044,7 @@ fn fake_network_test() { }); hops[1].fee_msat = chan_4.1.contents.fee_base_msat as u64 + chan_4.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.0.contents.fee_base_msat as u64 + chan_3.0.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; - let payment_preimage_1 = send_along_route(&nodes[1], Route { paths: vec![Path { hops }], payment_params: None }, &vec!(&nodes[2], &nodes[3], &nodes[1])[..], 1000000).0; + let payment_preimage_1 = send_along_route(&nodes[1], Route { paths: vec![Path { hops, blinded_tail: None }], payment_params: None }, &vec!(&nodes[2], &nodes[3], &nodes[1])[..], 1000000).0; let mut hops = Vec::with_capacity(3); hops.push(RouteHop { @@ -1073,7 +1073,7 @@ fn fake_network_test() { }); hops[1].fee_msat = chan_2.1.contents.fee_base_msat as u64 + chan_2.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.1.contents.fee_base_msat as u64 + chan_3.1.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; - let payment_hash_2 = send_along_route(&nodes[1], Route { paths: vec![Path { hops }], payment_params: None }, &vec!(&nodes[3], &nodes[2], &nodes[1])[..], 1000000).1; + let payment_hash_2 = send_along_route(&nodes[1], Route { paths: vec![Path { hops, blinded_tail: None }], payment_params: None }, &vec!(&nodes[3], &nodes[2], &nodes[1])[..], 1000000).1; // Claim the rebalances... fail_payment(&nodes[1], &vec!(&nodes[3], &nodes[2], &nodes[1])[..], payment_hash_2); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index df0b9031..36abad71 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -929,7 +929,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops. }, - ]}], + ], blinded_tail: None }], payment_params: None, }; diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 18763685..636bc13a 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1000,6 +1000,10 @@ impl OutboundPayments { path_errs.push(Err(APIError::InvalidRoute{err: "Path didn't go anywhere/had bogus size".to_owned()})); continue 'path_check; } + if path.blinded_tail.is_some() { + path_errs.push(Err(APIError::InvalidRoute{err: "Sending to blinded paths isn't supported yet".to_owned()})); + continue 'path_check; + } for (idx, hop) in path.hops.iter().enumerate() { if idx != path.hops.len() - 1 && hop.pubkey == our_node_id { path_errs.push(Err(APIError::InvalidRoute{err: "Path went through us but wasn't a simple rebalance loop to us".to_owned()})); @@ -1552,7 +1556,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 0, cltv_expiry_delta: 0, - }]}], + }], blinded_tail: None }], payment_params: Some(payment_params), }; router.expect_find_route(route_params.clone(), Ok(route.clone())); diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index b599bee3..96de3e10 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -1846,7 +1846,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 2, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -1854,7 +1854,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 2, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, ], payment_params: Some(route_params.payment_params.clone()), }; @@ -1867,7 +1867,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 4, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -1875,7 +1875,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 4, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, ], payment_params: Some(route_params.payment_params.clone()), }; @@ -1888,7 +1888,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 4, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, ], payment_params: Some(route_params.payment_params.clone()), }; @@ -2135,7 +2135,7 @@ fn retry_multi_path_single_failed_payment() { channel_features: nodes[1].node.channel_features(), fee_msat: 10_000, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2143,7 +2143,7 @@ fn retry_multi_path_single_failed_payment() { channel_features: nodes[1].node.channel_features(), fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, ], payment_params: Some(payment_params), }; @@ -2229,7 +2229,7 @@ fn immediate_retry_on_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, ], payment_params: Some(PaymentParameters::from_node_id(nodes[1].node.get_our_node_id(), TEST_FINAL_CLTV)), }; @@ -2324,7 +2324,7 @@ fn no_extra_retries_on_back_to_back_fail() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2339,7 +2339,7 @@ fn no_extra_retries_on_back_to_back_fail() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, - }]} + }], blinded_tail: None } ], payment_params: Some(PaymentParameters::from_node_id(nodes[2].node.get_our_node_id(), TEST_FINAL_CLTV)), }; @@ -2526,7 +2526,7 @@ fn test_simple_partial_retry() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2541,7 +2541,7 @@ fn test_simple_partial_retry() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, - }]} + }], blinded_tail: None } ], payment_params: Some(PaymentParameters::from_node_id(nodes[2].node.get_our_node_id(), TEST_FINAL_CLTV)), }; @@ -2692,7 +2692,7 @@ fn test_threaded_payment_retries() { channel_features: nodes[2].node.channel_features(), fee_msat: amt_msat / 1000, cltv_expiry_delta: 100, - }]}, + }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[2].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -2707,7 +2707,7 @@ fn test_threaded_payment_retries() { channel_features: nodes[3].node.channel_features(), fee_msat: amt_msat - amt_msat / 1000, cltv_expiry_delta: 100, - }]} + }], blinded_tail: None } ], payment_params: Some(PaymentParameters::from_node_id(nodes[2].node.get_our_node_id(), TEST_FINAL_CLTV)), }; diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index d4e7306c..0a2cb908 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -13,7 +13,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; -use crate::blinded_path::BlindedPath; +use crate::blinded_path::{BlindedHop, BlindedPath}; use crate::ln::PaymentHash; use crate::ln::channelmanager::{ChannelDetails, PaymentId}; use crate::ln::features::{ChannelFeatures, InvoiceFeatures, NodeFeatures}; @@ -227,10 +227,18 @@ pub struct RouteHop { /// to reach this node. pub channel_features: ChannelFeatures, /// The fee taken on this hop (for paying for the use of the *next* channel in the path). - /// For the last hop, this should be the full value of this path's part of the payment. + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPath`], this is the fee paid for use of the entire blinded path + /// * otherwise, this is the full value of this [`Path`]'s part of the payment + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath pub fee_msat: u64, - /// The CLTV delta added for this hop. For the last hop, this is the CLTV delta expected at the - /// destination. + /// The CLTV delta added for this hop. + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPath`], this is the CLTV delta for the entire blinded path + /// * otherwise, this is the CLTV delta expected at the destination + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath pub cltv_expiry_delta: u32, } @@ -243,30 +251,71 @@ impl_writeable_tlv_based!(RouteHop, { (10, cltv_expiry_delta, required), }); -/// A path in a [`Route`] to the payment recipient. +/// The blinded portion of a [`Path`], if we're routing to a recipient who provided blinded paths in +/// their BOLT12 [`Invoice`]. +/// +/// [`Invoice`]: crate::offers::invoice::Invoice +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct BlindedTail { + /// The hops of the [`BlindedPath`] provided by the recipient. + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath + pub hops: Vec, + /// The blinding point of the [`BlindedPath`] provided by the recipient. + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath + pub blinding_point: PublicKey, + /// Excess CLTV delta added to the recipient's CLTV expiry to deter intermediate nodes from + /// inferring the destination. May be 0. + pub excess_final_cltv_expiry_delta: u32, + /// The total amount paid on this [`Path`], excluding the fees. + pub final_value_msat: u64, +} + +impl_writeable_tlv_based!(BlindedTail, { + (0, hops, vec_type), + (2, blinding_point, required), + (4, excess_final_cltv_expiry_delta, required), + (6, final_value_msat, required), +}); + +/// A path in a [`Route`] to the payment recipient. Must always be at least length one. +/// If no [`Path::blinded_tail`] is present, then [`Path::hops`] length may be up to 19. #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Path { - /// The list of unblinded hops in this [`Path`]. + /// The list of unblinded hops in this [`Path`]. Must be at least length one. pub hops: Vec, + /// The blinded path at which this path terminates, if we're sending to one, and its metadata. + pub blinded_tail: Option, } impl Path { /// Gets the fees for a given path, excluding any excess paid to the recipient. pub fn fee_msat(&self) -> u64 { - // Do not count last hop of each path since that's the full value of the payment - self.hops.split_last().map(|(_, path_prefix)| path_prefix).unwrap_or(&[]) - .iter().map(|hop| &hop.fee_msat) - .sum() + match &self.blinded_tail { + Some(_) => self.hops.iter().map(|hop| hop.fee_msat).sum::(), + None => { + // Do not count last hop of each path since that's the full value of the payment + self.hops.split_last().map_or(0, + |(_, path_prefix)| path_prefix.iter().map(|hop| hop.fee_msat).sum()) + } + } } /// Gets the total amount paid on this [`Path`], excluding the fees. pub fn final_value_msat(&self) -> u64 { - self.hops.last().map_or(0, |hop| hop.fee_msat) + match &self.blinded_tail { + Some(blinded_tail) => blinded_tail.final_value_msat, + None => self.hops.last().map_or(0, |hop| hop.fee_msat) + } } /// Gets the final hop's CLTV expiry delta. - pub fn final_cltv_expiry_delta(&self) -> u32 { - self.hops.last().map_or(0, |hop| hop.cltv_expiry_delta) + pub fn final_cltv_expiry_delta(&self) -> Option { + match &self.blinded_tail { + Some(_) => None, + None => self.hops.last().map(|hop| hop.cltv_expiry_delta) + } } } @@ -274,11 +323,9 @@ impl Path { /// it can take multiple paths. Each path is composed of one or more hops through the network. #[derive(Clone, Hash, PartialEq, Eq)] pub struct Route { - /// The list of paths taken for a single (potentially-)multi-part payment. The pubkey of the - /// last [`RouteHop`] in each path must be the same. Each entry represents a list of hops, where - /// the last hop is the destination. Thus, this must always be at least length one. While the - /// maximum length of any given path is variable, keeping the length of any path less or equal to - /// 19 should currently ensure it is viable. + /// The list of [`Path`]s taken for a single (potentially-)multi-part payment. If no + /// [`BlindedTail`]s are present, then the pubkey of the last [`RouteHop`] in each path must be + /// the same. pub paths: Vec, /// The `payment_params` parameter passed to [`find_route`]. /// This is used by `ChannelManager` to track information which may be required for retries, @@ -340,7 +387,7 @@ impl Readable for Route { if hops.is_empty() { return Err(DecodeError::InvalidValue); } min_final_cltv_expiry_delta = cmp::min(min_final_cltv_expiry_delta, hops.last().unwrap().cltv_expiry_delta); - paths.push(Path { hops }); + paths.push(Path { hops, blinded_tail: None }); } let mut payment_params = None; read_tlv_fields!(reader, { @@ -2021,7 +2068,7 @@ where L::Target: Logger { for results_vec in selected_paths { let mut hops = Vec::with_capacity(results_vec.len()); for res in results_vec { hops.push(res?); } - paths.push(Path { hops }); + paths.push(Path { hops, blinded_tail: None }); } let route = Route { paths, @@ -5272,7 +5319,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0 }, - ]}], + ], blinded_tail: None }], payment_params: None, }; @@ -5294,7 +5341,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 }, - ]}, Path { hops: vec![ + ], blinded_tail: None }, Path { hops: vec![ RouteHop { pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), @@ -5305,7 +5352,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 }, - ]}], + ], blinded_tail: None }], payment_params: None, }; diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index 0756869c..6777ec8e 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -1864,7 +1864,7 @@ mod tests { path_hop(source_pubkey(), 41, 1), path_hop(target_pubkey(), 42, 2), path_hop(recipient_pubkey(), 43, amount_msat), - ], + ], blinded_tail: None, } } @@ -2289,7 +2289,7 @@ mod tests { assert_eq!(scorer.channel_penalty_msat(43, &node_b, &node_c, usage), 128); assert_eq!(scorer.channel_penalty_msat(44, &node_c, &node_d, usage), 128); - scorer.payment_path_failed(&Path { hops: path }, 43); + scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 43); assert_eq!(scorer.channel_penalty_msat(42, &node_a, &node_b, usage), 80); // Note that a default liquidity bound is used for B -> C as no channel exists @@ -2823,7 +2823,7 @@ mod tests { path_hop(source_pubkey(), 42, 1), path_hop(sender_pubkey(), 41, 0), ]; - scorer.payment_path_failed(&Path { hops: path }, 42); + scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 42); } #[test] diff --git a/lightning/src/util/macro_logger.rs b/lightning/src/util/macro_logger.rs index b7224be3..a9018f3d 100644 --- a/lightning/src/util/macro_logger.rs +++ b/lightning/src/util/macro_logger.rs @@ -68,6 +68,7 @@ impl<'a> core::fmt::Display for DebugRoute<'a> { for h in p.hops.iter() { writeln!(f, " node_id: {}, short_channel_id: {}, fee_msat: {}, cltv_expiry_delta: {}", log_pubkey!(h.pubkey), h.short_channel_id, h.fee_msat, h.cltv_expiry_delta)?; } + writeln!(f, " blinded_tail: {:?}", p.blinded_tail)?; } Ok(()) } -- 2.30.2