Skip to content

LSPS2: Add error handling events for failed client requests #3804

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 4 commits into
base: main
Choose a base branch
from

Conversation

martinsaposnic
Copy link
Contributor

Fixes #3459

Add GetInfoFailed and BuyRequestFailed event variants to LSPS2ClientEvent
to handle LSP error responses, similar to the OrderRequestFailed event
added to LSPS1.

Add GetInfoFailed and BuyRequestFailed event variants to LSPS2ClientEvent
to handle LSP error responses, similar to the OrderRequestFailed event
added to LSPS1.

Fixes lightningdevkit#3459
Update handle_get_info_error and handle_buy_error to:
- Emit GetInfoFailed and BuyRequestFailed events to notify users
- Return LightningError instead of Ok() to properly signal failures
- Use the actual error parameter instead of ignoring it

This ensures consistent error handling behavior across LSPS implementations.
Replace hardcoded BuyError responses with appropriate error types based on
the actual request (GetInfo vs Buy) when rate limiting is triggered.

This fixes incorrect error response types and provides an easy way to test
the error event handling added in this PR.
Add tests for the new error events:
- Test per-peer request limit rejection (GetInfoFailed)
- Test global request limit rejection (BuyRequestFailed)
- Test invalid token handling (GetInfoFailed)

These tests verify that error events are properly emitted when LSP
requests fail, ensuring the error handling behavior works end-to-end.
@ldk-reviews-bot
Copy link

ldk-reviews-bot commented May 27, 2025

I've assigned @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

codecov bot commented May 27, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 89.82%. Comparing base (101aa6f) to head (18e791d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3804      +/-   ##
==========================================
+ Coverage   89.76%   89.82%   +0.06%     
==========================================
  Files         159      159              
  Lines      128828   128850      +22     
  Branches   128828   128850      +22     
==========================================
+ Hits       115638   115737      +99     
+ Misses      10508    10432      -76     
+ Partials     2682     2681       -1     

☔ 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.

Copy link
Contributor

@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.

Thanks! Mostly looks good, just some comments.

Will review the test changes closer once #3712 landed and we rebased, as the same refactoring is happening there.

/// A request previously issued via [`LSPS2ClientHandler::select_opening_params`]
/// failed as the LSP returned an error response.
///
/// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, preexisting, but it seems we don't document on that method what the returned LSPSRequestId is for. Can we add that in a separate commit while we're here?

event_queue_notifier.enqueue(LSPS2ClientEvent::GetInfoFailed {
request_id: request_id.clone(),
counterparty_node_id: *counterparty_node_id,
error: error.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

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

We can avoid the clones here and below if we first construct the error result before enqueuing the event, which can then move request_id/error.

code: LSPS0_CLIENT_REJECTED_ERROR_CODE,
message: "Reached maximum number of pending requests. Please try again later."
Copy link
Contributor

Choose a reason for hiding this comment

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

No, it was very intentional that we hardcoded a standardized response here, because while we want to log the full details, the counterparty shouldn't learn anything about our rate limits.

Could you expand what else you mean with "incorrect error response types"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you expand what else you mean with "incorrect error response types"?

insert_pending_request is called by handle_get_info_request and handle_buy_request but it always returns BuyError, even if the request type is GetInfo. Now it returns GetInfoError for GetInfo requests and BuyError for Buy requests.

@ldk-reviews-bot
Copy link

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@ldk-reviews-bot
Copy link

✅ Added second reviewer: @joostjager

service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap();

let get_info_event = service_node.liquidity_manager.next_event().unwrap();
match get_info_event {
Copy link
Contributor

Choose a reason for hiding this comment

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

With something like this, you can keep the happy flow unindented for improved readability.

	let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo {
		request_id,
		counterparty_node_id,
		token,
	}) = service_node.liquidity_manager.next_event().unwrap() else {
		panic!("Unexpected event");
	};

Copy link
Contributor

Choose a reason for hiding this comment

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

Let-else bindings are only stabilized since rustc 1.65, so we can't use them currently due to our 1.63 MSRV.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm too bad. Still if let can I think remove one level of indent?

/// The node id of the LSP.
counterparty_node_id: PublicKey,
/// The error that was returned.
error: LSPSResponseError,
Copy link
Contributor

Choose a reason for hiding this comment

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

Would there be a point in putting these three fields in a struct and reusing that across lsps1 and 2?

Copy link
Contributor

Choose a reason for hiding this comment

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

No, please let's not break the current API pattern in a single place.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not in a single place, doing it everywhere. Just a thought, can understand you don't want to change existing API. Although in other cases, we aren't so conservative with API changes?

"Peer {} reached maximum number of total pending requests: {}",
if self.total_pending_requests.load(Ordering::Relaxed) >= MAX_TOTAL_PENDING_REQUESTS {
let error_message = format!(
"reached maximum number of total pending requests {}: {}",
Copy link
Contributor

Choose a reason for hiding this comment

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

Not capitalized anymore?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it useful to have counterparty_node_id in this message when the total is exceeded?

@@ -1226,45 +1226,43 @@ where
&self, peer_state_lock: &mut MutexGuard<'a, PeerState>, request_id: LSPSRequestId,
counterparty_node_id: PublicKey, request: LSPS2Request,
) -> (Result<(), LightningError>, Option<LSPSMessage>) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Pre-existing but shouldn't the type be Result<(), (LightningError, LSPSMessage>)?

Copy link
Contributor

@tnull tnull May 28, 2025

Choose a reason for hiding this comment

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

No, the method returns either Ok(()) or an error, and optionally a message that needs to be sent to the counterparty.

Copy link
Contributor

Choose a reason for hiding this comment

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

I thought that in this fn, it's indeed either Ok(()), or an error always accompanied by a msg (not optional). But maybe this is a useful type further up the call stack?

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.

LSPS2: Add LSPS2ClientEvent::RequestFailed event
4 participants