Skip to content

Commit 89e9b75

Browse files
committed
Validate next_splice_out_maximum_sat on both commitments
Wilmer's fuzzing runs caught a case where an advertised splice out maximum hit the debug assertions in `get_next_splice_out_maximum`. These debug assertions ensure that any adverstised splice out maximum passes the validation of splice contributions. The core issue is that we only read the local commitment when calculating the splice out maximum, but our splice validation requires that any splice out maximum is covered by the minimum of the holder's balances on the local and the remote commitments. Therefore, if a HTLC is dust on the local commitment, but non-dust on the remote commitment, and the holder is the funder of the channel, we advertise a splice out maximum that is not covered by the holder's balance on the remote commitment, and fails our validation of splice contributions. We now read both commitments when calculating the next splice out maximum, which fixes this issue.
1 parent 00146df commit 89e9b75

3 files changed

Lines changed: 220 additions & 28 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,6 +2554,9 @@ pub(super) struct FundingScope {
25542554
value_to_self_msat: u64, // Excluding all pending_htlcs, fees, and anchor outputs
25552555

25562556
/// minimum channel reserve for self to maintain - set by them.
2557+
#[cfg(any(test, feature = "_externalize_tests"))]
2558+
pub(super) counterparty_selected_channel_reserve_satoshis: Option<u64>,
2559+
#[cfg(not(any(test, feature = "_externalize_tests")))]
25572560
counterparty_selected_channel_reserve_satoshis: Option<u64>,
25582561

25592562
#[cfg(any(test, feature = "_externalize_tests"))]

lightning/src/ln/splicing_tests.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9304,3 +9304,175 @@ fn do_test_splice_out_initiator_reserve_breach_zero_fee_commitments(
93049304
acceptor.logger.assert_log("lightning::ln::channelmanager", cannot_splice_out, 1);
93059305
}
93069306
}
9307+
9308+
#[test]
9309+
fn test_splice_out_maximum_on_both_commitments_dust_on_fundee_commitment() {
9310+
use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels;
9311+
9312+
let chanmon_cfgs = create_chanmon_cfgs(2);
9313+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
9314+
let mut config = test_default_channel_config();
9315+
config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage =
9316+
100;
9317+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
9318+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
9319+
9320+
const CHANNEL_VALUE_SAT: u64 = 100_000;
9321+
const FEERATE: u32 = 253;
9322+
const TOTAL_ANCHORS_SAT: u64 = 2 * 330;
9323+
const NODE_0_DUST_LIMIT_SAT: u64 = 354;
9324+
const NODE_1_DUST_LIMIT_SAT: u64 = 10_000;
9325+
9326+
let (channel_id, _transaction) =
9327+
setup_0reserve_no_outputs_channels(&nodes, CHANNEL_VALUE_SAT, NODE_0_DUST_LIMIT_SAT);
9328+
9329+
{
9330+
let per_peer_state_lock;
9331+
let mut peer_state_lock;
9332+
let chan =
9333+
get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id);
9334+
chan.context_mut().counterparty_dust_limit_satoshis = NODE_1_DUST_LIMIT_SAT;
9335+
assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_0_DUST_LIMIT_SAT);
9336+
assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0);
9337+
assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0));
9338+
}
9339+
9340+
{
9341+
let per_peer_state_lock;
9342+
let mut peer_state_lock;
9343+
let chan =
9344+
get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id);
9345+
chan.context_mut().holder_dust_limit_satoshis = NODE_1_DUST_LIMIT_SAT;
9346+
assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_0_DUST_LIMIT_SAT);
9347+
assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0);
9348+
assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0));
9349+
}
9350+
9351+
let details = &nodes[0].node.list_channels()[0];
9352+
let channel_type = details.channel_type.clone().unwrap();
9353+
assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies());
9354+
9355+
// This HTLC is only present on node 0's commitment
9356+
const SNEAKY_HTLC_SAT: u64 = 5_000;
9357+
9358+
let (_, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], SNEAKY_HTLC_SAT * 1000);
9359+
9360+
let node_0_details = &nodes[0].node.list_channels()[0];
9361+
let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 0, &channel_type);
9362+
let expected_next_splice_out_maximum_sat = CHANNEL_VALUE_SAT
9363+
- SNEAKY_HTLC_SAT
9364+
- TOTAL_ANCHORS_SAT
9365+
- reserved_fee_sat
9366+
- NODE_1_DUST_LIMIT_SAT;
9367+
assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat);
9368+
let node_1_details = &nodes[1].node.list_channels()[0];
9369+
assert_eq!(node_1_details.next_splice_out_maximum_sat, 0);
9370+
9371+
fail_payment(&nodes[0], &[&nodes[1]], payment_hash);
9372+
9373+
let details = &nodes[0].node.list_channels()[0];
9374+
let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 2, &channel_type);
9375+
let expected_available_capacity_sat = CHANNEL_VALUE_SAT - TOTAL_ANCHORS_SAT - reserved_fee_sat;
9376+
assert_eq!(details.next_outbound_htlc_limit_msat, expected_available_capacity_sat * 1000);
9377+
let node_0_payment_sat = expected_available_capacity_sat;
9378+
send_payment(&nodes[0], &[&nodes[1]], node_0_payment_sat * 1000);
9379+
9380+
// Make sure the local output is now gone from node 1's commitment
9381+
assert!(TOTAL_ANCHORS_SAT + reserved_fee_sat < NODE_1_DUST_LIMIT_SAT);
9382+
9383+
let details = &nodes[1].node.list_channels()[0];
9384+
let expected_next_splice_out_maximum_sat = node_0_payment_sat - NODE_1_DUST_LIMIT_SAT;
9385+
assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat);
9386+
9387+
let details = &nodes[0].node.list_channels()[0];
9388+
let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 1, &channel_type);
9389+
let expected_next_splice_out_maximum_sat =
9390+
CHANNEL_VALUE_SAT - node_0_payment_sat - TOTAL_ANCHORS_SAT - reserved_fee_sat;
9391+
assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat);
9392+
}
9393+
9394+
#[test]
9395+
fn test_splice_out_maximum_on_both_commitments_dust_on_funder_commitment() {
9396+
use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels;
9397+
9398+
let chanmon_cfgs = create_chanmon_cfgs(2);
9399+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
9400+
let mut config = test_default_channel_config();
9401+
config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage =
9402+
100;
9403+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
9404+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
9405+
9406+
const CHANNEL_VALUE_SAT: u64 = 100_000;
9407+
const FEERATE: u32 = 253;
9408+
const TOTAL_ANCHORS_SAT: u64 = 2 * 330;
9409+
const NODE_0_DUST_LIMIT_SAT: u64 = 10_000;
9410+
const NODE_1_DUST_LIMIT_SAT: u64 = 354;
9411+
9412+
let (channel_id, _transaction) =
9413+
setup_0reserve_no_outputs_channels(&nodes, CHANNEL_VALUE_SAT, NODE_1_DUST_LIMIT_SAT);
9414+
9415+
{
9416+
let per_peer_state_lock;
9417+
let mut peer_state_lock;
9418+
let chan =
9419+
get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id);
9420+
chan.context_mut().holder_dust_limit_satoshis = NODE_0_DUST_LIMIT_SAT;
9421+
assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_1_DUST_LIMIT_SAT);
9422+
assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0);
9423+
assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0));
9424+
}
9425+
9426+
{
9427+
let per_peer_state_lock;
9428+
let mut peer_state_lock;
9429+
let chan =
9430+
get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id);
9431+
chan.context_mut().counterparty_dust_limit_satoshis = NODE_0_DUST_LIMIT_SAT;
9432+
assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_1_DUST_LIMIT_SAT);
9433+
assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0);
9434+
assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0));
9435+
}
9436+
9437+
let details = &nodes[0].node.list_channels()[0];
9438+
let channel_type = details.channel_type.clone().unwrap();
9439+
assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies());
9440+
9441+
// This HTLC is only present on node 1's commitment
9442+
const SNEAKY_HTLC_SAT: u64 = 5_000;
9443+
9444+
let (_, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], SNEAKY_HTLC_SAT * 1000);
9445+
9446+
let node_0_details = &nodes[0].node.list_channels()[0];
9447+
let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 0, &channel_type);
9448+
let expected_next_splice_out_maximum_sat = CHANNEL_VALUE_SAT
9449+
- SNEAKY_HTLC_SAT
9450+
- TOTAL_ANCHORS_SAT
9451+
- reserved_fee_sat
9452+
- NODE_0_DUST_LIMIT_SAT;
9453+
assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat);
9454+
let node_1_details = &nodes[1].node.list_channels()[0];
9455+
assert_eq!(node_1_details.next_splice_out_maximum_sat, 0);
9456+
9457+
fail_payment(&nodes[0], &[&nodes[1]], payment_hash);
9458+
9459+
let details = &nodes[0].node.list_channels()[0];
9460+
let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 2, &channel_type);
9461+
let expected_available_capacity_sat = CHANNEL_VALUE_SAT - TOTAL_ANCHORS_SAT - reserved_fee_sat;
9462+
assert_eq!(details.next_outbound_htlc_limit_msat, expected_available_capacity_sat * 1000);
9463+
let node_0_payment_sat = expected_available_capacity_sat;
9464+
send_payment(&nodes[0], &[&nodes[1]], node_0_payment_sat * 1000);
9465+
9466+
// Make sure the local output is now gone from node 0's commitment
9467+
assert!(TOTAL_ANCHORS_SAT + reserved_fee_sat < NODE_0_DUST_LIMIT_SAT);
9468+
9469+
let details = &nodes[1].node.list_channels()[0];
9470+
let expected_next_splice_out_maximum_sat = node_0_payment_sat - NODE_0_DUST_LIMIT_SAT;
9471+
assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat);
9472+
9473+
let details = &nodes[0].node.list_channels()[0];
9474+
let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 1, &channel_type);
9475+
let expected_next_splice_out_maximum_sat =
9476+
CHANNEL_VALUE_SAT - node_0_payment_sat - TOTAL_ANCHORS_SAT - reserved_fee_sat;
9477+
assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat);
9478+
}

lightning/src/sign/tx_builder.rs

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,18 @@ fn get_next_commitment_stats(
358358
// 3) s < (100h + 100 - 100d - c) / 99
359359
fn get_next_splice_out_maximum_sat(
360360
is_outbound_from_holder: bool, channel_value_satoshis: u64, local_balance_before_fee_msat: u64,
361-
remote_balance_before_fee_msat: u64, feerate_per_kw: u32, nondust_htlc_count: usize,
362-
post_splice_delta_above_reserve_sat: u64, channel_constraints: &ChannelConstraints,
363-
channel_type: &ChannelTypeFeatures,
361+
remote_balance_before_fee_msat: u64, local_nondust_htlc_count: usize,
362+
remote_nondust_htlc_count: usize, feerate_per_kw: u32, spiked_feerate: u32,
363+
channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures,
364364
) -> u64 {
365+
let post_splice_delta_above_reserve_sat = if is_outbound_from_holder {
366+
let nondust_htlc_count = cmp::max(local_nondust_htlc_count, remote_nondust_htlc_count);
367+
let commit_tx_fee_sat =
368+
commit_tx_fee_sat(spiked_feerate, nondust_htlc_count + 1, channel_type);
369+
commit_tx_fee_sat
370+
} else {
371+
0
372+
};
365373
let local_balance_before_fee_sat = local_balance_before_fee_msat / 1000;
366374
let mut next_splice_out_maximum_sat = if channel_constraints
367375
.counterparty_selected_channel_reserve_satoshis
@@ -424,37 +432,47 @@ fn get_next_splice_out_maximum_sat(
424432
}
425433
max_splice_out_sat
426434
} else {
427-
// In a zero-reserve channel, the holder is free to withdraw up to its `post_splice_delta_above_reserve_sat`
435+
// In a zero-reserve channel, the holder is free to withdraw up to its `post_splice_delta_above_reserve_sat`.
428436
local_balance_before_fee_sat.saturating_sub(post_splice_delta_above_reserve_sat)
429437
};
430438

431-
// We only bother to check the local commitment here, the counterparty will check its own commitment.
432-
//
433439
// If the current `next_splice_out_maximum_sat` would produce a local commitment with no
434440
// outputs, bump this maximum such that, after the splice, the holder's balance covers at
435441
// least `dust_limit_satoshis` and, if they are the funder, `current_tx_fee_sat`.
436442
// We don't include an additional non-dust inbound HTLC in the `current_tx_fee_sat`,
437443
// because we don't mind if the holder dips below their dust limit to cover the fee for that
438444
// inbound non-dust HTLC.
439-
if !has_output(
440-
is_outbound_from_holder,
441-
local_balance_before_fee_msat.saturating_sub(next_splice_out_maximum_sat * 1000),
442-
remote_balance_before_fee_msat,
443-
feerate_per_kw,
444-
nondust_htlc_count,
445+
//
446+
// We use the regular feerate instead of the spiked feerate here as zero-reserve is not
447+
// allowed on legacy channels.
448+
let current_tx_fee_sat = commit_tx_fee_sat(feerate_per_kw, 0, channel_type);
449+
let mut trim_splice_out_max_if_no_outputs = |nondust_htlc_count, dust_limit_satoshis| {
450+
if !has_output(
451+
is_outbound_from_holder,
452+
local_balance_before_fee_msat.saturating_sub(next_splice_out_maximum_sat * 1000),
453+
remote_balance_before_fee_msat,
454+
feerate_per_kw,
455+
nondust_htlc_count,
456+
dust_limit_satoshis,
457+
channel_type,
458+
) {
459+
let min_balance_sat = if is_outbound_from_holder {
460+
dust_limit_satoshis.saturating_add(current_tx_fee_sat)
461+
} else {
462+
dust_limit_satoshis
463+
};
464+
next_splice_out_maximum_sat =
465+
(local_balance_before_fee_msat / 1000).saturating_sub(min_balance_sat);
466+
}
467+
};
468+
trim_splice_out_max_if_no_outputs(
469+
local_nondust_htlc_count,
445470
channel_constraints.holder_dust_limit_satoshis,
446-
channel_type,
447-
) {
448-
let dust_limit_satoshis = channel_constraints.holder_dust_limit_satoshis;
449-
let current_tx_fee_sat = commit_tx_fee_sat(feerate_per_kw, 0, channel_type);
450-
let min_balance_sat = if is_outbound_from_holder {
451-
dust_limit_satoshis.saturating_add(current_tx_fee_sat)
452-
} else {
453-
dust_limit_satoshis
454-
};
455-
next_splice_out_maximum_sat =
456-
(local_balance_before_fee_msat / 1000).saturating_sub(min_balance_sat);
457-
}
471+
);
472+
trim_splice_out_max_if_no_outputs(
473+
remote_nondust_htlc_count,
474+
channel_constraints.counterparty_dust_limit_satoshis,
475+
);
458476

459477
if channel_value_satoshis < next_splice_out_maximum_sat + MIN_CHANNEL_VALUE_SATOSHIS {
460478
next_splice_out_maximum_sat =
@@ -568,11 +586,10 @@ fn get_available_balances(
568586
channel_value_satoshis,
569587
local_balance_before_fee_msat,
570588
remote_balance_before_fee_msat,
571-
feerate_per_kw,
572-
// The number of non-dust HTLCs on the local commitment at the current feerate
573589
local_nondust_htlc_count,
574-
// The post-splice minimum balance of the holder
575-
if is_outbound_from_holder { local_min_commit_tx_fee_sat } else { 0 },
590+
remote_nondust_htlc_count,
591+
feerate_per_kw,
592+
spiked_feerate,
576593
&channel_constraints,
577594
channel_type,
578595
);

0 commit comments

Comments
 (0)