Skip to content

Commit 3d87598

Browse files
committed
lsp_plugin: add client side check for zero_conf
We only allow zero_conf channels if we approved the a jit-channel from the LSP in advance. Signed-off-by: Peter Neuroth <[email protected]>
1 parent 5452586 commit 3d87598

File tree

3 files changed

+105
-21
lines changed

3 files changed

+105
-21
lines changed

plugins/lsps-plugin/src/client.rs

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use cln_lsps::lsps2::model::{
1717
use cln_lsps::util;
1818
use cln_lsps::LSP_FEATURE_BIT;
1919
use cln_plugin::options;
20-
use cln_rpc::model::requests::{DatastoreMode, DatastoreRequest, ListpeersRequest};
20+
use cln_rpc::model::requests::{
21+
DatastoreMode, DatastoreRequest, DeldatastoreRequest, ListdatastoreRequest, ListpeersRequest,
22+
};
2123
use cln_rpc::model::responses::InvoiceResponse;
2224
use cln_rpc::primitives::{AmountOrAny, PublicKey, ShortChannelId};
2325
use cln_rpc::ClnRpc;
@@ -238,7 +240,6 @@ async fn on_lsps_lsps2_approve(
238240
) -> Result<serde_json::Value, anyhow::Error> {
239241
let req: ClnRpcLsps2Approve = serde_json::from_value(v)?;
240242
let ds_rec = DatastoreRecord {
241-
lsp_id: req.lsp_id,
242243
jit_channel_scid: req.jit_channel_scid,
243244
client_trusts_lsp: req.client_trusts_lsp.unwrap_or_default(),
244245
};
@@ -253,11 +254,7 @@ async fn on_lsps_lsps2_approve(
253254
hex: None,
254255
mode: Some(DatastoreMode::CREATE_OR_REPLACE),
255256
string: Some(ds_rec_json),
256-
key: vec![
257-
"lsps".to_string(),
258-
"client".to_string(),
259-
req.jit_channel_scid.to_string(),
260-
],
257+
key: vec!["lsps".to_string(), "client".to_string(), req.lsp_id],
261258
};
262259
let _ds_res = cln_client.call_typed(&ds_req).await?;
263260
Ok(serde_json::Value::default())
@@ -494,19 +491,52 @@ async fn on_htlc_accepted(
494491
Ok(value)
495492
}
496493

494+
/// Allows `zero_conf` channels to the client if the LSP is on the allowlist.
497495
async fn on_openchannel(
498-
_p: cln_plugin::Plugin<State>,
499-
_v: serde_json::Value,
496+
p: cln_plugin::Plugin<State>,
497+
v: serde_json::Value,
500498
) -> Result<serde_json::Value, anyhow::Error> {
501-
// Fixme: Register a list of trusted LSPs and check if LSP is allowlisted.
502-
// And if we expect a channel to be opened.
503-
// - either datastore or invoice label possible.
504-
info!("Allowing zero-conf channel from LSP");
505-
Ok(serde_json::json!({
506-
"result": "continue",
507-
"reserve": "0msat",
508-
"mindepth": 0,
509-
}))
499+
#[derive(Deserialize)]
500+
struct Request {
501+
id: String,
502+
}
503+
504+
let req: Request = serde_json::from_value(v.get("openchannel").unwrap().clone())
505+
.context("Failed to parse request JSON")?;
506+
let dir = p.configuration().lightning_dir;
507+
let rpc_path = Path::new(&dir).join(&p.configuration().rpc_file);
508+
let mut cln_client = cln_rpc::ClnRpc::new(rpc_path.clone()).await?;
509+
510+
let ds_req = ListdatastoreRequest {
511+
key: Some(vec![
512+
"lsps".to_string(),
513+
"client".to_string(),
514+
req.id.clone(),
515+
]),
516+
};
517+
let ds_res = cln_client.call_typed(&ds_req).await?;
518+
if let Some(_rec) = ds_res.datastore.iter().next() {
519+
info!("Allowing zero-conf channel from LSP {}", &req.id);
520+
let ds_req = DeldatastoreRequest {
521+
generation: None,
522+
key: vec!["lsps".to_string(), "client".to_string(), req.id.clone()],
523+
};
524+
if let Some(err) = cln_client.call_typed(&ds_req).await.err() {
525+
// We can do nothing but report that there was an issue deleting the
526+
// datastore record.
527+
warn!("Failed to delete LSP record from datastore: {}", err);
528+
}
529+
// Fixme: Check that we actually use client-trusts-LSP mode - can be
530+
// found in the ds record.
531+
return Ok(serde_json::json!({
532+
"result": "continue",
533+
"reserve": "0msat",
534+
"mindepth": 0,
535+
}));
536+
} else {
537+
// Not a requested JIT-channel opening, continue.
538+
Ok(serde_json::json!({"result": "continue"}))
539+
}
510540
}
511541

512542
async fn on_lsps_listprotocols(
@@ -659,7 +689,6 @@ struct ClnRpcLsps2Approve {
659689

660690
#[derive(Debug, Clone, Serialize, Deserialize)]
661691
struct DatastoreRecord {
662-
lsp_id: String,
663692
jit_channel_scid: ShortChannelId,
664693
client_trusts_lsp: bool,
665694
}

plugins/lsps-plugin/src/lsps2/handler.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,7 @@ impl<A: ClnApi> HtlcAcceptedHookHandler<A> {
507507
push_msat: None,
508508
request_amt: None,
509509
reserve: None,
510-
channel_type: None, // Fimxe: Core-Lightning is complaining that it doesn't support these channel_types
511-
// channel_type: Some(vec![46, 50]), // Sets `option_zeroconf` and `option_scid_alias`
510+
channel_type: Some(vec![12, 22, 50]),
512511
utxos: None,
513512
amount: AmountOrAll::Amount(Amount::from_msat(cap)),
514513
id: ds_rec.peer_id,

tests/test_cln_lsps.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,59 @@ def test_lsps2_buyjitchannel_no_mpp_var_invoice(node_factory, bitcoind):
158158
# l1 should have gotten a jit-channel.
159159
chs = l1.rpc.listpeerchannels()['channels']
160160
assert len(chs) == 1
161+
162+
163+
def test_lsps2_non_approved_zero_conf(node_factory, bitcoind):
164+
""" Checks that we don't allow zerof_conf channels from an LSP if we did
165+
not approve it first.
166+
"""
167+
# We need a policy service to fetch from.
168+
plugin = os.path.join(os.path.dirname(__file__), 'plugins/lsps2_policy.py')
169+
170+
l1, l2, l3= node_factory.get_nodes(3, opts=[
171+
{"dev-lsps-client-enabled": None},
172+
{
173+
"dev-lsps-service-enabled": None,
174+
"dev-lsps2-service-enabled": None,
175+
"dev-lsps2-promise-secret": "00" * 32,
176+
"plugin": plugin,
177+
"fee-base": 0, # We are going to deduct our fee anyways,
178+
"fee-per-satoshi": 0, # We are going to deduct our fee anyways,
179+
},
180+
{"disable-mpp": None},
181+
])
182+
183+
# Give the LSP some funds to open jit-channels
184+
addr = l2.rpc.newaddr()['bech32']
185+
bitcoind.rpc.sendtoaddress(addr, 1)
186+
bitcoind.generate_block(1)
187+
188+
node_factory.join_nodes([l3, l2], fundchannel=True, wait_for_announce=True)
189+
node_factory.join_nodes([l1, l2], fundchannel=False)
190+
191+
chanid = only_one(l3.rpc.listpeerchannels(l2.info['id'])['channels'])['short_channel_id']
192+
193+
fee_opt = l1.rpc.lsps_lsps2_getinfo(lsp_id=l2.info['id'])['opening_fee_params_menu'][0]
194+
buy_res = l1.rpc.lsps_lsps2_buy(lsp_id=l2.info['id'], opening_fee_params=fee_opt)
195+
196+
hint = [[{
197+
"id": l2.info['id'],
198+
"short_channel_id": buy_res['jit_channel_scid'],
199+
"fee_base_msat": 0,
200+
"fee_proportional_millionths": 0,
201+
"cltv_expiry_delta": buy_res['lsp_cltv_expiry_delta'],
202+
}]]
203+
204+
bolt11 = l1.dev_invoice(
205+
amount_msat="any",
206+
description="lsp-invoice-1",
207+
label="lsp-invoice-1",
208+
dev_routes=hint,
209+
)['bolt11']
210+
211+
with pytest.raises(ValueError):
212+
l3.rpc.pay(bolt11, amount_msat=10000000)
213+
214+
# l1 shouldn't have a new channel.
215+
chs = l1.rpc.listpeerchannels()['channels']
216+
assert len(chs) == 0

0 commit comments

Comments
 (0)