Skip to content

Conversation

joostjager
Copy link
Contributor

@joostjager joostjager commented Aug 19, 2025

Wires up async payments functionality in ldk-node. This PR only covers the static invoice creation part. In a follow up, the full mechanism will be put in place to hold an htlc sender-side until the receiver comes online again.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Aug 19, 2025

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

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

I realize this is in draft for good reason, just leaving some early comments here.

@joostjager joostjager self-assigned this Aug 21, 2025
@joostjager joostjager force-pushed the static-invoice-server branch 2 times, most recently from 4bcdc95 to e2f293b Compare August 27, 2025 14:50
min_interval: Duration,
}

impl RateLimiter {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Keeping it simple here


pub(crate) struct StaticInvoiceStore {
kv_store: Arc<DynStore>,
rate_limiter: Mutex<RateLimiter>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Needed to add Mutex here even though I don't think it is used concurrently. Otherwise handle_event needed to be made mutable, the Arc higher up no longer working, etc.

}
}

pub(crate) async fn handle_static_invoice_requested(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Async, prepare for async kv store.

@joostjager joostjager force-pushed the static-invoice-server branch 6 times, most recently from 0c92e33 to 5c43a3b Compare September 3, 2025 12:54
@joostjager joostjager requested a review from tnull September 4, 2025 12:01
@joostjager joostjager marked this pull request as ready for review September 4, 2025 12:04
@joostjager joostjager force-pushed the static-invoice-server branch from 5c43a3b to 472cf60 Compare September 5, 2025 10:14
@joostjager
Copy link
Contributor Author

Added bindings

@joostjager joostjager force-pushed the static-invoice-server branch from 472cf60 to 16ad843 Compare September 5, 2025 12:36
@joostjager
Copy link
Contributor Author

Made rate limiter a bit less simplistic by adding a leaky bucket. Otherwise the second call of two consecutive calls immediately fails.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @tnull! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Took a first look, have yet to review the rate limiter logic and tests closer.

src/lib.rs Outdated
/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
pub fn blinded_paths_for_async_recipient(
&self, recipient_id: Vec<u8>,
) -> Result<Vec<u8>, Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the Rust API, we def. still want to return the actual BlindedMessagePaths, just for the uniffi interface it's okay to fallback to a bytes for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why would it be required to know the details of the blinded paths for the Rust API and not for the other APIs? Not sure which use that unlocks.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why would it be required to know the details of the blinded paths for the Rust API and not for the other APIs? Not sure which use that unlocks.

Users might want to expect what they're paying to. It's fine to take a shortcut here for bindings, but please use the full Rust type in the Rust API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not about what they are paying to. It's the path that the receiver can use to contact the static invoice server. Because it is blinded, they still don't know where that server is potentially?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We try to use the proper Rust types where possible, and I really don't see why we shouldn't in this case. In case of set_paths_to_static_invoice_server users could theoretically handcraft a blinded path somehow if their service implementation doesn't exactly meet our serialization and try to use it. Here we'd just match that API type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think consistency between languages is more important. Especially if the handcrafted blinded path is speculative. But no blocker, I've updated the code with distinct method signatures.

@joostjager joostjager force-pushed the static-invoice-server branch 2 times, most recently from 04a3c30 to 514676c Compare September 8, 2025 13:37
@joostjager
Copy link
Contributor Author

Unit tests added for static invoice store and rate limiter.

@joostjager
Copy link
Contributor Author

Added async services config flag

@joostjager joostjager force-pushed the static-invoice-server branch from b1ca4e2 to 5b489c2 Compare September 9, 2025 09:09
@joostjager joostjager requested a review from tnull September 9, 2025 09:54
@joostjager joostjager force-pushed the static-invoice-server branch 5 times, most recently from 278669e to 4225a5f Compare September 10, 2025 09:00
@joostjager joostjager changed the base branch from main to develop September 10, 2025 09:00
Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Mostly looks good, some comments/questions.

{
Ok(_) => {},
Err(e) => {
log_error!(self.logger, "Failed to persist static invoice: {}", e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to return Err(ReplayEvent()) here and below in case of persistence failure to be able to retry?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Also static_invoice_persisted shouldn't be called in that case. Fixed here and below.

src/io/mod.rs Outdated

// Static invoices will be persisted at "static_invoices/<sha256(recipient_id)>/<invoice_slot>".
//
// Example: static_invoices/039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81/001f
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is out-of-date now that we don't serialize invoice_slot as hex?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, let's move this example to the static invoice store, as this is just the primary namespace const. We should however cross-link to the the respective LDK docs here so that readers know what 'static invoices' are in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Decimal invoice slot, example moved, LDK link added.

src/io/mod.rs Outdated
// Static invoices will be persisted at "static_invoices/<sha256(recipient_id)>/<invoice_slot>".
//
// Example: static_invoices/039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81/001f
pub(crate) const STATIC_INVOICES_PRIMARY_NAMESPACE: &str = "static_invoices";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
pub(crate) const STATIC_INVOICES_PRIMARY_NAMESPACE: &str = "static_invoices";
pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices";

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if I like this better. It's not the store that is stored, but the invoices. I don't really mind either. Changed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This and the other new files need a module description and a copyright header.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added


pub(crate) struct StaticInvoiceStore {
kv_store: Arc<DynStore>,
request_rate_limiter: Mutex<RateLimiter>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we really need two independent rate limiters here or would a single one not suffice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought that it wouldn't be nice if a recipient can't update their invoice because someone else is keeping the limited active with requests.

mod tests {
use std::{sync::Arc, time::Duration};

use bitcoin::{
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: As mentioned elsewhere previously, module-level grouping of imports would be preferred.

I'm starting to wonder if we should go 'bleeding edge' here and introduce a nightly rustfmt CI job, which would indeed support dealing with the import groups for us:

group_imports = "StdExternalCrate"
reorder_imports = true
imports_granularity = "Module"

Copy link
Contributor Author

@joostjager joostjager Sep 11, 2025

Choose a reason for hiding this comment

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

That would be great. FIxing import groups isn't something that humans should be required to do, especially because some people don't even care for them.

logger: Arc<Logger>,
}

impl Bolt12Payment {
pub(crate) fn new(
channel_manager: Arc<ChannelManager>, payment_store: Arc<PaymentStore>,
is_running: Arc<RwLock<bool>>, logger: Arc<Logger>,
async_payment_services_enabled: bool, is_running: Arc<RwLock<bool>>, logger: Arc<Logger>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, let's just forward the full Config as we might need it for other things in the future, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't want to add a dependency to the full object if a bool suffices. Also makes it more clear what part of config matters. Maybe we don't need things in the future. But no strong objection, so changed.

/// Will only return an offer if [`Bolt12Payment::set_paths_to_static_invoice_server`] was called and we succeeded
/// in interactively building a [`StaticInvoice`] with the static invoice server.
///
/// Useful for posting offers to receive payments later, such as posting an offer on a website.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Mind adding an 'experimental feature' note here and on the other main APIs?

Copy link
Contributor Author

@joostjager joostjager Sep 11, 2025

Choose a reason for hiding this comment

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

I can do that, but in what regard is it experimental? Added

)
}

pub(crate) async fn handle_persist_static_invoice(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to check whether we actually know/have a channel open with the recipient? Or is all that fully handled on the LDK side already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline. At this point, I think it is part of the integration of ldk-node into the wider system to ensure that blinded_paths_for_async_recipient isn't called without restrictions.

@joostjager joostjager force-pushed the static-invoice-server branch from 4225a5f to e76d6d7 Compare September 11, 2025 10:05
@joostjager
Copy link
Contributor Author

Squashed previous fixups and added new fix up that addresses review comments.

@joostjager joostjager requested a review from tnull September 11, 2025 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

3 participants