Skip to content

Commit d417cd3

Browse files
committed
plugin: Implement LSP selection for lsp_invoice
1 parent f5b9bb6 commit d417cd3

File tree

6 files changed

+163
-22
lines changed

6 files changed

+163
-22
lines changed

libs/gl-client-py/tests/test_plugin.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,11 @@ def test_lsps_plugin_calls(clients, bitcoind, node_factory, lsps_server):
227227
s.connect_peer(lsps_server.info["id"], f"localhost:{lsps_server.port}")
228228

229229
# Try the pyo3 bindings from gl-client-py
230-
res = s.lsp_invoice(label="lbl2", description="description", amount_msat=42)
231-
inv = s.decodepay(res["bolt11"])
230+
res = s.lsp_invoice(label="lbl2", description="description", amount_msat=31337)
231+
from pyln.proto import Invoice
232+
inv = Invoice.decode(res.bolt11)
232233
pprint(inv)
233-
234234
# Only one routehint, with only one hop, the LSP to the destination
235-
assert len(inv["routes"]) == 1 and len(inv["routes"][0]) == 1
236-
assert inv["description"] == "description"
237-
rh = inv["routes"][0][0]
238-
assert rh["pubkey"] == lsp_id
235+
assert len(inv.route_hints.route_hints) == 1
236+
rh = inv.route_hints.route_hints[0]
237+
assert rh["pubkey"] == bytes.fromhex(lsp_id)

libs/gl-client/src/signer/model/greenlight.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ pub fn decode_request(uri: &str, p: &[u8]) -> anyhow::Result<Request> {
1212
"/greenlight.Node/TrampolinePay" => {
1313
Request::TrampolinePay(crate::pb::TrampolinePayRequest::decode(p)?)
1414
}
15+
"/greenlight.Node/LspInvoice" => {
16+
Request::LspInvoice(crate::pb::LspInvoiceRequest::decode(p)?)
17+
}
18+
1519
uri => return Err(anyhow!("Unknown URI {}, can't decode payload", uri)),
1620
})
1721
}

libs/gl-client/src/signer/model/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod greenlight;
99
#[derive(Clone, Debug)]
1010
pub enum Request {
1111
GlConfig(greenlight::GlConfig),
12+
LspInvoice(greenlight::LspInvoiceRequest),
1213
Getinfo(cln::GetinfoRequest),
1314
ListPeers(cln::ListpeersRequest),
1415
ListFunds(cln::ListfundsRequest),

libs/gl-plugin/src/node/mod.rs

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ static LIMITER: OnceCell<RateLimiter<NotKeyed, InMemoryState, MonotonicClock>> =
3434
static RPC_CLIENT: OnceCell<Arc<Mutex<cln_rpc::ClnRpc>>> = OnceCell::const_new();
3535
static RPC_POLL_INTERVAL: Duration = Duration::from_millis(500);
3636

37+
const OPT_SUPPORTS_LSPS: usize = 729;
38+
3739
pub async fn get_rpc<P: AsRef<Path>>(path: P) -> Arc<Mutex<cln_rpc::ClnRpc>> {
3840
RPC_CLIENT
3941
.get_or_init(|| async {
@@ -186,15 +188,31 @@ impl Node for PluginNodeServer {
186188
req: Request<pb::LspInvoiceRequest>,
187189
) -> Result<Response<pb::LspInvoiceResponse>, Status> {
188190
let req: pb::LspInvoiceRequest = req.into_inner();
189-
let invreq: crate::requests::InvoiceRequest = req.into();
191+
let mut invreq: crate::requests::LspInvoiceRequest = req.into();
190192
let rpc_arc = get_rpc(&self.rpc_path).await;
191193

192194
let mut rpc = rpc_arc.lock().await;
195+
196+
// In case the client did not specify an LSP to work with,
197+
// let's enumerate them, and select the best option ourselves.
198+
let lsps = self.get_lsps_offers(&mut rpc).await.map_err(|_e| {
199+
Status::not_found("Could not retrieve LSPS peers for invoice negotiation.")
200+
})?;
201+
202+
if lsps.len() < 1 {
203+
return Err(Status::not_found(
204+
"Could not find an LSP peer to negotiate the LSPS2 channel for this invoice.",
205+
));
206+
}
207+
208+
let lsp = &lsps[0];
209+
log::info!("Selecting {:?} for invoice negotiation", lsp);
210+
invreq.lsp_id = lsp.node_id.to_owned();
211+
193212
let res = rpc
194213
.call_typed(&invreq)
195214
.await
196215
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;
197-
198216
Ok(Response::new(res.into()))
199217
}
200218

@@ -595,6 +613,12 @@ impl Node for PluginNodeServer {
595613

596614
use cln_grpc::pb::node_server::NodeServer;
597615

616+
#[derive(Clone, Debug)]
617+
struct Lsps2Offer {
618+
node_id: String,
619+
params: Vec<crate::responses::OpeningFeeParams>,
620+
}
621+
598622
impl PluginNodeServer {
599623
pub async fn run(self) -> Result<()> {
600624
let addr = self.grpc_binding.parse().unwrap();
@@ -655,17 +679,99 @@ impl PluginNodeServer {
655679
return Ok(());
656680
}
657681

682+
async fn list_peers(
683+
&self,
684+
rpc: &mut cln_rpc::ClnRpc,
685+
) -> Result<cln_rpc::model::responses::ListpeersResponse, Error> {
686+
rpc.call_typed(&cln_rpc::model::requests::ListpeersRequest {
687+
id: None,
688+
level: None,
689+
})
690+
.await
691+
.map_err(|e| e.into())
692+
}
693+
694+
async fn get_lsps_offers(&self, rpc: &mut cln_rpc::ClnRpc) -> Result<Vec<Lsps2Offer>, Error> {
695+
// Collect peers offering LSP functionality
696+
let lpeers = self.list_peers(rpc).await?;
697+
698+
// Filter out the ones that do not announce the LSPs features.
699+
// TODO: Re-enable the filtering once the cln-lsps-service plugin announces the features.
700+
let _lsps: Vec<cln_rpc::model::responses::ListpeersPeers> = lpeers
701+
.peers
702+
.into_iter()
703+
//.filter(|p| has_feature(
704+
// hex::decode(p.features.clone().unwrap_or_default()).expect("featurebits are hex"),
705+
// OPT_SUPPORTS_LSPS
706+
//))
707+
.collect();
708+
709+
// Query all peers for their LSPS offers, but with a brief
710+
// timeout so the invoice creation isn't help up too long.
711+
let futs: Vec<
712+
tokio::task::JoinHandle<(
713+
String,
714+
Result<
715+
Result<crate::responses::LspGetinfoResponse, cln_rpc::RpcError>,
716+
tokio::time::error::Elapsed,
717+
>,
718+
)>,
719+
> = _lsps
720+
.into_iter()
721+
.map(|peer| {
722+
let rpc_path = self.rpc_path.clone();
723+
tokio::spawn(async move {
724+
let peer_id = format!("{:x}", peer.id);
725+
let mut rpc = cln_rpc::ClnRpc::new(rpc_path.clone()).await.unwrap();
726+
let req = crate::requests::LspGetinfoRequest {
727+
lsp_id: peer_id.clone(),
728+
token: None,
729+
};
730+
731+
(
732+
peer_id,
733+
tokio::time::timeout(
734+
tokio::time::Duration::from_secs(2),
735+
rpc.call_typed(&req),
736+
)
737+
.await,
738+
)
739+
})
740+
})
741+
.collect();
742+
743+
let mut res = vec![];
744+
for f in futs {
745+
match f.await {
746+
//TODO We need to drag the node_id along.
747+
Ok((node_id, Ok(Ok(r)))) => res.push(Lsps2Offer {
748+
node_id: node_id,
749+
params: r.opening_fee_params_menu,
750+
}),
751+
Ok((node_id, Err(e))) => warn!(
752+
"Error fetching LSPS menu items from peer_id={}: {:?}",
753+
node_id, e
754+
),
755+
Ok((node_id, Ok(Err(e)))) => warn!(
756+
"Error fetching LSPS menu items from peer_id={}: {:?}",
757+
node_id, e
758+
),
759+
Err(_) => warn!("Timeout fetching LSPS menu items"),
760+
}
761+
}
762+
763+
log::info!("Gathered {} LSP menus", res.len());
764+
log::trace!("LSP menus: {:?}", &res);
765+
766+
Ok(res)
767+
}
768+
658769
async fn get_reconnect_peers(
659770
&self,
660771
) -> Result<Vec<cln_rpc::model::requests::ConnectRequest>, Error> {
661772
let rpc_arc = get_rpc(&self.rpc_path).await;
662773
let mut rpc = rpc_arc.lock().await;
663-
let peers = rpc
664-
.call_typed(&cln_rpc::model::requests::ListpeersRequest {
665-
id: None,
666-
level: None,
667-
})
668-
.await?;
774+
let peers = self.list_peers(&mut rpc).await?;
669775

670776
let mut requests: Vec<cln_rpc::model::requests::ConnectRequest> = peers
671777
.peers

libs/gl-plugin/src/requests.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ pub struct Keysend {
209209
pub struct ListIncoming {}
210210

211211
#[derive(Debug, Clone, Serialize)]
212-
pub struct InvoiceRequest {
212+
pub struct LspInvoiceRequest {
213213
pub lsp_id: String,
214214
#[serde(skip_serializing_if = "Option::is_none")]
215215
pub token: Option<String>,
@@ -218,11 +218,18 @@ pub struct InvoiceRequest {
218218
pub label: String,
219219
}
220220

221+
#[derive(Debug, Clone, Serialize)]
222+
pub struct LspGetinfoRequest {
223+
pub lsp_id: String,
224+
#[serde(skip_serializing_if = "Option::is_none")]
225+
pub token: Option<String>,
226+
}
227+
221228
use cln_rpc::model::TypedRequest;
222229

223-
impl From<crate::pb::LspInvoiceRequest> for InvoiceRequest {
224-
fn from(o: crate::pb::LspInvoiceRequest) -> InvoiceRequest {
225-
InvoiceRequest {
230+
impl From<crate::pb::LspInvoiceRequest> for LspInvoiceRequest {
231+
fn from(o: crate::pb::LspInvoiceRequest) -> LspInvoiceRequest {
232+
LspInvoiceRequest {
226233
lsp_id: o.lsp_id,
227234
token: match o.token.as_ref() {
228235
"" => None,
@@ -240,14 +247,20 @@ impl From<crate::pb::LspInvoiceRequest> for InvoiceRequest {
240247
}
241248
}
242249

243-
impl TypedRequest for InvoiceRequest {
250+
impl TypedRequest for LspInvoiceRequest {
244251
type Response = super::responses::InvoiceResponse;
245-
246252
fn method(&self) -> &str {
247253
"lsps-lsps2-invoice"
248254
}
249255
}
250256

257+
impl TypedRequest for LspGetinfoRequest {
258+
type Response = super::responses::LspGetinfoResponse;
259+
fn method(&self) -> &str {
260+
"lsps-lsps2-getinfo"
261+
}
262+
}
263+
251264
#[cfg(test)]
252265
mod test {
253266
use super::*;
@@ -271,7 +284,7 @@ mod test {
271284
];
272285

273286
for t in tests {
274-
let _actual: super::InvoiceRequest = t.into();
287+
let _actual: super::LspInvoiceRequest = t.into();
275288
}
276289
}
277290
}

libs/gl-plugin/src/responses.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,24 @@ pub struct InvoiceResponse {
343343
pub payment_secret: String,
344344
}
345345

346+
#[derive(Debug, Clone, Deserialize)]
347+
pub struct LspGetinfoResponse {
348+
pub opening_fee_params_menu: Vec<OpeningFeeParams>,
349+
350+
}
351+
#[derive(Debug, Clone, Deserialize)]
352+
#[serde(deny_unknown_fields)] // LSPS2 requires the client to fail if a field is unrecognized.
353+
pub struct OpeningFeeParams {
354+
pub min_fee_msat: String,
355+
pub proportional: u64,
356+
pub valid_until: String,
357+
pub min_lifetime: u32,
358+
pub max_client_to_self_delay: u32,
359+
pub min_payment_size_msat: String ,
360+
pub max_payment_size_msat: String ,
361+
pub promise: String, // Max 512 bytes
362+
}
363+
346364
impl From<InvoiceResponse> for crate::pb::LspInvoiceResponse {
347365
fn from(o: InvoiceResponse) -> crate::pb::LspInvoiceResponse {
348366
crate::pb::LspInvoiceResponse {

0 commit comments

Comments
 (0)