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);
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);
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 {
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,
Ok(Some(Event::PaymentPathSuccessful {
payment_id,
payment_hash,
- path: Path { hops: path.unwrap() },
+ path: Path { hops: path.unwrap(), blinded_tail: None },
}))
};
f()
Ok(Some(Event::ProbeSuccessful {
payment_id,
payment_hash,
- path: Path { hops: path.unwrap() },
+ path: Path { hops: path.unwrap(), blinded_tail: None },
}))
};
f()
Ok(Some(Event::ProbeFailed {
payment_id,
payment_hash,
- path: Path { hops: path.unwrap() },
+ path: Path { hops: path.unwrap(), blinded_tail: None },
short_channel_id,
}))
};
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]),
// 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 {
});
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 {
});
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);
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,
};
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()}));
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()));
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(),
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()),
};
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(),
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()),
};
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()),
};
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(),
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),
};
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)),
};
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(),
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)),
};
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(),
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)),
};
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(),
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)),
};
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};
/// 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,
}
(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<BlindedHop>,
+ /// 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<RouteHop>,
+ /// The blinded path at which this path terminates, if we're sending to one, and its metadata.
+ pub blinded_tail: Option<BlindedTail>,
}
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::<u64>(),
+ 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<u32> {
+ match &self.blinded_tail {
+ Some(_) => None,
+ None => self.hops.last().map(|hop| hop.cltv_expiry_delta)
+ }
}
}
/// 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<Path>,
/// The `payment_params` parameter passed to [`find_route`].
/// This is used by `ChannelManager` to track information which may be required for retries,
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, {
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,
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,
};
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(),
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,
};
path_hop(source_pubkey(), 41, 1),
path_hop(target_pubkey(), 42, 2),
path_hop(recipient_pubkey(), 43, amount_msat),
- ],
+ ], blinded_tail: None,
}
}
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
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]
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(())
}