From 86810c9f492ed7d19e077674adea11c2d9de016c Mon Sep 17 00:00:00 2001 From: Nazarii Shcherbak Date: Tue, 25 Aug 2026 17:47:54 +0200 Subject: [PATCH 1/5] gl-plugin: add LspInvoiceMeta structure In order to get an invoice, lsp_invoice RPC call is used, however, no additional information about the requested invoice is stored. That is unfortunate, since we may need this data to detect whether the incomming payment actually used for opening JIT channel or not. Besides, we don't store any information about the original invoice (e.g. the original amount), so this structure includes that information. --- libs/gl-plugin/src/storage.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/libs/gl-plugin/src/storage.rs b/libs/gl-plugin/src/storage.rs index d360f6926..e6c31abdd 100644 --- a/libs/gl-plugin/src/storage.rs +++ b/libs/gl-plugin/src/storage.rs @@ -2,6 +2,7 @@ pub use gl_client::persist::State; use log::debug; +use serde::{Deserialize, Serialize}; use thiserror::Error; use tonic::async_trait; @@ -66,3 +67,22 @@ impl StateStore for SledStateStore { .map_err(|e| e.into()) } } + +/// A structure that is used for storing invoices that are requested through +/// [Node::lsp_invoice](pb::node_server::Node::lsp_invoice) RPC call. lsp_invoice +/// call does not guarantee that the returned invoice is for requesting JIT +/// channel - if there is a channel with enough liquidity, a simple bolt11 invoice +/// is created. +/// +/// This structure is stored in CLN datastore. The reason of why do we need this +/// structure instead of querying invoices table is that we want to distinguish +/// incomming payments whether they were for JIT channel opening or just a simple +/// payment. Currently, CLN does not allow to do so, that's why this workaround +/// exists. +#[derive(Serialize, Deserialize)] +pub struct LspInvoiceMeta { + pub label: String, + pub payment_hash: String, + pub requested_amount_msat: u64, + pub bolt11: String, +} From 9a802a7fba24f3f28e0fc249d826660da038ee7b Mon Sep 17 00:00:00 2001 From: Nazarevsky Date: Tue, 25 Aug 2026 17:56:57 +0200 Subject: [PATCH 2/5] gl-plugin: update lsp_invoice RPC call to store invoice information When calling lsp_invoice RPC call, two outcomes may be produced: - the client has enough liquidity in channels to receive a payment - a common bolt11 invoice is requested; - the client has not enough liquidity in channels to receive a payment - a JIT channel requested from an LSP; For both variants we use datastore in order to store additional meta information for the invoice. It is worth to state that the key by which the invoice meta distinguished is invoice's label. My first intention was to use payment hash, however, we cannot rely on invoice's payment hashes because invoice payment hashes for an LSP (with original amount) and for a client (with reduced amount) are the same in case of unspecified amount. --- libs/gl-plugin/src/node/mod.rs | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index f1722f954..ea71190bc 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -1,12 +1,12 @@ use crate::config::Config; use crate::pb::{self, node_server::Node}; -use crate::storage::StateStore; +use crate::storage::{LspInvoiceMeta, StateStore}; use crate::{messages, Event}; use crate::{stager, tramp}; -use anyhow::{Context, Error, Result}; +use anyhow::{anyhow, Context, Error, Result}; use base64::{engine::general_purpose, Engine as _}; use bytes::BufMut; -use cln_rpc::Notification; +use cln_rpc::{ClnRpc, Notification}; use gl_client::metrics::{savings_percent, signer_state_request_wire_bytes}; use gl_client::persist::{State, StateSketch}; use governor::{ @@ -263,6 +263,16 @@ impl Node for PluginNodeServer { .await .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + let meta = LspInvoiceMeta { + label: req.label.clone(), + payment_hash: res.payment_hash.to_string(), + requested_amount_msat: req.amount_msat, + bolt11: res.bolt11.clone(), + }; + + write_lsp_invoice_meta(rpc, meta).await + .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + return Ok(Response::new(pb::LspInvoiceResponse { bolt11: res.bolt11, created_index: res.created_index.unwrap_or(0) as u32, @@ -323,6 +333,9 @@ impl Node for PluginNodeServer { .div_ceil(1_000_000); std::cmp::max(min_fee, proportional_fee) }; + + let requested_amount_msat = req.amount_msat.clone(); + let invoice_label = req.label.clone(); // Use the new RPC method name for versions > v25.05gl1 let mut res = if *version > *"v25.05gl1" { @@ -340,6 +353,20 @@ impl Node for PluginNodeServer { }; res.opening_fee_msat = opening_fee_msat; + + // A JIT channel has now been negotiated with the LSP for this + // invoice. So, we're storing some data with the original requested + // amount. + let meta = LspInvoiceMeta { + label: invoice_label.clone(), + payment_hash: res.payment_hash.clone(), + requested_amount_msat, + bolt11: res.bolt11.clone(), + }; + + write_lsp_invoice_meta(rpc, meta).await + .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + Ok(Response::new(res.into())) } @@ -834,6 +861,33 @@ impl Node for PluginNodeServer { } } +/// Writes `LspInvoiceMeta` using datastore request. `LspInvoiceMeta` is useful for defining +/// some additional information regarding invoice being requested trough Greenlight. +async fn write_lsp_invoice_meta(mut rpc: tokio::sync::MutexGuard<'_, ClnRpc>, meta: LspInvoiceMeta) + -> Result<()> { + let record_serialized = serde_json::to_string(&meta) + .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + + let datastore_req = cln_rpc::model::requests::DatastoreRequest { + key: vec![ + "gl".to_string(), + "jit_channels".to_string(), + meta.label, + ], + string: Some(record_serialized), + hex: None, + mode: Some(cln_rpc::model::requests::DatastoreMode::CREATE_OR_REPLACE), + generation: None, + }; + + rpc.call_typed(&datastore_req).await.map_err( + |e| + anyhow!("Failed to store JIT channel negotiation data in datastore: {}", e) + )?; + + Ok(()) +} + use cln_grpc::pb::node_server::NodeServer; #[derive(Clone, Debug)] From c479f0c343cd4b01d53047a970c44f47b1307311 Mon Sep 17 00:00:00 2001 From: Nazarevsky Date: Tue, 1 Sep 2026 15:43:06 +0200 Subject: [PATCH 3/5] gl-plugin: change LspInvoiceMeta, make write_lsp_invoice_meta log error instead of returning, minor fixes on function signatures There is a bunch minor fixes provided in this commit: - now LspInvoiceMeta contains expected_amount_msat and lsp_id. In lsp_invoice RPC call those are bounded to default values in case of the standart invoice request (not JIT opening). In other case, lsp_id is taken from the active offer and expected_amount_msat is a saturating subtraction of requested_amount_msat and opening_fee_msat; - previously write_lsp_invoice_meta returned an error. Changed it to logging an error instead of returning it; - change write_lsp_invoice_meta signature; - in write_lsp_invoice_meta make record_serialized return Result instead of Status. --- libs/gl-plugin/src/node/mod.rs | 20 +++++++++++++------- libs/gl-plugin/src/storage.rs | 2 ++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index ea71190bc..2aef13041 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -267,11 +267,14 @@ impl Node for PluginNodeServer { label: req.label.clone(), payment_hash: res.payment_hash.to_string(), requested_amount_msat: req.amount_msat, + expected_amount_msat: 0, bolt11: res.bolt11.clone(), + lsp_id: "".to_string(), }; - write_lsp_invoice_meta(rpc, meta).await - .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + if let Err(e) = write_lsp_invoice_meta(&mut rpc, meta).await { + warn!("Failed to write LSP invoice meta: {}", e); + } return Ok(Response::new(pb::LspInvoiceResponse { bolt11: res.bolt11, @@ -361,11 +364,14 @@ impl Node for PluginNodeServer { label: invoice_label.clone(), payment_hash: res.payment_hash.clone(), requested_amount_msat, + expected_amount_msat: requested_amount_msat.saturating_sub(opening_fee_msat), bolt11: res.bolt11.clone(), + lsp_id, }; - write_lsp_invoice_meta(rpc, meta).await - .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + if let Err(e) = write_lsp_invoice_meta(&mut rpc, meta).await { + warn!("Failed to write LSP invoice meta: {}", e); + } Ok(Response::new(res.into())) } @@ -863,10 +869,10 @@ impl Node for PluginNodeServer { /// Writes `LspInvoiceMeta` using datastore request. `LspInvoiceMeta` is useful for defining /// some additional information regarding invoice being requested trough Greenlight. -async fn write_lsp_invoice_meta(mut rpc: tokio::sync::MutexGuard<'_, ClnRpc>, meta: LspInvoiceMeta) +async fn write_lsp_invoice_meta(rpc: &mut ClnRpc, meta: LspInvoiceMeta) -> Result<()> { - let record_serialized = serde_json::to_string(&meta) - .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + let record_serialized = + serde_json::to_string(&meta).context("failed to serialize LspInvoiceMeta")?; let datastore_req = cln_rpc::model::requests::DatastoreRequest { key: vec![ diff --git a/libs/gl-plugin/src/storage.rs b/libs/gl-plugin/src/storage.rs index e6c31abdd..80a106d98 100644 --- a/libs/gl-plugin/src/storage.rs +++ b/libs/gl-plugin/src/storage.rs @@ -84,5 +84,7 @@ pub struct LspInvoiceMeta { pub label: String, pub payment_hash: String, pub requested_amount_msat: u64, + pub expected_amount_msat: u64, pub bolt11: String, + pub lsp_id: String, } From 89734d182121e2c2a265e8099f24a4237ec0669b Mon Sep 17 00:00:00 2001 From: Nazarevsky Date: Wed, 2 Sep 2026 11:56:03 +0200 Subject: [PATCH 4/5] gl-plugin: make lsp_invoice store LspInvoiceMeta only on JIT channel request --- libs/gl-plugin/src/node/mod.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index 2aef13041..90b4e6183 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -263,19 +263,6 @@ impl Node for PluginNodeServer { .await .map_err(|e| Status::new(Code::Internal, e.to_string()))?; - let meta = LspInvoiceMeta { - label: req.label.clone(), - payment_hash: res.payment_hash.to_string(), - requested_amount_msat: req.amount_msat, - expected_amount_msat: 0, - bolt11: res.bolt11.clone(), - lsp_id: "".to_string(), - }; - - if let Err(e) = write_lsp_invoice_meta(&mut rpc, meta).await { - warn!("Failed to write LSP invoice meta: {}", e); - } - return Ok(Response::new(pb::LspInvoiceResponse { bolt11: res.bolt11, created_index: res.created_index.unwrap_or(0) as u32, From 453d4d8e58f3ef804e3f4d72cbff8af938e85521 Mon Sep 17 00:00:00 2001 From: Nazarevsky Date: Wed, 2 Sep 2026 12:13:17 +0200 Subject: [PATCH 5/5] gl-plugin: rename LspInvoiceMeta to JitRequestMeta, update descriptions Since we moved from storing metadata on an every invoice request to only JIT requets, the data structure name does not longer correspond to it's purpose. Therefore changed it to more appropriate one + updated description. --- libs/gl-plugin/src/node/mod.rs | 6 +++--- libs/gl-plugin/src/storage.rs | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index 90b4e6183..4afc07cbc 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::pb::{self, node_server::Node}; -use crate::storage::{LspInvoiceMeta, StateStore}; +use crate::storage::{JitRequestMeta, StateStore}; use crate::{messages, Event}; use crate::{stager, tramp}; use anyhow::{anyhow, Context, Error, Result}; @@ -347,7 +347,7 @@ impl Node for PluginNodeServer { // A JIT channel has now been negotiated with the LSP for this // invoice. So, we're storing some data with the original requested // amount. - let meta = LspInvoiceMeta { + let meta = JitRequestMeta { label: invoice_label.clone(), payment_hash: res.payment_hash.clone(), requested_amount_msat, @@ -856,7 +856,7 @@ impl Node for PluginNodeServer { /// Writes `LspInvoiceMeta` using datastore request. `LspInvoiceMeta` is useful for defining /// some additional information regarding invoice being requested trough Greenlight. -async fn write_lsp_invoice_meta(rpc: &mut ClnRpc, meta: LspInvoiceMeta) +async fn write_lsp_invoice_meta(rpc: &mut ClnRpc, meta: JitRequestMeta) -> Result<()> { let record_serialized = serde_json::to_string(&meta).context("failed to serialize LspInvoiceMeta")?; diff --git a/libs/gl-plugin/src/storage.rs b/libs/gl-plugin/src/storage.rs index 80a106d98..ea0c41861 100644 --- a/libs/gl-plugin/src/storage.rs +++ b/libs/gl-plugin/src/storage.rs @@ -68,23 +68,30 @@ impl StateStore for SledStateStore { } } -/// A structure that is used for storing invoices that are requested through -/// [Node::lsp_invoice](pb::node_server::Node::lsp_invoice) RPC call. lsp_invoice -/// call does not guarantee that the returned invoice is for requesting JIT -/// channel - if there is a channel with enough liquidity, a simple bolt11 invoice -/// is created. +/// A structure that is used for storing JIT channel requests metadata that is +/// requested through [Node::lsp_invoice](pb::node_server::Node::lsp_invoice) +/// RPC call. /// /// This structure is stored in CLN datastore. The reason of why do we need this /// structure instead of querying invoices table is that we want to distinguish /// incomming payments whether they were for JIT channel opening or just a simple -/// payment. Currently, CLN does not allow to do so, that's why this workaround +/// payment. Currently, CLN does not allow this, that's why this workaround /// exists. #[derive(Serialize, Deserialize)] -pub struct LspInvoiceMeta { +pub struct JitRequestMeta { + /// A label of the requested invoice. pub label: String, + /// Payment hash of the requested invoice. pub payment_hash: String, + /// The requested amount of msats. pub requested_amount_msat: u64, + /// The expected (reduced) amount if msats. + /// + /// Note that expected_amount_msat <= requested_amount_msat since + /// expected_amount_msat = requested_amount_msat + lsp_fee. pub expected_amount_msat: u64, + /// Original Bolt11 invoice which includes requested_amount_msat. pub bolt11: String, + /// ID of the LSP through which the invoice was requested. pub lsp_id: String, }