Skip to content

gl-plugin: make lsp_invoice RPC call store additional invoice metadata - #742

Open
Nazarevsky wants to merge 2 commits into
mainfrom
feature/lsp-invoice-meta-storage
Open

gl-plugin: make lsp_invoice RPC call store additional invoice metadata#742
Nazarevsky wants to merge 2 commits into
mainfrom
feature/lsp-invoice-meta-storage

Conversation

@Nazarevsky

Copy link
Copy Markdown
Member

Problem

Currently, when a client requests for lsp invoice (JIT channel opening), no information about the original invoice is stored. Basically, the client hands out the requested invoice to a payer but receives only reduced amount. We'd like to have a way of understanding whether the requested invoice was for channel opening or a simple payment.

Solution

Introduced a structure LspInvoiceMeta which contains all information that we need to store (specifically, bolt11 invoice). Use datastore requests in order to store metadata. LspInvoiceMeta is stored in both cases - if the invoice was requested from the LSP or not.

Changes

  • introduced LspInvoiceMeta structure for storing meta
  • updated lsp_invoice RPC call to store LspInvoiceMeta

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.
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.
@Nazarevsky
Nazarevsky requested a review from nepet August 25, 2026 16:14
@Nazarevsky Nazarevsky self-assigned this Aug 25, 2026

@nepet nepet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Nazarevsky for putting this together.

The approach makes sense and the doc comment on LspInvoiceMeta does a good job explaining why this workaround is needed, which will save the next person a lot of time.

One general note to keep in mind, not a blocker: nothing cleans these entries up on invoice expiry or payment so far, so they accumulate indefinitely. May not be a problem though, if we only store "lsp_invoices" and if we don't expect them to be created indefinitely.

Comment on lines +269 to +278
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()))?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to store the invoice meta data for a regular invoice. It will just double what we already have. Storing it for a jit-channel invoice is enough to identify it as such: If it's in the datastore, it automatically is a jit-channel invoice.

pub label: String,
pub payment_hash: String,
pub requested_amount_msat: u64,
pub bolt11: String,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we could also store the peer_id and the expected_amount_msat. But feel free to leave it out if it's not required.

Comment on lines +370 to +371
write_lsp_invoice_meta(rpc, meta).await
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to fail the whole RPC call in the (unexpected) case that we can't write the meta data? We already negotiated a jit-channel invoice and stored it on CLN. I think it's enough to just log::warn and continue. We won't loose much.


/// 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The signature is a bit weird, compared to the other callers of ClnRpc in this file. This function takes ownership of the lock, which means that the caller can't use rpc afterwards. Every other helper in this file takes &mut rpc:

async write_lsp_invoice_meta(rpc: &mut ClnRpc, meta: LspInvoiceMeta)

Comment on lines +871 to +872
let record_serialized = serde_json::to_string(&meta)
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This function returns an anyhow::Result. The extra serialization into a tonic::Status seems a bit useless. It's coerced into anyhow::Error and the caller converts it back into a tonic::Status.

Suggested change
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")?;

Comment on lines +874 to +879
let datastore_req = cln_rpc::model::requests::DatastoreRequest {
key: vec![
"gl".to_string(),
"jit_channels".to_string(),
meta.label,
],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on what info the reading part actually has, I'd rather use the payment_hash as the preferred key instead of the label - which by the way can be an ugly (maybe even unbounded) string.

Suggested change
let datastore_req = cln_rpc::model::requests::DatastoreRequest {
key: vec![
"gl".to_string(),
"jit_channels".to_string(),
meta.label,
],
let datastore_req = cln_rpc::model::requests::DatastoreRequest {
key: vec![
"gl".to_string(),
"lsp_invoices".to_string(),
meta.payment_hash,
],

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants