From c0872b25325f0321c605d1ab54a91e7051a4a28e 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 363304074a1562f1bdbf29c08cb1a4badd0ad571 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 0ac92c773..5aeb8f736 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::{ @@ -266,6 +266,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, @@ -326,6 +336,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" { @@ -343,6 +356,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())) } @@ -837,6 +864,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 72692f50601fdc6827eccadeb8d8dd33791b3055 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 5aeb8f736..a0cce478e 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -270,11 +270,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, @@ -364,11 +367,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())) } @@ -866,10 +872,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 a3019b684a92ea06c50e73967d72d885a978286a 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 a0cce478e..c985a4b21 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -266,19 +266,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 afe8879f42351390004c740c275ff2ef0e703d75 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 c985a4b21..c2c37e67f 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}; @@ -350,7 +350,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, @@ -859,7 +859,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, }