Skip to content

Commit eea9fb4

Browse files
elnoshclaude
andcommitted
simln-lib: enforce HTLC policy limits against the correct party
`check_outgoing_addition` previously checked every limit against the sending node's own policy. Split each limit to the party that owns it: * htlc_minimum_msat / htlc_maximum_msat are forwarder-advertised fields of the sender's own gossiped channel_update (populated into the graph from `self.policy`), so they are enforced against `self.policy`. * max_accepted_htlcs / max_htlc_value_in_flight_msat are receiver- negotiated (BOLT-2 channel open, never gossiped), so they are enforced against the counterparty's policy. Keeping this at add time means the final recipient's inbound limits are enforced too, since it never adds an outgoing HTLC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2b7c397 commit eea9fb4

2 files changed

Lines changed: 193 additions & 47 deletions

File tree

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ If you want to run the cli in a containerized environment, see the docker set up
304304

305305
## Advanced Usage - Network Simulation
306306

307-
If you are looking to simulate payments are large lightning networks
307+
If you are looking to simulate payments on large lightning networks
308308
without the resource consumption of setting up a large cluster of nodes,
309309
you may be interested in dispatching payments on a simulated network.
310310

@@ -375,9 +375,20 @@ nodes by their pubkey (aliases are not yet supported).
375375
}
376376
```
377377

378-
Note that you need to provide forwarding policies in each direction,
379-
because each participant in the channel sets their own forwarding
380-
policy and restrictions on their counterparty.
378+
Note that you need to provide a policy in each direction, because each
379+
participant in the channel sets their own forwarding policy and
380+
restrictions on their counterparty. Each `node_N` entry is that node's
381+
own policy for the direction it forwards in. The fields split across
382+
the two parties as follows:
383+
384+
* Forwarder-advertised limits (`min_htlc_size_msat`,
385+
`max_htlc_size_msat`, `cltv_expiry_delta`, `base_fee`, `fee_rate_prop`)
386+
belong to the node forwarding the HTLC and are enforced against that
387+
node's own policy. These are the values a sender learns from gossip and
388+
uses during pathfinding.
389+
* Counterparty-negotiated limits (`max_htlc_count`, `max_in_flight_msat`)
390+
constrain what may be offered inbound and are enforced against the
391+
receiving counterparty's policy.
381392

382393

383394
### Random Activity Exclusions

simln-lib/src/sim_node.rs

Lines changed: 178 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -227,18 +227,27 @@ impl ChannelState {
227227
Ok(Ok(()))
228228
}
229229

230-
/// Checks whether the proposed HTLC can be added to the channel as an outgoing HTLC. This requires that we have
231-
/// sufficient liquidity, and that the restrictions on our in flight htlc balance and count are not violated by
232-
/// the addition of the HTLC. Specification sanity checks (such as reasonable CLTV) are also included, as this
233-
/// is where we'd check it in real life.
234-
fn check_outgoing_addition(&self, htlc: &Htlc) -> Result<(), ForwardingError> {
230+
/// Checks whether the proposed HTLC can be added to the channel as an outgoing HTLC. Each limit is read from the
231+
/// party that owns it:
232+
///
233+
/// * `htlc_minimum_msat` / `htlc_maximum_msat` are forwarder-advertised: they are part of the gossiped
234+
/// `channel_update` for the sending node's own direction (populated into the graph from `self.policy`), so they
235+
/// are enforced against `self.policy`.
236+
/// * `max_accepted_htlcs` / `max_htlc_value_in_flight_msat` are receiver-negotiated (BOLT-2 channel open, never
237+
/// gossiped): they bound the HTLCs the counterparty will accept in flight towards it, so they are enforced
238+
/// against `counterparty_policy`.
239+
fn check_outgoing_addition(
240+
&self,
241+
htlc: &Htlc,
242+
counterparty_policy: &ChannelPolicy,
243+
) -> Result<(), ForwardingError> {
235244
fail_forwarding_inequality!(htlc.amount_msat, >, self.policy.max_htlc_size_msat, MoreThanMaximum);
236245
fail_forwarding_inequality!(htlc.amount_msat, <, self.policy.min_htlc_size_msat, LessThanMinimum);
237246
fail_forwarding_inequality!(
238-
self.in_flight.len() as u64 + 1, >, self.policy.max_htlc_count, ExceedsInFlightCount
247+
self.in_flight.len() as u64 + 1, >, counterparty_policy.max_htlc_count, ExceedsInFlightCount
239248
);
240249
fail_forwarding_inequality!(
241-
self.in_flight_total() + htlc.amount_msat, >, self.policy.max_in_flight_msat, ExceedsInFlightTotal
250+
self.in_flight_total() + htlc.amount_msat, >, counterparty_policy.max_in_flight_msat, ExceedsInFlightTotal
242251
);
243252
fail_forwarding_inequality!(htlc.amount_msat, >, self.local_balance_msat, InsufficientBalance);
244253
fail_forwarding_inequality!(htlc.cltv_expiry, >, 500000000, ExpiryInSeconds);
@@ -256,8 +265,9 @@ impl ChannelState {
256265
&mut self,
257266
hash: PaymentHash,
258267
htlc: Htlc,
268+
counterparty_policy: &ChannelPolicy,
259269
) -> Result<Result<u64, ForwardingError>, CriticalError> {
260-
if let Err(fwd_err) = self.check_outgoing_addition(&htlc) {
270+
if let Err(fwd_err) = self.check_outgoing_addition(&htlc, counterparty_policy) {
261271
return Ok(Err(fwd_err));
262272
}
263273

@@ -393,8 +403,15 @@ impl SimulatedChannel {
393403
}
394404

395405
self.sanity_check()?;
396-
self.get_node_mut(sending_node)?
397-
.add_outgoing_htlc(hash, htlc)
406+
let (sender, counterparty) = if sending_node == &self.node_1.policy.pubkey {
407+
(&mut self.node_1, &self.node_2)
408+
} else if sending_node == &self.node_2.policy.pubkey {
409+
(&mut self.node_2, &self.node_1)
410+
} else {
411+
return Err(CriticalError::NodeNotFound(*sending_node));
412+
};
413+
414+
sender.add_outgoing_htlc(hash, htlc, &counterparty.policy)
398415
}
399416

400417
/// Performs a sanity check on the total balances in a channel. Note that we do not currently include on-chain
@@ -1718,6 +1735,8 @@ mod tests {
17181735
let mut channel_state =
17191736
ChannelState::new(create_test_policy(local_balance / 2), local_balance);
17201737

1738+
let policy = channel_state.policy.clone();
1739+
17211740
// Basic sanity check that we Initialize the channel correctly.
17221741
assert_channel_balances!(channel_state, local_balance, 0, 0);
17231742

@@ -1730,7 +1749,9 @@ mod tests {
17301749
cltv_expiry: 40,
17311750
};
17321751

1733-
assert!(channel_state.add_outgoing_htlc(hash_1, htlc_1).is_ok());
1752+
assert!(channel_state
1753+
.add_outgoing_htlc(hash_1, htlc_1, &policy)
1754+
.is_ok());
17341755
assert_channel_balances!(
17351756
channel_state,
17361757
local_balance - htlc_1.amount_msat,
@@ -1741,7 +1762,7 @@ mod tests {
17411762
// Try to add a htlc with the same payment hash and assert that we fail because we enforce one htlc per hash
17421763
// at present.
17431764
assert!(matches!(
1744-
channel_state.add_outgoing_htlc(hash_1, htlc_1),
1765+
channel_state.add_outgoing_htlc(hash_1, htlc_1, &policy),
17451766
Err(CriticalError::PaymentHashExists(_))
17461767
));
17471768

@@ -1752,7 +1773,9 @@ mod tests {
17521773
cltv_expiry: 40,
17531774
};
17541775

1755-
assert!(channel_state.add_outgoing_htlc(hash_2, htlc_2).is_ok());
1776+
assert!(channel_state
1777+
.add_outgoing_htlc(hash_2, htlc_2, &policy)
1778+
.is_ok());
17561779
assert_channel_balances!(
17571780
channel_state,
17581781
local_balance - htlc_1.amount_msat - htlc_2.amount_msat,
@@ -1834,46 +1857,56 @@ mod tests {
18341857
let mut channel_state =
18351858
ChannelState::new(create_test_policy(local_balance / 2), local_balance);
18361859

1860+
// Size limits (min/max htlc size) are read from our own policy, while the in-flight limits (count and total)
1861+
// are read from the counterparty's.
1862+
let mut counterparty = channel_state.policy.clone();
1863+
counterparty.max_in_flight_msat = 30_000;
1864+
counterparty.max_htlc_count = 4;
1865+
18371866
let mut htlc = Htlc {
18381867
amount_msat: channel_state.policy.max_htlc_size_msat + 1,
18391868
cltv_expiry: channel_state.policy.cltv_expiry_delta,
18401869
};
1841-
// HTLC maximum size exceeded.
18421870
assert!(matches!(
1843-
channel_state.check_outgoing_addition(&htlc),
1871+
channel_state.check_outgoing_addition(&htlc, &counterparty),
18441872
Err(ForwardingError::MoreThanMaximum(_, _))
18451873
));
18461874

1847-
// Beneath HTLC minimum size.
18481875
htlc.amount_msat = channel_state.policy.min_htlc_size_msat - 1;
18491876
assert!(matches!(
1850-
channel_state.check_outgoing_addition(&htlc),
1877+
channel_state.check_outgoing_addition(&htlc, &counterparty),
18511878
Err(ForwardingError::LessThanMinimum(_, _))
18521879
));
18531880

1854-
// Add two large htlcs so that we will start to run into our in-flight total amount limit.
18551881
let hash_1 = PaymentHash([1; 32]);
18561882
let htlc_1 = Htlc {
1857-
amount_msat: channel_state.policy.max_in_flight_msat / 2,
1883+
amount_msat: counterparty.max_in_flight_msat / 2,
18581884
cltv_expiry: channel_state.policy.cltv_expiry_delta,
18591885
};
18601886

1861-
assert!(channel_state.check_outgoing_addition(&htlc_1).is_ok());
1862-
assert!(channel_state.add_outgoing_htlc(hash_1, htlc_1).is_ok());
1887+
assert!(channel_state
1888+
.check_outgoing_addition(&htlc_1, &counterparty)
1889+
.is_ok());
1890+
assert!(channel_state
1891+
.add_outgoing_htlc(hash_1, htlc_1, &counterparty)
1892+
.is_ok());
18631893

18641894
let hash_2 = PaymentHash([2; 32]);
18651895
let htlc_2 = Htlc {
1866-
amount_msat: channel_state.policy.max_in_flight_msat / 2,
1896+
amount_msat: counterparty.max_in_flight_msat / 2,
18671897
cltv_expiry: channel_state.policy.cltv_expiry_delta,
18681898
};
18691899

1870-
assert!(channel_state.check_outgoing_addition(&htlc_2).is_ok());
1871-
assert!(channel_state.add_outgoing_htlc(hash_2, htlc_2).is_ok());
1900+
assert!(channel_state
1901+
.check_outgoing_addition(&htlc_2, &counterparty)
1902+
.is_ok());
1903+
assert!(channel_state
1904+
.add_outgoing_htlc(hash_2, htlc_2, &counterparty)
1905+
.is_ok());
18721906

1873-
// Now, assert that we can't add even our smallest htlc size, because we're hit our in-flight amount limit.
18741907
htlc.amount_msat = channel_state.policy.min_htlc_size_msat;
18751908
assert!(matches!(
1876-
channel_state.check_outgoing_addition(&htlc),
1909+
channel_state.check_outgoing_addition(&htlc, &counterparty),
18771910
Err(ForwardingError::ExceedsInFlightTotal(_, _))
18781911
));
18791912

@@ -1884,11 +1917,14 @@ mod tests {
18841917
assert!(channel_state.remove_outgoing_htlc(&hash_2).is_ok());
18851918
channel_state.settle_outgoing_htlc(htlc_2.amount_msat, true);
18861919

1887-
// Now we're going to add many htlcs so that we hit our in-flight count limit (unique payment hash per htlc).
1888-
for i in 0..channel_state.policy.max_htlc_count {
1920+
for i in 0..counterparty.max_htlc_count {
18891921
let hash = PaymentHash([i.try_into().unwrap(); 32]);
1890-
assert!(channel_state.check_outgoing_addition(&htlc).is_ok());
1891-
assert!(channel_state.add_outgoing_htlc(hash, htlc).is_ok());
1922+
assert!(channel_state
1923+
.check_outgoing_addition(&htlc, &counterparty)
1924+
.is_ok());
1925+
assert!(channel_state
1926+
.add_outgoing_htlc(hash, htlc, &counterparty)
1927+
.is_ok());
18921928
}
18931929

18941930
// Try to add one more htlc and we should be rejected.
@@ -1898,34 +1934,35 @@ mod tests {
18981934
};
18991935

19001936
assert!(matches!(
1901-
channel_state.check_outgoing_addition(&htlc_3),
1937+
channel_state.check_outgoing_addition(&htlc_3, &counterparty),
19021938
Err(ForwardingError::ExceedsInFlightCount(_, _))
19031939
));
19041940

19051941
// Resolve all in-flight htlcs.
1906-
for i in 0..channel_state.policy.max_htlc_count {
1942+
for i in 0..counterparty.max_htlc_count {
19071943
let hash = PaymentHash([i.try_into().unwrap(); 32]);
19081944
assert!(channel_state.remove_outgoing_htlc(&hash).is_ok());
19091945
channel_state.settle_outgoing_htlc(htlc.amount_msat, true)
19101946
}
19111947

1912-
// Add and settle another htlc to move more liquidity away from our local balance.
1913-
let hash_4 = PaymentHash([1; 32]);
19141948
let htlc_4 = Htlc {
19151949
amount_msat: channel_state.policy.max_htlc_size_msat,
19161950
cltv_expiry: channel_state.policy.cltv_expiry_delta,
19171951
};
1918-
assert!(channel_state.check_outgoing_addition(&htlc_4).is_ok());
1919-
assert!(channel_state.add_outgoing_htlc(hash_4, htlc_4).is_ok());
1920-
assert!(channel_state.remove_outgoing_htlc(&hash_4).is_ok());
1921-
channel_state.settle_outgoing_htlc(htlc_4.amount_msat, true);
1922-
1923-
// Finally, assert that we don't have enough balance to forward our largest possible htlc (because of all the
1924-
// htlcs that we've settled) and assert that we fail to a large htlc. The balance assertion here is just a
1925-
// sanity check for the test, which will fail if we change the amounts settled/failed in the test.
1952+
for hash in [PaymentHash([10; 32]), PaymentHash([11; 32])] {
1953+
assert!(channel_state
1954+
.add_outgoing_htlc(hash, htlc_4, &counterparty)
1955+
.is_ok());
1956+
assert!(channel_state.remove_outgoing_htlc(&hash).is_ok());
1957+
channel_state.settle_outgoing_htlc(htlc_4.amount_msat, true);
1958+
}
1959+
1960+
// Finally, assert that we don't have enough balance to forward our largest possible htlc. The balance
1961+
// assertion here is just a sanity check for the test, which will fail if we change the amounts settled in the
1962+
// test.
19261963
assert!(channel_state.local_balance_msat < channel_state.policy.max_htlc_size_msat);
19271964
assert!(matches!(
1928-
channel_state.check_outgoing_addition(&htlc_4),
1965+
channel_state.check_outgoing_addition(&htlc_4, &counterparty),
19291966
Err(ForwardingError::InsufficientBalance(_, _))
19301967
));
19311968
}
@@ -2073,6 +2110,104 @@ mod tests {
20732110
));
20742111
}
20752112

2113+
#[test]
2114+
fn test_add_htlc_policy_ownership() {
2115+
let capacity_msat = 500_000_000;
2116+
2117+
// A policy that trips none of the limits under test, so only the party deliberately made strict can reject.
2118+
let permissive = || {
2119+
let mut policy = create_test_policy(capacity_msat / 2);
2120+
policy.min_htlc_size_msat = 1;
2121+
policy.max_htlc_size_msat = capacity_msat;
2122+
policy.max_htlc_count = 483;
2123+
policy.max_in_flight_msat = capacity_msat;
2124+
policy
2125+
};
2126+
2127+
// Builds a channel with node_1 as the sender and node_2 as the receiver under the given policies.
2128+
let build_channel = |sender_policy: ChannelPolicy, receiver_policy: ChannelPolicy| {
2129+
let node_1 = ChannelState::new(sender_policy, capacity_msat);
2130+
let node_2 = ChannelState::new(receiver_policy, 0);
2131+
(
2132+
node_1.policy.pubkey,
2133+
SimulatedChannel {
2134+
capacity_msat,
2135+
short_channel_id: ShortChannelID::from(123),
2136+
node_1,
2137+
node_2,
2138+
exclude_capacity: false,
2139+
},
2140+
)
2141+
};
2142+
2143+
let htlc = Htlc {
2144+
amount_msat: 1000,
2145+
cltv_expiry: 40,
2146+
};
2147+
2148+
// An HTLC above the sender's advertised maximum is rejected, even though the receiver's maximum is higher.
2149+
let mut sender_policy = permissive();
2150+
sender_policy.max_htlc_size_msat = 2000;
2151+
let (sender, mut channel) = build_channel(sender_policy, permissive());
2152+
assert!(matches!(
2153+
channel.add_htlc(
2154+
&sender,
2155+
PaymentHash([1; 32]),
2156+
Htlc {
2157+
amount_msat: 3000,
2158+
cltv_expiry: 40
2159+
}
2160+
),
2161+
Ok(Err(ForwardingError::MoreThanMaximum(_, _)))
2162+
));
2163+
2164+
// An HTLC below the sender's advertised minimum is rejected, even though the receiver's minimum is lower.
2165+
let mut sender_policy = permissive();
2166+
sender_policy.min_htlc_size_msat = 10_000;
2167+
let (sender, mut channel) = build_channel(sender_policy, permissive());
2168+
assert!(matches!(
2169+
channel.add_htlc(
2170+
&sender,
2171+
PaymentHash([2; 32]),
2172+
Htlc {
2173+
amount_msat: 5000,
2174+
cltv_expiry: 40
2175+
}
2176+
),
2177+
Ok(Err(ForwardingError::LessThanMinimum(_, _)))
2178+
));
2179+
2180+
// The receiver's `max_accepted_htlcs` bounds the in-flight HTLC count: with a limit of 1 the second HTLC is
2181+
// rejected, even though the sender's own policy (483) would allow it.
2182+
let mut receiver_policy = permissive();
2183+
receiver_policy.max_htlc_count = 1;
2184+
let (sender, mut channel) = build_channel(permissive(), receiver_policy);
2185+
assert!(channel
2186+
.add_htlc(&sender, PaymentHash([3; 32]), htlc)
2187+
.is_ok());
2188+
assert!(matches!(
2189+
channel.add_htlc(&sender, PaymentHash([4; 32]), htlc),
2190+
Ok(Err(ForwardingError::ExceedsInFlightCount(_, _)))
2191+
));
2192+
2193+
// The receiver's `max_htlc_value_in_flight_msat` bounds the in-flight value: a 6000 msat HTLC exceeds the
2194+
// receiver's 5000 msat limit, even though the sender permits the full channel capacity in flight.
2195+
let mut receiver_policy = permissive();
2196+
receiver_policy.max_in_flight_msat = 5000;
2197+
let (sender, mut channel) = build_channel(permissive(), receiver_policy);
2198+
assert!(matches!(
2199+
channel.add_htlc(
2200+
&sender,
2201+
PaymentHash([5; 32]),
2202+
Htlc {
2203+
amount_msat: 6000,
2204+
cltv_expiry: 40
2205+
}
2206+
),
2207+
Ok(Err(ForwardingError::ExceedsInFlightTotal(_, _)))
2208+
));
2209+
}
2210+
20762211
mock! {
20772212
Network{}
20782213

0 commit comments

Comments
 (0)