Skip to content

Commit ec237dd

Browse files
committed
Mind htlc_minimum_msat when truncating overpaid value
At truncating the overpaid value, if we fall below htlc_minimum_msat, reach it by increasing fees.
1 parent 2110da2 commit ec237dd

File tree

1 file changed

+228
-67
lines changed

1 file changed

+228
-67
lines changed

lightning/src/routing/router.rs

Lines changed: 228 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ struct PathBuildingHop {
186186
/// an estimated cost of reaching this hop.
187187
/// Might get stale when fees are recomputed. Primarily for internal use.
188188
total_fee_msat: u64,
189+
/// This is useful for update_value_and_recompute_fees to make sure
190+
/// we don't fall below the minimum. Should not be updated manually and
191+
/// generally should not be accessed.
192+
htlc_minimum_msat: u64,
189193
}
190194

191195
impl PathBuildingHop {
@@ -230,14 +234,52 @@ impl PaymentPath {
230234

231235
// If an amount transferred by the path is updated, the fees should be adjusted.
232236
// Any other way to change fees may result in an inconsistency.
233-
// The caller should make sure values don't fall below htlc_minimum_msat of the used channels.
237+
// Also, it's only safe to reduce the value, to not violate limits like
238+
// htlc_minimum_msat.
234239
fn update_value_and_recompute_fees(&mut self, value_msat: u64) {
240+
if value_msat == self.hops.last().unwrap().route_hop.fee_msat {
241+
// Nothing to change.
242+
return;
243+
}
244+
assert!(value_msat < self.hops.last().unwrap().route_hop.fee_msat);
245+
235246
let mut total_fee_paid_msat = 0 as u64;
236-
for i in (1..self.hops.len()).rev() {
237-
let cur_hop_amount_msat = total_fee_paid_msat + value_msat;
247+
for i in (0..self.hops.len()).rev() {
248+
let last_hop = i == self.hops.len() - 1;
249+
250+
// For non-last-hop, this value will represent the fees paid on the current hop. It
251+
// will consist of the fees for the use of the next hop, and extra fees to match
252+
// htlc_minimum_msat of the current channel. Last hop is handled separately.
253+
let mut cur_hop_fees_msat = 0;
254+
if !last_hop {
255+
cur_hop_fees_msat = self.hops.get(i + 1).unwrap().hop_use_fee_msat;
256+
}
257+
238258
let mut cur_hop = self.hops.get_mut(i).unwrap();
239259
cur_hop.next_hops_fee_msat = total_fee_paid_msat;
240-
if let Some(new_fee) = compute_fees(cur_hop_amount_msat, cur_hop.channel_fees) {
260+
// Overpay in fees if we can't save these funds due to htlc_minimum_msat.
261+
// We try to account for htlc_minimum_msat in scoring (add_entry!), so that nodes don't
262+
// set it too high just to maliciously take more fees by exploiting this
263+
// match htlc_minimum_msat logic.
264+
let mut cur_hop_transferred_amount_msat = total_fee_paid_msat + value_msat;
265+
if let Some(extra_fees_msat) = cur_hop.htlc_minimum_msat.checked_sub(cur_hop_transferred_amount_msat) {
266+
cur_hop_transferred_amount_msat += extra_fees_msat;
267+
total_fee_paid_msat += extra_fees_msat;
268+
cur_hop_fees_msat += extra_fees_msat;
269+
}
270+
271+
if last_hop {
272+
// Final hop is a special case: it usually has just value_msat (by design), but also
273+
// it still could overpay for the htlc_minimum_msat.
274+
cur_hop.route_hop.fee_msat = cur_hop_transferred_amount_msat;
275+
} else {
276+
// Propagate updated fees for the use of the channels to one hop back, where they
277+
// will be actually paid (fee_msat). The last hop is handled above separately.
278+
cur_hop.route_hop.fee_msat = cur_hop_fees_msat;
279+
}
280+
281+
// Fee for the use of the current hop which will be deducted on the previous hop.
282+
if let Some(new_fee) = compute_fees(cur_hop_transferred_amount_msat, cur_hop.channel_fees) {
241283
cur_hop.hop_use_fee_msat = new_fee;
242284
total_fee_paid_msat += new_fee;
243285
} else {
@@ -247,16 +289,6 @@ impl PaymentPath {
247289
unreachable!();
248290
}
249291
}
250-
251-
// Propagate updated fees for the use of the channels to one hop back,
252-
// where they will be actually paid (fee_msat).
253-
// For the last hop it will represent the value being transferred over this path.
254-
for i in 0..self.hops.len() - 1 {
255-
let next_hop_use_fee_msat = self.hops.get(i + 1).unwrap().hop_use_fee_msat;
256-
self.hops.get_mut(i).unwrap().route_hop.fee_msat = next_hop_use_fee_msat;
257-
}
258-
self.hops.last_mut().unwrap().route_hop.fee_msat = value_msat;
259-
self.hops.last_mut().unwrap().hop_use_fee_msat = 0;
260292
}
261293
}
262294

@@ -466,17 +498,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
466498
None => unreachable!(),
467499
};
468500

469-
// If HTLC minimum is larger than the whole value we're going to transfer, we shouldn't bother
470-
// even considering this channel, because it won't be useful.
501+
// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
502+
// bother considering this channel.
503+
// Since we're choosing amount_to_transfer_over_msat as maximum possible, it can
504+
// be only reduced later (not increased), so this channel should just be skipped
505+
// as not sufficient.
471506
// TODO: Explore simply adding fee to hit htlc_minimum_msat
472-
// This is separate from checking amount_to_transfer_over_msat, which also should be
473-
// lower-bounded by htlc_minimum_msat. Since we're choosing it as maximum possible,
474-
// this channel should just be skipped if it's not sufficient.
475-
// amount_to_transfer_over_msat can be both larger and smaller than final_value_msat,
476-
// so this can't be derived.
477-
let final_value_satisfies_htlc_minimum_msat = final_value_msat >= $directional_info.htlc_minimum_msat;
478-
if final_value_satisfies_htlc_minimum_msat &&
479-
contributes_sufficient_value && amount_to_transfer_over_msat >= $directional_info.htlc_minimum_msat {
507+
if contributes_sufficient_value && amount_to_transfer_over_msat >= $directional_info.htlc_minimum_msat {
480508
let hm_entry = dist.entry(&$src_node_id);
481509
let old_entry = hm_entry.or_insert_with(|| {
482510
// If there was previously no known way to access
@@ -507,6 +535,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
507535
next_hops_fee_msat: u64::max_value(),
508536
hop_use_fee_msat: u64::max_value(),
509537
total_fee_msat: u64::max_value(),
538+
htlc_minimum_msat: $directional_info.htlc_minimum_msat,
510539
}
511540
});
512541

@@ -554,7 +583,29 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
554583
// (considering the cost to "reach" this channel from the route destination,
555584
// the cost of using this channel,
556585
// and the cost of routing to the source node of this channel).
557-
if old_entry.total_fee_msat > total_fee_msat {
586+
// Also, consider that htlc_minimum_msat_difference, because we might end up
587+
// paying it. Consider the following exploit:
588+
// we use 2 paths to transfer 1.5 BTC. One of them is 0-fee normal 1 BTC path,
589+
// and for the other one we picked a 1sat-fee path with htlc_minimum_msat of
590+
// 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
591+
// by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
592+
// to this channel.
593+
// TODO: this scoring could be smarter (e.g. 0.5*htlc_minimum_msat here).
594+
let mut old_cost = old_entry.total_fee_msat;
595+
if let Some(increased_old_cost) = old_cost.checked_add(old_entry.htlc_minimum_msat) {
596+
old_cost = increased_old_cost;
597+
} else {
598+
old_cost = u64::max_value();
599+
}
600+
601+
let mut new_cost = total_fee_msat;
602+
if let Some(increased_new_cost) = new_cost.checked_add($directional_info.htlc_minimum_msat) {
603+
new_cost = increased_new_cost;
604+
} else {
605+
new_cost = u64::max_value();
606+
}
607+
608+
if new_cost < old_cost {
558609
targets.push(new_graph_node);
559610
old_entry.next_hops_fee_msat = $next_hops_fee_msat;
560611
old_entry.hop_use_fee_msat = hop_use_fee_msat;
@@ -568,6 +619,9 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
568619
cltv_expiry_delta: $directional_info.cltv_expiry_delta as u32,
569620
};
570621
old_entry.channel_fees = $directional_info.fees;
622+
// It's probably fine to replace the old entry, because the new one
623+
// passed the htlc_minimum-related checks above.
624+
old_entry.htlc_minimum_msat = $directional_info.htlc_minimum_msat;
571625
}
572626
}
573627
}
@@ -852,7 +906,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
852906
// Sort by value so that we drop many really-low values first, since
853907
// fewer paths is better: the payment is less likely to fail.
854908
// TODO: this could also be optimized by also sorting by feerate_per_sat_routed,
855-
// so that the sender pays less fees overall.
909+
// so that the sender pays less fees overall. And also htlc_minimum_msat.
856910
cur_route.sort_by_key(|path| path.get_value_msat());
857911
// We should make sure that at least 1 path left.
858912
let mut paths_left = cur_route.len();
@@ -878,11 +932,20 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
878932

879933
// Step (7).
880934
// Now, substract the overpaid value from the most-expensive path.
935+
// TODO: this could also be optimized by also sorting by feerate_per_sat_routed,
936+
// so that the sender pays less fees overall. And also htlc_minimum_msat.
881937
cur_route.sort_by_key(|path| { path.hops.iter().map(|hop| hop.channel_fees.proportional_millionths as u64).sum::<u64>() });
882-
let expensive_payment_path = cur_route.last_mut().unwrap();
883-
let expensive_path_new_value_msat = expensive_payment_path.get_value_msat() - overpaid_value_msat;
884-
// TODO: make sure expensive_path_new_value_msat doesn't cause to fall behind htlc_minimum_msat.
885-
expensive_payment_path.update_value_and_recompute_fees(expensive_path_new_value_msat);
938+
for expensive_payment_path in &mut cur_route {
939+
if let Some(expensive_path_new_value_msat) = expensive_payment_path.get_value_msat().checked_sub(overpaid_value_msat) {
940+
expensive_payment_path.update_value_and_recompute_fees(expensive_path_new_value_msat);
941+
break;
942+
} else {
943+
// We already dropped all the small channels above, meaning all the
944+
// remaining channels are larger than remaining overpaid_value_msat.
945+
// Thus, this can't be negative.
946+
unreachable!();
947+
}
948+
}
886949
break;
887950
}
888951
}
@@ -1385,33 +1448,6 @@ mod tests {
13851448
let (our_privkey, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
13861449

13871450
// Simple route to 2 via 1
1388-
// Should fail if htlc_minimum can't be reached
1389-
1390-
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
1391-
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1392-
short_channel_id: 2,
1393-
timestamp: 2,
1394-
flags: 0,
1395-
cltv_expiry_delta: 0,
1396-
htlc_minimum_msat: 50_000,
1397-
htlc_maximum_msat: OptionalField::Absent,
1398-
fee_base_msat: 0,
1399-
fee_proportional_millionths: 0,
1400-
excess_data: Vec::new()
1401-
});
1402-
// Make 0 fee.
1403-
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], UnsignedChannelUpdate {
1404-
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1405-
short_channel_id: 4,
1406-
timestamp: 2,
1407-
flags: 0,
1408-
cltv_expiry_delta: 0,
1409-
htlc_minimum_msat: 0,
1410-
htlc_maximum_msat: OptionalField::Absent,
1411-
fee_base_msat: 0,
1412-
fee_proportional_millionths: 0,
1413-
excess_data: Vec::new()
1414-
});
14151451

14161452
// Disable other paths
14171453
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
@@ -1475,16 +1511,7 @@ mod tests {
14751511
excess_data: Vec::new()
14761512
});
14771513

1478-
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], None, &Vec::new(), 49_999, 42, Arc::clone(&logger)) {
1479-
assert_eq!(err, "Failed to find a path to the given destination");
1480-
} else { panic!(); }
1481-
1482-
// A payment above the minimum should pass
1483-
let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], None, &Vec::new(), 50_000, 42, Arc::clone(&logger)).unwrap();
1484-
assert_eq!(route.paths[0].len(), 2);
1485-
1486-
// This was a check against final_value_msat.
1487-
// Now let's check against amount_to_transfer_over_msat.
1514+
// Check against amount_to_transfer_over_msat.
14881515
// We will be transferring 300_000_000 msat.
14891516
// Set minimal HTLC of 200_000_000 msat.
14901517
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
@@ -1540,6 +1567,140 @@ mod tests {
15401567
assert_eq!(route.paths[0].len(), 2);
15411568
}
15421569

1570+
#[test]
1571+
fn htlc_minimum_overpay_test() {
1572+
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
1573+
let (our_privkey, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
1574+
1575+
// A route to node#2 via two paths.
1576+
// One path allows transferring 35-40 sats, another one also allows 35-40 sats.
1577+
// Thus, they can't send 60 without overpaying.
1578+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
1579+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1580+
short_channel_id: 2,
1581+
timestamp: 2,
1582+
flags: 0,
1583+
cltv_expiry_delta: 0,
1584+
htlc_minimum_msat: 35_000,
1585+
htlc_maximum_msat: OptionalField::Present(40_000),
1586+
fee_base_msat: 0,
1587+
fee_proportional_millionths: 0,
1588+
excess_data: Vec::new()
1589+
});
1590+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
1591+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1592+
short_channel_id: 12,
1593+
timestamp: 3,
1594+
flags: 0,
1595+
cltv_expiry_delta: 0,
1596+
htlc_minimum_msat: 35_000,
1597+
htlc_maximum_msat: OptionalField::Present(40_000),
1598+
fee_base_msat: 0,
1599+
fee_proportional_millionths: 0,
1600+
excess_data: Vec::new()
1601+
});
1602+
1603+
// Make 0 fee.
1604+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[7], UnsignedChannelUpdate {
1605+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1606+
short_channel_id: 13,
1607+
timestamp: 2,
1608+
flags: 0,
1609+
cltv_expiry_delta: 0,
1610+
htlc_minimum_msat: 0,
1611+
htlc_maximum_msat: OptionalField::Absent,
1612+
fee_base_msat: 0,
1613+
fee_proportional_millionths: 0,
1614+
excess_data: Vec::new()
1615+
});
1616+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], UnsignedChannelUpdate {
1617+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1618+
short_channel_id: 4,
1619+
timestamp: 2,
1620+
flags: 0,
1621+
cltv_expiry_delta: 0,
1622+
htlc_minimum_msat: 0,
1623+
htlc_maximum_msat: OptionalField::Absent,
1624+
fee_base_msat: 0,
1625+
fee_proportional_millionths: 0,
1626+
excess_data: Vec::new()
1627+
});
1628+
1629+
// Disable other paths
1630+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
1631+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1632+
short_channel_id: 1,
1633+
timestamp: 3,
1634+
flags: 2, // to disable
1635+
cltv_expiry_delta: 0,
1636+
htlc_minimum_msat: 0,
1637+
htlc_maximum_msat: OptionalField::Absent,
1638+
fee_base_msat: 0,
1639+
fee_proportional_millionths: 0,
1640+
excess_data: Vec::new()
1641+
});
1642+
1643+
let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], None, &Vec::new(), 60_000, 42, Arc::clone(&logger)).unwrap();
1644+
// Overpay fees to hit htlc_minimum_msat.
1645+
let overpaid_fees = route.paths[0][0].fee_msat + route.paths[1][0].fee_msat;
1646+
// TODO: this could be better balanced to overpay 10k and not 15k.
1647+
assert_eq!(overpaid_fees, 15_000);
1648+
1649+
// Now, test that if there are 2 paths, a "cheaper" by fee path wouldn't be prioritized
1650+
// while taking even more fee to match htlc_minimum_msat.
1651+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
1652+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1653+
short_channel_id: 12,
1654+
timestamp: 4,
1655+
flags: 0,
1656+
cltv_expiry_delta: 0,
1657+
htlc_minimum_msat: 65_000,
1658+
htlc_maximum_msat: OptionalField::Present(80_000),
1659+
fee_base_msat: 0,
1660+
fee_proportional_millionths: 0,
1661+
excess_data: Vec::new()
1662+
});
1663+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
1664+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1665+
short_channel_id: 2,
1666+
timestamp: 3,
1667+
flags: 0,
1668+
cltv_expiry_delta: 0,
1669+
htlc_minimum_msat: 0,
1670+
htlc_maximum_msat: OptionalField::Absent,
1671+
fee_base_msat: 0,
1672+
fee_proportional_millionths: 0,
1673+
excess_data: Vec::new()
1674+
});
1675+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], UnsignedChannelUpdate {
1676+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
1677+
short_channel_id: 4,
1678+
timestamp: 4,
1679+
flags: 0,
1680+
cltv_expiry_delta: 0,
1681+
htlc_minimum_msat: 0,
1682+
htlc_maximum_msat: OptionalField::Absent,
1683+
fee_base_msat: 0,
1684+
fee_proportional_millionths: 100_000,
1685+
excess_data: Vec::new()
1686+
});
1687+
1688+
let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], None, &Vec::new(), 60_000, 42, Arc::clone(&logger)).unwrap();
1689+
// Fine to overpay for htlc_minimum_msat if it allows us to save fee.
1690+
assert_eq!(route.paths.len(), 1);
1691+
assert_eq!(route.paths[0][0].short_channel_id, 12);
1692+
let fees = route.paths[0][0].fee_msat;
1693+
assert_eq!(fees, 5_000);
1694+
1695+
let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], None, &Vec::new(), 50_000, 42, Arc::clone(&logger)).unwrap();
1696+
// Not fine to overpay for htlc_minimum_msat if it requires paying more than fee on
1697+
// the other channel.
1698+
assert_eq!(route.paths.len(), 1);
1699+
assert_eq!(route.paths[0][0].short_channel_id, 2);
1700+
let fees = route.paths[0][0].fee_msat;
1701+
assert_eq!(fees, 5_000);
1702+
}
1703+
15431704
#[test]
15441705
fn disable_channels_test() {
15451706
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();

0 commit comments

Comments
 (0)