From 5260e81033c6008fdcef1cbf973243268f4ca373 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 3 Jan 2020 19:31:40 -0500 Subject: [PATCH] Expand the Route object to include multiple paths. Rather big diff, but its all mechanical and doesn't introduce any new features. --- fuzz/src/chanmon_consistency.rs | 8 +- lightning/src/ln/channelmanager.rs | 58 +-- lightning/src/ln/functional_test_utils.rs | 10 +- lightning/src/ln/functional_tests.rs | 74 ++-- lightning/src/ln/onion_utils.rs | 34 +- lightning/src/ln/router.rs | 435 ++++++++++++---------- lightning/src/util/macro_logger.rs | 7 +- 7 files changed, 334 insertions(+), 292 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 4981833b..5f4fc133 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -409,14 +409,14 @@ pub fn do_test(data: &[u8]) { let payment_hash = Sha256::hash(&[payment_id; 1]); payment_id = payment_id.wrapping_add(1); if let Err(_) = $source.send_payment(Route { - hops: vec![RouteHop { + paths: vec![vec![RouteHop { pubkey: $dest.0.get_our_node_id(), node_features: NodeFeatures::empty(), short_channel_id: $dest.1, channel_features: ChannelFeatures::empty(), fee_msat: 5000000, cltv_expiry_delta: 200, - }], + }]], }, PaymentHash(payment_hash.into_inner()), &None) { // Probably ran out of funds test_return!(); @@ -426,7 +426,7 @@ pub fn do_test(data: &[u8]) { let payment_hash = Sha256::hash(&[payment_id; 1]); payment_id = payment_id.wrapping_add(1); if let Err(_) = $source.send_payment(Route { - hops: vec![RouteHop { + paths: vec![vec![RouteHop { pubkey: $middle.0.get_our_node_id(), node_features: NodeFeatures::empty(), short_channel_id: $middle.1, @@ -440,7 +440,7 @@ pub fn do_test(data: &[u8]) { channel_features: ChannelFeatures::empty(), fee_msat: 5000000, cltv_expiry_delta: 200, - }], + }]], }, PaymentHash(payment_hash.into_inner()), &None) { // Probably ran out of funds test_return!(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index efcf2159..d1395e12 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -30,7 +30,7 @@ use chain::transaction::OutPoint; use ln::channel::{Channel, ChannelError}; use ln::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateErr, ManyChannelMonitor, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY}; use ln::features::{InitFeatures, NodeFeatures}; -use ln::router::Route; +use ln::router::{Route, RouteHop}; use ln::msgs; use ln::onion_utils; use ln::msgs::{ChannelMessageHandler, DecodeError, LightningError}; @@ -136,7 +136,7 @@ struct ClaimableHTLC { pub(super) enum HTLCSource { PreviousHopData(HTLCPreviousHopData), OutboundRoute { - route: Route, + path: Vec, session_priv: SecretKey, /// Technically we can recalculate this from the route, but we cache it here to avoid /// doing a double-pass on route when we get a failure back @@ -147,7 +147,7 @@ pub(super) enum HTLCSource { impl HTLCSource { pub fn dummy() -> Self { HTLCSource::OutboundRoute { - route: Route { hops: Vec::new() }, + path: Vec::new(), session_priv: SecretKey::from_slice(&[1; 32]).unwrap(), first_hop_htlc_msat: 0, } @@ -1231,13 +1231,16 @@ impl ChannelMan /// bit set (either as required or as available). If multiple paths are present in the Route, /// we assume the invoice had the basic_mpp feature set. pub fn send_payment(&self, route: Route, payment_hash: PaymentHash, payment_secret: &Option) -> Result<(), APIError> { - if route.hops.len() < 1 || route.hops.len() > 20 { - return Err(APIError::RouteError{err: "Route didn't go anywhere/had bogus size"}); + if route.paths.len() < 1 || route.paths.len() > 1 { + return Err(APIError::RouteError{err: "We currently don't support MPP, and we need at least one path"}); + } + if route.paths[0].len() < 1 || route.paths[0].len() > 20 { + return Err(APIError::RouteError{err: "Path didn't go anywhere/had bogus size"}); } let our_node_id = self.get_our_node_id(); - for (idx, hop) in route.hops.iter().enumerate() { - if idx != route.hops.len() - 1 && hop.pubkey == our_node_id { - return Err(APIError::RouteError{err: "Route went through us but wasn't a simple rebalance loop to us"}); + for (idx, hop) in route.paths[0].iter().enumerate() { + if idx != route.paths[0].len() - 1 && hop.pubkey == our_node_id { + return Err(APIError::RouteError{err: "Path went through us but wasn't a simple rebalance loop to us"}); } } @@ -1245,9 +1248,9 @@ impl ChannelMan let cur_height = self.latest_block_height.load(Ordering::Acquire) as u32 + 1; - let onion_keys = secp_call!(onion_utils::construct_onion_keys(&self.secp_ctx, &route, &session_priv), + let onion_keys = secp_call!(onion_utils::construct_onion_keys(&self.secp_ctx, &route.paths[0], &session_priv), APIError::RouteError{err: "Pubkey along hop was maliciously selected"}); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route, payment_secret, cur_height)?; + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], payment_secret, cur_height)?; if onion_utils::route_size_insane(&onion_payloads) { return Err(APIError::RouteError{err: "Route size too large considering onion data"}); } @@ -1257,7 +1260,7 @@ impl ChannelMan let err: Result<(), _> = loop { let mut channel_lock = self.channel_state.lock().unwrap(); - let id = match channel_lock.short_to_id.get(&route.hops.first().unwrap().short_channel_id) { + let id = match channel_lock.short_to_id.get(&route.paths[0].first().unwrap().short_channel_id) { None => return Err(APIError::ChannelUnavailable{err: "No channel available with first hop!"}), Some(id) => id.clone(), }; @@ -1265,14 +1268,14 @@ impl ChannelMan let channel_state = &mut *channel_lock; if let hash_map::Entry::Occupied(mut chan) = channel_state.by_id.entry(id) { match { - if chan.get().get_their_node_id() != route.hops.first().unwrap().pubkey { + if chan.get().get_their_node_id() != route.paths[0].first().unwrap().pubkey { return Err(APIError::RouteError{err: "Node ID mismatch on first hop!"}); } if !chan.get().is_live() { return Err(APIError::ChannelUnavailable{err: "Peer for first hop currently disconnected/pending monitor update!"}); } break_chan_entry!(self, chan.get_mut().send_htlc_and_commit(htlc_msat, payment_hash.clone(), htlc_cltv, HTLCSource::OutboundRoute { - route: route.clone(), + path: route.paths[0].clone(), session_priv: session_priv.clone(), first_hop_htlc_msat: htlc_msat, }, onion_packet), channel_state, chan) @@ -1288,7 +1291,7 @@ impl ChannelMan } channel_state.pending_msg_events.push(events::MessageSendEvent::UpdateHTLCs { - node_id: route.hops.first().unwrap().pubkey, + node_id: route.paths[0].first().unwrap().pubkey, updates: msgs::CommitmentUpdate { update_add_htlcs: vec![update_add], update_fulfill_htlcs: Vec::new(), @@ -1305,7 +1308,7 @@ impl ChannelMan return Ok(()); }; - match handle_error!(self, err, route.hops.first().unwrap().pubkey) { + match handle_error!(self, err, route.paths[0].first().unwrap().pubkey) { Ok(_) => unreachable!(), Err(e) => { Err(APIError::ChannelUnavailable { err: e.err }) } } @@ -1750,7 +1753,7 @@ impl ChannelMan //between the branches here. We should make this async and move it into the forward HTLCs //timer handling. match source { - HTLCSource::OutboundRoute { ref route, .. } => { + HTLCSource::OutboundRoute { ref path, .. } => { log_trace!(self, "Failing outbound payment HTLC with payment_hash {}", log_bytes!(payment_hash.0)); mem::drop(channel_state_lock); match &onion_error { @@ -1792,7 +1795,7 @@ impl ChannelMan self.pending_events.lock().unwrap().push( events::Event::PaymentFailed { payment_hash: payment_hash.clone(), - rejected_by_dest: route.hops.len() == 1, + rejected_by_dest: path.len() == 1, #[cfg(test)] error_code: Some(*failure_code), } @@ -1856,9 +1859,19 @@ impl ChannelMan let mut channel_state = Some(self.channel_state.lock().unwrap()); let removed_source = channel_state.as_mut().unwrap().claimable_htlcs.remove(&(payment_hash, *payment_secret)); if let Some(mut sources) = removed_source { + assert!(!sources.is_empty()); + let valid_mpp_amount = if let &Some(ref data) = &sources[0].payment_data { + assert!(payment_secret.is_some()); + data.total_msat == expected_amount + } else { + assert!(payment_secret.is_none()); + false + }; + + let mut claimed_any_htlcs = false; for htlc in sources.drain(..) { if channel_state.is_none() { channel_state = Some(self.channel_state.lock().unwrap()); } - if htlc.value < expected_amount || htlc.value > expected_amount * 2 { + if !valid_mpp_amount && (htlc.value < expected_amount || htlc.value > expected_amount * 2) { let mut htlc_msat_data = byte_utils::be64_to_array(htlc.value).to_vec(); let mut height_data = byte_utils::be32_to_array(self.latest_block_height.load(Ordering::Acquire) as u32).to_vec(); htlc_msat_data.append(&mut height_data); @@ -1867,9 +1880,10 @@ impl ChannelMan HTLCFailReason::Reason { failure_code: 0x4000|15, data: htlc_msat_data }); } else { self.claim_funds_internal(channel_state.take().unwrap(), HTLCSource::PreviousHopData(htlc.prev_hop), payment_preimage); + claimed_any_htlcs = true; } } - true + claimed_any_htlcs } else { false } } fn claim_funds_internal(&self, mut channel_state_lock: MutexGuard>, source: HTLCSource, payment_preimage: PaymentPreimage) { @@ -3271,9 +3285,9 @@ impl Writeable for HTLCSource { 0u8.write(writer)?; hop_data.write(writer)?; }, - &HTLCSource::OutboundRoute { ref route, ref session_priv, ref first_hop_htlc_msat } => { + &HTLCSource::OutboundRoute { ref path, ref session_priv, ref first_hop_htlc_msat } => { 1u8.write(writer)?; - route.write(writer)?; + path.write(writer)?; session_priv.write(writer)?; first_hop_htlc_msat.write(writer)?; } @@ -3287,7 +3301,7 @@ impl Readable for HTLCSource { match ::read(reader)? { 0 => Ok(HTLCSource::PreviousHopData(Readable::read(reader)?)), 1 => Ok(HTLCSource::OutboundRoute { - route: Readable::read(reader)?, + path: Readable::read(reader)?, session_priv: Readable::read(reader)?, first_hop_htlc_msat: Readable::read(reader)?, }), diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 4b73ad0a..5a655f6c 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -897,8 +897,9 @@ pub const TEST_FINAL_CLTV: u32 = 32; pub fn route_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], recv_value: u64) -> (PaymentPreimage, PaymentHash) { let route = origin_node.router.get_route(&expected_route.last().unwrap().node.get_our_node_id(), None, &Vec::new(), recv_value, TEST_FINAL_CLTV).unwrap(); - assert_eq!(route.hops.len(), expected_route.len()); - for (node, hop) in expected_route.iter().zip(route.hops.iter()) { + assert_eq!(route.paths.len(), 1); + assert_eq!(route.paths[0].len(), expected_route.len()); + for (node, hop) in expected_route.iter().zip(route.paths[0].iter()) { assert_eq!(hop.pubkey, node.node.get_our_node_id()); } @@ -907,8 +908,9 @@ pub fn route_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: pub fn route_over_limit<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], recv_value: u64) { let route = origin_node.router.get_route(&expected_route.last().unwrap().node.get_our_node_id(), None, &Vec::new(), recv_value, TEST_FINAL_CLTV).unwrap(); - assert_eq!(route.hops.len(), expected_route.len()); - for (node, hop) in expected_route.iter().zip(route.hops.iter()) { + assert_eq!(route.paths.len(), 1); + assert_eq!(route.paths[0].len(), expected_route.len()); + for (node, hop) in expected_route.iter().zip(route.paths[0].iter()) { assert_eq!(hop.pubkey, node.node.get_our_node_id()); } diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index da6999ff..5ac00e6e 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1226,7 +1226,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 { hops }, &vec!(&nodes[2], &nodes[3], &nodes[1])[..], 1000000).0; + let payment_preimage_1 = send_along_route(&nodes[1], Route { paths: vec![hops] }, &vec!(&nodes[2], &nodes[3], &nodes[1])[..], 1000000).0; let mut hops = Vec::with_capacity(3); hops.push(RouteHop { @@ -1255,7 +1255,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 { hops }, &vec!(&nodes[3], &nodes[2], &nodes[1])[..], 1000000).1; + let payment_hash_2 = send_along_route(&nodes[1], Route { paths: vec![hops] }, &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); @@ -1562,7 +1562,7 @@ fn do_channel_reserve_test(test_recv: bool) { // attempt to send amt_msat > their_max_htlc_value_in_flight_msat { let (route, our_payment_hash, _) = get_route_and_payment_hash!(recv_value_0 + 1); - assert!(route.hops.iter().rev().skip(1).all(|h| h.fee_msat == feemsat)); + assert!(route.paths[0].iter().rev().skip(1).all(|h| h.fee_msat == feemsat)); let err = nodes[0].node.send_payment(route, our_payment_hash, &None).err().unwrap(); match err { APIError::ChannelUnavailable{err} => assert_eq!(err, "Cannot send value that would put us over the max HTLC value in flight our peer will accept"), @@ -1651,8 +1651,8 @@ fn do_channel_reserve_test(test_recv: bool) { }).expect("RNG is bad!"); let cur_height = nodes[0].node.latest_block_height.load(Ordering::Acquire) as u32 + 1; - let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route, &session_priv).unwrap(); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route, &None, cur_height).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv).unwrap(); + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], &None, cur_height).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &our_payment_hash); let msg = msgs::UpdateAddHTLC { channel_id: chan_1.2, @@ -3038,8 +3038,8 @@ fn fail_backward_pending_htlc_upon_channel_failure() { }; let current_height = nodes[1].node.latest_block_height.load(Ordering::Acquire) as u32 + 1; - let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::build_onion_payloads(&route, &None, current_height).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route, &session_priv).unwrap(); + let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::build_onion_payloads(&route.paths[0], &None, current_height).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv).unwrap(); let onion_routing_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash); // Send a 0-msat update_add_htlc to fail the channel. @@ -5470,8 +5470,8 @@ fn test_onion_failure() { run_onion_failure_test("invalid_realm", 0, &nodes, &route, &payment_hash, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let cur_height = nodes[0].node.latest_block_height.load(Ordering::Acquire) as u32 + 1; - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); - let (mut onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads(&route, &None, cur_height).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); + let (mut onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], &None, cur_height).unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { new_payloads.push(BogusOnionHopData::new(payload)); @@ -5486,8 +5486,8 @@ fn test_onion_failure() { run_onion_failure_test("invalid_realm", 3, &nodes, &route, &payment_hash, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let cur_height = nodes[0].node.latest_block_height.load(Ordering::Acquire) as u32 + 1; - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); - let (mut onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads(&route, &None, cur_height).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); + let (mut onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], &None, cur_height).unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { new_payloads.push(BogusOnionHopData::new(payload)); @@ -5507,57 +5507,57 @@ fn test_onion_failure() { }, |msg| { // 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, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[0].shared_secret[..], NODE|2, &[0;0]); - }, ||{}, true, Some(NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.hops[0].pubkey, is_permanent: false})); + }, ||{}, true, Some(NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.paths[0][0].pubkey, is_permanent: false})); // final node failure run_onion_failure_test_with_fail_intercept("temporary_node_failure", 200, &nodes, &route, &payment_hash, |_msg| {}, |msg| { // 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, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[1].shared_secret[..], NODE|2, &[0;0]); }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash, &None); - }, true, Some(NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.hops[1].pubkey, is_permanent: false})); + }, true, Some(NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.paths[0][1].pubkey, is_permanent: false})); // intermediate node failure run_onion_failure_test_with_fail_intercept("permanent_node_failure", 100, &nodes, &route, &payment_hash, |msg| { msg.amount_msat -= 1; }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[0].shared_secret[..], PERM|NODE|2, &[0;0]); - }, ||{}, true, Some(PERM|NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.hops[0].pubkey, is_permanent: true})); + }, ||{}, true, Some(PERM|NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.paths[0][0].pubkey, is_permanent: true})); // final node failure run_onion_failure_test_with_fail_intercept("permanent_node_failure", 200, &nodes, &route, &payment_hash, |_msg| {}, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[1].shared_secret[..], PERM|NODE|2, &[0;0]); }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash, &None); - }, false, Some(PERM|NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.hops[1].pubkey, is_permanent: true})); + }, false, Some(PERM|NODE|2), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.paths[0][1].pubkey, is_permanent: true})); // intermediate node failure run_onion_failure_test_with_fail_intercept("required_node_feature_missing", 100, &nodes, &route, &payment_hash, |msg| { msg.amount_msat -= 1; }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[0].shared_secret[..], PERM|NODE|3, &[0;0]); }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash, &None); - }, true, Some(PERM|NODE|3), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.hops[0].pubkey, is_permanent: true})); + }, true, Some(PERM|NODE|3), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.paths[0][0].pubkey, is_permanent: true})); // final node failure run_onion_failure_test_with_fail_intercept("required_node_feature_missing", 200, &nodes, &route, &payment_hash, |_msg| {}, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[1].shared_secret[..], PERM|NODE|3, &[0;0]); }, ||{ nodes[2].node.fail_htlc_backwards(&payment_hash, &None); - }, false, Some(PERM|NODE|3), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.hops[1].pubkey, is_permanent: true})); + }, false, Some(PERM|NODE|3), Some(msgs::HTLCFailChannelUpdate::NodeFailure{node_id: route.paths[0][1].pubkey, is_permanent: true})); run_onion_failure_test("invalid_onion_version", 0, &nodes, &route, &payment_hash, |msg| { msg.onion_routing_packet.version = 1; }, ||{}, true, Some(BADONION|PERM|4), None); @@ -5572,7 +5572,7 @@ fn test_onion_failure() { msg.amount_msat -= 1; }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[0].shared_secret[..], UPDATE|7, &ChannelUpdate::dummy().encode_with_len()[..]); }, ||{}, true, Some(UPDATE|7), Some(msgs::HTLCFailChannelUpdate::ChannelUpdateMessage{msg: ChannelUpdate::dummy()})); @@ -5580,7 +5580,7 @@ fn test_onion_failure() { msg.amount_msat -= 1; }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[0].shared_secret[..], PERM|8, &[0;0]); // short_channel_id from the processing node }, ||{}, true, Some(PERM|8), Some(msgs::HTLCFailChannelUpdate::ChannelClosed{short_channel_id: channels[1].0.contents.short_channel_id, is_permanent: true})); @@ -5589,20 +5589,20 @@ fn test_onion_failure() { msg.amount_msat -= 1; }, |msg| { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); msg.reason = onion_utils::build_first_hop_failure_packet(&onion_keys[0].shared_secret[..], PERM|9, &[0;0]); // short_channel_id from the processing node }, ||{}, true, Some(PERM|9), Some(msgs::HTLCFailChannelUpdate::ChannelClosed{short_channel_id: channels[1].0.contents.short_channel_id, is_permanent: true})); let mut bogus_route = route.clone(); - bogus_route.hops[1].short_channel_id -= 1; + bogus_route.paths[0][1].short_channel_id -= 1; run_onion_failure_test("unknown_next_peer", 0, &nodes, &bogus_route, &payment_hash, |_| {}, ||{}, true, Some(PERM|10), - Some(msgs::HTLCFailChannelUpdate::ChannelClosed{short_channel_id: bogus_route.hops[1].short_channel_id, is_permanent:true})); + Some(msgs::HTLCFailChannelUpdate::ChannelClosed{short_channel_id: bogus_route.paths[0][1].short_channel_id, is_permanent:true})); let amt_to_forward = nodes[1].node.channel_state.lock().unwrap().by_id.get(&channels[1].2).unwrap().get_their_htlc_minimum_msat() - 1; let mut bogus_route = route.clone(); - let route_len = bogus_route.hops.len(); - bogus_route.hops[route_len-1].fee_msat = amt_to_forward; + let route_len = bogus_route.paths[0].len(); + bogus_route.paths[0][route_len-1].fee_msat = amt_to_forward; run_onion_failure_test("amount_below_minimum", 0, &nodes, &bogus_route, &payment_hash, |_| {}, ||{}, true, Some(UPDATE|11), Some(msgs::HTLCFailChannelUpdate::ChannelUpdateMessage{msg: ChannelUpdate::dummy()})); //TODO: with new config API, we will be able to generate both valid and @@ -5670,9 +5670,9 @@ fn test_onion_failure() { let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let mut route = route.clone(); let height = 1; - route.hops[1].cltv_expiry_delta += CLTV_FAR_FAR_AWAY + route.hops[0].cltv_expiry_delta + 1; - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route, &session_priv).unwrap(); - let (onion_payloads, _, htlc_cltv) = onion_utils::build_onion_payloads(&route, &None, height).unwrap(); + route.paths[0][1].cltv_expiry_delta += CLTV_FAR_FAR_AWAY + route.paths[0][0].cltv_expiry_delta + 1; + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); + let (onion_payloads, _, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], &None, height).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash); msg.cltv_expiry = htlc_cltv; msg.onion_routing_packet = onion_packet; @@ -5762,7 +5762,7 @@ fn test_update_add_htlc_bolt2_sender_value_below_minimum_msat() { let mut route = nodes[0].router.get_route(&nodes[1].node.get_our_node_id(), None, &[], 100000, TEST_FINAL_CLTV).unwrap(); let (_, our_payment_hash) = get_payment_preimage_hash!(nodes[0]); - route.hops[0].fee_msat = 100; + route.paths[0][0].fee_msat = 100; let err = nodes[0].node.send_payment(route, our_payment_hash, &None); @@ -5786,7 +5786,7 @@ fn test_update_add_htlc_bolt2_sender_zero_value_msat() { let mut route = nodes[0].router.get_route(&nodes[1].node.get_our_node_id(), None, &[], 100000, TEST_FINAL_CLTV).unwrap(); let (_, our_payment_hash) = get_payment_preimage_hash!(nodes[0]); - route.hops[0].fee_msat = 0; + route.paths[0][0].fee_msat = 0; let err = nodes[0].node.send_payment(route, our_payment_hash, &None); @@ -5992,8 +5992,8 @@ fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { }).expect("RNG is bad!"); let cur_height = nodes[0].node.latest_block_height.load(Ordering::Acquire) as u32 + 1; - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::signing_only(), &route, &session_priv).unwrap(); - let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route, &None, cur_height).unwrap(); + let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::signing_only(), &route.paths[0], &session_priv).unwrap(); + let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], &None, cur_height).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &our_payment_hash); let mut msg = msgs::UpdateAddHTLC { diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index ebea1334..ef0d6d2e 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1,6 +1,6 @@ use ln::channelmanager::{PaymentHash, PaymentSecret, HTLCSource}; use ln::msgs; -use ln::router::{Route,RouteHop}; +use ln::router::RouteHop; use util::byte_utils; use util::chacha20::ChaCha20; use util::errors::{self, APIError}; @@ -63,11 +63,11 @@ pub(super) fn gen_ammag_from_shared_secret(shared_secret: &[u8]) -> [u8; 32] { // can only fail if an intermediary hop has an invalid public key or session_priv is invalid #[inline] -pub(super) fn construct_onion_keys_callback (secp_ctx: &Secp256k1, route: &Route, session_priv: &SecretKey, mut callback: FType) -> Result<(), secp256k1::Error> { +pub(super) fn construct_onion_keys_callback (secp_ctx: &Secp256k1, path: &Vec, session_priv: &SecretKey, mut callback: FType) -> Result<(), secp256k1::Error> { let mut blinded_priv = session_priv.clone(); let mut blinded_pub = PublicKey::from_secret_key(secp_ctx, &blinded_priv); - for hop in route.hops.iter() { + for hop in path.iter() { let shared_secret = SharedSecret::new(&hop.pubkey, &blinded_priv); let mut sha = Sha256::engine(); @@ -87,10 +87,10 @@ pub(super) fn construct_onion_keys_callback(secp_ctx: &Secp256k1, route: &Route, session_priv: &SecretKey) -> Result, secp256k1::Error> { - let mut res = Vec::with_capacity(route.hops.len()); +pub(super) fn construct_onion_keys(secp_ctx: &Secp256k1, path: &Vec, session_priv: &SecretKey) -> Result, secp256k1::Error> { + let mut res = Vec::with_capacity(path.len()); - construct_onion_keys_callback(secp_ctx, route, session_priv, |shared_secret, _blinding_factor, ephemeral_pubkey, _| { + construct_onion_keys_callback(secp_ctx, path, session_priv, |shared_secret, _blinding_factor, ephemeral_pubkey, _| { let (rho, mu) = gen_rho_mu_from_shared_secret(&shared_secret[..]); res.push(OnionKeys { @@ -108,13 +108,13 @@ pub(super) fn construct_onion_keys(secp_ctx: &Secp256k1, starting_htlc_offset: u32) -> Result<(Vec, u64, u32), APIError> { +pub(super) fn build_onion_payloads(path: &Vec, payment_secret_option: &Option, starting_htlc_offset: u32) -> Result<(Vec, u64, u32), APIError> { let mut cur_value_msat = 0u64; let mut cur_cltv = starting_htlc_offset; let mut last_short_channel_id = 0; - let mut res: Vec = Vec::with_capacity(route.hops.len()); + let mut res: Vec = Vec::with_capacity(path.len()); - for (idx, hop) in route.hops.iter().rev().enumerate() { + for (idx, hop) in path.iter().rev().enumerate() { // First hop gets special values so that it can check, on receipt, that everything is // exactly as it should be (and the next hop isn't trying to probe to find out if we're // the intended recipient). @@ -318,7 +318,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type: /// OutboundRoute). /// Returns update, a boolean indicating that the payment itself failed, and the error code. pub(super) fn process_onion_failure(secp_ctx: &Secp256k1, logger: &Arc, htlc_source: &HTLCSource, mut packet_decrypted: Vec) -> (Option, bool, Option) { - if let &HTLCSource::OutboundRoute { ref route, ref session_priv, ref first_hop_htlc_msat } = htlc_source { + if let &HTLCSource::OutboundRoute { ref path, ref session_priv, ref first_hop_htlc_msat } = htlc_source { let mut res = None; let mut htlc_msat = *first_hop_htlc_msat; let mut error_code_ret = None; @@ -326,7 +326,7 @@ pub(super) fn process_onion_failure(secp_ctx: &Secp256k1< let mut is_from_final_node = false; // Handle packed channel/node updates for passing back for the route handler - construct_onion_keys_callback(secp_ctx, route, session_priv, |shared_secret, _, _, route_hop| { + construct_onion_keys_callback(secp_ctx, path, session_priv, |shared_secret, _, _, route_hop| { next_route_hop_ix += 1; if res.is_some() { return; } @@ -341,7 +341,7 @@ pub(super) fn process_onion_failure(secp_ctx: &Secp256k1< chacha.process(&packet_decrypted, &mut decryption_tmp[..]); packet_decrypted = decryption_tmp; - is_from_final_node = route.hops.last().unwrap().pubkey == route_hop.pubkey; + is_from_final_node = path.last().unwrap().pubkey == route_hop.pubkey; if let Ok(err_packet) = msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) { let um = gen_um_from_shared_secret(&shared_secret[..]); @@ -374,7 +374,7 @@ pub(super) fn process_onion_failure(secp_ctx: &Secp256k1< } else if error_code & PERM == PERM { fail_channel_update = if payment_failed {None} else {Some(msgs::HTLCFailChannelUpdate::ChannelClosed { - short_channel_id: route.hops[next_route_hop_ix - if next_route_hop_ix == route.hops.len() { 1 } else { 0 }].short_channel_id, + short_channel_id: path[next_route_hop_ix - if next_route_hop_ix == path.len() { 1 } else { 0 }].short_channel_id, is_permanent: true, })}; } @@ -485,7 +485,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let route = Route { - hops: vec!( + paths: vec![vec![ RouteHop { pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), @@ -511,13 +511,13 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // Test vectors are garbage and not generateble from a RouteHop, we fill in payloads manually }, - ), + ]], }; let session_priv = SecretKey::from_slice(&hex::decode("4141414141414141414141414141414141414141414141414141414141414141").unwrap()[..]).unwrap(); - let onion_keys = super::construct_onion_keys(&secp_ctx, &route, &session_priv).unwrap(); - assert_eq!(onion_keys.len(), route.hops.len()); + let onion_keys = super::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv).unwrap(); + assert_eq!(onion_keys.len(), route.paths[0].len()); onion_keys } diff --git a/lightning/src/ln/router.rs b/lightning/src/ln/router.rs index 95007250..c7ebe86d 100644 --- a/lightning/src/ln/router.rs +++ b/lightning/src/ln/router.rs @@ -47,19 +47,10 @@ pub struct RouteHop { pub cltv_expiry_delta: u32, } -/// A route from us through the network to a destination -#[derive(Clone, PartialEq)] -pub struct Route { - /// The list of hops, NOT INCLUDING our own, where the last hop is the destination. Thus, this - /// must always be at least length one. By protocol rules, this may not currently exceed 20 in - /// length. - pub hops: Vec, -} - -impl Writeable for Route { +impl Writeable for Vec { fn write(&self, writer: &mut W) -> Result<(), ::std::io::Error> { - (self.hops.len() as u8).write(writer)?; - for hop in self.hops.iter() { + (self.len() as u8).write(writer)?; + for hop in self.iter() { hop.pubkey.write(writer)?; hop.node_features.write(writer)?; hop.short_channel_id.write(writer)?; @@ -71,8 +62,8 @@ impl Writeable for Route { } } -impl Readable for Route { - fn read(reader: &mut R) -> Result { +impl Readable for Vec { + fn read(reader: &mut R) -> Result, DecodeError> { let hops_count: u8 = Readable::read(reader)?; let mut hops = Vec::with_capacity(hops_count as usize); for _ in 0..hops_count { @@ -85,9 +76,41 @@ impl Readable for Route { cltv_expiry_delta: Readable::read(reader)?, }); } - Ok(Route { - hops - }) + Ok(hops) + } +} + +/// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP, +/// it can take multiple paths. Each path is composed of one or more hops through the network. +#[derive(Clone, PartialEq)] +pub struct Route { + /// The list of routes 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, NOT INCLUDING our own, 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 to less than 20 should currently + /// ensure it is viable. + pub paths: Vec>, +} + +impl Writeable for Route { + fn write(&self, writer: &mut W) -> Result<(), ::std::io::Error> { + (self.paths.len() as u64).write(writer)?; + for hops in self.paths.iter() { + hops.write(writer)?; + } + Ok(()) + } +} + +impl Readable for Route { + fn read(reader: &mut R) -> Result { + let path_count: u64 = Readable::read(reader)?; + let mut paths = Vec::with_capacity(cmp::min(path_count, 128) as usize); + for _ in 0..path_count { + paths.push(Readable::read(reader)?); + } + Ok(Route { paths }) } } @@ -868,14 +891,14 @@ impl Router { let short_channel_id = chan.short_channel_id.expect("first_hops should be filled in with usable channels, not pending ones"); if chan.remote_network_id == *target { return Ok(Route { - hops: vec![RouteHop { + paths: vec![vec![RouteHop { pubkey: chan.remote_network_id, node_features: NodeFeatures::with_known_relevant_init_flags(&chan.counterparty_features), short_channel_id, channel_features: ChannelFeatures::with_known_relevant_init_flags(&chan.counterparty_features), fee_msat: final_value_msat, cltv_expiry_delta: final_cltv, - }], + }]], }); } first_hop_targets.insert(chan.remote_network_id, (short_channel_id, chan.counterparty_features.clone())); @@ -1032,7 +1055,7 @@ impl Router { } res.last_mut().unwrap().fee_msat = final_value_msat; res.last_mut().unwrap().cltv_expiry_delta = final_cltv; - let route = Route { hops: res }; + let route = Route { paths: vec![res] }; log_trace!(self, "Got route: {}", log_route!(route)); return Ok(route); } @@ -1497,21 +1520,21 @@ mod tests { { // Simple route to 3 via 2 let route = router.get_route(&node3, None, &Vec::new(), 100, 42).unwrap(); - assert_eq!(route.hops.len(), 2); + assert_eq!(route.paths[0].len(), 2); - assert_eq!(route.hops[0].pubkey, node2); - assert_eq!(route.hops[0].short_channel_id, 2); - assert_eq!(route.hops[0].fee_msat, 100); - assert_eq!(route.hops[0].cltv_expiry_delta, (4 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &id_to_feature_flags!(2)); - assert_eq!(route.hops[0].channel_features.le_flags(), &id_to_feature_flags!(2)); + assert_eq!(route.paths[0][0].pubkey, node2); + assert_eq!(route.paths[0][0].short_channel_id, 2); + assert_eq!(route.paths[0][0].fee_msat, 100); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags!(2)); + assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags!(2)); - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 4); - assert_eq!(route.hops[1].fee_msat, 100); - assert_eq!(route.hops[1].cltv_expiry_delta, 42); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(4)); + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 4); + assert_eq!(route.paths[0][1].fee_msat, 100); + assert_eq!(route.paths[0][1].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(4)); } { // Disable channels 4 and 12 by requiring unknown feature bits @@ -1539,21 +1562,21 @@ mod tests { is_live: true, }]; let route = router.get_route(&node3, Some(&our_chans), &Vec::new(), 100, 42).unwrap(); - assert_eq!(route.hops.len(), 2); + assert_eq!(route.paths[0].len(), 2); - assert_eq!(route.hops[0].pubkey, node8); - assert_eq!(route.hops[0].short_channel_id, 42); - assert_eq!(route.hops[0].fee_msat, 200); - assert_eq!(route.hops[0].cltv_expiry_delta, (13 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &vec![0b11]); // it should also override our view of their features - assert_eq!(route.hops[0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion + assert_eq!(route.paths[0][0].pubkey, node8); + assert_eq!(route.paths[0][0].short_channel_id, 42); + assert_eq!(route.paths[0][0].fee_msat, 200); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (13 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &vec![0b11]); // it should also override our view of their features + assert_eq!(route.paths[0][0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 13); - assert_eq!(route.hops[1].fee_msat, 100); - assert_eq!(route.hops[1].cltv_expiry_delta, 42); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(13)); + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 13); + assert_eq!(route.paths[0][1].fee_msat, 100); + assert_eq!(route.paths[0][1].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(13)); } { // Re-enable channels 4 and 12 by wiping the unknown feature bits @@ -1588,21 +1611,21 @@ mod tests { is_live: true, }]; let route = router.get_route(&node3, Some(&our_chans), &Vec::new(), 100, 42).unwrap(); - assert_eq!(route.hops.len(), 2); + assert_eq!(route.paths[0].len(), 2); - assert_eq!(route.hops[0].pubkey, node8); - assert_eq!(route.hops[0].short_channel_id, 42); - assert_eq!(route.hops[0].fee_msat, 200); - assert_eq!(route.hops[0].cltv_expiry_delta, (13 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &vec![0b11]); // it should also override our view of their features - assert_eq!(route.hops[0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion + assert_eq!(route.paths[0][0].pubkey, node8); + assert_eq!(route.paths[0][0].short_channel_id, 42); + assert_eq!(route.paths[0][0].fee_msat, 200); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (13 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &vec![0b11]); // it should also override our view of their features + assert_eq!(route.paths[0][0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 13); - assert_eq!(route.hops[1].fee_msat, 100); - assert_eq!(route.hops[1].cltv_expiry_delta, 42); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(13)); + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 13); + assert_eq!(route.paths[0][1].fee_msat, 100); + assert_eq!(route.paths[0][1].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(13)); } { // Re-enable nodes 1, 2, and 8 @@ -1618,28 +1641,28 @@ mod tests { { // Route to 1 via 2 and 3 because our channel to 1 is disabled let route = router.get_route(&node1, None, &Vec::new(), 100, 42).unwrap(); - assert_eq!(route.hops.len(), 3); - - assert_eq!(route.hops[0].pubkey, node2); - assert_eq!(route.hops[0].short_channel_id, 2); - assert_eq!(route.hops[0].fee_msat, 200); - assert_eq!(route.hops[0].cltv_expiry_delta, (4 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &id_to_feature_flags!(2)); - assert_eq!(route.hops[0].channel_features.le_flags(), &id_to_feature_flags!(2)); - - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 4); - assert_eq!(route.hops[1].fee_msat, 100); - assert_eq!(route.hops[1].cltv_expiry_delta, (3 << 8) | 2); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(4)); - - assert_eq!(route.hops[2].pubkey, node1); - assert_eq!(route.hops[2].short_channel_id, 3); - assert_eq!(route.hops[2].fee_msat, 100); - assert_eq!(route.hops[2].cltv_expiry_delta, 42); - assert_eq!(route.hops[2].node_features.le_flags(), &id_to_feature_flags!(1)); - assert_eq!(route.hops[2].channel_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0].len(), 3); + + assert_eq!(route.paths[0][0].pubkey, node2); + assert_eq!(route.paths[0][0].short_channel_id, 2); + assert_eq!(route.paths[0][0].fee_msat, 200); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags!(2)); + assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags!(2)); + + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 4); + assert_eq!(route.paths[0][1].fee_msat, 100); + assert_eq!(route.paths[0][1].cltv_expiry_delta, (3 << 8) | 2); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(4)); + + assert_eq!(route.paths[0][2].pubkey, node1); + assert_eq!(route.paths[0][2].short_channel_id, 3); + assert_eq!(route.paths[0][2].fee_msat, 100); + assert_eq!(route.paths[0][2].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags!(1)); + assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags!(3)); } { // If we specify a channel to node8, that overrides our local channel view and that gets used @@ -1655,21 +1678,21 @@ mod tests { is_live: true, }]; let route = router.get_route(&node3, Some(&our_chans), &Vec::new(), 100, 42).unwrap(); - assert_eq!(route.hops.len(), 2); + assert_eq!(route.paths[0].len(), 2); - assert_eq!(route.hops[0].pubkey, node8); - assert_eq!(route.hops[0].short_channel_id, 42); - assert_eq!(route.hops[0].fee_msat, 200); - assert_eq!(route.hops[0].cltv_expiry_delta, (13 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &vec![0b11]); - assert_eq!(route.hops[0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion + assert_eq!(route.paths[0][0].pubkey, node8); + assert_eq!(route.paths[0][0].short_channel_id, 42); + assert_eq!(route.paths[0][0].fee_msat, 200); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (13 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &vec![0b11]); + assert_eq!(route.paths[0][0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 13); - assert_eq!(route.hops[1].fee_msat, 100); - assert_eq!(route.hops[1].cltv_expiry_delta, 42); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(13)); + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 13); + assert_eq!(route.paths[0][1].fee_msat, 100); + assert_eq!(route.paths[0][1].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(13)); } let mut last_hops = vec!(RouteHint { @@ -1697,44 +1720,44 @@ mod tests { { // Simple test across 2, 3, 5, and 4 via a last_hop channel let route = router.get_route(&node7, None, &last_hops, 100, 42).unwrap(); - assert_eq!(route.hops.len(), 5); - - assert_eq!(route.hops[0].pubkey, node2); - assert_eq!(route.hops[0].short_channel_id, 2); - assert_eq!(route.hops[0].fee_msat, 100); - assert_eq!(route.hops[0].cltv_expiry_delta, (4 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &id_to_feature_flags!(2)); - assert_eq!(route.hops[0].channel_features.le_flags(), &id_to_feature_flags!(2)); - - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 4); - assert_eq!(route.hops[1].fee_msat, 0); - assert_eq!(route.hops[1].cltv_expiry_delta, (6 << 8) | 1); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(4)); - - assert_eq!(route.hops[2].pubkey, node5); - assert_eq!(route.hops[2].short_channel_id, 6); - assert_eq!(route.hops[2].fee_msat, 0); - assert_eq!(route.hops[2].cltv_expiry_delta, (11 << 8) | 1); - assert_eq!(route.hops[2].node_features.le_flags(), &id_to_feature_flags!(5)); - assert_eq!(route.hops[2].channel_features.le_flags(), &id_to_feature_flags!(6)); - - assert_eq!(route.hops[3].pubkey, node4); - assert_eq!(route.hops[3].short_channel_id, 11); - assert_eq!(route.hops[3].fee_msat, 0); - assert_eq!(route.hops[3].cltv_expiry_delta, (8 << 8) | 1); + assert_eq!(route.paths[0].len(), 5); + + assert_eq!(route.paths[0][0].pubkey, node2); + assert_eq!(route.paths[0][0].short_channel_id, 2); + assert_eq!(route.paths[0][0].fee_msat, 100); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags!(2)); + assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags!(2)); + + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 4); + assert_eq!(route.paths[0][1].fee_msat, 0); + assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(4)); + + assert_eq!(route.paths[0][2].pubkey, node5); + assert_eq!(route.paths[0][2].short_channel_id, 6); + assert_eq!(route.paths[0][2].fee_msat, 0); + assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1); + assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags!(5)); + assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags!(6)); + + assert_eq!(route.paths[0][3].pubkey, node4); + assert_eq!(route.paths[0][3].short_channel_id, 11); + assert_eq!(route.paths[0][3].fee_msat, 0); + assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1); // If we have a peer in the node map, we'll use their features here since we don't have // a way of figuring out their features from the invoice: - assert_eq!(route.hops[3].node_features.le_flags(), &id_to_feature_flags!(4)); - assert_eq!(route.hops[3].channel_features.le_flags(), &id_to_feature_flags!(11)); + assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags!(4)); + assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags!(11)); - assert_eq!(route.hops[4].pubkey, node7); - assert_eq!(route.hops[4].short_channel_id, 8); - assert_eq!(route.hops[4].fee_msat, 100); - assert_eq!(route.hops[4].cltv_expiry_delta, 42); - assert_eq!(route.hops[4].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet - assert_eq!(route.hops[4].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly + assert_eq!(route.paths[0][4].pubkey, node7); + assert_eq!(route.paths[0][4].short_channel_id, 8); + assert_eq!(route.paths[0][4].fee_msat, 100); + assert_eq!(route.paths[0][4].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet + assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly } { // Simple test with outbound channel to 4 to test that last_hops and first_hops connect @@ -1750,100 +1773,100 @@ mod tests { is_live: true, }]; let route = router.get_route(&node7, Some(&our_chans), &last_hops, 100, 42).unwrap(); - assert_eq!(route.hops.len(), 2); + assert_eq!(route.paths[0].len(), 2); - assert_eq!(route.hops[0].pubkey, node4); - assert_eq!(route.hops[0].short_channel_id, 42); - assert_eq!(route.hops[0].fee_msat, 0); - assert_eq!(route.hops[0].cltv_expiry_delta, (8 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &vec![0b11]); - assert_eq!(route.hops[0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion + assert_eq!(route.paths[0][0].pubkey, node4); + assert_eq!(route.paths[0][0].short_channel_id, 42); + assert_eq!(route.paths[0][0].fee_msat, 0); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (8 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &vec![0b11]); + assert_eq!(route.paths[0][0].channel_features.le_flags(), &Vec::new()); // No feature flags will meet the relevant-to-channel conversion - assert_eq!(route.hops[1].pubkey, node7); - assert_eq!(route.hops[1].short_channel_id, 8); - assert_eq!(route.hops[1].fee_msat, 100); - assert_eq!(route.hops[1].cltv_expiry_delta, 42); - assert_eq!(route.hops[1].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet - assert_eq!(route.hops[1].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly + assert_eq!(route.paths[0][1].pubkey, node7); + assert_eq!(route.paths[0][1].short_channel_id, 8); + assert_eq!(route.paths[0][1].fee_msat, 100); + assert_eq!(route.paths[0][1].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][1].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet + assert_eq!(route.paths[0][1].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly } last_hops[0].fee_base_msat = 1000; { // Revert to via 6 as the fee on 8 goes up let route = router.get_route(&node7, None, &last_hops, 100, 42).unwrap(); - assert_eq!(route.hops.len(), 4); - - assert_eq!(route.hops[0].pubkey, node2); - assert_eq!(route.hops[0].short_channel_id, 2); - assert_eq!(route.hops[0].fee_msat, 200); // fee increased as its % of value transferred across node - assert_eq!(route.hops[0].cltv_expiry_delta, (4 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &id_to_feature_flags!(2)); - assert_eq!(route.hops[0].channel_features.le_flags(), &id_to_feature_flags!(2)); - - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 4); - assert_eq!(route.hops[1].fee_msat, 100); - assert_eq!(route.hops[1].cltv_expiry_delta, (7 << 8) | 1); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(4)); - - assert_eq!(route.hops[2].pubkey, node6); - assert_eq!(route.hops[2].short_channel_id, 7); - assert_eq!(route.hops[2].fee_msat, 0); - assert_eq!(route.hops[2].cltv_expiry_delta, (10 << 8) | 1); + assert_eq!(route.paths[0].len(), 4); + + assert_eq!(route.paths[0][0].pubkey, node2); + assert_eq!(route.paths[0][0].short_channel_id, 2); + assert_eq!(route.paths[0][0].fee_msat, 200); // fee increased as its % of value transferred across node + assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags!(2)); + assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags!(2)); + + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 4); + assert_eq!(route.paths[0][1].fee_msat, 100); + assert_eq!(route.paths[0][1].cltv_expiry_delta, (7 << 8) | 1); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(4)); + + assert_eq!(route.paths[0][2].pubkey, node6); + assert_eq!(route.paths[0][2].short_channel_id, 7); + assert_eq!(route.paths[0][2].fee_msat, 0); + assert_eq!(route.paths[0][2].cltv_expiry_delta, (10 << 8) | 1); // If we have a peer in the node map, we'll use their features here since we don't have // a way of figuring out their features from the invoice: - assert_eq!(route.hops[2].node_features.le_flags(), &id_to_feature_flags!(6)); - assert_eq!(route.hops[2].channel_features.le_flags(), &id_to_feature_flags!(7)); + assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags!(6)); + assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags!(7)); - assert_eq!(route.hops[3].pubkey, node7); - assert_eq!(route.hops[3].short_channel_id, 10); - assert_eq!(route.hops[3].fee_msat, 100); - assert_eq!(route.hops[3].cltv_expiry_delta, 42); - assert_eq!(route.hops[3].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet - assert_eq!(route.hops[3].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly + assert_eq!(route.paths[0][3].pubkey, node7); + assert_eq!(route.paths[0][3].short_channel_id, 10); + assert_eq!(route.paths[0][3].fee_msat, 100); + assert_eq!(route.paths[0][3].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][3].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet + assert_eq!(route.paths[0][3].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly } { // ...but still use 8 for larger payments as 6 has a variable feerate let route = router.get_route(&node7, None, &last_hops, 2000, 42).unwrap(); - assert_eq!(route.hops.len(), 5); - - assert_eq!(route.hops[0].pubkey, node2); - assert_eq!(route.hops[0].short_channel_id, 2); - assert_eq!(route.hops[0].fee_msat, 3000); - assert_eq!(route.hops[0].cltv_expiry_delta, (4 << 8) | 1); - assert_eq!(route.hops[0].node_features.le_flags(), &id_to_feature_flags!(2)); - assert_eq!(route.hops[0].channel_features.le_flags(), &id_to_feature_flags!(2)); - - assert_eq!(route.hops[1].pubkey, node3); - assert_eq!(route.hops[1].short_channel_id, 4); - assert_eq!(route.hops[1].fee_msat, 0); - assert_eq!(route.hops[1].cltv_expiry_delta, (6 << 8) | 1); - assert_eq!(route.hops[1].node_features.le_flags(), &id_to_feature_flags!(3)); - assert_eq!(route.hops[1].channel_features.le_flags(), &id_to_feature_flags!(4)); - - assert_eq!(route.hops[2].pubkey, node5); - assert_eq!(route.hops[2].short_channel_id, 6); - assert_eq!(route.hops[2].fee_msat, 0); - assert_eq!(route.hops[2].cltv_expiry_delta, (11 << 8) | 1); - assert_eq!(route.hops[2].node_features.le_flags(), &id_to_feature_flags!(5)); - assert_eq!(route.hops[2].channel_features.le_flags(), &id_to_feature_flags!(6)); - - assert_eq!(route.hops[3].pubkey, node4); - assert_eq!(route.hops[3].short_channel_id, 11); - assert_eq!(route.hops[3].fee_msat, 1000); - assert_eq!(route.hops[3].cltv_expiry_delta, (8 << 8) | 1); + assert_eq!(route.paths[0].len(), 5); + + assert_eq!(route.paths[0][0].pubkey, node2); + assert_eq!(route.paths[0][0].short_channel_id, 2); + assert_eq!(route.paths[0][0].fee_msat, 3000); + assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1); + assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags!(2)); + assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags!(2)); + + assert_eq!(route.paths[0][1].pubkey, node3); + assert_eq!(route.paths[0][1].short_channel_id, 4); + assert_eq!(route.paths[0][1].fee_msat, 0); + assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1); + assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags!(3)); + assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags!(4)); + + assert_eq!(route.paths[0][2].pubkey, node5); + assert_eq!(route.paths[0][2].short_channel_id, 6); + assert_eq!(route.paths[0][2].fee_msat, 0); + assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1); + assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags!(5)); + assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags!(6)); + + assert_eq!(route.paths[0][3].pubkey, node4); + assert_eq!(route.paths[0][3].short_channel_id, 11); + assert_eq!(route.paths[0][3].fee_msat, 1000); + assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1); // If we have a peer in the node map, we'll use their features here since we don't have // a way of figuring out their features from the invoice: - assert_eq!(route.hops[3].node_features.le_flags(), &id_to_feature_flags!(4)); - assert_eq!(route.hops[3].channel_features.le_flags(), &id_to_feature_flags!(11)); - - assert_eq!(route.hops[4].pubkey, node7); - assert_eq!(route.hops[4].short_channel_id, 8); - assert_eq!(route.hops[4].fee_msat, 2000); - assert_eq!(route.hops[4].cltv_expiry_delta, 42); - assert_eq!(route.hops[4].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet - assert_eq!(route.hops[4].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly + assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags!(4)); + assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags!(11)); + + assert_eq!(route.paths[0][4].pubkey, node7); + assert_eq!(route.paths[0][4].short_channel_id, 8); + assert_eq!(route.paths[0][4].fee_msat, 2000); + assert_eq!(route.paths[0][4].cltv_expiry_delta, 42); + assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::new()); // We dont pass flags in from invoices yet + assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::new()); // We can't learn any flags from invoices, sadly } { // Test Router serialization/deserialization diff --git a/lightning/src/util/macro_logger.rs b/lightning/src/util/macro_logger.rs index d16bd48a..1f9cb1ad 100644 --- a/lightning/src/util/macro_logger.rs +++ b/lightning/src/util/macro_logger.rs @@ -73,8 +73,11 @@ macro_rules! log_funding_info { pub(crate) struct DebugRoute<'a>(pub &'a Route); impl<'a> std::fmt::Display for DebugRoute<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - for h in self.0.hops.iter() { - write!(f, "node_id: {}, short_channel_id: {}, fee_msat: {}, cltv_expiry_delta: {}\n", log_pubkey!(h.pubkey), h.short_channel_id, h.fee_msat, h.cltv_expiry_delta)?; + for (idx, p) in self.0.paths.iter().enumerate() { + write!(f, "path {}:\n", idx)?; + for h in p.iter() { + write!(f, " node_id: {}, short_channel_id: {}, fee_msat: {}, cltv_expiry_delta: {}\n", log_pubkey!(h.pubkey), h.short_channel_id, h.fee_msat, h.cltv_expiry_delta)?; + } } Ok(()) } -- 2.30.2