Skip to content

Commit 91a57a5

Browse files
committed
Fix trampoline routing fee with no max_fee_amount specified
1 parent dcacda2 commit 91a57a5

File tree

5 files changed

+224
-94
lines changed

5 files changed

+224
-94
lines changed

crates/fiber-lib/src/fiber/graph.rs

Lines changed: 77 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,8 +1196,8 @@ where
11961196
payment_data: &SendPaymentData,
11971197
) -> Result<Vec<PaymentHopData>, PathFindError> {
11981198
info!(
1199-
"entered build_route: amount: {}, amount_low_bound: {:?}",
1200-
amount, amount_low_bound
1199+
"entered build_route: amount: {}, amount_low_bound: {:?}, max_fee_amount: {:?}",
1200+
amount, amount_low_bound, max_fee_amount
12011201
);
12021202
let source = self.get_source_pubkey();
12031203
let target = payment_data.target_pubkey;
@@ -1307,36 +1307,8 @@ where
13071307
// Prefer the per-call override from `build_route` when present, else fall back to
13081308
// the budget carried in `payment_data`.
13091309
let max_fee_amount = max_fee_amount.or(payment_data.max_fee_amount);
1310-
let total_fee_budget = max_fee_amount.unwrap_or(0);
13111310

13121311
let secp = Secp256k1::new();
1313-
let trampoline_forward_expiry_delta =
1314-
|base_final: u64,
1315-
remaining_trampoline_hops: &[crate::fiber::payment::TrampolineHop],
1316-
tlc_expiry_limit: u64|
1317-
-> Result<u64, PathFindError> {
1318-
let slack = remaining_trampoline_hops
1319-
.iter()
1320-
.map(|h| h.tlc_expiry_delta.unwrap_or(DEFAULT_TLC_EXPIRY_DELTA))
1321-
.try_fold(0u64, |acc, d| {
1322-
acc.checked_add(d).ok_or_else(|| {
1323-
PathFindError::Other("trampoline tlc_expiry_delta overflow".to_string())
1324-
})
1325-
})?;
1326-
1327-
let total = base_final.checked_add(slack).ok_or_else(|| {
1328-
PathFindError::Other("trampoline tlc_expiry_delta overflow".to_string())
1329-
})?;
1330-
1331-
if total > tlc_expiry_limit {
1332-
return Err(PathFindError::Other(format!(
1333-
"trampoline tlc_expiry_delta exceeds tlc_expiry_limit: {} > {}",
1334-
total, tlc_expiry_limit
1335-
)));
1336-
}
1337-
1338-
Ok(total)
1339-
};
13401312

13411313
if let Some(hops) = payment_data.trampoline_hops().filter(|h| !h.is_empty()) {
13421314
let first = hops[0].pubkey;
@@ -1372,7 +1344,7 @@ where
13721344
// `forward_amounts[idx]` is the *minimum incoming amount* required by the next hop to
13731345
// cover its own trampoline service fee.
13741346
let mut forward_amounts = vec![0u128; hops.len()];
1375-
let mut min_incoming_for_service = vec![0u128; hops.len()];
1347+
let mut forwarding_amounts = vec![0u128; hops.len()];
13761348
let mut next_amount_to_forward = final_amount;
13771349
for (idx, hop) in hops.iter().enumerate().rev() {
13781350
forward_amounts[idx] = next_amount_to_forward;
@@ -1388,55 +1360,57 @@ where
13881360
)?;
13891361

13901362
next_amount_to_forward = next_amount_to_forward.saturating_add(fee);
1391-
min_incoming_for_service[idx] = next_amount_to_forward;
1363+
forwarding_amounts[idx] = next_amount_to_forward;
13921364
}
13931365

13941366
let amount_to_first_trampoline = next_amount_to_forward;
13951367
let trampoline_service_fee_total =
13961368
amount_to_first_trampoline.saturating_sub(final_amount);
13971369

1398-
// Only enforce the service-fee budget constraint when the caller explicitly
1399-
// provided `max_fee_amount`. When unset, we treat it as "no explicit fee cap"
1400-
// (matching the historical behavior of these graph unit tests).
1401-
if max_fee_amount.is_some() && trampoline_service_fee_total > total_fee_budget {
1402-
return Err(PathFindError::Other(format!(
1403-
"max_fee_amount too low for trampoline service fees: required={}, budget={}",
1404-
trampoline_service_fee_total, total_fee_budget
1405-
)));
1370+
// if max_fee_amount is not specified, use default fee calculation
1371+
let single_forward_fee = calculate_tlc_forward_fee(
1372+
amount_to_first_trampoline,
1373+
crate::fiber::channel::DEFAULT_FEE_RATE as u128,
1374+
)
1375+
.map_err(|e| PathFindError::Other(format!("invalid default fee calculation: {e}")))?;
1376+
let default_min_fee_amount =
1377+
trampoline_service_fee_total + hops.len() as u128 * single_forward_fee;
1378+
1379+
// Give a recommend max fee amount that is 10x the single forward fee for routing
1380+
let default_max_fee_amount =
1381+
trampoline_service_fee_total + hops.len() as u128 * single_forward_fee * 10;
1382+
1383+
if let Some(total_fee) = max_fee_amount {
1384+
if default_min_fee_amount > total_fee {
1385+
return Err(PathFindError::Other(format!(
1386+
"max_fee_amount too low for trampoline service fees: recommend_minimal_fee={}, maximal_fee={} current_fee={}",
1387+
default_min_fee_amount, default_max_fee_amount, total_fee
1388+
)));
1389+
}
14061390
}
14071391

1392+
let total_fee_budget = max_fee_amount.unwrap_or(default_min_fee_amount);
14081393
let remaining_budget = total_fee_budget.saturating_sub(trampoline_service_fee_total);
14091394

1410-
// Split remaining budget proportionally to forwarded amounts:
1395+
// Split remaining budget evenly across:
14111396
// - index 0: payer routing to first trampoline
14121397
// - index i+1: routing budget available at trampoline i
1413-
let mut budget_weights: Vec<u128> = Vec::with_capacity(hops.len() + 1);
1414-
budget_weights.push(amount_to_first_trampoline);
1415-
debug_assert_eq!(forward_amounts.len(), hops.len());
1416-
for amount in &forward_amounts {
1417-
budget_weights.push(*amount);
1418-
}
1419-
1420-
let weight_sum: u128 = budget_weights.iter().copied().sum();
1421-
let mut budgets = vec![0u128; budget_weights.len()];
1422-
if remaining_budget > 0 && weight_sum > 0 {
1423-
let mut allocated = 0u128;
1424-
for (idx, w) in budget_weights.iter().copied().enumerate() {
1425-
let b = remaining_budget.saturating_mul(w) / weight_sum;
1426-
budgets[idx] = b;
1427-
allocated = allocated.saturating_add(b);
1428-
}
1429-
1430-
// Distribute rounding remainder deterministically.
1431-
let remainder = remaining_budget.saturating_sub(allocated);
1432-
debug_assert!(remainder < budgets.len() as u128);
1433-
let r = remainder as usize;
1434-
for budget in budgets.iter_mut().take(r) {
1398+
let mut budgets = vec![0u128; hops.len() + 1];
1399+
if remaining_budget > 0 {
1400+
let slots = budgets.len() as u128;
1401+
let base = remaining_budget / slots;
1402+
let remainder = (remaining_budget % slots) as usize;
1403+
budgets.iter_mut().for_each(|b| *b = base);
1404+
for budget in budgets.iter_mut().take(remainder) {
14351405
*budget = budget.saturating_add(1);
14361406
}
14371407
}
14381408

1439-
let payer_routing_budget = budgets[0];
1409+
let payer_routing_budget = if max_fee_amount.is_some() {
1410+
Some(budgets[0])
1411+
} else {
1412+
None
1413+
};
14401414
let per_trampoline_routing_budgets = &budgets[1..];
14411415

14421416
// The first trampoline must receive the total expected incoming amount, which consists of:
@@ -1449,13 +1423,17 @@ where
14491423
let amount_to_trampoline = amount_to_first_trampoline
14501424
.saturating_add(per_trampoline_routing_budgets.first().copied().unwrap_or(0));
14511425

1426+
debug!(
1427+
"now got amount_to_trampoline: {:?} budgets: {:?}",
1428+
amount_to_trampoline, budgets
1429+
);
14521430
let route_to_trampoline = self.find_path(
14531431
source,
14541432
first,
14551433
Some(amount_to_trampoline),
1456-
Some(payer_routing_budget),
1434+
payer_routing_budget,
14571435
payment_data.udt_type_script.clone(),
1458-
trampoline_forward_expiry_delta(
1436+
self.trampoline_forward_expiry_delta(
14591437
payment_data.final_tlc_expiry_delta,
14601438
hops,
14611439
payment_data.tlc_expiry_limit,
@@ -1512,7 +1490,7 @@ where
15121490
next_node_id,
15131491
amount_to_forward,
15141492
build_max_fee_amount,
1515-
tlc_expiry_delta: trampoline_forward_expiry_delta(
1493+
tlc_expiry_delta: self.trampoline_forward_expiry_delta(
15161494
payment_data.final_tlc_expiry_delta,
15171495
remaining_trampoline_hops,
15181496
payment_data.tlc_expiry_limit,
@@ -1546,7 +1524,7 @@ where
15461524
hops: route_to_trampoline,
15471525
amount: amount_to_trampoline,
15481526
trampoline_onion: Some(trampoline_onion),
1549-
final_hop_expiry_delta_override: Some(trampoline_forward_expiry_delta(
1527+
final_hop_expiry_delta_override: Some(self.trampoline_forward_expiry_delta(
15501528
payment_data.final_tlc_expiry_delta,
15511529
hops,
15521530
payment_data.tlc_expiry_limit,
@@ -1558,6 +1536,35 @@ where
15581536
Err(PathFindError::NoPathFound)
15591537
}
15601538

1539+
fn trampoline_forward_expiry_delta(
1540+
&self,
1541+
base_final: u64,
1542+
remaining_trampoline_hops: &[crate::fiber::payment::TrampolineHop],
1543+
tlc_expiry_limit: u64,
1544+
) -> Result<u64, PathFindError> {
1545+
let slack = remaining_trampoline_hops
1546+
.iter()
1547+
.map(|h| h.tlc_expiry_delta.unwrap_or(DEFAULT_TLC_EXPIRY_DELTA))
1548+
.try_fold(0u64, |acc, d| {
1549+
acc.checked_add(d).ok_or_else(|| {
1550+
PathFindError::Other("trampoline tlc_expiry_delta overflow".to_string())
1551+
})
1552+
})?;
1553+
1554+
let total = base_final.checked_add(slack).ok_or_else(|| {
1555+
PathFindError::Other("trampoline tlc_expiry_delta overflow".to_string())
1556+
})?;
1557+
1558+
if total > tlc_expiry_limit {
1559+
return Err(PathFindError::Other(format!(
1560+
"trampoline tlc_expiry_delta exceeds tlc_expiry_limit: {} > {}",
1561+
total, tlc_expiry_limit
1562+
)));
1563+
}
1564+
1565+
Ok(total)
1566+
}
1567+
15611568
fn find_path_with_payment_data(
15621569
&self,
15631570
source: Pubkey,
@@ -2125,8 +2132,8 @@ where
21252132
route: Option<&[RouterHop]>,
21262133
) -> Result<Vec<RouterHop>, PathFindError> {
21272134
debug!(
2128-
"begin inner_find_path from {:?} to {:?} amount: {:?}",
2129-
source, target, amount
2135+
"begin inner_find_path from {:?} to {:?} amount: {:?} max_fee: {:?}",
2136+
source, target, amount, max_fee_amount
21302137
);
21312138
let started_time = crate::time::Instant::now();
21322139
let nodes_len = self.nodes.len();

crates/fiber-lib/src/fiber/payment.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1712,11 +1712,13 @@ where
17121712
iteration += 1;
17131713

17141714
debug!(
1715-
"build route iteration {}, target_amount: {} amount_low_bound: {:?} remain_amount: {}",
1715+
"build route iteration {}, target_amount: {} amount_low_bound: {:?} remain_amount: {}, max_parts: {}, max_fee: {:?}",
17161716
iteration,
17171717
target_amount,
17181718
amount_low_bound,
17191719
remain_amount,
1720+
session.max_parts(),
1721+
max_fee,
17201722
);
17211723
#[cfg(not(target_arch = "wasm32"))]
17221724
let build_route_result = self
@@ -1739,6 +1741,7 @@ where
17391741

17401742
match build_route_result {
17411743
Err(e) => {
1744+
error!("Here failed to build route: {}", e);
17421745
let error = format!("Failed to build route, {}", e);
17431746
return Err(Error::SendPaymentError(error));
17441747
}

crates/fiber-lib/src/fiber/tests/graph.rs

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,10 +1009,9 @@ fn test_graph_trampoline_routing_fee_rate_explicit_zero_allows_zero_fee_budget()
10091009

10101010
let route = network
10111011
.graph
1012-
.build_route(payment_data.amount, None, None, &payment_data)
1013-
.expect("trampoline route should be built with zero fee budget when fee_rate is omitted");
1014-
assert_eq!(route.len(), 2);
1015-
assert_eq!(route[0].next_hop, Some(t1.into()));
1012+
.build_route(payment_data.amount, None, None, &payment_data);
1013+
let err = route.unwrap_err().to_string();
1014+
assert!(err.contains("max_fee_amount too low for trampoline service fees"));
10161015
}
10171016

10181017
#[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -1042,7 +1041,7 @@ fn test_graph_trampoline_routing_fee_fields_match_precompute() {
10421041

10431042
let payment_hash = Hash256::default();
10441043
let final_amount: u128 = 1000;
1045-
let max_fee_amount: u128 = 10;
1044+
let max_fee_amount: u128 = 15;
10461045

10471046
let mut h1 = crate::fiber::payment::TrampolineHop::new(t1.into());
10481047
h1.fee_rate = Some(1_000);
@@ -1090,24 +1089,14 @@ fn test_graph_trampoline_routing_fee_fields_match_precompute() {
10901089
);
10911090
let remaining_budget = max_fee_amount.saturating_sub(service_fee_total);
10921091

1093-
let mut weights = Vec::with_capacity(hops.len() + 1);
1094-
weights.push(amount_to_first_trampoline);
1095-
for a in forward_amounts.iter().copied() {
1096-
weights.push(a);
1097-
}
1098-
let weight_sum: u128 = weights.iter().copied().sum();
1099-
let mut budgets = vec![0u128; weights.len()];
1100-
if remaining_budget > 0 && weight_sum > 0 {
1101-
let mut allocated = 0u128;
1102-
for (i, w) in weights.iter().copied().enumerate() {
1103-
let b = remaining_budget.saturating_mul(w) / weight_sum;
1104-
budgets[i] = b;
1105-
allocated = allocated.saturating_add(b);
1106-
}
1107-
let remainder = remaining_budget.saturating_sub(allocated);
1108-
debug_assert!(remainder < budgets.len() as u128);
1109-
for i in 0..(remainder as usize) {
1110-
budgets[i] = budgets[i].saturating_add(1);
1092+
let mut budgets = vec![0u128; hops.len() + 1];
1093+
if remaining_budget > 0 {
1094+
let slots = budgets.len() as u128;
1095+
let base = remaining_budget / slots;
1096+
let remainder = (remaining_budget % slots) as usize;
1097+
budgets.iter_mut().for_each(|b| *b = base);
1098+
for budget in budgets.iter_mut().take(remainder) {
1099+
*budget = budget.saturating_add(1);
11111100
}
11121101
}
11131102

crates/fiber-lib/src/fiber/tests/payment.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,53 @@ async fn test_send_payment_prefer_newer_channels() {
225225
assert_eq!(source_node.get_inflight_payment_count().await, 0);
226226
}
227227

228+
#[tokio::test]
229+
async fn test_send_payment_keysend_without_max_fee() {
230+
init_tracing();
231+
232+
let (nodes, _channels) = create_n_nodes_network(
233+
&[
234+
((0, 1), (MIN_RESERVED_CKB + 10000000000, MIN_RESERVED_CKB)),
235+
((1, 2), (MIN_RESERVED_CKB + 10000000000, MIN_RESERVED_CKB)),
236+
],
237+
3,
238+
)
239+
.await;
240+
let [mut node_0, _node_1, node_2] = nodes.try_into().expect("2 nodes");
241+
let source_node = &mut node_0;
242+
let target_pubkey = node_2.pubkey;
243+
244+
let res = source_node
245+
.send_payment(SendPaymentCommand {
246+
target_pubkey: Some(target_pubkey),
247+
amount: Some(10000000),
248+
keysend: Some(true),
249+
dry_run: true,
250+
..Default::default()
251+
})
252+
.await
253+
.unwrap();
254+
255+
eprintln!("res: {:?}", res);
256+
257+
assert_eq!(res.fee, 10000);
258+
259+
let res = source_node
260+
.send_payment(SendPaymentCommand {
261+
target_pubkey: Some(target_pubkey),
262+
amount: Some(10000000),
263+
keysend: Some(true),
264+
..Default::default()
265+
})
266+
.await
267+
.unwrap();
268+
let payment_hash = res.payment_hash;
269+
source_node.wait_until_success(payment_hash).await;
270+
let payment = source_node.get_payment_result(payment_hash).await;
271+
272+
eprintln!("payment info: {:?}", payment);
273+
}
274+
228275
#[tokio::test]
229276
async fn test_send_payment_prefer_channels_with_larger_balance() {
230277
init_tracing();

0 commit comments

Comments
 (0)