Skip to content

Commit 25f4a54

Browse files
committed
Explicitly support counterparty setting 0 channel reserve
A peer providing a channel_reserve_satoshis of 0 (or less than our dust limit) is insecure, but only for them. Because some LSPs do it with some level of trust of the clients (for a substantial UX improvement), we explicitly allow it. Because its unlikely to happen often in normal testing, we test it explicitly here.
1 parent f69311c commit 25f4a54

File tree

4 files changed

+104
-28
lines changed

4 files changed

+104
-28
lines changed

lightning/src/ln/channel.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,9 @@ pub(super) struct MonitorRestoreUpdates {
387387
/// the channel. Sadly, there isn't really a good number for this - if we expect to have no new
388388
/// HTLCs for days we may need this to suffice for feerate increases across days, but that may
389389
/// leave the channel less usable as we hold a bigger reserve.
390-
#[cfg(fuzzing)]
390+
#[cfg(any(fuzzing, test))]
391391
pub const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2;
392-
#[cfg(not(fuzzing))]
392+
#[cfg(not(any(fuzzing, test)))]
393393
const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2;
394394

395395
/// If we fail to see a funding transaction confirmed on-chain within this many blocks after the
@@ -516,19 +516,30 @@ pub(super) struct Channel<Signer: Sign> {
516516
channel_creation_height: u32,
517517

518518
counterparty_dust_limit_satoshis: u64,
519+
519520
#[cfg(test)]
520521
pub(super) holder_dust_limit_satoshis: u64,
521522
#[cfg(not(test))]
522523
holder_dust_limit_satoshis: u64,
524+
523525
#[cfg(test)]
524526
pub(super) counterparty_max_htlc_value_in_flight_msat: u64,
525527
#[cfg(not(test))]
526528
counterparty_max_htlc_value_in_flight_msat: u64,
529+
530+
#[cfg(test)]
531+
pub(super) holder_max_htlc_value_in_flight_msat: u64,
532+
#[cfg(not(test))]
527533
holder_max_htlc_value_in_flight_msat: u64,
528534

529535
/// minimum channel reserve for self to maintain - set by them.
530536
counterparty_selected_channel_reserve_satoshis: Option<u64>,
537+
538+
#[cfg(test)]
539+
pub(super) holder_selected_channel_reserve_satoshis: u64,
540+
#[cfg(not(test))]
531541
holder_selected_channel_reserve_satoshis: u64,
542+
532543
counterparty_htlc_minimum_msat: u64,
533544
holder_htlc_minimum_msat: u64,
534545
#[cfg(test)]
@@ -868,12 +879,13 @@ impl<Signer: Sign> Channel<Signer> {
868879

869880
/// Creates a new channel from a remote sides' request for one.
870881
/// Assumes chain_hash has already been checked and corresponds with what we expect!
871-
pub fn new_from_req<K: Deref, F: Deref>(
882+
pub fn new_from_req<K: Deref, F: Deref, L: Deref>(
872883
fee_estimator: &F, keys_provider: &K, counterparty_node_id: PublicKey, their_features: &InitFeatures,
873-
msg: &msgs::OpenChannel, user_id: u64, config: &UserConfig, current_chain_height: u32
884+
msg: &msgs::OpenChannel, user_id: u64, config: &UserConfig, current_chain_height: u32, logger: &L
874885
) -> Result<Channel<Signer>, ChannelError>
875886
where K::Target: KeysInterface<Signer = Signer>,
876-
F::Target: FeeEstimator
887+
F::Target: FeeEstimator,
888+
L::Target: Logger,
877889
{
878890
// First check the channel type is known, failing before we do anything else if we don't
879891
// support this channel type.
@@ -921,9 +933,6 @@ impl<Signer: Sign> Channel<Signer> {
921933
if msg.dust_limit_satoshis > msg.funding_satoshis {
922934
return Err(ChannelError::Close(format!("dust_limit_satoshis {} was larger than funding_satoshis {}. Peer never wants payout outputs?", msg.dust_limit_satoshis, msg.funding_satoshis)));
923935
}
924-
if msg.dust_limit_satoshis > msg.channel_reserve_satoshis {
925-
return Err(ChannelError::Close(format!("Bogus; channel reserve ({}) is less than dust limit ({})", msg.channel_reserve_satoshis, msg.dust_limit_satoshis)));
926-
}
927936
let full_channel_value_msat = (msg.funding_satoshis - msg.channel_reserve_satoshis) * 1000;
928937
if msg.htlc_minimum_msat >= full_channel_value_msat {
929938
return Err(ChannelError::Close(format!("Minimum htlc value ({}) was larger than full channel value ({})", msg.htlc_minimum_msat, full_channel_value_msat)));
@@ -980,7 +989,8 @@ impl<Signer: Sign> Channel<Signer> {
980989
return Err(ChannelError::Close(format!("Suitable channel reserve not found. remote_channel_reserve was ({}). dust_limit_satoshis is ({}).", holder_selected_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS)));
981990
}
982991
if msg.channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
983-
return Err(ChannelError::Close(format!("channel_reserve_satoshis ({}) is smaller than our dust limit ({})", msg.channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS)));
992+
log_debug!(logger, "channel_reserve_satoshis ({}) is smaller than our dust limit ({}). We can broadcast stale states without any risk, implying this channel is very insecure for our counterparty.",
993+
msg.channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS);
984994
}
985995
if holder_selected_channel_reserve_satoshis < msg.dust_limit_satoshis {
986996
return Err(ChannelError::Close(format!("Dust limit ({}) too high for the channel reserve we require the remote to keep ({})", msg.dust_limit_satoshis, holder_selected_channel_reserve_satoshis)));
@@ -1712,9 +1722,6 @@ impl<Signer: Sign> Channel<Signer> {
17121722
if msg.channel_reserve_satoshis > self.channel_value_satoshis {
17131723
return Err(ChannelError::Close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", msg.channel_reserve_satoshis, self.channel_value_satoshis)));
17141724
}
1715-
if msg.channel_reserve_satoshis < self.holder_dust_limit_satoshis {
1716-
return Err(ChannelError::Close(format!("Peer never wants payout outputs? channel_reserve_satoshis was ({}). dust_limit is ({})", msg.channel_reserve_satoshis, self.holder_dust_limit_satoshis)));
1717-
}
17181725
if msg.dust_limit_satoshis > self.holder_selected_channel_reserve_satoshis {
17191726
return Err(ChannelError::Close(format!("Dust limit ({}) is bigger than our channel reserve ({})", msg.dust_limit_satoshis, self.holder_selected_channel_reserve_satoshis)));
17201727
}
@@ -5912,6 +5919,7 @@ mod tests {
59125919
let seed = [42; 32];
59135920
let network = Network::Testnet;
59145921
let keys_provider = test_utils::TestKeysInterface::new(&seed, network);
5922+
let logger = test_utils::TestLogger::new();
59155923

59165924
// Go through the flow of opening a channel between two nodes, making sure
59175925
// they have different dust limits.
@@ -5925,7 +5933,7 @@ mod tests {
59255933
// Make sure A's dust limit is as we expect.
59265934
let open_channel_msg = node_a_chan.get_open_channel(genesis_block(network).header.block_hash());
59275935
let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap());
5928-
let node_b_chan = Channel::<EnforcingSigner>::new_from_req(&&feeest, &&keys_provider, node_b_node_id, &InitFeatures::known(), &open_channel_msg, 7, &config, 0).unwrap();
5936+
let node_b_chan = Channel::<EnforcingSigner>::new_from_req(&&feeest, &&keys_provider, node_b_node_id, &InitFeatures::known(), &open_channel_msg, 7, &config, 0, &&logger).unwrap();
59295937

59305938
// Node B --> Node A: accept channel, explicitly setting B's dust limit.
59315939
let mut accept_channel_msg = node_b_chan.get_accept_channel();
@@ -6043,7 +6051,7 @@ mod tests {
60436051
// Create Node B's channel by receiving Node A's open_channel message
60446052
let open_channel_msg = node_a_chan.get_open_channel(chain_hash);
60456053
let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap());
6046-
let mut node_b_chan = Channel::<EnforcingSigner>::new_from_req(&&feeest, &&keys_provider, node_b_node_id, &InitFeatures::known(), &open_channel_msg, 7, &config, 0).unwrap();
6054+
let mut node_b_chan = Channel::<EnforcingSigner>::new_from_req(&&feeest, &&keys_provider, node_b_node_id, &InitFeatures::known(), &open_channel_msg, 7, &config, 0, &&logger).unwrap();
60476055

60486056
// Node B --> Node A: accept channel
60496057
let accept_channel_msg = node_b_chan.get_accept_channel();

lightning/src/ln/channelmanager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3683,7 +3683,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
36833683
}
36843684

36853685
let channel = Channel::new_from_req(&self.fee_estimator, &self.keys_manager, counterparty_node_id.clone(),
3686-
&their_features, msg, 0, &self.default_configuration, self.best_block.read().unwrap().height())
3686+
&their_features, msg, 0, &self.default_configuration, self.best_block.read().unwrap().height(), &self.logger)
36873687
.map_err(|e| MsgHandleErrInternal::from_chan_no_close(e, msg.temporary_channel_id))?;
36883688
let mut channel_state_lock = self.channel_state.lock().unwrap();
36893689
let channel_state = &mut *channel_state_lock;

lightning/src/ln/functional_test_utils.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -526,19 +526,16 @@ pub fn create_funding_transaction<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, expected_
526526
_ => panic!("Unexpected event"),
527527
}
528528
}
529-
530-
pub fn create_chan_between_nodes_with_value_init<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_value: u64, push_msat: u64, a_flags: InitFeatures, b_flags: InitFeatures) -> Transaction {
531-
let create_chan_id = node_a.node.create_channel(node_b.node.get_our_node_id(), channel_value, push_msat, 42, None).unwrap();
532-
node_b.node.handle_open_channel(&node_a.node.get_our_node_id(), a_flags, &get_event_msg!(node_a, MessageSendEvent::SendOpenChannel, node_b.node.get_our_node_id()));
533-
node_a.node.handle_accept_channel(&node_b.node.get_our_node_id(), b_flags, &get_event_msg!(node_b, MessageSendEvent::SendAcceptChannel, node_a.node.get_our_node_id()));
534-
529+
pub fn sign_funding_transaction<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_value: u64, expected_temporary_channel_id: [u8; 32]) -> Transaction {
535530
let (temporary_channel_id, tx, funding_output) = create_funding_transaction(node_a, channel_value, 42);
536-
assert_eq!(temporary_channel_id, create_chan_id);
531+
assert_eq!(temporary_channel_id, expected_temporary_channel_id);
537532

538533
node_a.node.funding_transaction_generated(&temporary_channel_id, tx.clone()).unwrap();
539534
check_added_monitors!(node_a, 0);
540535

541-
node_b.node.handle_funding_created(&node_a.node.get_our_node_id(), &get_event_msg!(node_a, MessageSendEvent::SendFundingCreated, node_b.node.get_our_node_id()));
536+
let funding_created_msg = get_event_msg!(node_a, MessageSendEvent::SendFundingCreated, node_b.node.get_our_node_id());
537+
assert_eq!(funding_created_msg.temporary_channel_id, expected_temporary_channel_id);
538+
node_b.node.handle_funding_created(&node_a.node.get_our_node_id(), &funding_created_msg);
542539
{
543540
let mut added_monitors = node_b.chain_monitor.added_monitors.lock().unwrap();
544541
assert_eq!(added_monitors.len(), 1);
@@ -564,6 +561,18 @@ pub fn create_chan_between_nodes_with_value_init<'a, 'b, 'c>(node_a: &Node<'a, '
564561
tx
565562
}
566563

564+
pub fn create_chan_between_nodes_with_value_init<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_value: u64, push_msat: u64, a_flags: InitFeatures, b_flags: InitFeatures) -> Transaction {
565+
let create_chan_id = node_a.node.create_channel(node_b.node.get_our_node_id(), channel_value, push_msat, 42, None).unwrap();
566+
let open_channel_msg = get_event_msg!(node_a, MessageSendEvent::SendOpenChannel, node_b.node.get_our_node_id());
567+
assert_eq!(open_channel_msg.temporary_channel_id, create_chan_id);
568+
node_b.node.handle_open_channel(&node_a.node.get_our_node_id(), a_flags, &open_channel_msg);
569+
let accept_channel_msg = get_event_msg!(node_b, MessageSendEvent::SendAcceptChannel, node_a.node.get_our_node_id());
570+
assert_eq!(accept_channel_msg.temporary_channel_id, create_chan_id);
571+
node_a.node.handle_accept_channel(&node_b.node.get_our_node_id(), b_flags, &accept_channel_msg);
572+
573+
sign_funding_transaction(node_a, node_b, channel_value, create_chan_id)
574+
}
575+
567576
pub fn create_chan_between_nodes_with_value_confirm_first<'a, 'b, 'c, 'd>(node_recv: &'a Node<'b, 'c, 'c>, node_conf: &'a Node<'b, 'c, 'd>, tx: &Transaction, conf_height: u32) {
568577
confirm_transaction_at(node_conf, tx, conf_height);
569578
connect_blocks(node_conf, CHAN_CONFIRM_DEPTH - 1);

lightning/src/ln/functional_tests.rs

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use chain::channelmonitor::{ChannelMonitor, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PER
1818
use chain::transaction::OutPoint;
1919
use chain::keysinterface::BaseSign;
2020
use ln::{PaymentPreimage, PaymentSecret, PaymentHash};
21-
use ln::channel::{COMMITMENT_TX_BASE_WEIGHT, COMMITMENT_TX_WEIGHT_PER_HTLC, CONCURRENT_INBOUND_HTLC_FEE_BUFFER, MIN_AFFORDABLE_HTLC_COUNT};
21+
use ln::channel::{COMMITMENT_TX_BASE_WEIGHT, COMMITMENT_TX_WEIGHT_PER_HTLC, CONCURRENT_INBOUND_HTLC_FEE_BUFFER, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT};
2222
use ln::channelmanager::{ChannelManager, ChannelManagerReadArgs, PaymentId, RAACommitmentOrder, PaymentSendFailure, BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA};
2323
use ln::channel::{Channel, ChannelError};
2424
use ln::{chan_utils, onion_utils};
@@ -108,8 +108,6 @@ fn test_insane_channel_opens() {
108108

109109
insane_open_helper("Peer never wants payout outputs?", |mut msg| { msg.dust_limit_satoshis = msg.funding_satoshis + 1 ; msg });
110110

111-
insane_open_helper(r"Bogus; channel reserve \(\d+\) is less than dust limit \(\d+\)", |mut msg| { msg.dust_limit_satoshis = msg.channel_reserve_satoshis + 1; msg });
112-
113111
insane_open_helper(r"Minimum htlc value \(\d+\) was larger than full channel value \(\d+\)", |mut msg| { msg.htlc_minimum_msat = (msg.funding_satoshis - msg.channel_reserve_satoshis) * 1000; msg });
114112

115113
insane_open_helper("They wanted our payments to be delayed by a needlessly long period", |mut msg| { msg.to_self_delay = MAX_LOCAL_BREAKDOWN_TIMEOUT + 1; msg });
@@ -119,6 +117,67 @@ fn test_insane_channel_opens() {
119117
insane_open_helper("max_accepted_htlcs was 484. It must not be larger than 483", |mut msg| { msg.max_accepted_htlcs = 484; msg });
120118
}
121119

120+
fn do_test_counterparty_no_reserve(send_from_initiator: bool) {
121+
// A peer providing a channel_reserve_satoshis of 0 (or less than our dust limit) is insecure,
122+
// but only for them. Because some LSPs do it with some level of trust of the clients (for a
123+
// substantial UX improvement), we explicitly allow it. Because it's unlikely to happen often
124+
// in normal testing, we test it explicitly here.
125+
let chanmon_cfgs = create_chanmon_cfgs(2);
126+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
127+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
128+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
129+
130+
// Have node0 initiate a channel to node1 with aforementioned parameters
131+
let mut push_amt = 100_000_000;
132+
let feerate_per_kw = 253;
133+
push_amt -= feerate_per_kw as u64 * (COMMITMENT_TX_BASE_WEIGHT + 4 * COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000 * 1000;
134+
push_amt -= Channel::<EnforcingSigner>::get_holder_selected_channel_reserve_satoshis(100_000) * 1000;
135+
136+
let temp_channel_id = nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, if send_from_initiator { 0 } else { push_amt }, 42, None).unwrap();
137+
let mut open_channel_message = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
138+
if !send_from_initiator {
139+
open_channel_message.channel_reserve_satoshis = 0;
140+
open_channel_message.max_htlc_value_in_flight_msat = 100_000_000;
141+
}
142+
nodes[1].node.handle_open_channel(&nodes[0].node.get_our_node_id(), InitFeatures::known(), &open_channel_message);
143+
144+
// Extract the channel accept message from node1 to node0
145+
let mut accept_channel_message = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, nodes[0].node.get_our_node_id());
146+
if send_from_initiator {
147+
accept_channel_message.channel_reserve_satoshis = 0;
148+
accept_channel_message.max_htlc_value_in_flight_msat = 100_000_000;
149+
}
150+
nodes[0].node.handle_accept_channel(&nodes[1].node.get_our_node_id(), InitFeatures::known(), &accept_channel_message);
151+
{
152+
let mut lock;
153+
let mut chan = get_channel_ref!(if send_from_initiator { &nodes[1] } else { &nodes[0] }, lock, temp_channel_id);
154+
chan.holder_selected_channel_reserve_satoshis = 0;
155+
chan.holder_max_htlc_value_in_flight_msat = 100_000_000;
156+
}
157+
158+
let funding_tx = sign_funding_transaction(&nodes[0], &nodes[1], 100_000, temp_channel_id);
159+
let funding_msgs = create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &funding_tx);
160+
create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &funding_msgs.0);
161+
162+
// nodes[0] should now be able to send the full balance to nodes[1], violating nodes[1]'s
163+
// security model if it ever tries to send funds back to nodes[0] (but that's not our problem).
164+
if send_from_initiator {
165+
send_payment(&nodes[0], &[&nodes[1]], 100_000_000
166+
// Note that for outbound channels we have to consider the commitment tx fee and the
167+
// "fee spike buffer", which is currently a multiple of the total commitment tx fee as
168+
// well as an additional HTLC.
169+
- FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE * commit_tx_fee_msat(feerate_per_kw, 2));
170+
} else {
171+
send_payment(&nodes[1], &[&nodes[0]], push_amt);
172+
}
173+
}
174+
175+
#[test]
176+
fn test_counterparty_no_reserve() {
177+
do_test_counterparty_no_reserve(true);
178+
do_test_counterparty_no_reserve(false);
179+
}
180+
122181
#[test]
123182
fn test_async_inbound_update_fee() {
124183
let chanmon_cfgs = create_chanmon_cfgs(2);
@@ -7089,7 +7148,7 @@ fn test_user_configurable_csv_delay() {
70897148
nodes[1].node.create_channel(nodes[0].node.get_our_node_id(), 1000000, 1000000, 42, None).unwrap();
70907149
let mut open_channel = get_event_msg!(nodes[1], MessageSendEvent::SendOpenChannel, nodes[0].node.get_our_node_id());
70917150
open_channel.to_self_delay = 200;
7092-
if let Err(error) = Channel::new_from_req(&&test_utils::TestFeeEstimator { sat_per_kw: Mutex::new(253) }, &nodes[0].keys_manager, nodes[1].node.get_our_node_id(), &InitFeatures::known(), &open_channel, 0, &low_our_to_self_config, 0) {
7151+
if let Err(error) = Channel::new_from_req(&&test_utils::TestFeeEstimator { sat_per_kw: Mutex::new(253) }, &nodes[0].keys_manager, nodes[1].node.get_our_node_id(), &InitFeatures::known(), &open_channel, 0, &low_our_to_self_config, 0, &nodes[0].logger) {
70937152
match error {
70947153
ChannelError::Close(err) => { assert!(regex::Regex::new(r"Configured with an unreasonable our_to_self_delay \(\d+\) putting user funds at risks").unwrap().is_match(err.as_str())); },
70957154
_ => panic!("Unexpected event"),
@@ -7118,7 +7177,7 @@ fn test_user_configurable_csv_delay() {
71187177
nodes[1].node.create_channel(nodes[0].node.get_our_node_id(), 1000000, 1000000, 42, None).unwrap();
71197178
let mut open_channel = get_event_msg!(nodes[1], MessageSendEvent::SendOpenChannel, nodes[0].node.get_our_node_id());
71207179
open_channel.to_self_delay = 200;
7121-
if let Err(error) = Channel::new_from_req(&&test_utils::TestFeeEstimator { sat_per_kw: Mutex::new(253) }, &nodes[0].keys_manager, nodes[1].node.get_our_node_id(), &InitFeatures::known(), &open_channel, 0, &high_their_to_self_config, 0) {
7180+
if let Err(error) = Channel::new_from_req(&&test_utils::TestFeeEstimator { sat_per_kw: Mutex::new(253) }, &nodes[0].keys_manager, nodes[1].node.get_our_node_id(), &InitFeatures::known(), &open_channel, 0, &high_their_to_self_config, 0, &nodes[0].logger) {
71227181
match error {
71237182
ChannelError::Close(err) => { assert!(regex::Regex::new(r"They wanted our payments to be delayed by a needlessly long period\. Upper limit: \d+\. Actual: \d+").unwrap().is_match(err.as_str())); },
71247183
_ => panic!("Unexpected event"),

0 commit comments

Comments
 (0)