|
6 | 6 | // Verifies locked_msat rises when a probe is dispatched and returns |
7 | 7 | // to zero once the probe resolves. |
8 | 8 | // |
| 9 | +// locked_msat_accounts_for_routing_fees |
| 10 | +// Asserts the exact locked_msat (delivered amount + per-hop fee) for a single |
| 11 | +// in-flight probe, proving fees are tracked and not just the delivered amount. |
| 12 | +// |
9 | 13 | // exhausted_probe_budget_blocks_new_probes |
10 | 14 | // Samples locked_msat across multiple probe cycles and asserts it never |
11 | 15 | // exceeds the configured max_locked_msat budget cap. |
@@ -200,6 +204,93 @@ async fn probe_budget_increments_and_decrements() { |
200 | 204 | node_c.stop().unwrap(); |
201 | 205 | } |
202 | 206 |
|
| 207 | +/// Verifies that `locked_msat` accounts for routing fees, not just the delivered amount: |
| 208 | +/// a probe along A→B→C locks `delivered amount + per-hop fee` on the first-hop channel. |
| 209 | +/// |
| 210 | +/// The budget is sized to exactly one probe's worth, so at most one probe is in flight and |
| 211 | +/// the observed `locked_msat` is deterministic. The existing budget test only checks that it |
| 212 | +/// is non-zero; this asserts the precise value, which a fees-excluded accounting would miss. |
| 213 | +#[tokio::test(flavor = "multi_thread")] |
| 214 | +async fn locked_msat_accounts_for_routing_fees() { |
| 215 | + // First hop carries the delivered amount plus this per-hop fee (see `build_probe_path`). |
| 216 | + const FIRST_HOP_FEE_MSAT: u64 = 1000; |
| 217 | + const LOCKED_PER_PROBE_MSAT: u64 = PROBE_AMOUNT_MSAT + FIRST_HOP_FEE_MSAT; |
| 218 | + |
| 219 | + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); |
| 220 | + let chain_source = random_chain_source(&bitcoind, &electrsd); |
| 221 | + |
| 222 | + let node_b = setup_node(&chain_source, random_config(false)); |
| 223 | + let node_c = setup_node(&chain_source, random_config(false)); |
| 224 | + |
| 225 | + let mut config_a = random_config(false); |
| 226 | + let strategy = FixedPathStrategy::new(); |
| 227 | + config_a.probing = Some( |
| 228 | + ProbingConfigBuilder::custom(strategy.clone()) |
| 229 | + .interval(Duration::from_millis(PROBING_INTERVAL_MILLISECONDS)) |
| 230 | + // Budget for exactly one in-flight probe so locked_msat is deterministic. |
| 231 | + .max_locked_msat(LOCKED_PER_PROBE_MSAT) |
| 232 | + .build(), |
| 233 | + ); |
| 234 | + let node_a = setup_node(&chain_source, config_a); |
| 235 | + |
| 236 | + let addr_a = node_a.onchain_payment().new_address().unwrap(); |
| 237 | + let addr_b = node_b.onchain_payment().new_address().unwrap(); |
| 238 | + premine_and_distribute_funds( |
| 239 | + &bitcoind.client, |
| 240 | + &electrsd.client, |
| 241 | + vec![addr_a, addr_b], |
| 242 | + Amount::from_sat(2_000_000), |
| 243 | + ) |
| 244 | + .await; |
| 245 | + node_a.sync_wallets().unwrap(); |
| 246 | + node_b.sync_wallets().unwrap(); |
| 247 | + |
| 248 | + open_channel(&node_a, &node_b, 1_000_000, true, &electrsd).await; |
| 249 | + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; |
| 250 | + node_b.sync_wallets().unwrap(); |
| 251 | + open_channel(&node_b, &node_c, 1_000_000, true, &electrsd).await; |
| 252 | + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; |
| 253 | + |
| 254 | + node_a.sync_wallets().unwrap(); |
| 255 | + node_b.sync_wallets().unwrap(); |
| 256 | + node_c.sync_wallets().unwrap(); |
| 257 | + |
| 258 | + expect_channel_ready_event!(node_a, node_b.node_id()); |
| 259 | + expect_event!(node_b, ChannelReady); |
| 260 | + expect_event!(node_b, ChannelReady); |
| 261 | + expect_event!(node_c, ChannelReady); |
| 262 | + |
| 263 | + strategy.set_path(build_probe_path(&node_a, &node_b, &node_c, PROBE_AMOUNT_MSAT)); |
| 264 | + wait_for_channel_ready_to_send(&node_a, &node_b, LOCKED_PER_PROBE_MSAT).await; |
| 265 | + wait_for_channel_ready_to_send(&node_b, &node_c, PROBE_AMOUNT_MSAT).await; |
| 266 | + strategy.start_probing(); |
| 267 | + |
| 268 | + // Capture locked_msat the moment the first probe goes in flight. With a single-probe |
| 269 | + // budget the value is only ever 0 or exactly one probe's worth, so the first non-zero |
| 270 | + // reading is the full first-hop HTLC. |
| 271 | + let locked = tokio::time::timeout(Duration::from_secs(30), async { |
| 272 | + loop { |
| 273 | + let locked = node_a.prober().unwrap().locked_msat(); |
| 274 | + if locked > 0 { |
| 275 | + break locked; |
| 276 | + } |
| 277 | + tokio::time::sleep(Duration::from_millis(100)).await; |
| 278 | + } |
| 279 | + }) |
| 280 | + .await |
| 281 | + .expect("locked_msat never increased — no probe was dispatched"); |
| 282 | + |
| 283 | + assert_eq!( |
| 284 | + locked, LOCKED_PER_PROBE_MSAT, |
| 285 | + "locked_msat must equal the delivered amount plus routing fees, not just the delivered amount" |
| 286 | + ); |
| 287 | + |
| 288 | + strategy.stop_probing(); |
| 289 | + node_a.stop().unwrap(); |
| 290 | + node_b.stop().unwrap(); |
| 291 | + node_c.stop().unwrap(); |
| 292 | +} |
| 293 | + |
203 | 294 | /// Verifies that `locked_msat` is restored after the node is stopped and restarted |
204 | 295 | /// while a probe is still in flight. |
205 | 296 | /// |
|
0 commit comments