Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle private channel hop hints #487

Merged
merged 41 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b57d66f
Simplify adjust_target_for_route_self
contrun Jan 16, 2025
fe3561f
Make HopHints less like constraints to path-finding
contrun Jan 14, 2025
94b8a42
Comment out some hop hints payment tests
contrun Jan 16, 2025
2fc1557
Use channel outpoint in HopHint
contrun Jan 16, 2025
3829864
Update rpc docs
contrun Jan 16, 2025
3545d75
Refactor updating of heap and distances in finding path
contrun Jan 16, 2025
966e126
Use hop hints to find path
contrun Jan 16, 2025
102b66b
Add unit tests for hop hints
contrun Jan 16, 2025
b60cafd
Fix fee calculation with the wrong ChannelUpdate fee rate
contrun Jan 17, 2025
86af4d6
Add test for pay self fee rate
contrun Jan 17, 2025
75d9874
Use the right ChannelUpdate
contrun Jan 17, 2025
99a09bf
Add comment on we should use outbound channel's fee rate
contrun Jan 17, 2025
78d52a5
Fix balance checking becasue outbound liquidity is used now
contrun Jan 17, 2025
0b6445e
Fix the direction of ChannelUpdate in mock graph environment
contrun Jan 17, 2025
5813c8e
Split test test_channel_update_tlc_sync_up for tlc value and channel …
contrun Jan 18, 2025
d2b5793
Fix use the wrong amount to check tlc_minimum_value limit
contrun Jan 18, 2025
1847ec7
Check tlc forwarding fee
contrun Jan 18, 2025
be2973c
Check payment failure with outdated fee rate
contrun Jan 18, 2025
183a811
Fix TLC validity checking
contrun Jan 18, 2025
356a179
Update rpc documents
contrun Jan 20, 2025
75b1227
Merge branch 'fix-fee-calculation-with-wrong-ChannelUpdate-fee_rate' …
contrun Jan 20, 2025
6bc496f
Fix forwarding fee in finding path with hop hints
contrun Jan 20, 2025
6c90c76
Fix fee calculation while adjusting target
contrun Jan 20, 2025
7ed0faa
Fix using the wrong amount for adjusting target
contrun Jan 20, 2025
0e31287
Remove explicitly obtaining balance from store
contrun Jan 20, 2025
3dd662a
Add migration
contrun Jan 20, 2025
ff2a0c9
Merge remote-tracking branch 'nervosnetwork/develop' into handle-priv…
contrun Jan 22, 2025
5b55482
Remove some unused imports
contrun Jan 22, 2025
8a3ec86
Merge remote-tracking branch 'nervosnetwork/develop' into handle-priv…
contrun Feb 26, 2025
2b5c9a2
Add more debug info
contrun Feb 26, 2025
6b37020
Reproduce error for pending failed tlc calculation
contrun Feb 27, 2025
4e367fc
Do not counted in confirmed failed tlc in check and build settlement …
chenyukang Feb 27, 2025
08c64cd
Merge remote-tracking branch 'nervosnetwork/develop' into handle-priv…
contrun Mar 4, 2025
38003a7
Merge remote-tracking branch 'chenyukang/fix-failed-tlc-error' into h…
contrun Mar 4, 2025
0b4be98
Merge remote-tracking branch 'nervosnetwork/develop' into handle-priv…
contrun Mar 5, 2025
3b5984f
Remove migration
contrun Mar 6, 2025
b8b2ba8
Change log level
contrun Mar 6, 2025
ab161b4
Add TODO for updating test cases
contrun Mar 6, 2025
88bc9d0
Make eval_and_update a function
contrun Mar 6, 2025
2874d91
Remove added trace logs
contrun Mar 6, 2025
45feb5b
Merge remote-tracking branch 'nervosnetwork/develop' into handle-priv…
contrun Mar 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions src/fiber/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4679,12 +4679,6 @@ impl ChannelActorState {
let received_tlc_value = self.get_received_tlc_balance();
debug_assert!(self.to_remote_amount >= received_tlc_value);
if received_tlc_value + tlc.amount > self.to_remote_amount {
debug!(
"Adding tlc {:?} with amount {} exceeds remote balance {}",
tlc.tlc_id,
tlc.amount,
self.to_remote_amount - received_tlc_value
);
return Err(ProcessingChannelError::TlcAmountExceedLimit);
}
}
Expand Down Expand Up @@ -6825,6 +6819,7 @@ impl ChannelActorState {
let mut offered_fulfilled = 0;
let mut received_pending = 0;
let mut received_fulfilled = 0;

for info in pending_tlcs {
if info.is_offered() {
let confirmed_remove_reason = (info.outbound_status()
Expand Down Expand Up @@ -6862,7 +6857,6 @@ impl ChannelActorState {
}
}
}

let to_local_value =
self.to_local_amount + received_fulfilled - offered_pending - offered_fulfilled;
let to_remote_value =
Expand Down
298 changes: 183 additions & 115 deletions src/fiber/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,96 @@ where
Ok(hops_data)
}

// A helper function to evaluate whether an edge should be added to the heap of nodes to visit.
// We will check the accumulated probability of this edge to be a successful payment, and evaluate
// the distance from the source node to the final payee. If the distance is shorter than the current
// distance, we will update the distance and add the node to the heap.
// This should be called after some sanity checks on the edge, e.g. the amount to send
// is less than the capacity and the expiry is less than the limit.
#[allow(clippy::too_many_arguments)]
fn eval_and_update(
&self,

// The channel outpoint of the edge.
channel_outpoint: &OutPoint,
// The capacity of the channel.
channel_capacity: u128,
// The source node of the edge.
from: Pubkey,
// The target node of the edge.
to: Pubkey,
// The amount that the source node will send to the target node.
amount_to_send: u128,
// The amount that the source node will receive from forwarding `amount_to_send` to the target.
fee: u128,
// The TLC expiry for the TLC that the target node will receive.
incoming_tlc_expiry: u64,
// The difference in TLC expiry between the TLC that the source node will receive
// and the TLC that the target node will receive.
tlc_expiry_delta: u64,
// The probability of a successful payment from current target to the final payee.
cur_probability: f64,
// The weight accumulated from the payment path from current target to the final payee.
cur_weight: u128,
// The distances from nodes to the final payee.
distances: &mut HashMap<Pubkey, NodeHeapElement>,
// The priority queue of nodes to be visited (sorted by distance and probability).
nodes_heap: &mut NodeHeap,
) {
let probability = cur_probability
* self.history.eval_probability(
from,
to,
channel_outpoint,
amount_to_send,
channel_capacity,
);

debug!(
"probability: {} for channel_outpoint: {:?} from: {:?} => to: {:?}",
probability, channel_outpoint, from, to
);
if probability < DEFAULT_MIN_PROBABILITY {
debug!("probability is too low: {:?}", probability);
return;
}
let agg_weight = self.edge_weight(amount_to_send, fee, tlc_expiry_delta);
let weight = cur_weight + agg_weight;
let distance = self.calculate_distance_based_probability(probability, weight);

if let Some(node) = distances.get(&from) {
if distance >= node.distance {
return;
}
}
let total_amount = amount_to_send + fee;
let total_tlc_expiry = incoming_tlc_expiry + tlc_expiry_delta;
let node = NodeHeapElement {
node_id: from,
weight,
distance,
amount_to_send: total_amount,
incoming_tlc_expiry: total_tlc_expiry,
fee_charged: fee,
probability,
next_hop: Some(PathEdge {
target: to,
channel_outpoint: channel_outpoint.clone(),
// Here we need to use the amount accumulated so far (i.e. with the fees in current hop)
// because the fee here is for the receiving node to forward the amount to the next node.
// So the total amount in AddTlc packet should include the fee.
amount_received: amount_to_send,
// We need to use cur_hop.incoming_tlc_expiry instead of incoming_tlc_expiry here
// because we need the expiry for the AddTlc packet sent from source to target.
// cur_hop.incoming_tlc_expiry is the expiry time for the TLC that is going to be received by the target,
// while incoming_tlc_expiry is the expiry time for the TLC that is going to be received by the source.
incoming_tlc_expiry,
}),
};
distances.insert(node.node_id, node.clone());
nodes_heap.push_or_fix(node);
}

// the algorithm works from target-to-source to find the shortest path
#[allow(clippy::too_many_arguments)]
pub fn find_path(
Expand Down Expand Up @@ -947,36 +1037,69 @@ where
));
}

let hop_hint_map: HashMap<(Pubkey, bool), OutPoint> = hop_hints
.into_iter()
.map(|hint| {
(
(hint.pubkey, hint.inbound),
OutPoint::new(hint.channel_funding_tx.into(), 0),
)
})
.collect::<HashMap<_, _>>();

let mut target = target;
let mut expiry = final_tlc_expiry_delta;
let mut amount = amount;
let mut last_edge = None;

if route_to_self {
let (edge, t, e, f) = self.adjust_target_for_route_self(
&hop_hint_map,
&hop_hints,
amount,
final_tlc_expiry_delta,
source,
target,
)?;
assert_ne!(target, t);
target = t;
expiry += e;
amount += f;
last_edge = Some(edge);
} else {
// The calculation of probability and distance requires a capacity of the channel.
// We don't know the capacity of the channels in the hop hints. We just assume that the capacity
// of these channels is sufficiently large. We will use 10 times the amount as the capacity.
// See also https://github.com/lightningnetwork/lnd/blob/506586a37e18446af6ce63723e44b2d849bd7fc1/routing/pathfind.go#L46-L49
let sufficiently_large_capacity = 10 * amount;
for hint in hop_hints {
let HopHint {
pubkey: from,
channel_outpoint,
fee_rate,
tlc_expiry_delta,
} = hint;
// Say we have a payment path A -- channel 1 --> B -- channel 2 --> C.
// For now, all the fees that B will receive are calculated based on the fee rate B sets in channel 1.
// We didn't use the outbound fees for B in channel 2 at all. This is different from lnd,
// which calculates both the inbound fees in channel 1 and the outbound fees in channel 2.
// For now, we set the fees to be 0. We may need to change this in the future.
match calculate_tlc_forward_fee(amount, fee_rate as u128) {
Ok(fee) => {
self.eval_and_update(
&channel_outpoint,
sufficiently_large_capacity,
from,
target,
amount,
fee,
expiry,
tlc_expiry_delta,
1.0,
0,
&mut distances,
&mut nodes_heap,
);
}
Err(err) => {
return Err(PathFindError::PathFind(format!(
"calculate_tlc_forward_fee error: {:?}",
err
)));
}
}
}
}
assert_ne!(source, target);

// initialize the target node
nodes_heap.push(NodeHeapElement {
node_id: target,
Expand All @@ -1001,17 +1124,6 @@ where
continue;
}

if let Some(channel) = hop_hint_map.get(&(from, false)) {
if channel != channel_info.out_point() {
continue;
}
}
if let Some(channel) = hop_hint_map.get(&(to, true)) {
if channel != channel_info.out_point() {
continue;
}
}

if let Some(last_edge) = &last_edge {
if &last_edge.channel_outpoint == channel_info.out_point() {
continue;
Expand Down Expand Up @@ -1086,59 +1198,20 @@ where
continue;
}

let probability = cur_hop.probability
* self.history.eval_probability(
from,
to,
channel_info.out_point(),
amount_to_send,
channel_info.capacity(),
);

debug!(
"probability: {} for channel_outpoint: {:?} from: {:?} => to: {:?}",
probability,
self.eval_and_update(
channel_info.out_point(),
channel_info.capacity(),
from,
to
to,
next_hop_received_amount,
fee,
cur_hop.incoming_tlc_expiry,
expiry_delta,
cur_hop.probability,
cur_hop.weight,
&mut distances,
&mut nodes_heap,
);
if probability < DEFAULT_MIN_PROBABILITY {
debug!("probability is too low: {:?}", probability);
continue;
}
let agg_weight =
self.edge_weight(amount_to_send, fee, channel_update.tlc_expiry_delta);
let weight = cur_hop.weight + agg_weight;
let distance = self.calculate_distance_based_probability(probability, weight);

if let Some(node) = distances.get(&from) {
if distance >= node.distance {
continue;
}
}
let node = NodeHeapElement {
node_id: from,
weight,
distance,
amount_to_send,
incoming_tlc_expiry,
fee_charged: fee,
probability,
next_hop: Some(PathEdge {
target: to,
channel_outpoint: channel_info.out_point().clone(),
// The amount_received is the amount that next hop is going to receive.
// That is exactly next_hop_received_amount.
amount_received: next_hop_received_amount,
// We need to use cur_hop.incoming_tlc_expiry instead of incoming_tlc_expiry here
// because we need the expiry for the AddTlc packet sent from source to target.
// cur_hop.incoming_tlc_expiry is the expiry time for the TLC that is going to be received by the target,
// while incoming_tlc_expiry is the expiry time for the TLC that is going to be received by the source.
incoming_tlc_expiry: cur_hop.incoming_tlc_expiry,
}),
};
distances.insert(node.node_id, node.clone());
nodes_heap.push_or_fix(node);
}
}

Expand Down Expand Up @@ -1171,62 +1244,57 @@ where

fn adjust_target_for_route_self(
&self,
hop_hint_map: &HashMap<(Pubkey, bool), OutPoint>,
hop_hints: &[HopHint],
amount: u128,
expiry: u64,
source: Pubkey,
target: Pubkey,
node: Pubkey,
) -> Result<(PathEdge, Pubkey, u64, u128), PathFindError> {
let direct_channels: Vec<(Pubkey, Pubkey, &ChannelInfo, &ChannelUpdateInfo)> = self
.get_node_inbounds(source)
.filter(|(_, _, channel_info, _)| {
if let Some(channel) = hop_hint_map.get(&(source, true)) {
// if there is a hop hint for node -> source,
// try to use this channel as the last candidate hop for route self
// and we event don't check the direct balance of channel,
// hop hint's priority is higher than direct balance
return channel == channel_info.out_point();
}
if let Some(channel) = hop_hint_map.get(&(source, false)) {
// if there is a hop hint for source -> node,
// then we can not set this node as the last candidate hop for route self
// so skip this channel
if channel == channel_info.out_point() {
return false;
}
}
if let Some(state) = self
.store
.get_channel_state_by_outpoint(channel_info.out_point())
{
let balance = state.to_remote_amount;
return balance >= amount;
}
// normal code path will not reach here, we must can get balance for direct channels
// anyway, check the capacity here for safety
return channel_info.capacity() >= amount;
let mut channels: Vec<(Pubkey, OutPoint, u64, u64)> = hop_hints
.iter()
.map(|hint| {
let pubkey = hint.pubkey;
let outpoint = hint.channel_outpoint.clone();
let expiry = hint.tlc_expiry_delta;
let fee_rate = hint.fee_rate;
(pubkey, outpoint, expiry, fee_rate)
})
.collect();
for (from, _, channel_info, channel_update) in self.get_node_inbounds(node) {
if let Some(balance) = channel_update.outbound_liquidity {
if balance < amount {
continue;
}
}
// normal code path will not reach here, we must can get balance for direct channels
// anyway, check the capacity here for safety
if channel_info.capacity() < amount {
continue;
}
channels.push((
from,
channel_info.out_point().clone(),
channel_update.tlc_expiry_delta,
channel_update.fee_rate,
))
}

// a proper hop hint for route self will limit the direct_channels to only one
// if there are multiple channels, we will randomly select a channel from the source node for route to self
// so that the following part of algorithm will always trying to find a path without cycle
if let Some(&(from, to, channel_info, channel_update)) =
direct_channels.choose(&mut thread_rng())
if let Some((from, outpoint, tlc_expiry_delta, fee_rate)) =
channels.choose(&mut thread_rng())
{
assert_ne!(target, from);
assert_ne!(node, *from);
let fee = calculate_tlc_forward_fee(amount, *fee_rate as u128).map_err(|err| {
PathFindError::PathFind(format!("calculate_tlc_forward_fee error: {:?}", err))
})?;
let last_edge = PathEdge {
target: to,
channel_outpoint: channel_info.out_point().clone(),
target: node,
channel_outpoint: outpoint.clone(),
amount_received: amount,
incoming_tlc_expiry: expiry,
};
let fee = calculate_tlc_forward_fee(amount, channel_update.fee_rate as u128).map_err(
|err| {
PathFindError::PathFind(format!("calculate_tlc_forward_fee error: {:?}", err))
},
)?;
Ok((last_edge, from, channel_update.tlc_expiry_delta, fee))
Ok((last_edge, *from, *tlc_expiry_delta, fee))
} else {
return Err(PathFindError::PathFind(
"no direct channel found for source node".to_string(),
Expand Down
Loading