Skip to content

Commit c754e2f

Browse files
authored
Merge pull request #873 from tankyleo/2026-04-zero-reserve
Add tests and improve documentation for zero reserve channels
2 parents 64e3154 + fe77868 commit c754e2f

File tree

5 files changed

+183
-36
lines changed

5 files changed

+183
-36
lines changed

src/lib.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,7 +1149,7 @@ impl Node {
11491149
fn open_channel_inner(
11501150
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: FundingAmount,
11511151
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
1152-
announce_for_forwarding: bool, set_0reserve: bool,
1152+
announce_for_forwarding: bool, disable_counterparty_reserve: bool,
11531153
) -> Result<UserChannelId, Error> {
11541154
if !*self.is_running.read().expect("lock") {
11551155
return Err(Error::NotRunning);
@@ -1219,7 +1219,7 @@ impl Node {
12191219
.expect("a 16-byte slice should convert into a [u8; 16]"),
12201220
);
12211221

1222-
let result = if set_0reserve {
1222+
let result = if disable_counterparty_reserve {
12231223
self.channel_manager.create_channel_to_trusted_peer_0reserve(
12241224
peer_info.node_id,
12251225
channel_amount_sats,
@@ -1239,7 +1239,7 @@ impl Node {
12391239
)
12401240
};
12411241

1242-
let zero_reserve_string = if set_0reserve { "0reserve " } else { "" };
1242+
let zero_reserve_string = if disable_counterparty_reserve { "0reserve " } else { "" };
12431243

12441244
match result {
12451245
Ok(_) => {
@@ -1449,8 +1449,8 @@ impl Node {
14491449
/// Connect to a node and open a new unannounced channel, in which the target node can
14501450
/// spend its entire balance.
14511451
///
1452-
/// This channel allows the target node to try to steal your funds with no financial
1453-
/// penalty, so this channel should only be opened to nodes you trust.
1452+
/// This channel allows the target node to try to steal your channel balance with no
1453+
/// financial penalty, so this channel should only be opened to nodes you trust.
14541454
///
14551455
/// Disconnects and reconnects are handled automatically.
14561456
///
@@ -1484,8 +1484,8 @@ impl Node {
14841484
/// minus fees and anchor reserves. The target node will be able to spend its entire channel
14851485
/// balance.
14861486
///
1487-
/// This channel allows the target node to try to steal your funds with no financial
1488-
/// penalty, so this channel should only be opened to nodes you trust.
1487+
/// This channel allows the target node to try to steal your channel balance with no
1488+
/// financial penalty, so this channel should only be opened to nodes you trust.
14891489
///
14901490
/// Disconnects and reconnects are handled automatically.
14911491
///

src/liquidity.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,14 @@ pub struct LSPS2ServiceConfig {
142142
///
143143
/// [`bLIP-52`]: https://github.com/lightning/blips/blob/master/blip-0052.md#trust-models
144144
pub client_trusts_lsp: bool,
145-
/// When set, clients that we open channels to will be allowed to spend their entire channel
146-
/// balance. This allows clients to try to steal your funds with no financial penalty, so
147-
/// this should only be set if you trust your clients.
148-
pub allow_client_0reserve: bool,
145+
/// When set, we will allow clients to spend their entire channel balance in the channels
146+
/// we open to them. This allows clients to try to steal your channel balance with
147+
/// no financial penalty, so this should only be set if you trust your clients.
148+
///
149+
/// See [`Node::open_0reserve_channel`] to manually open these channels.
150+
///
151+
/// [`Node::open_0reserve_channel`]: crate::Node::open_0reserve_channel
152+
pub disable_client_reserve: bool,
149153
}
150154

151155
pub(crate) struct LiquiditySourceBuilder<L: Deref>
@@ -792,7 +796,7 @@ where
792796
config.channel_config.forwarding_fee_base_msat = 0;
793797
config.channel_config.forwarding_fee_proportional_millionths = 0;
794798

795-
let result = if service_config.allow_client_0reserve {
799+
let result = if service_config.disable_client_reserve {
796800
self.channel_manager.create_channel_to_trusted_peer_0reserve(
797801
their_network_key,
798802
channel_amount_sats,
@@ -819,7 +823,7 @@ where
819823
// the pending requests and regularly retry opening the channel until we
820824
// succeed.
821825
let zero_reserve_string =
822-
if service_config.allow_client_0reserve { "0reserve " } else { "" };
826+
if service_config.disable_client_reserve { "0reserve " } else { "" };
823827
log_error!(
824828
self.logger,
825829
"Failed to open LSPS2 {}channel to {}: {:?}",

tests/common/mod.rs

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@ pub async fn splice_in_with_all(
790790

791791
pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
792792
node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool,
793-
expect_anchor_channel: bool, force_close: bool,
793+
disable_node_b_reserve: bool, expect_anchor_channel: bool, force_close: bool,
794794
) {
795795
let addr_a = node_a.onchain_payment().new_address().unwrap();
796796
let addr_b = node_b.onchain_payment().new_address().unwrap();
@@ -846,15 +846,27 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
846846
println!("\nA -- open_channel -> B");
847847
let funding_amount_sat = 2_080_000;
848848
let push_msat = (funding_amount_sat / 2) * 1000; // balance the channel
849-
node_a
850-
.open_announced_channel(
851-
node_b.node_id(),
852-
node_b.listening_addresses().unwrap().first().unwrap().clone(),
853-
funding_amount_sat,
854-
Some(push_msat),
855-
None,
856-
)
857-
.unwrap();
849+
if disable_node_b_reserve {
850+
node_a
851+
.open_0reserve_channel(
852+
node_b.node_id(),
853+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
854+
funding_amount_sat,
855+
Some(push_msat),
856+
None,
857+
)
858+
.unwrap();
859+
} else {
860+
node_a
861+
.open_announced_channel(
862+
node_b.node_id(),
863+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
864+
funding_amount_sat,
865+
Some(push_msat),
866+
None,
867+
)
868+
.unwrap();
869+
}
858870

859871
assert_eq!(node_a.list_peers().first().unwrap().node_id, node_b.node_id());
860872
assert!(node_a.list_peers().first().unwrap().is_persisted);
@@ -913,6 +925,22 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
913925
node_b_anchor_reserve_sat
914926
);
915927

928+
// Note that only node B has 0-reserve, we don't yet have an API to allow the opener of the
929+
// channel to have 0-reserve.
930+
if disable_node_b_reserve {
931+
assert_eq!(node_b.list_channels()[0].unspendable_punishment_reserve, Some(0));
932+
assert_eq!(node_b.list_channels()[0].outbound_capacity_msat, push_msat);
933+
assert_eq!(node_b.list_channels()[0].next_outbound_htlc_limit_msat, push_msat);
934+
935+
assert_eq!(node_b.list_balances().total_lightning_balance_sats * 1000, push_msat);
936+
let LightningBalance::ClaimableOnChannelClose { amount_satoshis, .. } =
937+
node_b.list_balances().lightning_balances[0]
938+
else {
939+
panic!("Unexpected `LightningBalance` variant");
940+
};
941+
assert_eq!(amount_satoshis * 1000, push_msat);
942+
}
943+
916944
let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
917945
let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
918946

@@ -1267,6 +1295,39 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
12671295
2
12681296
);
12691297

1298+
if disable_node_b_reserve {
1299+
let node_a_outbound_capacity_msat = node_a.list_channels()[0].outbound_capacity_msat;
1300+
let node_a_reserve_msat =
1301+
node_a.list_channels()[0].unspendable_punishment_reserve.unwrap() * 1000;
1302+
// TODO: Zero-fee commitment channels are anchor channels, but do not allocate any
1303+
// funds to the anchor, so this will need to be updated when we ship these channels
1304+
// in ldk-node.
1305+
let node_a_anchors_msat = if expect_anchor_channel { 2 * 330 * 1000 } else { 0 };
1306+
let funding_amount_msat = node_a.list_channels()[0].channel_value_sats * 1000;
1307+
// Node B does not have any reserve, so we only subtract a few items on node A's
1308+
// side to arrive at node B's capacity
1309+
let node_b_capacity_msat = funding_amount_msat
1310+
- node_a_outbound_capacity_msat
1311+
- node_a_reserve_msat
1312+
- node_a_anchors_msat;
1313+
let got_capacity_msat = node_b.list_channels()[0].outbound_capacity_msat;
1314+
assert_eq!(got_capacity_msat, node_b_capacity_msat);
1315+
assert_ne!(got_capacity_msat, 0);
1316+
// Sanity check to make sure this is a non-trivial amount
1317+
assert!(got_capacity_msat > 15_000_000);
1318+
1319+
// This is a private channel, so node B can send 100% of the value over
1320+
assert_eq!(node_b.list_channels()[0].next_outbound_htlc_limit_msat, node_b_capacity_msat);
1321+
1322+
node_b.spontaneous_payment().send(node_b_capacity_msat, node_a.node_id(), None).unwrap();
1323+
expect_event!(node_b, PaymentSuccessful);
1324+
expect_event!(node_a, PaymentReceived);
1325+
1326+
node_a.spontaneous_payment().send(node_b_capacity_msat, node_b.node_id(), None).unwrap();
1327+
expect_event!(node_a, PaymentSuccessful);
1328+
expect_event!(node_b, PaymentReceived);
1329+
}
1330+
12701331
println!("\nB close_channel (force: {})", force_close);
12711332
tokio::time::sleep(Duration::from_secs(1)).await;
12721333
if force_close {

tests/integration_tests_rust.rs

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,44 +48,125 @@ async fn channel_full_cycle() {
4848
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
4949
let chain_source = random_chain_source(&bitcoind, &electrsd);
5050
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
51-
do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false)
52-
.await;
51+
do_channel_full_cycle(
52+
node_a,
53+
node_b,
54+
&bitcoind.client,
55+
&electrsd.client,
56+
false,
57+
false,
58+
true,
59+
false,
60+
)
61+
.await;
5362
}
5463

5564
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5665
async fn channel_full_cycle_force_close() {
5766
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
5867
let chain_source = random_chain_source(&bitcoind, &electrsd);
5968
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
60-
do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true)
61-
.await;
69+
do_channel_full_cycle(
70+
node_a,
71+
node_b,
72+
&bitcoind.client,
73+
&electrsd.client,
74+
false,
75+
false,
76+
true,
77+
true,
78+
)
79+
.await;
6280
}
6381

6482
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6583
async fn channel_full_cycle_force_close_trusted_no_reserve() {
6684
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
6785
let chain_source = random_chain_source(&bitcoind, &electrsd);
6886
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true);
69-
do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true)
70-
.await;
87+
do_channel_full_cycle(
88+
node_a,
89+
node_b,
90+
&bitcoind.client,
91+
&electrsd.client,
92+
false,
93+
false,
94+
true,
95+
true,
96+
)
97+
.await;
7198
}
7299

73100
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
74101
async fn channel_full_cycle_0conf() {
75102
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
76103
let chain_source = random_chain_source(&bitcoind, &electrsd);
77104
let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false);
78-
do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false)
79-
.await;
105+
do_channel_full_cycle(
106+
node_a,
107+
node_b,
108+
&bitcoind.client,
109+
&electrsd.client,
110+
true,
111+
false,
112+
true,
113+
false,
114+
)
115+
.await;
80116
}
81117

82118
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
83119
async fn channel_full_cycle_legacy_staticremotekey() {
84120
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
85121
let chain_source = random_chain_source(&bitcoind, &electrsd);
86122
let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false);
87-
do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false)
88-
.await;
123+
do_channel_full_cycle(
124+
node_a,
125+
node_b,
126+
&bitcoind.client,
127+
&electrsd.client,
128+
false,
129+
false,
130+
false,
131+
false,
132+
)
133+
.await;
134+
}
135+
136+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
137+
async fn channel_full_cycle_0reserve() {
138+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
139+
let chain_source = random_chain_source(&bitcoind, &electrsd);
140+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
141+
do_channel_full_cycle(
142+
node_a,
143+
node_b,
144+
&bitcoind.client,
145+
&electrsd.client,
146+
false,
147+
true,
148+
true,
149+
false,
150+
)
151+
.await;
152+
}
153+
154+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
155+
async fn channel_full_cycle_0conf_0reserve() {
156+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
157+
let chain_source = random_chain_source(&bitcoind, &electrsd);
158+
let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false);
159+
do_channel_full_cycle(
160+
node_a,
161+
node_b,
162+
&bitcoind.client,
163+
&electrsd.client,
164+
true,
165+
true,
166+
true,
167+
false,
168+
)
169+
.await;
89170
}
90171

91172
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -1705,7 +1786,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) {
17051786
min_channel_opening_fee_msat: 0,
17061787
max_client_to_self_delay: 1024,
17071788
client_trusts_lsp,
1708-
allow_client_0reserve: false,
1789+
disable_client_reserve: false,
17091790
};
17101791

17111792
let service_config = random_config(true);
@@ -2024,7 +2105,7 @@ async fn lsps2_client_trusts_lsp() {
20242105
min_channel_opening_fee_msat: 0,
20252106
max_client_to_self_delay: 1024,
20262107
client_trusts_lsp: true,
2027-
allow_client_0reserve: false,
2108+
disable_client_reserve: false,
20282109
};
20292110

20302111
let service_config = random_config(true);
@@ -2199,7 +2280,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
21992280
min_channel_opening_fee_msat: 0,
22002281
max_client_to_self_delay: 1024,
22012282
client_trusts_lsp: false,
2202-
allow_client_0reserve: false,
2283+
disable_client_reserve: false,
22032284
};
22042285

22052286
let service_config = random_config(true);

tests/integration_tests_vss.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async fn channel_full_cycle_with_vss_store() {
5454
&bitcoind.client,
5555
&electrsd.client,
5656
false,
57+
false,
5758
true,
5859
false,
5960
)

0 commit comments

Comments
 (0)