@@ -397,6 +397,27 @@ impl PeerState {
397397 } ) ;
398398 }
399399
400+ /// Removes a terminal order from state, allowing the operator to reclaim memory and free
401+ /// per-peer quota.
402+ ///
403+ /// Only orders in the `CompletedAndChannelOpened` or `FailedAndRefunded` terminal states
404+ /// may be pruned. For `FailedAndRefunded` orders this may be called before the payment
405+ /// invoice has expired, allowing the client to create new orders without being blocked by
406+ /// DoS protections.
407+ pub ( super ) fn prune_order ( & mut self , order_id : & LSPS1OrderId ) -> Result < ( ) , PeerStateError > {
408+ match self . outbound_channels_by_order_id . get ( order_id) {
409+ None => return Err ( PeerStateError :: UnknownOrderId ) ,
410+ Some ( order) => match order. state {
411+ ChannelOrderState :: CompletedAndChannelOpened { .. }
412+ | ChannelOrderState :: FailedAndRefunded { .. } => { } ,
413+ _ => return Err ( PeerStateError :: OrderNotPrunable ) ,
414+ } ,
415+ }
416+ self . outbound_channels_by_order_id . remove ( order_id) ;
417+ self . needs_persist |= true ;
418+ Ok ( ( ) )
419+ }
420+
400421 fn pending_requests_and_unpaid_orders ( & self ) -> usize {
401422 let pending_requests = self . pending_requests . len ( ) ;
402423 // We exclude paid and completed orders.
@@ -428,6 +449,7 @@ pub(super) enum PeerStateError {
428449 UnknownOrderId ,
429450 InvalidStateTransition ( ChannelOrderStateError ) ,
430451 TooManyPendingRequests ,
452+ OrderNotPrunable ,
431453}
432454
433455impl fmt:: Display for PeerStateError {
@@ -438,6 +460,9 @@ impl fmt::Display for PeerStateError {
438460 Self :: UnknownOrderId => write ! ( f, "unknown order id" ) ,
439461 Self :: InvalidStateTransition ( e) => write ! ( f, "{}" , e) ,
440462 Self :: TooManyPendingRequests => write ! ( f, "too many pending requests" ) ,
463+ Self :: OrderNotPrunable => {
464+ write ! ( f, "order is not in a terminal state and cannot be pruned" )
465+ } ,
441466 }
442467 }
443468}
@@ -778,4 +803,123 @@ mod tests {
778803 // Available in CompletedAndChannelOpened
779804 assert_eq ! ( state. channel_info( ) , Some ( & channel_info) ) ;
780805 }
806+
807+ fn create_test_order_params ( ) -> LSPS1OrderParams {
808+ LSPS1OrderParams {
809+ lsp_balance_sat : 100_000 ,
810+ client_balance_sat : 0 ,
811+ required_channel_confirmations : 0 ,
812+ funding_confirms_within_blocks : 6 ,
813+ channel_expiry_blocks : 144 ,
814+ token : None ,
815+ announce_channel : false ,
816+ }
817+ }
818+
819+ #[ test]
820+ fn test_prune_order_completed ( ) {
821+ let mut peer_state = PeerState :: default ( ) ;
822+ let order_id = LSPS1OrderId ( "order1" . to_string ( ) ) ;
823+ let payment_info = create_test_payment_info_bolt11_only ( ) ;
824+ peer_state. new_order (
825+ order_id. clone ( ) ,
826+ create_test_order_params ( ) ,
827+ LSPSDateTime :: from_str ( "2035-01-01T00:00:00Z" ) . unwrap ( ) ,
828+ payment_info,
829+ ) ;
830+
831+ peer_state. order_payment_received ( & order_id, PaymentMethod :: Bolt11 ) . unwrap ( ) ;
832+ peer_state. order_channel_opened ( & order_id, create_test_channel_info ( ) ) . unwrap ( ) ;
833+
834+ assert ! ( peer_state. prune_order( & order_id) . is_ok( ) ) ;
835+ assert ! ( peer_state. get_order( & order_id) . is_err( ) ) ;
836+ }
837+
838+ #[ test]
839+ fn test_prune_order_failed_and_refunded ( ) {
840+ let mut peer_state = PeerState :: default ( ) ;
841+ let order_id = LSPS1OrderId ( "order2" . to_string ( ) ) ;
842+ // Use a non-expired invoice (expires_at in the future) to verify we bypass expiry check.
843+ let payment_info = create_test_payment_info_bolt11_only ( ) ;
844+ peer_state. new_order (
845+ order_id. clone ( ) ,
846+ create_test_order_params ( ) ,
847+ LSPSDateTime :: from_str ( "2035-01-01T00:00:00Z" ) . unwrap ( ) ,
848+ payment_info,
849+ ) ;
850+ peer_state. order_failed_and_refunded ( & order_id) . unwrap ( ) ;
851+
852+ // Must succeed even though the invoice has not expired yet.
853+ assert ! ( peer_state. prune_order( & order_id) . is_ok( ) ) ;
854+ assert ! ( peer_state. get_order( & order_id) . is_err( ) ) ;
855+ }
856+
857+ #[ test]
858+ fn test_prune_order_non_terminal_fails ( ) {
859+ let mut peer_state = PeerState :: default ( ) ;
860+
861+ // ExpectingPayment is not prunable.
862+ let expecting_id = LSPS1OrderId ( "expecting" . to_string ( ) ) ;
863+ peer_state. new_order (
864+ expecting_id. clone ( ) ,
865+ create_test_order_params ( ) ,
866+ LSPSDateTime :: from_str ( "2035-01-01T00:00:00Z" ) . unwrap ( ) ,
867+ create_test_payment_info_bolt11_only ( ) ,
868+ ) ;
869+ assert ! ( matches!(
870+ peer_state. prune_order( & expecting_id) ,
871+ Err ( PeerStateError :: OrderNotPrunable )
872+ ) ) ;
873+
874+ // OrderPaid is not prunable.
875+ let paid_id = LSPS1OrderId ( "paid" . to_string ( ) ) ;
876+ peer_state. new_order (
877+ paid_id. clone ( ) ,
878+ create_test_order_params ( ) ,
879+ LSPSDateTime :: from_str ( "2035-01-01T00:00:00Z" ) . unwrap ( ) ,
880+ create_test_payment_info_bolt11_only ( ) ,
881+ ) ;
882+ peer_state. order_payment_received ( & paid_id, PaymentMethod :: Bolt11 ) . unwrap ( ) ;
883+ assert ! ( matches!( peer_state. prune_order( & paid_id) , Err ( PeerStateError :: OrderNotPrunable ) ) ) ;
884+ }
885+
886+ #[ test]
887+ fn test_prune_order_unknown_fails ( ) {
888+ let mut peer_state = PeerState :: default ( ) ;
889+ let unknown_id = LSPS1OrderId ( "nonexistent" . to_string ( ) ) ;
890+ assert ! ( matches!( peer_state. prune_order( & unknown_id) , Err ( PeerStateError :: UnknownOrderId ) ) ) ;
891+ }
892+
893+ #[ test]
894+ fn test_prune_order_frees_quota ( ) {
895+ let mut peer_state = PeerState :: default ( ) ;
896+
897+ // Fill up to the limit with FailedAndRefunded orders.
898+ for i in 0 ..MAX_PENDING_REQUESTS_PER_PEER {
899+ let order_id = LSPS1OrderId ( format ! ( "order{}" , i) ) ;
900+ peer_state. new_order (
901+ order_id. clone ( ) ,
902+ create_test_order_params ( ) ,
903+ LSPSDateTime :: from_str ( "2035-01-01T00:00:00Z" ) . unwrap ( ) ,
904+ create_test_payment_info_bolt11_only ( ) ,
905+ ) ;
906+ peer_state. order_failed_and_refunded ( & order_id) . unwrap ( ) ;
907+ }
908+
909+ // Registering another request must fail: quota is exhausted.
910+ let dummy_request = LSPS1Request :: GetInfo ( Default :: default ( ) ) ;
911+ assert ! ( matches!(
912+ peer_state. register_request( LSPSRequestId ( "r0" . to_string( ) ) , dummy_request. clone( ) ) ,
913+ Err ( PeerStateError :: TooManyPendingRequests )
914+ ) ) ;
915+
916+ // Prune one FailedAndRefunded order.
917+ let first_id = LSPS1OrderId ( "order0" . to_string ( ) ) ;
918+ peer_state. prune_order ( & first_id) . unwrap ( ) ;
919+
920+ // Now registering a new request must succeed.
921+ assert ! ( peer_state
922+ . register_request( LSPSRequestId ( "r1" . to_string( ) ) , dummy_request)
923+ . is_ok( ) ) ;
924+ }
781925}
0 commit comments