Shakedown zero reserve channels
What changed, and why it matters
This commit adds a large set of unit tests for a new 'zero reserve' channel feature in the Lightning Dev Kit. Zero-reserve channels let one peer spend almost all of its channel balance, which is normally disallowed because it can leave a channel with no money left to pay on-chain fees during a dispute. The tests exercise the new APIs, check that balances and fee buffers are computed correctly, and verify that dangerous edge cases (such as a commitment transaction with no outputs at all) are rejected or handled safely. The commit itself only contains test code, so it does not introduce a live vulnerability, but it documents behavior that could be risky if the production logic has bugs.
Treat this as a test-only commit. Review the corresponding production implementation of create_channel_to_trusted_peer_0reserve, accept_inbound_channel_from_trusted_peer, and the reserve/fee-buffer calculations in the channel/channelmanager modules to ensure the safety checks exercised here are actually enforced. If the production code is already merged, run these tests and consider additional fuzzing around zero-reserve edge cases.
Security signals we found
Tests for zero-reserve channels, which relax the normal channel_reserve_satoshis security boundary
Tests verify rejection paths when HTLCs would leave a commitment transaction with no outputs
Tests cover force-close behavior and anchor-output handling under zero-reserve conditions
Tests combine zero-reserve with zero-conf, another trust-dependent feature
No production code changes; only unit-test additions
Evidence from the diff
The diff is a single-file addition to lightning/src/ln/htlc_reserve_unit_tests.rs. It imports several channel/fee helpers and then adds ~1000 lines of tests covering: create_channel_to_trusted_peer_0reserve, accept_inbound_channel_from_trusted_peer with TrustedChannelFeatures::ZeroReserve, zero-reserve channels with no commitment outputs, keyed-anchor and P2A-anchor zero-fee-commitment variants, force-close with a single shared-anchor output, and the combination of zero-conf plus zero-reserve. The tests assert on next_outbound_htlc_limit_msat, unspendable_punishment_reserve, channel_type, minimum_depth, and force-close transaction shape. They also verify that certain oversized HTLCs are rejected with ‘Remote HTLC add would overdraw remaining funds’ or ‘balance exhausted on remote commitment’. No production code is changed in this commit.
Changed components
lightning/src/ln/htlc_reserve_unit_tests.rsChannel reserve and HTLC limit logic (tested, not modified)Zero-fee commitment / anchor output logic (tested, not modified)Zero-conf inbound channel acceptance (tested, not modified)Inspect captured patch +1060 / −6
diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs
index 862d947..3c91808 100644
--- a/lightning/src/ln/htlc_reserve_unit_tests.rs
+++ b/lightning/src/ln/htlc_reserve_unit_tests.rs
@@ -2,30 +2,34 @@
use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaymentPurpose};
use crate::ln::chan_utils::{
- self, commitment_tx_base_weight, second_stage_tx_fees_sat, CommitmentTransaction,
- COMMITMENT_TX_WEIGHT_PER_HTLC,
+ self, commit_tx_fee_sat, commitment_tx_base_weight, second_stage_tx_fees_sat,
+ shared_anchor_script_pubkey, CommitmentTransaction, COMMITMENT_TX_WEIGHT_PER_HTLC,
+ TRUC_CHILD_MAX_WEIGHT,
};
use crate::ln::channel::{
- get_holder_selected_channel_reserve_satoshis, Channel, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE,
- MIN_AFFORDABLE_HTLC_COUNT, MIN_CHAN_DUST_LIMIT_SATOSHIS,
+ get_holder_selected_channel_reserve_satoshis, Channel, ANCHOR_OUTPUT_VALUE_SATOSHI,
+ FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT,
+ MIN_CHAN_DUST_LIMIT_SATOSHIS,
};
-use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder};
+use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder, TrustedChannelFeatures};
use crate::ln::functional_test_utils::*;
use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
use crate::ln::onion_utils::{self, AttributionData};
use crate::ln::outbound_payment::RecipientOnionFields;
+use crate::ln::types::ChannelId;
use crate::routing::router::PaymentParameters;
use crate::sign::ecdsa::EcdsaChannelSigner;
use crate::sign::tx_builder::{SpecTxBuilder, TxBuilder};
use crate::sign::ChannelSigner;
use crate::types::features::ChannelTypeFeatures;
-use crate::types::payment::PaymentPreimage;
+use crate::types::payment::{PaymentHash, PaymentPreimage};
use crate::util::config::UserConfig;
use crate::util::errors::APIError;
use lightning_macros::xtest;
use bitcoin::secp256k1::{Secp256k1, SecretKey};
+use bitcoin::{Amount, Transaction};
fn do_test_counterparty_no_reserve(send_from_initiator: bool) {
// A peer providing a channel_reserve_satoshis of 0 (or less than our dust limit) is insecure,
@@ -2423,3 +2427,1053 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) {
check_added_monitors(&nodes[1], 3);
}
}
+
+#[xtest(feature = "_externalize_tests")]
+fn test_create_channel_to_trusted_peer_0reserve() {
+ let mut config = test_default_channel_config();
+
+ // Legacy channels
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone());
+ assert_eq!(channel_type, ChannelTypeFeatures::only_static_remote_key());
+
+ // Anchor channels
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone());
+ assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies());
+
+ // 0FC channels
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+ let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone());
+ assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_fee_commitments());
+}
+
+fn do_test_create_channel_to_trusted_peer_0reserve(mut config: UserConfig) -> ChannelTypeFeatures {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_a_id = nodes[0].node.get_our_node_id();
+ let node_b_id = nodes[1].node.get_our_node_id();
+
+ let channel_value_sat = 100_000;
+
+ let temp_channel_id = nodes[0]
+ .node
+ .create_channel_to_trusted_peer_0reserve(node_b_id, channel_value_sat, 0, 42, None, None)
+ .unwrap();
+ let mut open_channel_message =
+ get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
+ handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel_message);
+ let mut accept_channel_message =
+ get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
+ nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_message);
+ let funding_tx = sign_funding_transaction(&nodes[0], &nodes[1], 100_000, temp_channel_id);
+ let funding_msgs =
+ create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &funding_tx);
+ create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &funding_msgs.0);
+
+ let details = &nodes[0].node.list_channels()[0];
+ let reserve_sat = details.unspendable_punishment_reserve.unwrap();
+ assert_ne!(reserve_sat, 0);
+ let channel_type = details.channel_type.clone().unwrap();
+ let feerate_per_kw = details.feerate_sat_per_1000_weight.unwrap();
+ let anchors_sat =
+ if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() {
+ 2 * 330
+ } else {
+ 0
+ };
+ let spike_multiple = if channel_type == ChannelTypeFeatures::only_static_remote_key() {
+ FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32
+ } else {
+ 1
+ };
+ let spiked_feerate = spike_multiple * feerate_per_kw;
+ let reserved_commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(
+ spiked_feerate,
+ 2, // We reserve space for two HTLCs, the next outbound non-dust HTLC, and the fee spike buffer HTLC
+ &channel_type,
+ );
+
+ let max_outbound_htlc_sat =
+ channel_value_sat - anchors_sat - reserved_commit_tx_fee_sat - reserve_sat;
+ assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000);
+ send_payment(&nodes[0], &[&nodes[1]], max_outbound_htlc_sat * 1000);
+
+ let details = &nodes[1].node.list_channels()[0];
+ assert_eq!(details.unspendable_punishment_reserve.unwrap(), 0);
+ // Assert that the fundee can send back the full amount they just received, since they have 0-reserve.
+ assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000);
+ send_payment(&nodes[1], &[&nodes[0]], max_outbound_htlc_sat * 1000);
+
+ channel_type
+}
+
+#[xtest(feature = "_externalize_tests")]
+fn test_accept_inbound_channel_from_trusted_peer_0reserve() {
+ let mut config = test_default_channel_config();
+
+ // Legacy channels
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone());
+ assert_eq!(channel_type, ChannelTypeFeatures::only_static_remote_key());
+
+ // Anchor channels
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+ let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone());
+ assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies());
+
+ // 0FC channels
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+ let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone());
+ assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_fee_commitments());
+}
+
+fn do_test_accept_inbound_channel_from_trusted_peer_0reserve(
+ mut config: UserConfig,
+) -> ChannelTypeFeatures {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_a_id = nodes[0].node.get_our_node_id();
+ let node_b_id = nodes[1].node.get_our_node_id();
+
+ let channel_value_sat = 100_000;
+
+ nodes[0].node.create_channel(node_b_id, channel_value_sat, 0, 42, None, None).unwrap();
+
+ let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
+ nodes[1].node.handle_open_channel(node_a_id, &open_channel);
+ let events = nodes[1].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events[0] {
+ Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => {
+ nodes[1]
+ .node
+ .accept_inbound_channel_from_trusted_peer(
+ &chan_id,
+ &node_a_id,
+ 0,
+ TrustedChannelFeatures::ZeroReserve,
+ None,
+ )
+ .unwrap();
+ },
+ _ => panic!("Unexpected event"),
+ };
+
+ let mut accept_channel_msg =
+ get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
+ nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg);
+
+ let (chan_id, tx, _) = create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42);
+
+ nodes[0].node.funding_transaction_generated(chan_id, node_b_id, tx.clone()).unwrap();
+ nodes[1].node.handle_funding_created(
+ node_a_id,
+ &get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id),
+ );
+ check_added_monitors(&nodes[1], 1);
+ expect_channel_pending_event(&nodes[1], &node_a_id);
+
+ nodes[0].node.handle_funding_signed(
+ node_b_id,
+ &get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id),
+ );
+ check_added_monitors(&nodes[0], 1);
+ expect_channel_pending_event(&nodes[0], &node_b_id);
+
+ let (channel_ready, _channel_id) =
+ create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &tx);
+ let (announcement, as_update, bs_update) =
+ create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready);
+ update_nodes_with_chan_announce(&nodes, 0, 1, &announcement, &as_update, &bs_update);
+
+ let details = &nodes[0].node.list_channels()[0];
+ assert_eq!(details.unspendable_punishment_reserve.unwrap(), 0);
+ let channel_type = details.channel_type.clone().unwrap();
+ let feerate_per_kw = details.feerate_sat_per_1000_weight.unwrap();
+ let anchors_sat =
+ if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() {
+ 2 * 330
+ } else {
+ 0
+ };
+ let spike_multiple = if channel_type == ChannelTypeFeatures::only_static_remote_key() {
+ FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32
+ } else {
+ 1
+ };
+ let spiked_feerate = spike_multiple * feerate_per_kw;
+ let reserved_commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(
+ spiked_feerate,
+ 2, // We reserve space for two HTLCs, the next outbound non-dust HTLC, and the fee spike buffer HTLC
+ &channel_type,
+ );
+
+ let max_outbound_htlc_sat = channel_value_sat - reserved_commit_tx_fee_sat - anchors_sat;
+ assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000);
+ send_payment(&nodes[0], &[&nodes[1]], max_outbound_htlc_sat * 1000);
+
+ let details = &nodes[1].node.list_channels()[0];
+ let reserve_sat = details.unspendable_punishment_reserve.unwrap();
+ assert_ne!(reserve_sat, 0);
+ let max_outbound_htlc_sat = max_outbound_htlc_sat - reserve_sat;
+ assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000);
+ send_payment(&nodes[1], &[&nodes[0]], max_outbound_htlc_sat * 1000);
+
+ channel_type
+}
+
+enum LegacyChannelsNoOutputs {
+ PaymentSucceeds,
+ FailsReceiverUpdateAddHTLC,
+ FailsReceiverCanAcceptHTLCA,
+ FailsReceiverCanAcceptHTLCB,
+}
+
+#[xtest(feature = "_externalize_tests")]
+fn test_0reserve_no_outputs() {
+ do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::PaymentSucceeds);
+ do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCA);
+ do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCB);
+ do_test_0reserve_no_outputs_legacy(LegacyChannelsNoOutputs::FailsReceiverUpdateAddHTLC);
+
+ do_test_0reserve_no_outputs_keyed_anchors(true);
+ do_test_0reserve_no_outputs_keyed_anchors(false);
+
+ do_test_0reserve_no_outputs_p2a_anchor();
+}
+
+fn setup_0reserve_no_outputs_channels<'a, 'b, 'c, 'd>(
+ nodes: &'a Vec<Node<'b, 'c, 'd>>, channel_value_sat: u64, dust_limit_satoshis: u64,
+) -> (ChannelId, Transaction) {
+ let node_a_id = nodes[0].node.get_our_node_id();
+ let node_b_id = nodes[1].node.get_our_node_id();
+
+ // Create a channel with an identical, high dust limit and zero-reserve on both sides to make our lives easier
+
+ nodes[0]
+ .node
+ .create_channel_to_trusted_peer_0reserve(node_b_id, channel_value_sat, 0, 42, None, None)
+ .unwrap();
+
+ let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
+ open_channel.common_fields.dust_limit_satoshis = dust_limit_satoshis;
+ nodes[1].node.handle_open_channel(node_a_id, &open_channel);
+ let events = nodes[1].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events[0] {
+ Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => {
+ nodes[1]
+ .node
+ .accept_inbound_channel_from_trusted_peer(
+ &chan_id,
+ &node_a_id,
+ 0,
+ TrustedChannelFeatures::ZeroReserve,
+ None,
+ )
+ .unwrap();
+ },
+ _ => panic!("Unexpected event"),
+ };
+
+ let mut accept_channel_msg =
+ get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
+ accept_channel_msg.common_fields.dust_limit_satoshis = dust_limit_satoshis;
+ nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg);
+
+ let (chan_id, tx, _) = create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42);
+
+ nodes[0].node.funding_transaction_generated(chan_id, node_b_id, tx.clone()).unwrap();
+ nodes[1].node.handle_funding_created(
+ node_a_id,
+ &get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id),
+ );
+ check_added_monitors(&nodes[1], 1);
+ expect_channel_pending_event(&nodes[1], &node_a_id);
+
+ nodes[0].node.handle_funding_signed(
+ node_b_id,
+ &get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id),
+ );
+ check_added_monitors(&nodes[0], 1);
+ expect_channel_pending_event(&nodes[0], &node_b_id);
+
+ assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
+ assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx);
+ nodes[0].tx_broadcaster.clear();
+
+ let (channel_ready, channel_id) =
+ create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &tx);
+ let (announcement, as_update, bs_update) =
+ create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready);
+ update_nodes_with_chan_announce(nodes, 0, 1, &announcement, &as_update, &bs_update);
+
+ {
+ let mut per_peer_lock;
+ let mut peer_state_lock;
+ let channel =
+ get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id);
+ if let Some(mut chan) = channel.as_funded_mut() {
+ chan.context.holder_dust_limit_satoshis = dust_limit_satoshis;
+ } else {
+ panic!("Unexpected Channel phase");
+ }
+ }
+
+ {
+ let mut per_peer_lock;
+ let mut peer_state_lock;
+ let channel =
+ get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id);
+ if let Some(mut chan) = channel.as_funded_mut() {
+ chan.context.holder_dust_limit_satoshis = dust_limit_satoshis;
+ } else {
+ panic!("Unexpected Channel phase");
+ }
+ }
+
+ (channel_id, tx)
+}
+
+fn do_test_0reserve_no_outputs_legacy(no_outputs_case: LegacyChannelsNoOutputs) {
+ let mut config = test_default_channel_config();
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false;
+
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
+
+ let channel_type = ChannelTypeFeatures::only_static_remote_key();
+
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_a_id = nodes[0].node.get_our_node_id();
+ let _node_b_id = nodes[1].node.get_our_node_id();
+
+ let feerate_per_kw = 253;
+ let spike_multiple = FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32;
+ let dust_limit_satoshis: u64 = 546;
+ let channel_value_sat = 1000;
+
+ let (channel_id, _funding_tx) =
+ setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis);
+ assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type);
+
+ // Sending the biggest dust HTLC possible trims our balance output!
+ let (timeout_tx_fee_sat, success_tx_fee_sat) =
+ second_stage_tx_fees_sat(&channel_type, spike_multiple * feerate_per_kw);
+ let max_dust_htlc_sat = dust_limit_satoshis + success_tx_fee_sat - 1;
+ assert!(
+ channel_value_sat
+ .saturating_sub(commit_tx_fee_sat(feerate_per_kw, 0, &channel_type))
+ .saturating_sub(max_dust_htlc_sat)
+ < dust_limit_satoshis
+ );
+
+ // We can't afford the fee for an additional non-dust HTLC + the fee spike HTLC, so we can only send
+ // dust HTLCs...
+ let min_local_nondust_htlc_sat = dust_limit_satoshis + timeout_tx_fee_sat;
+ assert!(
+ channel_value_sat - commit_tx_fee_sat(spike_multiple * feerate_per_kw, 2, &channel_type)
+ < min_local_nondust_htlc_sat
+ );
+
+ // We cannot trim our own balance output, otherwise we'd have no outputs on the commitment. We must
+ // also reserve enough fees to pay for an incoming non-dust HTLC, aka the fee spike buffer HTLC.
+ let min_value_sat = core::cmp::max(
+ commit_tx_fee_sat(spike_multiple * feerate_per_kw, 0, &channel_type) + dust_limit_satoshis,
+ commit_tx_fee_sat(spike_multiple * feerate_per_kw, 1, &channel_type),
+ );
+ // At this point the tighter requirement is "must have an output"
+ assert!(
+ commit_tx_fee_sat(spike_multiple * feerate_per_kw, 0, &channel_type) + dust_limit_satoshis
+ > commit_tx_fee_sat(spike_multiple * feerate_per_kw, 1, &channel_type)
+ );
+ // But say at 9sat/vb with default dust limit,
+ // the tighter requirement is actually "must have funds for an inbound HTLC" !
+ assert!(
+ commit_tx_fee_sat(9 * 250, 0, &channel_type) + 354
+ < commit_tx_fee_sat(9 * 250, 1, &channel_type)
+ );
+ let sender_amount_msat = (channel_value_sat - min_value_sat) * 1000;
+ let details_0 = &nodes[0].node.list_channels()[0];
+ assert_eq!(details_0.next_outbound_htlc_minimum_msat, 1000);
+ assert_eq!(details_0.next_outbound_htlc_limit_msat, sender_amount_msat);
+ assert!(details_0.next_outbound_htlc_limit_msat > details_0.next_outbound_htlc_minimum_msat);
+
+ let (sender_amount_msat, receiver_amount_msat) = match no_outputs_case {
+ LegacyChannelsNoOutputs::PaymentSucceeds => (sender_amount_msat, sender_amount_msat),
+ LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCA => {
+ // A dust HTLC with 1msat added to it will break counterparty `can_accept_incoming_htlc`
+ // validation, as this dust HTLC would push the holder's balance output below the
+ // dust limit at the spike multiple feerate.
+ (sender_amount_msat, sender_amount_msat + 1)
+ },
+ LegacyChannelsNoOutputs::FailsReceiverCanAcceptHTLCB => {
+ // In `validate_update_add_htlc`, we check that there is still some output present on
+ // the commitment given the *current* set of HTLCs, and the *current* feerate. So this
+ // HTLC will pass at `validate_update_add_htlc`, but will fail in
+ // `can_accept_incoming_htlc` due to failed fee spike buffer checks.
+ let receiver_amount_msat = (channel_value_sat
+ - commit_tx_fee_sat(feerate_per_kw, 0, &channel_type)
+ - dust_limit_satoshis)
+ * 1000;
+ (sender_amount_msat, receiver_amount_msat)
+ },
+ LegacyChannelsNoOutputs::FailsReceiverUpdateAddHTLC => {
+ // Same value as above, just add 1msat, and this fails at `validate_update_add_htlc`
+ let receiver_amount_msat = (channel_value_sat
+ - commit_tx_fee_sat(feerate_per_kw, 0, &channel_type)
+ - dust_limit_satoshis)
+ * 1000;
+ (sender_amount_msat, receiver_amount_msat + 1)
+ },
+ };
+
+ if let LegacyChannelsNoOutputs::PaymentSucceeds = no_outputs_case {
+ send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat);
+ // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back!
+ // Node 0 should *always* have the funds to cover the fee of a single non-dust HTLC from node 1.
+ assert_eq!(
+ nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat,
+ sender_amount_msat
+ );
+ send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat);
+ } else {
+ let (route, payment_hash, _, payment_secret) =
+ get_route_and_payment_hash!(nodes[0], nodes[1], sender_amount_msat);
+ let secp_ctx = Secp256k1::new();
+ let session_priv = SecretKey::from_slice(&[42; 32]).unwrap();
+ let cur_height = nodes[0].node.best_block.read().unwrap().height + 1;
+ let onion_keys =
+ onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv);
+ let recipient_onion_fields =
+ RecipientOnionFields::secret_only(payment_secret, sender_amount_msat);
+ let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads(
+ &route.paths[0],
+ &recipient_onion_fields,
+ cur_height,
+ &None,
+ None,
+ None,
+ )
+ .unwrap();
+ assert_eq!(htlc_msat, sender_amount_msat);
+ let onion_packet =
+ onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash)
+ .unwrap();
+ let msg = msgs::UpdateAddHTLC {
+ channel_id,
+ htlc_id: 0,
+ amount_msat: receiver_amount_msat,
+ payment_hash,
+ cltv_expiry: htlc_cltv,
+ onion_routing_packet: onion_packet,
+ skimmed_fee_msat: None,
+ blinding_point: None,
+ hold_htlc: None,
+ accountable: None,
+ };
+
+ nodes[1].node.handle_update_add_htlc(node_a_id, &msg);
+
+ if let LegacyChannelsNoOutputs::FailsReceiverUpdateAddHTLC = no_outputs_case {
+ nodes[1].logger.assert_log_contains(
+ "lightning::ln::channelmanager",
+ "Remote HTLC add would overdraw remaining funds",
+ 3,
+ );
+ assert_eq!(nodes[1].node.list_channels().len(), 0);
+ let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap();
+ assert_eq!(err_msg.data, "Remote HTLC add would overdraw remaining funds");
+ let reason = ClosureReason::ProcessingError {
+ err: "Remote HTLC add would overdraw remaining funds".to_string(),
+ };
+ check_added_monitors(&nodes[1], 1);
+ check_closed_event(&nodes[1], 1, reason, &[node_a_id], channel_value_sat);
+
+ return;
+ }
+
+ manually_trigger_update_fail_htlc(
+ &nodes,
+ channel_id,
+ channel_value_sat,
+ dust_limit_satoshis,
+ receiver_amount_msat,
+ htlc_cltv,
+ payment_hash,
+ );
+ }
+}
+
+fn manually_trigger_update_fail_htlc<'a, 'b, 'c, 'd>(
+ nodes: &'a Vec<Node<'b, 'c, 'd>>, channel_id: ChannelId, channel_value_sat: u64,
+ dust_limit_satoshis: u64, receiver_amount_msat: u64, htlc_cltv: u32, payment_hash: PaymentHash,
+) {
+ let node_a_id = nodes[0].node.get_our_node_id();
+ let node_b_id = nodes[1].node.get_our_node_id();
+ let secp_ctx = Secp256k1::new();
+
+ // Now manually create the commitment_signed message corresponding to the update_add
+ // nodes[0] just sent. In the code for construction of this message, "local" refers
+ // to the sender of the message, and "remote" refers to the receiver.
+
+ let feerate_per_kw = get_feerate!(nodes[0], nodes[1], channel_id);
+
+ const INITIAL_COMMITMENT_NUMBER: u64 = (1 << 48) - 1;
+
+ let (local_secret, next_local_point) = {
+ let per_peer_state = nodes[0].node.per_peer_state.read().unwrap();
+ let chan_lock = per_peer_state.get(&node_b_id).unwrap().lock().unwrap();
+ let local_chan =
+ chan_lock.channel_by_id.get(&channel_id).and_then(Channel::as_funded).unwrap();
+ let chan_signer = local_chan.get_signer();
+ // Make the signer believe we validated another commitment, so we can release the secret
+ chan_signer.get_enforcement_state().last_holder_commitment -= 1;
+
+ (
+ chan_signer.release_commitment_secret(INITIAL_COMMITMENT_NUMBER).unwrap(),
+ chan_signer.get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 2, &secp_ctx).unwrap(),
+ )
+ };
+ let remote_point = {
+ let per_peer_lock;
+ let mut peer_state_lock;
+
+ let channel =
+ get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id);
+ let chan_signer = channel.as_funded().unwrap().get_signer();
+ chan_signer.get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 1, &secp_ctx).unwrap()
+ };
+
+ // Build the remote commitment transaction so we can sign it, and then later use the
+ // signature for the commitment_signed message.
+ let accepted_htlc_info = chan_utils::HTLCOutputInCommitment {
+ offered: false,
+ amount_msat: receiver_amount_msat,
+ cltv_expiry: htlc_cltv,
+ payment_hash,
+ transaction_output_index: Some(1),
+ };
+
+ let local_chan_balance_msat = channel_value_sat * 1000;
+ let commitment_number = INITIAL_COMMITMENT_NUMBER - 1;
+
+ let res = {
+ let per_peer_lock;
+ let mut peer_state_lock;
+
+ let channel =
+ get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id);
+ let chan_signer = channel.as_funded().unwrap().get_signer();
+
+ let (commitment_tx, _stats) = SpecTxBuilder {}.build_commitment_transaction(
+ false,
+ commitment_number,
+ &remote_point,
+ &channel.funding().channel_transaction_parameters,
+ &secp_ctx,
+ local_chan_balance_msat,
+ vec![accepted_htlc_info],
+ feerate_per_kw,
+ dust_limit_satoshis,
+ &nodes[0].logger,
+ );
+ let params = &channel.funding().channel_transaction_parameters;
+ chan_signer
+ .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx)
+ .unwrap()
+ };
+
+ let commit_signed_msg = msgs::CommitmentSigned {
+ channel_id,
+ signature: res.0,
+ htlc_signatures: res.1,
+ funding_txid: None,
+ };
+
+ // Send the commitment_signed message to the nodes[1].
+ nodes[1].node.handle_commitment_signed(node_a_id, &commit_signed_msg);
+ let _ = nodes[1].node.get_and_clear_pending_msg_events();
+
+ // Send the RAA to nodes[1].
+ let raa_msg = msgs::RevokeAndACK {
+ channel_id,
+ per_commitment_secret: local_secret,
+ next_per_commitment_point: next_local_point,
+ release_htlc_message_paths: Vec::new(),
+ };
+ nodes[1].node.handle_revoke_and_ack(node_a_id, &raa_msg);
+ expect_and_process_pending_htlcs(&nodes[1], false);
+
+ expect_htlc_handling_failed_destinations!(
+ nodes[1].node.get_and_clear_pending_events(),
+ &[HTLCHandlingFailureType::Receive { payment_hash }]
+ );
+
+ let events = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+
+ // Make sure the HTLC failed in the way we expect.
+ match events[0] {
+ MessageSendEvent::UpdateHTLCs {
+ updates: msgs::CommitmentUpdate { ref update_fail_htlcs, .. },
+ ..
+ } => {
+ assert_eq!(update_fail_htlcs.len(), 1);
+ update_fail_htlcs[0].clone()
+ },
+ _ => panic!("Unexpected event"),
+ };
+ nodes[1].logger.assert_log(
+ "lightning::ln::channel",
+ "Attempting to fail HTLC due to balance exhausted on remote commitment".to_string(),
+ 1,
+ );
+
+ check_added_monitors(&nodes[1], 3);
+}
+
+fn do_test_0reserve_no_outputs_keyed_anchors(payment_success: bool) {
+ let mut config = test_default_channel_config();
+
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
+
+ let channel_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies();
+
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_a_id = nodes[0].node.get_our_node_id();
+ let _node_b_id = nodes[1].node.get_our_node_id();
+
+ let feerate_per_kw = 253;
+ let anchors_sat = 2 * ANCHOR_OUTPUT_VALUE_SATOSHI;
+ let dust_limit_satoshis: u64 = 546;
+ let channel_value_sat = {
+ // min opener balance is the fee for 4 HTLCs, the anchors, and the dust limit
+ let min_channel_size =
+ commit_tx_fee_sat(feerate_per_kw, MIN_AFFORDABLE_HTLC_COUNT, &channel_type)
+ + anchors_sat + dust_limit_satoshis;
+ assert!(min_channel_size > 1002);
+ min_channel_size
+ };
+
+ let (channel_id, _funding_tx) =
+ setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis);
+ assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type);
+
+ // Sending the biggest dust HTLC possible trims our balance output!
+ let max_dust_htlc_sat = dust_limit_satoshis - 1;
+ assert!(
+ channel_value_sat
+ .saturating_sub(anchors_sat)
+ .saturating_sub(commit_tx_fee_sat(feerate_per_kw, 0, &channel_type))
+ .saturating_sub(max_dust_htlc_sat)
+ < dust_limit_satoshis
+ );
+
+ // We can afford the fee for an additional non-dust HTLC plus the fee spike HTLC, so we can send
+ // non-dust HTLCs
+ let capacity_minus_max_commitment_fee_sat =
+ channel_value_sat - anchors_sat - commit_tx_fee_sat(feerate_per_kw, 2, &channel_type);
+ assert!(capacity_minus_max_commitment_fee_sat > dust_limit_satoshis);
+ // And since the biggest dust HTLC results in no outputs on the commitment,
+ // we can *only* send non-dust HTLCs
+ let details_0 = &nodes[0].node.list_channels()[0];
+ assert_eq!(details_0.next_outbound_htlc_minimum_msat, dust_limit_satoshis * 1000);
+ assert_eq!(
+ details_0.next_outbound_htlc_limit_msat,
+ capacity_minus_max_commitment_fee_sat * 1000
+ );
+
+ // Send the smallest non-dust HTLC possible, this will pass both holder and counterparty validation
+ //
+ // One msat below the non-dust HTLC value will break counterparty validation at
+ // `validate_update_add_htlc`. This is why we don't bother taking a look at the range between the
+ // failure of `can_accept_incoming_htlc` and the failure of `validate_update_add_htlc`.
+ let sender_amount_msat = dust_limit_satoshis * 1000;
+
+ let (sender_amount_msat, receiver_amount_msat) = if payment_success {
+ (sender_amount_msat, sender_amount_msat)
+ } else {
+ (sender_amount_msat, sender_amount_msat - 1)
+ };
+
+ if payment_success {
+ send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat);
+ // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back!
+ // Node 0 should *always* have the funds to cover the fee of a single non-dust HTLC from node 1.
+ assert_eq!(
+ nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat,
+ sender_amount_msat
+ );
+ send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat);
+ } else {
+ let (route, payment_hash, _, payment_secret) =
+ get_route_and_payment_hash!(nodes[0], nodes[1], sender_amount_msat);
+ let secp_ctx = Secp256k1::new();
+ let session_priv = SecretKey::from_slice(&[42; 32]).unwrap();
+ let cur_height = nodes[0].node.best_block.read().unwrap().height + 1;
+ let onion_keys =
+ onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv);
+ let recipient_onion_fields =
+ RecipientOnionFields::secret_only(payment_secret, sender_amount_msat);
+ let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads(
+ &route.paths[0],
+ &recipient_onion_fields,
+ cur_height,
+ &None,
+ None,
+ None,
+ )
+ .unwrap();
+ assert_eq!(htlc_msat, sender_amount_msat);
+ let onion_packet =
+ onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash)
+ .unwrap();
+ let msg = msgs::UpdateAddHTLC {
+ channel_id,
+ htlc_id: 0,
+ amount_msat: receiver_amount_msat,
+ payment_hash,
+ cltv_expiry: htlc_cltv,
+ onion_routing_packet: onion_packet,
+ skimmed_fee_msat: None,
+ blinding_point: None,
+ hold_htlc: None,
+ accountable: None,
+ };
+
+ nodes[1].node.handle_update_add_htlc(node_a_id, &msg);
+
+ nodes[1].logger.assert_log_contains(
+ "lightning::ln::channelmanager",
+ "Remote HTLC add would overdraw remaining funds",
+ 3,
+ );
+ assert_eq!(nodes[1].node.list_channels().len(), 0);
+ let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap();
+ assert_eq!(err_msg.data, "Remote HTLC add would overdraw remaining funds");
+ let reason = ClosureReason::ProcessingError {
+ err: "Remote HTLC add would overdraw remaining funds".to_string(),
+ };
+ check_added_monitors(&nodes[1], 1);
+ check_closed_event(&nodes[1], 1, reason, &[node_a_id], channel_value_sat);
+ }
+}
+
+fn do_test_0reserve_no_outputs_p2a_anchor() {
+ let mut config = test_default_channel_config();
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
+
+ let channel_type = ChannelTypeFeatures::anchors_zero_fee_commitments();
+
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let _node_a_id = nodes[0].node.get_our_node_id();
+ let _node_b_id = nodes[1].node.get_our_node_id();
+
+ let dust_limit_satoshis: u64 = 546;
+ let channel_value_sat = 1000;
+
+ let _channel_id =
+ setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis);
+ assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type);
+
+ // Sending the biggest dust HTLC possible trims our balance output!
+ let max_dust_htlc_sat = dust_limit_satoshis - 1;
+ assert!(channel_value_sat.saturating_sub(max_dust_htlc_sat) < dust_limit_satoshis);
+
+ // We'll always have the P2A output on the commitment, so we are free to send any size HTLC,
+ // including those that result in only a single output on the commitment, the P2A output.
+ let details_0 = &nodes[0].node.list_channels()[0];
+ assert_eq!(details_0.next_outbound_htlc_minimum_msat, 1000);
+ // 0FC + 0-reserve baby!
+ assert_eq!(details_0.next_outbound_htlc_limit_msat, channel_value_sat * 1000);
+
+ // Send the max size dust HTLC; this results in a commitment with only the P2A output present
+ let sender_amount_msat = max_dust_htlc_sat * 1000;
+
+ send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat);
+ // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back!
+ assert_eq!(nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat, sender_amount_msat);
+ send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat);
+}
+
+#[xtest(feature = "_externalize_tests")]
+pub fn test_0reserve_force_close_with_single_p2a_output() {
+ do_test_0reserve_force_close_with_single_p2a_output(false);
+ do_test_0reserve_force_close_with_single_p2a_output(true);
+}
+
+fn do_test_0reserve_force_close_with_single_p2a_output(high_feerate: bool) {
+ let mut config = test_default_channel_config();
+ config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false;
+ config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ if high_feerate {
+ let mut feerate_lock = chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap();
+ *feerate_lock = 2500;
+ }
+ if high_feerate {
+ let mut feerate_lock = chanmon_cfgs[1].fee_estimator.sat_per_kw.lock().unwrap();
+ *feerate_lock = 2500;
+ }
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
+
+ let channel_type = ChannelTypeFeatures::anchors_zero_fee_commitments();
+
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let coinbase_tx = provide_anchor_reserves(&nodes);
+
+ let _node_a_id = nodes[0].node.get_our_node_id();
+ let _node_b_id = nodes[1].node.get_our_node_id();
+
+ let dust_limit_satoshis: u64 = 546;
+ // This is the fundee 1000sat reserve + 2 min HTLCs
+ let channel_value_sat = 1002;
+
+ let (channel_id, funding_tx) =
+ setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis);
+ assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type);
+
+ // Send the smallest HTLC possible that trims our own balance output, this will be a dust HTLC
+ let htlc_sat = channel_value_sat - dust_limit_satoshis + 1;
+ assert!(htlc_sat < dust_limit_satoshis);
+ route_payment(&nodes[0], &[&nodes[1]], htlc_sat * 1000);
+
+ let commitment_tx = get_local_commitment_txn!(nodes[0], channel_id).pop().unwrap();
+ let commitment_txid = commitment_tx.compute_txid();
+
+ let message = "Channel force-closed".to_owned();
+ nodes[0]
+ .node
+ .force_close_broadcasting_latest_txn(
+ &channel_id,
+ &nodes[1].node.get_our_node_id(),
+ message.clone(),
+ )
+ .unwrap();
+ check_closed_broadcast(&nodes[0], 1, true);
+ check_added_monitors(&nodes[0], 1);
+ let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message };
+ check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], channel_value_sat);
+
+ let mut events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events.pop().unwrap() {
+ Event::BumpTransaction(bump_event) => {
+ nodes[0].bump_tx_handler.handle_event(&bump_event);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ let txns = nodes[0].tx_broadcaster.txn_broadcast();
+
+ if high_feerate {
+ assert_eq!(txns.len(), 2);
+ check_spends!(txns[1], txns[0], coinbase_tx);
+ assert!(txns[1].weight().to_wu() < TRUC_CHILD_MAX_WEIGHT);
+ assert_eq!(txns[1].input.len(), 2);
+ assert_eq!(txns[1].output.len(), 1);
+
+ assert_eq!(txns[0].compute_txid(), commitment_txid);
+ assert_eq!(txns[0].input.len(), 1);
+ assert_eq!(txns[0].output.len(), 1);
+ assert_eq!(txns[0].output[0].value, Amount::from_sat(240));
+ assert_eq!(txns[0].output[0].script_pubkey, shared_anchor_script_pubkey());
+ check_spends!(txns[0], funding_tx);
+
+ nodes[0].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!(
+ "Broadcasting anchor transaction {} to bump channel close with txid {}",
+ txns[1].compute_txid(),
+ txns[0].compute_txid()
+ ),
+ 1,
+ );
+ } else {
+ assert_eq!(txns.len(), 1);
+ assert_eq!(txns[0].compute_txid(), commitment_txid);
+ assert_eq!(txns[0].input.len(), 1);
+ assert_eq!(txns[0].output.len(), 1);
+ assert_eq!(txns[0].output[0].value, Amount::from_sat(240));
+ assert_eq!(txns[0].output[0].script_pubkey, shared_anchor_script_pubkey());
+ check_spends!(txns[0], funding_tx);
+
+ let weight = txns[0].weight();
+ let feerate = (channel_value_sat - 240) * 1000 / weight.to_wu();
+
+ nodes[0].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!(
+ "Pre-signed commitment {} already has feerate {} sat/kW above required 253 sat/kW, broadcasting.",
+ txns[0].compute_txid(),
+ feerate,
+ ),
+ 1,
+ );
+ }
+}
+
+#[xtest(feature = "_externalize_tests")]
+fn test_0reserve_zero_conf_combined() {
+ // Test that zero-reserve and zero-conf features work together: a channel that
+ // is immediately usable (no confirmations needed) and has zero reserve for the opener.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let mut config = test_default_channel_config();
+ config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_a_id = nodes[0].node.get_our_node_id();
+ let node_b_id = nodes[1].node.get_our_node_id();
+
+ let channel_value_sat = 100_000;
+
+ // Node 0 creates a channel to node 1.
+ nodes[0].node.create_channel(node_b_id, channel_value_sat, 0, 42, None, None).unwrap();
+ let open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
+
+ // Node 1 accepts with both zero-conf AND zero-reserve.
+ nodes[1].node.handle_open_channel(node_a_id, &open_channel);
+ let events = nodes[1].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events[0] {
+ Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => {
+ nodes[1]
+ .node
+ .accept_inbound_channel_from_trusted_peer(
+ &chan_id,
+ &node_a_id,
+ 0,
+ TrustedChannelFeatures::ZeroConfZeroReserve,
+ None,
+ )
+ .unwrap();
+ },
+ _ => panic!("Unexpected event"),
+ };
+
+ // Verify zero-conf: minimum_depth should be 0.
+ let accept_channel = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
+ assert_eq!(accept_channel.common_fields.minimum_depth, 0);
+ nodes[0].node.handle_accept_channel(node_b_id, &accept_channel);
+
+ // Create the funding transaction (no block confirmations needed for zero-conf).
+ let (temporary_channel_id, tx, _) =
+ create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42);
+ nodes[0]
+ .node
+ .funding_transaction_generated(temporary_channel_id, node_b_id, tx.clone())
+ .unwrap();
+ let funding_created = get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id);
+
+ // Node 1 handles funding_created and immediately sends both FundingSigned and ChannelReady.
+ nodes[1].node.handle_funding_created(node_a_id, &funding_created);
+ check_added_monitors(&nodes[1], 1);
+ let bs_signed_locked = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(bs_signed_locked.len(), 2);
+
+ let as_channel_ready;
+ match &bs_signed_locked[0] {
+ MessageSendEvent::SendFundingSigned { node_id, msg } => {
+ assert_eq!(*node_id, node_a_id);
+ nodes[0].node.handle_funding_signed(node_b_id, &msg);
+ expect_channel_pending_event(&nodes[0], &node_b_id);
+ expect_channel_pending_event(&nodes[1], &node_a_id);
+ check_added_monitors(&nodes[0], 1);
+
+ assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1);
+ assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx);
+ nodes[0].tx_broadcaster.clear();
+
+ as_channel_ready =
+ get_event_msg!(nodes[0], MessageSendEvent::SendChannelReady, node_b_id);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ match &bs_signed_locked[1] {
+ MessageSendEvent::SendChannelReady { node_id, msg } => {
+ assert_eq!(*node_id, node_a_id);
+ nodes[0].node.handle_channel_ready(node_b_id, &msg);
+ expect_channel_ready_event(&nodes[0], &node_b_id);
+ },
+ _ => panic!("Unexpected event"),
+ }
+
+ nodes[1].node.handle_channel_ready(node_a_id, &as_channel_ready);
+ expect_channel_ready_event(&nodes[1], &node_a_id);
+
+ let as_channel_update =
+ get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, node_b_id);
+ let bs_channel_update =
+ get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_a_id);
+ nodes[0].node.handle_channel_update(node_b_id, &bs_channel_update);
+ nodes[1].node.handle_channel_update(node_a_id, &as_channel_update);
+
+ // Channel should be immediately usable without any block confirmations.
+ assert_eq!(nodes[0].node.list_usable_channels().len(), 1);
+ assert_eq!(nodes[1].node.list_usable_channels().len(), 1);
+
+ // Verify zero-reserve: opener (node 0) should have 0 reserve.
+ let details_a = &nodes[0].node.list_channels()[0];
+ let node_0_reserve = details_a.unspendable_punishment_reserve.unwrap();
+ let node_0_max_htlc = details_a.next_outbound_htlc_limit_msat;
+ let channel_type = details_a.channel_type.clone().unwrap();
+ assert_eq!(node_0_reserve, 0);
+ assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies());
+ assert!(details_a.is_usable);
+ assert_eq!(details_a.confirmations.unwrap(), 0);
+ assert_eq!(
+ node_0_max_htlc,
+ (channel_value_sat - commit_tx_fee_sat(253, 2, &channel_type) - 2 * 330) * 1000
+ );
+
+ // Verify acceptor (node 1) has a non-zero reserve.
+ let details_b = &nodes[1].node.list_channels()[0];
+ assert_ne!(details_b.unspendable_punishment_reserve.unwrap(), 0);
+ assert!(details_b.is_usable);
+
+ // Send payments in both directions to verify the combined feature works end-to-end.
+ send_payment(&nodes[0], &[&nodes[1]], node_0_max_htlc);
+
+ let details_b = &nodes[1].node.list_channels()[0];
+ let node_1_reserve = details_b.unspendable_punishment_reserve.unwrap();
+ let node_1_max_htlc = details_b.next_outbound_htlc_limit_msat;
+ assert_eq!(node_1_reserve, 1000);
+ assert_eq!(node_1_max_htlc, node_0_max_htlc - node_1_reserve * 1000);
+ send_payment(&nodes[1], &[&nodes[0]], node_1_max_htlc);
+}
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.