Skip to content

lightning-liquidity: Pre-/Refactors to prepare for persistence #4008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

tnull
Copy link
Contributor

@tnull tnull commented Aug 13, 2025

Before we can introduce persistence to the lightning-liquidity crate, we make a number of pre-/refactors to make our lives easier. We split this out here to keep PR sizes manageable and to introducing too many conflicts with concurrent work.

In this PR, we move some LSPS2/LSPS5 state data to dedicated types, which will allow use to use our serialization macros in the next step. We also simplify the last_notification_sent tracking in LSPS5 (now only tracking a single timestamp for all notification methods), which was requested on a previous PR. We furthermore now reset the notification cooldown on peer disconnection (useful in case we somehow notified last while the peer was connected), and move to prune the LSPS5 service state only on peer_{dis}connected, which should be more than enough.

(cc @martinsaposnic)

tnull added 5 commits August 13, 2025 10:32
.. which streamlines the `PaymentQueue` API a bit, but most importantly
can more easily get persisted using macros in the next step.
While bLIP-55 describes that the service should wait at least some
cooldown between sending notifications per individual `method`, there is
nothing that keeps us from simplifying our approach to apply the
cooldown to *any* notifications sent, especially since we just reduced
the cooldown period to 1 minute elsewhere. Here, we therefore simplify
the `last_notification_sent` field to just be a `Option<LSPSDateTime>`.
If we happened to send a notification while the client is connected to
us, we would previously only reset the cooldown once the client connects
again.

While theoretically it would be preferable to never set the
`last_notification_sent` field to begin with if the client is connected
to us, allowing the service handler to query the peer connection state
would be unnecessarily complex. Here, we therefore simply opt to also
reset the `last_notification_sent` state once the peer disconnects from
us.
Going forward, we'll add serialization logic for LSPS5 types. To contain
the persisted state a bit better (and to align the model with LSPS1/2),
we refactor the `LSPS5ServiceHandler` to hold a `PeerState` object.
@tnull tnull requested a review from TheBlueMatt August 13, 2025 09:04
@tnull tnull self-assigned this Aug 13, 2025
@tnull tnull added lightning-liquidity weekly goal Someone wants to land this this week labels Aug 13, 2025
@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Aug 13, 2025

👋 Thanks for assigning @TheBlueMatt 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.

@tnull tnull moved this to Goal: Merge in Weekly Goals Aug 13, 2025
Copy link

codecov bot commented Aug 13, 2025

Codecov Report

❌ Patch coverage is 97.50000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.84%. Comparing base (3b16c77) to head (1d490f2).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
lightning-liquidity/src/lsps5/service.rs 97.41% 3 Missing ⚠️
lightning-liquidity/src/manager.rs 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4008      +/-   ##
==========================================
- Coverage   88.85%   88.84%   -0.01%     
==========================================
  Files         175      175              
  Lines      127682   127713      +31     
  Branches   127682   127713      +31     
==========================================
+ Hits       113449   113470      +21     
- Misses      11675    11684       +9     
- Partials     2558     2559       +1     
Flag Coverage Δ
fuzzing 21.85% <0.00%> (-0.01%) ⬇️
tests 88.68% <97.50%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tnull added 3 commits August 13, 2025 14:22
Previously, we'd constantly check whether or not we can prune stale
webhooks. While not wrong, it lead to a bunch of ~unnecessary
operations, especially given that we only prune once a day currently.
Here we move pruning to `peer_connected`/`peer_disconnected`, which is
similar to what we do for LSPS2, and should still be more than enough.
We add the license header to all files in `lightning-liquidity` where it
was absent.
@tnull tnull force-pushed the 2025-08-liquidity-persistence-prefactors branch from 7447f32 to 1d490f2 Compare August 13, 2025 12:23
webhook.last_used = now;
webhook.last_notification_sent = Some(now);
Copy link
Contributor

Choose a reason for hiding this comment

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

this was not introduced in this PR, but it's weird that last_used and last_notification_sent are set before send_notification(), which can return an error (the thing that can fail is the sign_message function which there is a good chance it will never fail, but worth mentioning IMO)

Comment on lines +164 to +166
let now = LSPSDateTime::new_from_duration_since_epoch(
self.time_provider.duration_since_epoch(),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

you can re-use the now value from above

_counterparty_node_id: counterparty_node_id,
last_used: now,
last_notification_sent: new_hash_map(),
if let Some(webhook) = peer_state_lock.webhook_mut(&params.app_name.clone()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the clone is not necessary here

}

// Returns whether the entire state is empty and can be pruned.
fn prune_stale_webhooks(&mut self, now: LSPSDateTime) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

this function name is kind of confusing, it sounds like an action but it's not

if let Some(webhook) = peer_state_lock.webhook_mut(&params.app_name.clone()) {
no_change = webhook.url == params.webhook;
if !no_change {
webhook.last_used = now
Copy link
Contributor

Choose a reason for hiding this comment

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

in here you need to set the webhook.url to params.webhook. if not, the update webhook functionality will be broken.

unfortunately, right now the tests are not testing the webhook update feature. they only test that the notification is sent with the updated url, but they don't test that the url is actually updated and persisted :(

here is a regression test that passes on main but fails on this branch

#[test]
fn webhook_update_affects_future_notifications() {
	let mock_time_provider = Arc::new(MockTimeProvider::new(1000));
	let time_provider = Arc::<MockTimeProvider>::clone(&mock_time_provider);
	let chanmon_cfgs = create_chanmon_cfgs(2);
	let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
	let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
	let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
	let (lsps_nodes, _) = lsps5_test_setup(nodes, time_provider);
	let LSPSNodes { service_node, client_node } = lsps_nodes;
	let service_node_id = service_node.inner.node.get_our_node_id();
	let client_node_id = client_node.inner.node.get_our_node_id();
	let client_handler = client_node.liquidity_manager.lsps5_client_handler().unwrap();
	let service_handler = service_node.liquidity_manager.lsps5_service_handler().unwrap();

	let app = "UpdateTestApp";
	let url_v1 = "https://example.org/v1";
	let url_v2 = "https://example.org/v2";

	// register v1
	client_handler.set_webhook(service_node_id, app.into(), url_v1.into()).unwrap();
	let req = get_lsps_message!(client_node, service_node_id);
	service_node.liquidity_manager.handle_custom_message(req, client_node_id).unwrap();
	let _ = service_node.liquidity_manager.next_event().unwrap(); // initial webhook_registered
	let resp = get_lsps_message!(service_node, client_node_id);
	client_node.liquidity_manager.handle_custom_message(resp, service_node_id).unwrap();
	let _ = client_node.liquidity_manager.next_event().unwrap();

	// update to v2
	client_handler.set_webhook(service_node_id, app.into(), url_v2.into()).unwrap();
	let upd_req = get_lsps_message!(client_node, service_node_id);
	service_node.liquidity_manager.handle_custom_message(upd_req, client_node_id).unwrap();
	let update_event = service_node.liquidity_manager.next_event().unwrap();
	match update_event {
		LiquidityEvent::LSPS5Service(LSPS5ServiceEvent::SendWebhookNotification {
			url, ..
		}) => {
			assert_eq!(url.as_str(), url_v2);
		},
		_ => panic!("Expected webhook_registered for update"),
	}
	let upd_resp = get_lsps_message!(service_node, client_node_id);
	client_node.liquidity_manager.handle_custom_message(upd_resp, service_node_id).unwrap();
	let _ = client_node.liquidity_manager.next_event().unwrap();

	// Advance past cooldown and send a notification again
	mock_time_provider.advance_time(NOTIFICATION_COOLDOWN_TIME.as_secs() + 1);
	service_handler.notify_payment_incoming(client_node_id).unwrap();
	let ev = service_node.liquidity_manager.next_event().unwrap();
	match ev {
		LiquidityEvent::LSPS5Service(LSPS5ServiceEvent::SendWebhookNotification {
			url,
			notification,
			..
		}) => {
			assert_eq!(notification.method, WebhookNotificationMethod::LSPS5PaymentIncoming);
			assert_eq!(url.as_str(), url_v2, "Should target updated URL");
		},
		_ => panic!("Expected SendWebhookNotification after update"),
	}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

you also need to set last_notification_sent to None so you don't carry the old cooldown to the new url

Copy link
Contributor

Choose a reason for hiding this comment

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

so you don't carry the old cooldown to the new url

we can add a test that asserts that a notification can be sent immediately after updating a webhook

@martinsaposnic
Copy link
Contributor

@tnull left a few small comments but otherwise looks good!

&mut self.webhooks
}

fn app_names(&self) -> Vec<LSPS5AppName> {
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 used in two places as app_names().len(), let's push the allocation to the callsite with a FixedSizeIterator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lightning-liquidity weekly goal Someone wants to land this this week
Projects
Status: Goal: Merge
Development

Successfully merging this pull request may close these issues.

4 participants