lightning-liquidity: Refactor LSPS1 service-side#4282
lightning-liquidity: Refactor LSPS1 service-side#4282tnull wants to merge 41 commits intolightningdevkit:mainfrom
lightning-liquidity: Refactor LSPS1 service-side#4282Conversation
|
👋 Thanks for assigning @TheBlueMatt as a reviewer! |
773316a to
fb519ab
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4282 +/- ##
==========================================
+ Coverage 85.91% 86.08% +0.16%
==========================================
Files 156 158 +2
Lines 103958 104995 +1037
Branches 103958 104995 +1037
==========================================
+ Hits 89317 90381 +1064
+ Misses 12122 12016 -106
- Partials 2519 2598 +79
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
b96685b to
c6eb6b3
Compare
c6eb6b3 to
a2aa7c3
Compare
|
Rebased to resolve conflicts. |
|
Should be good for review. |
|
🔔 1st Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
| && payment_details.onchain.is_some() | ||
| { | ||
| // bLIP-51: 'LSP MUST disable on-chain payments if the client omits this field.' | ||
| let err = "Onchain payments must be disabled if no refund_onchain_address is set.".to_string(); |
There was a problem hiding this comment.
This requirement doesn't appear to be documented in LSPS1ServiceEvent::RequestForPaymentDetails or LSPS1PaymentInfo, but I'm not sure we should bother erroring here vs just removing the unsupported option before sending the order to the peer?
There was a problem hiding this comment.
Ah, good point. Turns out we didn't expose refund_onchain_address anywhere. I now added a fixup documenting the requirement on both the method and the event, added a refund_onchain_address field to the event and a onchain_payment_required method allowing the LSP to reject a request for this reason.
I think auto-stripping the onchain payment variant isn't great as we might end up without any payment variant, and would need to auto-reject the request then. Might be preferable to leave that up to the LSP in general?
| ) -> ChannelOrder { | ||
| let state = ChannelOrderState::new(payment_details); | ||
| let channel_order = ChannelOrder { order_params, state, created_at }; | ||
| self.outbound_channels_by_order_id.insert(order_id, channel_order.clone()); |
There was a problem hiding this comment.
might want some kind of size limit here to limit dos, tho we could also just do it at the start of the flow.
There was a problem hiding this comment.
Yeah, I now added a commit following the approach we took for LSPS2, i.e., adding MAX_TOTAL_PEERS, MAX_REQUESTS_PER_PEER and MAX_TOTAL_PENDING_REQUESTS limits.
| let msg = LSPS1Message::Response(request_id.clone(), response).into(); | ||
| message_queue_notifier.enqueue(counterparty_node_id, msg); | ||
| let err = format!("Failed to handle request due to: {}", e); | ||
| let action = ErrorAction::IgnoreAndLog(Level::Error); |
There was a problem hiding this comment.
Here and likely elsewhere, we really can't log at Error just because someone sends us a bogus message.
There was a problem hiding this comment.
Hmm, note that we already silently fail if we can't parse the message at all. This basically follows what we do elsewhere in the codebase. I think we should do #3492 as a follow-up to make sure we follow the same approach everywhere. I'll tag that 0.3
There was a problem hiding this comment.
Hmm, okay. I'm not a fan of making things worse than it is only to fix it later, but...
| #[cfg(not(feature = "time"))] | ||
| { | ||
| // TODO: We need to find a way to check expiry times in no-std builds. | ||
| all_payment_details_expired = false; |
There was a problem hiding this comment.
Probably can just insta-remove things that are failed, at least.
There was a problem hiding this comment.
I don't think so, as we still want to show the Failed state back to the user querying their order status.
| self.state, | ||
| ChannelOrderState::ExpectingPayment { .. } | ||
| | ChannelOrderState::FailedAndRefunded { .. } | ||
| ); |
There was a problem hiding this comment.
Do we ever want to prune things that were completed? A two year old channel lease that expired 18 months ago probably isn't interesting?
There was a problem hiding this comment.
Yes, probably, and we have the same issue with LSPS2. I think it would be good to add a consistent API that works for both in a follow up. Could be to leave it to the user to manually call a prune method, or possibly set an auto-prune config flag in the respective service configs? Anyway, I'd prefer to leave that as a follow-up.
|
👋 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. |
3ae7578 to
4489325
Compare
|
🔔 1st Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
| let pending_orders = self | ||
| .outbound_channels_by_order_id | ||
| .iter() | ||
| .filter(|(_, v)| v.is_pending_payment()) |
There was a problem hiding this comment.
No, we cannot simply ignore some subset of things taking up memory because of their state. This is a DoS vector all the same.
There was a problem hiding this comment.
Hmm? But as long as we don't expose a way to cleanup the historical state, not only accounting for entries pending payment will lead to the client eventually "filling up" their quota of channels? (also cf #4282 (comment))
This really only copies what we already do for LSPS2, FWIW.
We add the serializations for all types that will be persisted as part of the `PeerState`.
We follow the model already employed in LSPS2/LSPS5 and implement state pruning and persistence for `LSPS1ServiceHandler` state. Signed-off-by: Elias Rohrer <dev@tnull.de>
.. we read the persisted state in `LiquidityManager::new` Signed-off-by: Elias Rohrer <dev@tnull.de>
Co-authored by Claude AI
As per spec, we check that the user provides at least one payment detail *and* that they don't provide onchain payment details if `refund_onchain_address` is unset.
.. as there's no need to do so.
We add a method that allows the LSP to signal to the client the token they used was invalid. We use the `102` error code as proposed in lightning/blips#68.
We test the just-added API. Co-authored by Claude AI
This refactors `ChannelOrder` to use an internal state machine enum `ChannelOrderState` that: - Encapsulates state-specific data in variants (e.g., `channel_info` only available in `CompletedAndChannelOpened`) - Provides type-safe state transitions - Replaces the generic `update_order_status` API with specific transition methods: `order_payment_received`, `order_channel_opened`, and `order_failed_and_refunded` The state machine has four states: - `ExpectingPayment`: Initial state, awaiting payment - `OrderPaid`: Payment received, awaiting channel open - `CompletedAndChannelOpened`: Terminal state with channel info - `FailedAndRefunded`: Terminal state for failed/refunded orders Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
Add two new integration tests to cover the new public API methods: - `lsps1_order_state_transitions`: Tests the full flow of `order_payment_received` followed by `order_channel_opened`, verifying that payment states are updated correctly and channel info is returned after the channel is opened. - `lsps1_order_failed_and_refunded`: Tests the `order_failed_and_refunded` method, verifying that payment states are set to Refunded. Co-Authored-By: HAL 9000
Add `lsps1_expired_orders_are_pruned_and_not_persisted` test that verifies: - Orders with expired payment details (expires_at in the past) are accessible before persist() is called - After persist() is called, expired orders in ExpectingPayment state are pruned and no longer accessible - Pruned orders are not recovered after restart, confirming that the pruning also removes the persisted state Co-Authored-By: HAL 9000
The bLIP-51 specification defines a `HOLD` intermediate payment state: - `EXPECT_PAYMENT` -> `HOLD` -> `PAID` (success path) - `EXPECT_PAYMENT` -> `REFUNDED` (failure before payment) - `HOLD` -> `REFUNDED` (failure after payment received) This commit adds the `Hold` variant to `LSPS1PaymentState` and updates the state machine transitions: - `payment_received()` now sets payment state to `Hold` (not `Paid`) - `channel_opened()` transitions payment state from `Hold` to `Paid` - Tests updated to verify the correct state at each transition This allows LSPs to properly communicate when a payment has been received but the channel has not yet been opened (e.g., Lightning HTLC held, or on-chain tx detected but channel funding not published). Co-Authored-By: HAL 9000
Turns out this was another variant we didn't actually use anywhere. So we're dropping it.
We previously had no way to reject requests in case the LSP requires onchain payment while the client not providing `refund_onchain_address`. Here we add a method allowing to do so.
Add per-peer and global rate limiting to `LSPS1ServiceHandler` to prevent resource exhaustion, mirroring the existing LSPS2 pattern. Introduce `MAX_PENDING_REQUESTS_PER_PEER` (10), `MAX_TOTAL_PENDING_REQUESTS` (1000), and `MAX_TOTAL_PEERS` (100000) constants and enforce them in `handle_create_order_request`. Rejected requests receive a `CreateOrderError` with `LSPS0_CLIENT_REJECTED_ERROR_CODE`. A `total_pending_requests` atomic counter tracks the global count, and a `verify_pending_request_counter` debug assertion ensures it stays in sync. Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
Signed-off-by: Elias Rohrer <dev@tnull.de>
ed6d0c9 to
104eb5f
Compare
|
Addressed pending comments and rebased to resolve some minor conflicts. Let me know if I can squash fixups. |
Closes #3480.
We 'refactor' (rewrite) the
LSPS1ServiceHandler, move state handling to a dedicatedPeerState, add an STM pattern, add persistence for the service state, add some more critical API paths, add test coverage, and finally remove thecfg(lsps1_service)flag.