From 4c03e469288430e592fecad4e889de2d7d42ebb1 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 23:35:19 -0300 Subject: [PATCH 01/36] looprpc: expose static loop-in monitor state --- liquidity/parameters.go | 11 +- liquidity/parameters_test.go | 53 +++++++ looprpc/client.pb.go | 288 ++++++++++++++++++++--------------- looprpc/client.proto | 12 +- looprpc/client.swagger.json | 11 +- 5 files changed, 246 insertions(+), 129 deletions(-) diff --git a/liquidity/parameters.go b/liquidity/parameters.go index 9e8e5cc0..3b365cc0 100644 --- a/liquidity/parameters.go +++ b/liquidity/parameters.go @@ -400,9 +400,16 @@ func rpcToFee(req *clientrpc.LiquidityParameters) (FeeLimit, error) { // rpcToRule switches on rpc rule type to convert to our rule interface. func rpcToRule(rule *clientrpc.LiquidityRule) (*SwapRule, error) { - swapType := swap.TypeOut - if rule.SwapType == clientrpc.SwapType_LOOP_IN { + var swapType swap.Type + switch rule.SwapType { + case clientrpc.SwapType_LOOP_OUT: + swapType = swap.TypeOut + + case clientrpc.SwapType_LOOP_IN: swapType = swap.TypeIn + + default: + return nil, fmt.Errorf("unknown swap type: %v", rule.SwapType) } switch rule.Type { diff --git a/liquidity/parameters_test.go b/liquidity/parameters_test.go index 0e54dbf1..7af73c0d 100644 --- a/liquidity/parameters_test.go +++ b/liquidity/parameters_test.go @@ -3,9 +3,62 @@ package liquidity import ( "testing" + clientrpc "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/swap" "github.com/stretchr/testify/require" ) +// TestRPCToRuleSwapType verifies RPC swap type conversion. +func TestRPCToRuleSwapType(t *testing.T) { + tests := []struct { + name string + swapType clientrpc.SwapType + wantType swap.Type + wantErr bool + }{ + { + name: "loop out", + swapType: clientrpc.SwapType_LOOP_OUT, + wantType: swap.TypeOut, + }, + { + name: "loop in", + swapType: clientrpc.SwapType_LOOP_IN, + wantType: swap.TypeIn, + }, + { + name: "static loop in rejected", + swapType: clientrpc.SwapType_STATIC_LOOP_IN, + wantErr: true, + }, + { + name: "unknown swap type rejected", + swapType: clientrpc.SwapType(99), + wantErr: true, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + rpcRule := &clientrpc.LiquidityRule{ + Type: clientrpc.LiquidityRuleType_THRESHOLD, + IncomingThreshold: 10, + OutgoingThreshold: 20, + SwapType: testCase.swapType, + } + + got, err := rpcToRule(rpcRule) + if testCase.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, testCase.wantType, got.Type) + }) + } +} + // TestValidateRestrictions tests validating client restrictions against a set // of server restrictions. func TestValidateRestrictions(t *testing.T) { diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index 3305bfcd..ec324648 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -80,6 +80,8 @@ const ( SwapType_LOOP_OUT SwapType = 0 // LOOP_IN indicates a loop in swap (on-chain to off-chain) SwapType_LOOP_IN SwapType = 1 + // STATIC_LOOP_IN indicates a static address loop in swap. + SwapType_STATIC_LOOP_IN SwapType = 2 ) // Enum value maps for SwapType. @@ -87,10 +89,12 @@ var ( SwapType_name = map[int32]string{ 0: "LOOP_OUT", 1: "LOOP_IN", + 2: "STATIC_LOOP_IN", } SwapType_value = map[string]int32{ - "LOOP_OUT": 0, - "LOOP_IN": 1, + "LOOP_OUT": 0, + "LOOP_IN": 1, + "STATIC_LOOP_IN": 2, } ) @@ -1474,8 +1478,12 @@ type SwapStatus struct { IdBytes []byte `protobuf:"bytes,11,opt,name=id_bytes,json=idBytes,proto3" json:"id_bytes,omitempty"` // The type of the swap. Type SwapType `protobuf:"varint,3,opt,name=type,proto3,enum=looprpc.SwapType" json:"type,omitempty"` - // State the swap is currently in, see State enum. + // Generic loop-in/loop-out state for swaps. State SwapState `protobuf:"varint,4,opt,name=state,proto3,enum=looprpc.SwapState" json:"state,omitempty"` + // Types that are valid to be assigned to StaticLoopInStateOptional: + // + // *SwapStatus_StaticLoopInState + StaticLoopInStateOptional isSwapStatus_StaticLoopInStateOptional `protobuf_oneof:"static_loop_in_state_optional"` // A failure reason for the swap, only set if the swap has failed. FailureReason FailureReason `protobuf:"varint,14,opt,name=failure_reason,json=failureReason,proto3,enum=looprpc.FailureReason" json:"failure_reason,omitempty"` // Initiation time of the swap. @@ -1578,6 +1586,22 @@ func (x *SwapStatus) GetState() SwapState { return SwapState_INITIATED } +func (x *SwapStatus) GetStaticLoopInStateOptional() isSwapStatus_StaticLoopInStateOptional { + if x != nil { + return x.StaticLoopInStateOptional + } + return nil +} + +func (x *SwapStatus) GetStaticLoopInState() StaticAddressLoopInSwapState { + if x != nil { + if x, ok := x.StaticLoopInStateOptional.(*SwapStatus_StaticLoopInState); ok { + return x.StaticLoopInState + } + } + return StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE +} + func (x *SwapStatus) GetFailureReason() FailureReason { if x != nil { return x.FailureReason @@ -1670,6 +1694,17 @@ func (x *SwapStatus) GetAssetInfo() *AssetLoopOutInfo { return nil } +type isSwapStatus_StaticLoopInStateOptional interface { + isSwapStatus_StaticLoopInStateOptional() +} + +type SwapStatus_StaticLoopInState struct { + // Static address loop-in FSM state when type is STATIC_LOOP_IN. + StaticLoopInState StaticAddressLoopInSwapState `protobuf:"varint,20,opt,name=static_loop_in_state,json=staticLoopInState,proto3,enum=looprpc.StaticAddressLoopInSwapState,oneof"` +} + +func (*SwapStatus_StaticLoopInState) isSwapStatus_StaticLoopInStateOptional() {} + type ListSwapsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Optional filter to only return swaps that match the filter. @@ -6704,14 +6739,15 @@ const file_client_proto_rawDesc = "" + "\x12htlc_address_p2wsh\x18\x05 \x01(\tR\x10htlcAddressP2wsh\x12*\n" + "\x11htlc_address_p2tr\x18\a \x01(\tR\x0fhtlcAddressP2tr\x12%\n" + "\x0eserver_message\x18\x06 \x01(\tR\rserverMessageJ\x04\b\x04\x10\x05\"\x10\n" + - "\x0eMonitorRequest\"\xb1\x05\n" + + "\x0eMonitorRequest\"\xac\x06\n" + "\n" + "SwapStatus\x12\x10\n" + "\x03amt\x18\x01 \x01(\x03R\x03amt\x12\x12\n" + "\x02id\x18\x02 \x01(\tB\x02\x18\x01R\x02id\x12\x19\n" + "\bid_bytes\x18\v \x01(\fR\aidBytes\x12%\n" + "\x04type\x18\x03 \x01(\x0e2\x11.looprpc.SwapTypeR\x04type\x12(\n" + - "\x05state\x18\x04 \x01(\x0e2\x12.looprpc.SwapStateR\x05state\x12=\n" + + "\x05state\x18\x04 \x01(\x0e2\x12.looprpc.SwapStateR\x05state\x12X\n" + + "\x14static_loop_in_state\x18\x14 \x01(\x0e2%.looprpc.StaticAddressLoopInSwapStateH\x00R\x11staticLoopInState\x12=\n" + "\x0efailure_reason\x18\x0e \x01(\x0e2\x16.looprpc.FailureReasonR\rfailureReason\x12'\n" + "\x0finitiation_time\x18\x05 \x01(\x03R\x0einitiationTime\x12(\n" + "\x10last_update_time\x18\x06 \x01(\x03R\x0elastUpdateTime\x12%\n" + @@ -6727,7 +6763,8 @@ const file_client_proto_rawDesc = "" + "\x11outgoing_chan_set\x18\x11 \x03(\x04R\x0foutgoingChanSet\x12\x14\n" + "\x05label\x18\x0f \x01(\tR\x05label\x128\n" + "\n" + - "asset_info\x18\x13 \x01(\v2\x19.looprpc.AssetLoopOutInfoR\tassetInfo\"s\n" + + "asset_info\x18\x13 \x01(\v2\x19.looprpc.AssetLoopOutInfoR\tassetInfoB\x1f\n" + + "\x1dstatic_loop_in_state_optional\"s\n" + "\x10ListSwapsRequest\x12B\n" + "\x10list_swap_filter\x18\x01 \x01(\v2\x18.looprpc.ListSwapsFilterR\x0elistSwapFilter\x12\x1b\n" + "\tmax_swaps\x18\x02 \x01(\x04R\bmaxSwaps\"\xf1\x02\n" + @@ -7088,10 +7125,11 @@ const file_client_proto_rawDesc = "" + "\x13asset_cost_offchain\x18\x03 \x01(\x04R\x11assetCostOffchain*;\n" + "\vAddressType\x12\x18\n" + "\x14ADDRESS_TYPE_UNKNOWN\x10\x00\x12\x12\n" + - "\x0eTAPROOT_PUBKEY\x10\x01*%\n" + + "\x0eTAPROOT_PUBKEY\x10\x01*9\n" + "\bSwapType\x12\f\n" + "\bLOOP_OUT\x10\x00\x12\v\n" + - "\aLOOP_IN\x10\x01*s\n" + + "\aLOOP_IN\x10\x01\x12\x12\n" + + "\x0eSTATIC_LOOP_IN\x10\x02*s\n" + "\tSwapState\x12\r\n" + "\tINITIATED\x10\x00\x12\x15\n" + "\x11PREIMAGE_REVEALED\x10\x01\x12\x12\n" + @@ -7321,121 +7359,122 @@ var file_client_proto_depIdxs = []int32{ 91, // 4: looprpc.LoopInRequest.route_hints:type_name -> looprpc.RouteHint 1, // 5: looprpc.SwapStatus.type:type_name -> looprpc.SwapType 2, // 6: looprpc.SwapStatus.state:type_name -> looprpc.SwapState - 3, // 7: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason - 88, // 8: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo - 20, // 9: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter - 9, // 10: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter - 18, // 11: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus - 24, // 12: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested - 25, // 13: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded - 26, // 14: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed - 91, // 15: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint - 85, // 16: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest - 86, // 17: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo - 91, // 18: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint - 40, // 19: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token - 41, // 20: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats - 41, // 21: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats - 47, // 22: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule - 0, // 23: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType - 89, // 24: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry - 4, // 25: looprpc.LiquidityParameters.loop_in_source:type_name -> looprpc.LoopInSource - 1, // 26: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType - 5, // 27: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType - 45, // 28: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters - 6, // 29: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason - 14, // 30: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest - 15, // 31: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest - 83, // 32: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest - 51, // 33: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified - 57, // 34: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation - 64, // 35: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut - 69, // 36: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo - 92, // 37: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint - 7, // 38: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState - 80, // 39: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit - 81, // 40: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal - 82, // 41: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap - 7, // 42: looprpc.Deposit.state:type_name -> looprpc.DepositState - 80, // 43: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit - 8, // 44: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState - 80, // 45: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit - 91, // 46: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint - 80, // 47: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit - 87, // 48: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint - 87, // 49: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint - 46, // 50: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams - 14, // 51: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest - 15, // 52: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest - 17, // 53: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest - 19, // 54: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest - 22, // 55: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest - 27, // 56: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest - 53, // 57: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest - 28, // 58: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest - 31, // 59: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest - 28, // 60: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest - 31, // 61: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest - 34, // 62: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest - 36, // 63: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest - 36, // 64: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest - 38, // 65: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest - 42, // 66: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest - 12, // 67: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest - 44, // 68: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest - 48, // 69: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest - 50, // 70: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest - 55, // 71: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest - 58, // 72: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest - 60, // 73: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest - 62, // 74: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest - 65, // 75: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest - 67, // 76: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest - 70, // 77: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest - 72, // 78: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest - 74, // 79: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest - 76, // 80: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest - 78, // 81: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest - 83, // 82: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest - 10, // 83: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest - 16, // 84: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse - 16, // 85: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse - 18, // 86: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus - 21, // 87: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse - 23, // 88: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse - 18, // 89: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus - 54, // 90: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse - 30, // 91: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse - 33, // 92: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse - 29, // 93: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse - 32, // 94: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse - 35, // 95: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse - 37, // 96: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse - 37, // 97: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse - 39, // 98: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse - 43, // 99: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse - 13, // 100: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse - 45, // 101: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters - 49, // 102: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse - 52, // 103: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse - 56, // 104: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse - 59, // 105: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse - 61, // 106: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse - 63, // 107: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse - 66, // 108: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse - 68, // 109: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse - 71, // 110: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse - 73, // 111: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse - 75, // 112: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse - 77, // 113: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse - 79, // 114: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse - 84, // 115: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse - 11, // 116: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse - 84, // [84:117] is the sub-list for method output_type - 51, // [51:84] is the sub-list for method input_type - 51, // [51:51] is the sub-list for extension type_name - 51, // [51:51] is the sub-list for extension extendee - 0, // [0:51] is the sub-list for field type_name + 8, // 7: looprpc.SwapStatus.static_loop_in_state:type_name -> looprpc.StaticAddressLoopInSwapState + 3, // 8: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason + 88, // 9: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo + 20, // 10: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter + 9, // 11: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter + 18, // 12: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus + 24, // 13: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested + 25, // 14: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded + 26, // 15: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed + 91, // 16: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint + 85, // 17: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest + 86, // 18: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo + 91, // 19: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint + 40, // 20: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token + 41, // 21: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats + 41, // 22: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats + 47, // 23: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule + 0, // 24: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType + 89, // 25: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry + 4, // 26: looprpc.LiquidityParameters.loop_in_source:type_name -> looprpc.LoopInSource + 1, // 27: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType + 5, // 28: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType + 45, // 29: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters + 6, // 30: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason + 14, // 31: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest + 15, // 32: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest + 83, // 33: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest + 51, // 34: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified + 57, // 35: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation + 64, // 36: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut + 69, // 37: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo + 92, // 38: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint + 7, // 39: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState + 80, // 40: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit + 81, // 41: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal + 82, // 42: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap + 7, // 43: looprpc.Deposit.state:type_name -> looprpc.DepositState + 80, // 44: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit + 8, // 45: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState + 80, // 46: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit + 91, // 47: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint + 80, // 48: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit + 87, // 49: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint + 87, // 50: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint + 46, // 51: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams + 14, // 52: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest + 15, // 53: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest + 17, // 54: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest + 19, // 55: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest + 22, // 56: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest + 27, // 57: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest + 53, // 58: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest + 28, // 59: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest + 31, // 60: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest + 28, // 61: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest + 31, // 62: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest + 34, // 63: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest + 36, // 64: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest + 36, // 65: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest + 38, // 66: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest + 42, // 67: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest + 12, // 68: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest + 44, // 69: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest + 48, // 70: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest + 50, // 71: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest + 55, // 72: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest + 58, // 73: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest + 60, // 74: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest + 62, // 75: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest + 65, // 76: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest + 67, // 77: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest + 70, // 78: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest + 72, // 79: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest + 74, // 80: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest + 76, // 81: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest + 78, // 82: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest + 83, // 83: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest + 10, // 84: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest + 16, // 85: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse + 16, // 86: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse + 18, // 87: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus + 21, // 88: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse + 23, // 89: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse + 18, // 90: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus + 54, // 91: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse + 30, // 92: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse + 33, // 93: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse + 29, // 94: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse + 32, // 95: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse + 35, // 96: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse + 37, // 97: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse + 37, // 98: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse + 39, // 99: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse + 43, // 100: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse + 13, // 101: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse + 45, // 102: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters + 49, // 103: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse + 52, // 104: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse + 56, // 105: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse + 59, // 106: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse + 61, // 107: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse + 63, // 108: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse + 66, // 109: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse + 68, // 110: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse + 71, // 111: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse + 73, // 112: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse + 75, // 113: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse + 77, // 114: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse + 79, // 115: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse + 84, // 116: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse + 11, // 117: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse + 85, // [85:118] is the sub-list for method output_type + 52, // [52:85] is the sub-list for method input_type + 52, // [52:52] is the sub-list for extension type_name + 52, // [52:52] is the sub-list for extension extendee + 0, // [0:52] is the sub-list for field type_name } func init() { file_client_proto_init() } @@ -7443,6 +7482,9 @@ func file_client_proto_init() { if File_client_proto != nil { return } + file_client_proto_msgTypes[8].OneofWrappers = []any{ + (*SwapStatus_StaticLoopInState)(nil), + } file_client_proto_msgTypes[13].OneofWrappers = []any{ (*SweepHtlcResponse_NotRequested)(nil), (*SweepHtlcResponse_Published)(nil), diff --git a/looprpc/client.proto b/looprpc/client.proto index 3ae690dc..844dade7 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -546,10 +546,17 @@ message SwapStatus { SwapType type = 3; /* - State the swap is currently in, see State enum. + Generic loop-in/loop-out state for swaps. */ SwapState state = 4; + oneof static_loop_in_state_optional { + /* + Static address loop-in FSM state when type is STATIC_LOOP_IN. + */ + StaticAddressLoopInSwapState static_loop_in_state = 20; + } + /* A failure reason for the swap, only set if the swap has failed. */ @@ -608,6 +615,9 @@ enum SwapType { // LOOP_IN indicates a loop in swap (on-chain to off-chain) LOOP_IN = 1; + + // STATIC_LOOP_IN indicates a static address loop in swap. + STATIC_LOOP_IN = 2; } enum SwapState { diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index a50814e4..1c94febb 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -3061,7 +3061,11 @@ }, "state": { "$ref": "#/definitions/looprpcSwapState", - "description": "State the swap is currently in, see State enum." + "description": "Generic loop-in/loop-out state for swaps." + }, + "static_loop_in_state": { + "$ref": "#/definitions/looprpcStaticAddressLoopInSwapState", + "description": "Static address loop-in FSM state when type is STATIC_LOOP_IN." }, "failure_reason": { "$ref": "#/definitions/looprpcFailureReason", @@ -3131,10 +3135,11 @@ "type": "string", "enum": [ "LOOP_OUT", - "LOOP_IN" + "LOOP_IN", + "STATIC_LOOP_IN" ], "default": "LOOP_OUT", - "title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)" + "description": " - LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)\n - STATIC_LOOP_IN: STATIC_LOOP_IN indicates a static address loop in swap." }, "looprpcSweepHtlcRequest": { "type": "object", From df7dfba12ab291eeff962364726a3a313956eea4 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 23:35:40 -0300 Subject: [PATCH 02/36] staticaddr/loopin: preserve loop-in last update time --- staticaddr/loopin/sql_store.go | 31 +++++++++++++++++++++++++---- staticaddr/loopin/sql_store_test.go | 15 +++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/staticaddr/loopin/sql_store.go b/staticaddr/loopin/sql_store.go index 8b36dbe4..9dc2a084 100644 --- a/staticaddr/loopin/sql_store.go +++ b/staticaddr/loopin/sql_store.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "strings" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" @@ -23,6 +24,12 @@ import ( const OutpointSeparator = ";" +// sqlStoreUpdateTime returns the PostgreSQL-compatible timestamp precision used +// for persisted loop-in update metadata. +func sqlStoreUpdateTime(clock clock.Clock) time.Time { + return clock.Now().Truncate(time.Microsecond) +} + var ( // ErrInvalidOutpoint is returned when an outpoint contains the outpoint // separator. @@ -288,13 +295,14 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, Fast: loopIn.Fast, } + updateTime := sqlStoreUpdateTime(s.clock) updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{ SwapHash: loopIn.SwapHash[:], - UpdateTimestamp: s.clock.Now(), + UpdateTimestamp: updateTime, UpdateState: string(loopIn.GetState()), } - return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), func(q Querier) error { err := q.InsertSwap(ctx, swapArgs) if err != nil { @@ -331,6 +339,13 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, return q.InsertStaticAddressMetaUpdate(ctx, updateArgs) }, ) + if err != nil { + return err + } + + loopIn.LastUpdateTime = updateTime + + return nil } // UpdateLoopIn updates the loop-in in the database. @@ -351,13 +366,14 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context, }, } + updateTime := sqlStoreUpdateTime(s.clock) updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{ SwapHash: loopIn.SwapHash[:], UpdateState: string(loopIn.GetState()), - UpdateTimestamp: s.clock.Now(), + UpdateTimestamp: updateTime, } - return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), func(q Querier) error { err := q.UpdateStaticAddressLoopIn(ctx, updateParams) if err != nil { @@ -367,6 +383,13 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context, return q.InsertStaticAddressMetaUpdate(ctx, updateArgs) }, ) + if err != nil { + return err + } + + loopIn.LastUpdateTime = updateTime + + return nil } // RecordStaticAddressRiskDecision stores the server's confirmation-risk diff --git a/staticaddr/loopin/sql_store_test.go b/staticaddr/loopin/sql_store_test.go index ffea0f06..c08d940f 100644 --- a/staticaddr/loopin/sql_store_test.go +++ b/staticaddr/loopin/sql_store_test.go @@ -252,7 +252,9 @@ func TestCreateLoopIn(t *testing.T) { // Set up test context objects. ctx := t.Context() testDb := loopdb.NewTestDB(t) - testClock := clock.NewTestClock(time.Now()) + createTime := time.Unix(1_717_171_717, 123_456_789).UTC() + expectedCreateTime := createTime.Truncate(time.Microsecond) + testClock := clock.NewTestClock(createTime) defer testDb.Close() depositStore := deposit.NewSqlStore(testDb.BaseDB) @@ -325,6 +327,7 @@ func TestCreateLoopIn(t *testing.T) { err = swapStore.CreateLoopIn(ctx, &swapPending) require.NoError(t, err) + require.Equal(t, expectedCreateTime, swapPending.LastUpdateTime) depositIDs, err := swapStore.DepositIDsForSwapHash( ctx, swapHashPending, @@ -349,6 +352,7 @@ func TestCreateLoopIn(t *testing.T) { require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()}, swap.DepositOutpoints) require.Equal(t, SignHtlcTx, swap.GetState()) + require.Equal(t, swapPending.LastUpdateTime, swap.LastUpdateTime) require.Equal( t, ConfirmationRiskDecisionNone, swap.ConfirmationRiskDecision, @@ -445,14 +449,15 @@ func TestCreateLoopIn(t *testing.T) { err = swapStore.UpdateLoopIn(ctx, &swapPending) require.NoError(t, err) + require.Equal( + t, updateTime.Truncate(time.Microsecond), + swapPending.LastUpdateTime, + ) swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending) require.NoError(t, err) require.Equal(t, Succeeded, swap.GetState()) - require.WithinDuration( - t, updateTime.UTC(), swap.LastUpdateTime.UTC(), - time.Microsecond, - ) + require.Equal(t, swapPending.LastUpdateTime, swap.LastUpdateTime) } // TestGetLoopInByHashOrdersDepositsBySnapshot ensures recovered deposits are From 34cf98e8e800e45ef00ddcf972b5ffc779a5042d Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 23:38:03 -0300 Subject: [PATCH 03/36] loopd: map static address loop-ins to swap status --- interface.go | 14 +- loopd/swapclient_server.go | 174 ++++++++++++++++++++++++- loopd/swapclient_server_test.go | 218 ++++++++++++++++++++++++++++++++ swap/type.go | 9 +- 4 files changed, 409 insertions(+), 6 deletions(-) diff --git a/interface.go b/interface.go index 10a86bfa..4b8623bf 100644 --- a/interface.go +++ b/interface.go @@ -4,6 +4,7 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/taproot-assets/rfqmath" @@ -476,8 +477,12 @@ type SwapInfoKit struct { LastUpdateTime time.Time } -// SwapInfo exposes common info fields for loop in and loop out swaps. +// SwapInfo exposes common info fields for traditional swaps and static address +// loop-ins. type SwapInfo struct { + // SwapStateData.State is authoritative for swap.TypeIn and swap.TypeOut. + // For swap.TypeStaticAddressLoopIn, StaticAddressLoopInState is + // authoritative and State remains its zero value, loopdb.StateInitiated. loopdb.SwapStateData loopdb.SwapContract @@ -488,9 +493,14 @@ type SwapInfo struct { // SwapHash stores the swap preimage hash. SwapHash lntypes.Hash - // SwapType describes whether this is a loop in or loop out swap. + // SwapType describes the kind of swap. SwapType swap.Type + // StaticAddressLoopInState stores the precise static address loop-in FSM + // state when SwapType is swap.TypeStaticAddressLoopIn. For traditional + // swaps, it remains the fsm.StateType zero value, fsm.EmptyState. + StaticAddressLoopInState fsm.StateType + // HtlcAddressP2WSH stores the address of the P2WSH (native segwit) // swap htlc. This is used for both loop-in and loop-out. HtlcAddressP2WSH btcutil.Address diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index d89dbbce..a76bc32b 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -411,6 +411,8 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, } var swapType looprpc.SwapType + staticLoopInState := looprpc. + StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE var ( htlcAddress string htlcAddressP2TR string @@ -437,6 +439,27 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, lastHop = loopSwap.LastHop[:] } + case swap.TypeStaticAddressLoopIn: + // Static loop-ins surface their precise FSM state through the + // optional oneof and keep the reconstructed HTLC P2WSH address, + // not the reusable static address. + swapType = looprpc.SwapType_STATIC_LOOP_IN + staticLoopInState = toClientStaticAddressLoopInState( + loopSwap.StaticAddressLoopInState, + ) + + if loopSwap.HtlcAddressP2WSH == nil { + return nil, errors.New( + "missing static address loop-in P2WSH HTLC address", + ) + } + htlcAddressP2WSH = loopSwap.HtlcAddressP2WSH.EncodeAddress() + htlcAddress = htlcAddressP2WSH + + if loopSwap.LastHop != nil { + lastHop = loopSwap.LastHop[:] + } + case swap.TypeOut: swapType = looprpc.SwapType_LOOP_OUT if loopSwap.HtlcAddressP2WSH != nil { @@ -478,7 +501,7 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, return nil, errors.New("unknown swap type") } - return &looprpc.SwapStatus{ + rpcSwap := &looprpc.SwapStatus{ Amt: int64(loopSwap.AmountRequested), Id: loopSwap.SwapHash.String(), IdBytes: loopSwap.SwapHash[:], @@ -497,7 +520,15 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, LastHop: lastHop, OutgoingChanSet: outGoingChanSet, AssetInfo: assetInfo, - }, nil + } + if swapType == looprpc.SwapType_STATIC_LOOP_IN { + rpcSwap.StaticLoopInStateOptional = + &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: staticLoopInState, + } + } + + return rpcSwap, nil } // Monitor will return a stream of swap updates for currently active swaps. @@ -2089,6 +2120,8 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context, }, nil } +// staticAddressLoopInTimestamp converts a non-zero timestamp to Unix nano +// form and preserves zero timestamps as zero. func staticAddressLoopInTimestamp(t time.Time) int64 { if t.IsZero() { return 0 @@ -2114,6 +2147,141 @@ func staticAddressLoopInSwapServerCost(swp *loopin.StaticAddressLoopIn) int64 { } } +// staticAddressLoopInSwapInfos loads the static-address loop-in manager swaps +// and converts them to client-facing swap info records. +func (s *swapClientServer) staticAddressLoopInSwapInfos( + ctx context.Context) ([]*loop.SwapInfo, error) { + + if s.staticLoopInManager == nil { + return nil, nil + } + + staticSwaps, err := s.staticLoopInManager.GetAllSwaps(ctx) + if err != nil { + return nil, err + } + + swapInfos := make([]*loop.SwapInfo, 0, len(staticSwaps)) + for _, swp := range staticSwaps { + if swp == nil { + continue + } + + swapInfo, err := s.staticAddressLoopInSwapInfo(ctx, swp) + if err != nil { + return nil, err + } + swapInfos = append(swapInfos, swapInfo) + } + + return swapInfos, nil +} + +// staticAddressLoopInSwapInfo converts one static-address loop-in into swap +// info using the daemon's current chain parameters. +func (s *swapClientServer) staticAddressLoopInSwapInfo(_ context.Context, + swp *loopin.StaticAddressLoopIn) (*loop.SwapInfo, error) { + + chainParams, err := s.network.ChainParams() + if err != nil { + return nil, fmt.Errorf("error getting chain params") + } + + return staticAddressLoopInSwapInfoWithChainParams(swp, chainParams) +} + +// staticAddressLoopInSwapInfoWithChainParams converts one static-address +// loop-in into swap info, including its reconstructed V2 P2WSH HTLC address. +func staticAddressLoopInSwapInfoWithChainParams( + swp *loopin.StaticAddressLoopIn, + chainParams *chaincfg.Params) (*loop.SwapInfo, error) { + + htlcAddress, err := staticAddressLoopInHtlcAddress(swp, chainParams) + if err != nil { + return nil, err + } + + var lastHop *route.Vertex + if len(swp.LastHop) > 0 { + vertex, err := route.NewVertexFromBytes(swp.LastHop) + if err != nil { + return nil, err + } + lastHop = &vertex + } + + amount := swp.TotalDepositAmount() + if swp.SelectedAmount > 0 { + amount = swp.SelectedAmount + } + + lastUpdate := swp.LastUpdateTime + if lastUpdate.IsZero() { + lastUpdate = swp.InitiationTime + } + + return &loop.SwapInfo{ + SwapStateData: loopdb.SwapStateData{ + // Mirror ListStaticAddressSwaps by reporting only the persisted + // client-visible server cost. On-chain and off-chain costs stay + // zero until static loop-ins persist real fee data. + Cost: loopdb.SwapCost{ + Server: btcutil.Amount( + staticAddressLoopInSwapServerCost(swp), + ), + }, + }, + SwapContract: loopdb.SwapContract{ + AmountRequested: amount, + CltvExpiry: swp.HtlcCltvExpiry, + MaxSwapFee: swp.MaxSwapFee, + InitiationTime: swp.InitiationTime, + Label: swp.Label, + ProtocolVersion: loopdb.ProtocolVersion( + swp.ProtocolVersion, + ), + }, + LastUpdate: lastUpdate, + SwapHash: swp.SwapHash, + SwapType: swap.TypeStaticAddressLoopIn, + StaticAddressLoopInState: swp.GetState(), + HtlcAddressP2WSH: htlcAddress, + LastHop: lastHop, + }, nil +} + +// staticAddressLoopInHtlcAddress reconstructs the V2 P2WSH HTLC address from +// the static-address loop-in's client and server keys. +func staticAddressLoopInHtlcAddress(swp *loopin.StaticAddressLoopIn, + chainParams *chaincfg.Params) (btcutil.Address, error) { + + if swp.ClientPubkey == nil { + return nil, errors.New("missing static address loop-in client HTLC key") + } + if swp.ServerPubkey == nil { + return nil, errors.New("missing static address loop-in server HTLC key") + } + + htlc, err := swap.NewHtlcV2( + swp.HtlcCltvExpiry, pubkeyTo33ByteSlice(swp.ClientPubkey), + pubkeyTo33ByteSlice(swp.ServerPubkey), swp.SwapHash, chainParams, + ) + if err != nil { + return nil, fmt.Errorf("construct static address loop-in HTLC: %w", err) + } + + return htlc.Address, nil +} + +// pubkeyTo33ByteSlice converts a compressed public key to a fixed 33-byte +// array. +func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte { + var pubkeyBytes [33]byte + copy(pubkeyBytes[:], pubkey.SerializeCompressed()) + + return pubkeyBytes +} + // GetStaticAddressSummary returns a summary of static address-related // information. Amongst deposits and withdrawals and their total values, it also // includes a list of detailed deposit information filtered by their state. @@ -2420,6 +2588,8 @@ func toClientDepositState(state fsm.StateType) looprpc.DepositState { } } +// toClientStaticAddressLoopInState maps the static-address loop-in FSM state +// to the RPC enum exposed to clients. func toClientStaticAddressLoopInState( state fsm.StateType) looprpc.StaticAddressLoopInSwapState { diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index f3fab032..ac516f76 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -25,6 +26,7 @@ import ( "github.com/lightninglabs/loop/staticaddr/script" "github.com/lightninglabs/loop/swap" mock_lnd "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" @@ -501,6 +503,222 @@ func TestListStaticAddressSwapsPopulatesTimingAndCosts(t *testing.T) { ) } +// TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH protects the RPC +// mapping invariant that static loop-ins expose their static type, static +// state, and P2WSH HTLC address without leaking a taproot HTLC address. +func TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH(t *testing.T) { + server := &swapClientServer{} + loopSwap := &loop.SwapInfo{ + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateInitiated, + }, + SwapContract: loopdb.SwapContract{ + InitiationTime: time.Now(), + }, + LastUpdate: time.Now(), + SwapHash: lntypes.Hash{1}, + SwapType: swap.TypeStaticAddressLoopIn, + StaticAddressLoopInState: loopin.SignHtlcTx, + HtlcAddressP2WSH: testnetAddr, + } + + rpcSwap, err := server.marshallSwap(t.Context(), loopSwap) + require.NoError(t, err) + require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, rpcSwap.Type) + require.Equal( + t, looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX, + rpcSwap.GetStaticLoopInState(), + ) + require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State) + require.Equal(t, testnetAddr.EncodeAddress(), rpcSwap.HtlcAddressP2Wsh) + require.Empty(t, rpcSwap.HtlcAddressP2Tr) +} + +// TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault asserts that +// static loop-in failures keep default legacy fields while preserving the +// precise static state. +func TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault( + t *testing.T) { + + tests := []struct { + name string + state fsm.StateType + wantStaticState looprpc.StaticAddressLoopInSwapState + }{ + { + name: "failed", + state: loopin.Failed, + wantStaticState: looprpc. + StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP, + }, + { + name: "succeeded transitioning failed", + state: loopin.SucceededTransitioningFailed, + wantStaticState: looprpc. + StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.SetState(test.state) + loopSwap, err := server.staticAddressLoopInSwapInfo( + t.Context(), staticLoopIn, + ) + require.NoError(t, err) + + rpcSwap, err := server.marshallSwap(t.Context(), loopSwap) + + require.NoError(t, err) + require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State) + require.Equal( + t, looprpc.FailureReason_FAILURE_REASON_NONE, + rpcSwap.FailureReason, + ) + require.Equal( + t, test.wantStaticState, + rpcSwap.GetStaticLoopInState(), + ) + }) + } +} + +// TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress protects the +// fail-closed HTLC-address invariant for static loop-ins missing the P2WSH +// address required by the client-facing RPC representation. +func TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress(t *testing.T) { + _, taprootAddress := newTestStaticAddressParams(t) + server := &swapClientServer{} + loopSwap := &loop.SwapInfo{ + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateInitiated, + }, + SwapContract: loopdb.SwapContract{ + InitiationTime: time.Now(), + }, + LastUpdate: time.Now(), + SwapHash: lntypes.Hash{1}, + SwapType: swap.TypeStaticAddressLoopIn, + StaticAddressLoopInState: loopin.SignHtlcTx, + HtlcAddressP2TR: taprootAddress, + } + + _, err := server.marshallSwap(t.Context(), loopSwap) + require.ErrorContains(t, err, "missing static address loop-in P2WSH HTLC address") +} + +// TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing protects the +// HTLC-address construction invariant that missing cooperative keys must not +// produce monitorable swap info. +func TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing(t *testing.T) { + server, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.ClientPubkey = nil + + _, err := server.staticAddressLoopInSwapInfo(t.Context(), staticLoopIn) + require.ErrorContains( + t, err, "missing static address loop-in client HTLC key", + ) +} + +func newGenericStaticLoopInServer(t *testing.T) (*swapClientServer, + *loopin.StaticAddressLoopIn) { + + server, staticLoopIn, _ := newGenericStaticLoopInServerWithStore(t) + + return server, staticLoopIn +} + +func newTestStaticAddressParams(t *testing.T) (*script.Parameters, + *btcutil.AddressTaproot) { + + t.Helper() + + const staticAddressExpiry = uint32(25) + + _, staticClientPubkey := mock_lnd.CreateKey(12) + _, staticServerPubkey := mock_lnd.CreateKey(13) + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(staticAddressExpiry), + staticClientPubkey, staticServerPubkey, + ) + require.NoError(t, err) + + staticPkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + taprootAddress, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(staticAddress.TaprootKey), + &chaincfg.TestNet3Params, + ) + require.NoError(t, err) + + return &script.Parameters{ + ClientPubkey: staticClientPubkey, + ServerPubkey: staticServerPubkey, + Expiry: staticAddressExpiry, + PkScript: staticPkScript, + }, taprootAddress +} + +func newGenericStaticLoopInServerWithStore(t *testing.T) (*swapClientServer, + *loopin.StaticAddressLoopIn, *mockStaticAddressLoopInStore) { + + t.Helper() + + _, clientPubkey := mock_lnd.CreateKey(10) + _, serverPubkey := mock_lnd.CreateKey(11) + addressParams, _ := newTestStaticAddressParams(t) + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{12, 13, 14}, + Index: 2, + } + staticDeposit := &deposit.Deposit{ + OutPoint: depositOutpoint, + Value: 51_000, + } + lastHop := route.Vertex{7, 8, 9} + + staticLoopIn := &loopin.StaticAddressLoopIn{ + SwapHash: lntypes.Hash{1, 2, 3}, + HtlcCltvExpiry: 700, + InitiationTime: time.Unix(100, 0).UTC(), + LastUpdateTime: time.Unix(200, 0).UTC(), + Label: "static-loop-in", + ClientPubkey: clientPubkey, + ServerPubkey: serverPubkey, + LastHop: lastHop[:], + QuotedSwapFee: 1_111, + SelectedAmount: 50_000, + DepositOutpoints: []string{depositOutpoint.String()}, + Deposits: []*deposit.Deposit{staticDeposit}, + AddressParams: addressParams, + } + staticLoopIn.SetState(loopin.PaymentReceived) + + depositStore := &mockDepositStore{ + byOutpoint: map[string]*deposit.Deposit{ + depositOutpoint.String(): staticDeposit, + }, + } + loopInStore := &mockStaticAddressLoopInStore{ + swaps: []*loopin.StaticAddressLoopIn{staticLoopIn}, + } + staticLoopInManager, err := loopin.NewManager(&loopin.Config{ + Store: loopInStore, + DepositManager: deposit.NewManager(&deposit.ManagerConfig{ + Store: depositStore, + }), + }, 1) + require.NoError(t, err) + + return &swapClientServer{ + network: lndclient.NetworkTestnet, + swaps: make(map[lntypes.Hash]loop.SwapInfo), + staticLoopInManager: staticLoopInManager, + }, staticLoopIn, loopInStore +} + // mockStaticAddressLoopInStore is a minimal in-memory loop-in store for RPC // response mapping tests. type mockStaticAddressLoopInStore struct { diff --git a/swap/type.go b/swap/type.go index 20942c74..b4558e15 100644 --- a/swap/type.go +++ b/swap/type.go @@ -9,10 +9,13 @@ const ( // TypeOut is a loop out swap. TypeOut + + // TypeStaticAddressLoopIn is a static-address loop-in swap. + TypeStaticAddressLoopIn ) -// IsOut returns true if the swap is a loop out swap, false if it is a loop in -// swap. +// IsOut returns true only if the swap is TypeOut; TypeIn and +// TypeStaticAddressLoopIn both return false. func (t Type) IsOut() bool { return t == TypeOut } @@ -23,6 +26,8 @@ func (t Type) String() string { return "In" case TypeOut: return "Out" + case TypeStaticAddressLoopIn: + return "StaticAddressLoopIn" default: return "Unknown" } From 7200a7ce09574224879f1fe090ede956e9842fa5 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 15 Jul 2026 14:20:45 -0300 Subject: [PATCH 04/36] loopd: synchronize cached swap lookups --- loopd/swapclient_server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index a76bc32b..2d4eb348 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -785,10 +785,13 @@ func (s *swapClientServer) SwapInfo(ctx context.Context, // Just return the server's in-memory cache here too as we also want to // return temporary failures to the client. + s.swapsLock.Lock() swp, ok := s.swaps[swapHash] + s.swapsLock.Unlock() if !ok { return nil, fmt.Errorf("swap with hash %s not found", req.Id) } + return s.marshallSwap(ctx, &swp) } From f5cc85f7f309fcd0c34b859bbde8417b645b056a Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 15 Jul 2026 14:21:07 -0300 Subject: [PATCH 05/36] loopd: include static loop-ins in monitor snapshots --- loopd/swapclient_server.go | 144 ++++++++++++-- loopd/swapclient_server_test.go | 324 +++++++++++++++++++++++++++++++- 2 files changed, 452 insertions(+), 16 deletions(-) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 2d4eb348..28b4f722 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -549,27 +549,28 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, // Start a notification queue for this subscriber. queue := queue.NewConcurrentQueue(20) queue.Start() + ctx := server.Context() - // Add this subscriber to the global subscriber list. Also create a - // snapshot of all pending and completed swaps within the lock, to - // prevent subscribers from receiving duplicate updates. s.swapsLock.Lock() id := s.nextSubscriberID s.nextSubscriberID++ s.subscribers[id] = queue.ChanIn() - - var pendingSwaps, completedSwaps []loop.SwapInfo - for _, swap := range s.swaps { - if swap.State.Type() == loopdb.StateTypePending { - pendingSwaps = append(pendingSwaps, swap) - } else { - completedSwaps = append(completedSwaps, swap) - } - } - + pendingSwaps, completedSwaps := s.monitorCachedSwaps() s.swapsLock.Unlock() + err := s.appendStaticAddressLoopInMonitorSnapshot( + ctx, &pendingSwaps, &completedSwaps, + ) + if err != nil { + s.swapsLock.Lock() + delete(s.subscribers, id) + s.swapsLock.Unlock() + queue.Stop() + + return err + } + defer func() { s.swapsLock.Lock() delete(s.subscribers, id) @@ -599,6 +600,13 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, ) }) + // Static-address loop-in updates can arrive from both the initial snapshot + // and the live queue. Build a high-water mark from the snapshot so we can + // suppress stale duplicate snapshot items without dropping newer live ones. + staticSnapshotHighWater := staticAddressLoopInMonitorHighWater( + filteredSwaps, + ) + // Return swaps to caller. for _, swap := range filteredSwaps { if err := send(swap); err != nil { @@ -616,6 +624,13 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, } swap := queueItem.(loop.SwapInfo) + if isInitialStaticAddressLoopInStale( + staticSnapshotHighWater, swap, + ) { + + continue + } + if err := send(swap); err != nil { return err } @@ -631,6 +646,102 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, } } +// staticAddressLoopInMonitorHighWater records the latest snapshot item for each +// static-address loop-in swap hash. +func staticAddressLoopInMonitorHighWater( + swaps []loop.SwapInfo) map[lntypes.Hash]staticAddressLoopInHighWater { + + highWater := make(map[lntypes.Hash]staticAddressLoopInHighWater) + for _, swp := range swaps { + if swp.SwapType != swap.TypeStaticAddressLoopIn { + continue + } + + current, ok := highWater[swp.SwapHash] + if !ok || swp.LastUpdate.After(current.lastUpdate) { + highWater[swp.SwapHash] = staticAddressLoopInHighWater{ + lastUpdate: swp.LastUpdate, + staticState: swp.StaticAddressLoopInState, + } + } + } + + return highWater +} + +// staticAddressLoopInHighWater stores the most recent snapshot timestamp and +// state for one static-address loop-in swap. +type staticAddressLoopInHighWater struct { + lastUpdate time.Time + staticState fsm.StateType +} + +// isInitialStaticAddressLoopInStale reports whether a live static-address +// loop-in update is older than the snapshot copy already sent, or equal to it +// with the same static FSM state. +func isInitialStaticAddressLoopInStale( + highWater map[lntypes.Hash]staticAddressLoopInHighWater, + swp loop.SwapInfo) bool { + + if swp.SwapType != swap.TypeStaticAddressLoopIn { + return false + } + current, ok := highWater[swp.SwapHash] + if !ok { + return false + } + + if swp.LastUpdate.Before(current.lastUpdate) { + return true + } + if swp.LastUpdate.After(current.lastUpdate) { + return false + } + + // Equal timestamps can race the initial DB snapshot, so match state too. + return swp.StaticAddressLoopInState == current.staticState +} + +// monitorCachedSwaps returns the current in-memory swaps split into pending and +// completed slices for monitor snapshot construction. +func (s *swapClientServer) monitorCachedSwaps() ([]loop.SwapInfo, + []loop.SwapInfo) { + + var pendingSwaps, completedSwaps []loop.SwapInfo + for _, swap := range s.swaps { + if swap.State.Type() == loopdb.StateTypePending { + pendingSwaps = append(pendingSwaps, swap) + } else { + completedSwaps = append(completedSwaps, swap) + } + } + + return pendingSwaps, completedSwaps +} + +// appendStaticAddressLoopInMonitorSnapshot appends the current static-address +// loop-in swaps to the monitor snapshot. +func (s *swapClientServer) appendStaticAddressLoopInMonitorSnapshot( + ctx context.Context, pendingSwaps, completedSwaps *[]loop.SwapInfo) error { + + staticSwaps, err := s.staticAddressLoopInSwapInfos(ctx) + if err != nil { + return err + } + for _, swap := range staticSwaps { + if slices.Contains( + loopin.FinalStates, swap.StaticAddressLoopInState, + ) { + + *completedSwaps = append(*completedSwaps, *swap) + } else { + *pendingSwaps = append(*pendingSwaps, *swap) + } + } + + return nil +} + // ListSwaps returns a list of all currently known swaps and their current // status. func (s *swapClientServer) ListSwaps(ctx context.Context, @@ -2745,7 +2856,12 @@ func (s *swapClientServer) processStatusUpdates(mainCtx context.Context) { // subscribers about the changes. case swp := <-s.statusChan: s.swapsLock.Lock() - s.swaps[swp.SwapHash] = swp + // Static loop-ins are broadcast to monitor subscribers, but they + // stay out of the legacy swap cache so ListSwaps and SwapInfo remain + // traditional-swap views. + if swp.SwapType != swap.TypeStaticAddressLoopIn { + s.swaps[swp.SwapHash] = swp + } for _, subscriber := range s.subscribers { select { diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index ac516f76..9d1acf3c 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -34,6 +34,7 @@ import ( "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -621,6 +622,231 @@ func TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing(t *testing.T) ) } +// TestMonitorSnapshotIncludesStaticAddressLoopIns protects the monitor snapshot +// invariant that pending static loop-ins are included alongside cached generic +// swaps with their static state and swap-specific HTLC address. +func TestMonitorSnapshotIncludesStaticAddressLoopIns(t *testing.T) { + ctx := t.Context() + server, staticLoopIn := newGenericStaticLoopInServer(t) + + pendingSwaps, completedSwaps := server.monitorCachedSwaps() + err := server.appendStaticAddressLoopInMonitorSnapshot( + ctx, &pendingSwaps, &completedSwaps, + ) + require.NoError(t, err) + require.Empty(t, completedSwaps) + require.Len(t, pendingSwaps, 1) + require.Equal(t, staticLoopIn.SwapHash, pendingSwaps[0].SwapHash) + require.Equal(t, swap.TypeStaticAddressLoopIn, pendingSwaps[0].SwapType) + require.Equal( + t, staticLoopIn.GetState(), + pendingSwaps[0].StaticAddressLoopInState, + ) + assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, pendingSwaps[0]) +} + +// TestMonitorSnapshotIncludesFinalStaticAddressLoopIns protects the monitor +// snapshot invariant that exact final static loop-in states are completed swaps. +func TestMonitorSnapshotIncludesFinalStaticAddressLoopIns(t *testing.T) { + server, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.SetState(loopin.Succeeded) + + pendingSwaps, completedSwaps := server.monitorCachedSwaps() + err := server.appendStaticAddressLoopInMonitorSnapshot( + t.Context(), &pendingSwaps, &completedSwaps, + ) + + require.NoError(t, err) + require.Empty(t, pendingSwaps) + require.Len(t, completedSwaps, 1) +} + +// TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate protects the +// monitor race invariant that live static loop-in updates arriving during the +// initial snapshot are deduplicated without dropping newer progress. +func TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate(t *testing.T) { + logger := btclog.NewSLogger( + btclog.NewDefaultHandler(os.Stdout), + ) + setLogger(logger.SubSystem(Subsystem)) + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + mainCtx, mainCancel := context.WithCancel(t.Context()) + defer mainCancel() + server, staticLoopIn, store := newGenericStaticLoopInServerWithStore(t) + server.statusChan = make(chan loop.SwapInfo) + server.subscribers = make(map[int]chan<- any) + server.mainCtx = mainCtx + + snapshotStarted := make(chan struct{}, 1) + releaseSnapshot := make(chan struct{}) + store.beforeGet = func() { + select { + case snapshotStarted <- struct{}{}: + default: + } + } + store.waitGet = releaseSnapshot + + go server.processStatusUpdates(mainCtx) + + monitorServer := &testMonitorServer{ + ctx: ctx, + sent: make(chan *looprpc.SwapStatus, 3), + } + errChan := make(chan error, 1) + go func() { + errChan <- server.Monitor(&looprpc.MonitorRequest{}, monitorServer) + }() + + select { + case <-snapshotStarted: + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn) + require.NoError(t, err) + staleUpdate := *staticUpdate + staleUpdate.State = loopdb.StateInitiated + staleUpdate.LastUpdate = staticUpdate.LastUpdate.Add(-time.Second) + server.statusChan <- staleUpdate + + server.statusChan <- *staticUpdate + close(releaseSnapshot) + + first := receiveMonitorUpdate(t, ctx, monitorServer.sent) + require.Equal(t, staticLoopIn.SwapHash[:], first.IdBytes) + require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, first.Type) + require.Equal( + t, looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED, + first.GetStaticLoopInState(), + ) + + nextUpdate := *staticUpdate + nextUpdate.State = loopdb.StateSuccess + nextUpdate.StaticAddressLoopInState = loopin.Succeeded + nextUpdate.LastUpdate = staticUpdate.LastUpdate.Add(time.Second) + server.statusChan <- nextUpdate + + second := receiveMonitorUpdate(t, ctx, monitorServer.sent) + require.Equal(t, staticLoopIn.SwapHash[:], second.IdBytes) + require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, second.Type) + require.Equal( + t, looprpc.StaticAddressLoopInSwapState_SUCCEEDED, + second.GetStaticLoopInState(), + ) + + cancel() + require.NoError(t, <-errChan) +} + +// TestStaticAddressLoopInHighWaterSuppressesExactDuplicate protects the +// high-water dedup invariant that an initial live update identical to the +// snapshot is treated as stale. +func TestStaticAddressLoopInHighWaterSuppressesExactDuplicate(t *testing.T) { + swapHash := lntypes.Hash{1, 2, 3} + lastUpdate := time.Unix(100, 0).UTC() + snapshot := loop.SwapInfo{ + SwapHash: swapHash, + SwapType: swap.TypeStaticAddressLoopIn, + LastUpdate: lastUpdate, + StaticAddressLoopInState: loopin.PaymentReceived, + } + highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{ + snapshot, + }) + + isStale := isInitialStaticAddressLoopInStale(highWater, snapshot) + + require.True(t, isStale) +} + +// TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState protects the +// high-water timing invariant that equal timestamps do not hide a distinct +// static loop-in state transition. +func TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState(t *testing.T) { + swapHash := lntypes.Hash{1, 2, 3} + lastUpdate := time.Unix(100, 0).UTC() + snapshot := loop.SwapInfo{ + SwapHash: swapHash, + SwapType: swap.TypeStaticAddressLoopIn, + LastUpdate: lastUpdate, + StaticAddressLoopInState: loopin.PaymentReceived, + } + liveUpdate := snapshot + liveUpdate.StaticAddressLoopInState = loopin.Succeeded + highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{ + snapshot, + }) + + isStale := isInitialStaticAddressLoopInStale(highWater, liveUpdate) + + require.False(t, isStale) +} + +// TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly protects the +// high-water cache invariant that stale suppression applies only to static +// loop-ins and cannot filter generic swap updates. +func TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly(t *testing.T) { + swapHash := lntypes.Hash{1, 2, 3} + lastUpdate := time.Unix(100, 0).UTC() + snapshot := loop.SwapInfo{ + SwapHash: swapHash, + SwapType: swap.TypeStaticAddressLoopIn, + LastUpdate: lastUpdate, + StaticAddressLoopInState: loopin.PaymentReceived, + } + highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{ + snapshot, + }) + olderStatic := snapshot + olderStatic.LastUpdate = lastUpdate.Add(-time.Second) + olderStatic.StaticAddressLoopInState = loopin.Succeeded + nonStatic := olderStatic + nonStatic.SwapType = swap.TypeOut + + staticStale := isInitialStaticAddressLoopInStale(highWater, olderStatic) + nonStaticStale := isInitialStaticAddressLoopInStale( + highWater, nonStatic, + ) + + require.True(t, staticStale) + require.False(t, nonStaticStale) +} + +// TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache protects the +// cache isolation invariant that static loop-in live updates reach subscribers +// without entering the generic swap cache. +func TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache(t *testing.T) { + ctx := t.Context() + server, staticLoopIn := newGenericStaticLoopInServer(t) + server.statusChan = make(chan loop.SwapInfo) + updates := make(chan any, 1) + server.subscribers = map[int]chan<- any{0: updates} + mainCtx, cancel := context.WithCancel(ctx) + defer cancel() + go server.processStatusUpdates(mainCtx) + + staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn) + require.NoError(t, err) + server.statusChan <- *staticUpdate + + select { + case update := <-updates: + require.Equal(t, staticLoopIn.SwapHash, update.(loop.SwapInfo).SwapHash) + + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + server.swapsLock.Lock() + _, cached := server.swaps[staticLoopIn.SwapHash] + server.swapsLock.Unlock() + require.False(t, cached) +} + func newGenericStaticLoopInServer(t *testing.T) (*swapClientServer, *loopin.StaticAddressLoopIn) { @@ -719,10 +945,93 @@ func newGenericStaticLoopInServerWithStore(t *testing.T) (*swapClientServer, }, staticLoopIn, loopInStore } +// assertStaticLoopInUsesSwapHtlcAddress verifies the static loop-in uses the +// swap HTLC P2WSH address expected by the fixture. +func assertStaticLoopInUsesSwapHtlcAddress(t *testing.T, + staticLoopIn *loopin.StaticAddressLoopIn, swapInfo loop.SwapInfo) { + + t.Helper() + + expectedAddress, err := staticAddressLoopInHtlcAddress( + staticLoopIn, &chaincfg.TestNet3Params, + ) + require.NoError(t, err) + require.Nil(t, swapInfo.HtlcAddressP2TR) + require.NotNil(t, swapInfo.HtlcAddressP2WSH) + require.Equal( + t, expectedAddress.EncodeAddress(), + swapInfo.HtlcAddressP2WSH.EncodeAddress(), + ) +} + +// testMonitorServer implements the monitor stream interface for tests. +type testMonitorServer struct { + ctx context.Context + sent chan *looprpc.SwapStatus +} + +// Send forwards monitor updates to the test channel until the context is canceled. +func (s *testMonitorServer) Send(swapStatus *looprpc.SwapStatus) error { + select { + case s.sent <- swapStatus: + return nil + + case <-s.ctx.Done(): + return s.ctx.Err() + } +} + +// SetHeader is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SetHeader(metadata.MD) error { + return nil +} + +// SendHeader is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SendHeader(metadata.MD) error { + return nil +} + +// SetTrailer is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SetTrailer(metadata.MD) {} + +// Context returns the stream context used by the test monitor server. +func (s *testMonitorServer) Context() context.Context { + return s.ctx +} + +// SendMsg is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SendMsg(any) error { + return nil +} + +// RecvMsg is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) RecvMsg(any) error { + return nil +} + +// receiveMonitorUpdate waits for a monitor update or fails if the context is canceled. +func receiveMonitorUpdate(t *testing.T, ctx context.Context, + updates <-chan *looprpc.SwapStatus) *looprpc.SwapStatus { + + t.Helper() + + select { + case update := <-updates: + return update + + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + return nil +} + // mockStaticAddressLoopInStore is a minimal in-memory loop-in store for RPC // response mapping tests. type mockStaticAddressLoopInStore struct { - swaps []*loopin.StaticAddressLoopIn + swaps []*loopin.StaticAddressLoopIn + beforeGet func() + waitGet <-chan struct{} } // CreateLoopIn satisfies the static loop-in store interface. @@ -741,9 +1050,20 @@ func (s *mockStaticAddressLoopInStore) UpdateLoopIn(_ context.Context, // GetStaticAddressLoopInSwapsByStates returns the configured loop-ins. func (s *mockStaticAddressLoopInStore) GetStaticAddressLoopInSwapsByStates( - _ context.Context, _ []fsm.StateType) ([]*loopin.StaticAddressLoopIn, + ctx context.Context, _ []fsm.StateType) ([]*loopin.StaticAddressLoopIn, error) { + if s.beforeGet != nil { + s.beforeGet() + } + if s.waitGet != nil { + select { + case <-s.waitGet: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + return s.swaps, nil } From e3e6205a4b579d8cf6f4cf052154db7e2f29bc8d Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 23:39:05 -0300 Subject: [PATCH 06/36] staticaddr/loopin: publish status updates after persistence --- loopd/daemon.go | 13 +++++- loopd/static_loopin_status_updater.go | 47 ++++++++++++++++++++ loopd/swapclient_server_test.go | 20 +++++++++ staticaddr/loopin/actions.go | 5 +++ staticaddr/loopin/actions_test.go | 62 +++++++++++++++++++++++++++ staticaddr/loopin/fsm.go | 16 +++++++ staticaddr/loopin/manager.go | 3 ++ staticaddr/loopin/manager_test.go | 42 +++++++++++++++++- 8 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 loopd/static_loopin_status_updater.go diff --git a/loopd/daemon.go b/loopd/daemon.go index 68b76141..f625aa27 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -110,6 +110,10 @@ type Daemon struct { macaroonService *lndclient.MacaroonService } +// staticLoopInStatusChanBuffer keeps the shared swap-status fanout from +// stalling static loop-in FSM progress during transient subscriber gaps. +const staticLoopInStatusChanBuffer = 20 + // New creates a new instance of the loop client daemon. func New(config *Config, lisCfg *ListenerCfg) *Daemon { return &Daemon{ @@ -681,6 +685,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { LightningClient: d.lnd.Client, } openChannelManager = openchannel.NewManager(openChannelCfg) + statusChan := make(chan loop.SwapInfo, staticLoopInStatusChanBuffer) // Run the deposit swap hash migration. err = loopin.MigrateDepositSwapHash( @@ -701,6 +706,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error { return err } + statusUpdater := &staticLoopInStatusUpdater{ + statusChan: statusChan, + mainCtx: d.mainCtx, + chainParams: d.lnd.ChainParams, + } staticLoopInManager, err = loopin.NewManager(&loopin.Config{ Server: staticAddressClient, @@ -718,6 +728,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { ChainParams: d.lnd.ChainParams, Signer: d.lnd.Signer, ValidateLoopInContract: loop.ValidateLoopInContract, + SendUpdate: statusUpdater.sendUpdate, MaxStaticAddrHtlcFeePercentage: d.cfg.MaxStaticAddrHtlcFeePercentage, MaxStaticAddrHtlcBackupFeePercentage: d.cfg.MaxStaticAddrHtlcBackupFeePercentage, }, blockHeight) @@ -783,7 +794,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { lnd: &d.lnd.LndServices, swaps: make(map[lntypes.Hash]loop.SwapInfo), subscribers: make(map[int]chan<- any), - statusChan: make(chan loop.SwapInfo), + statusChan: statusChan, mainCtx: d.mainCtx, reservationManager: reservationManager, instantOutManager: instantOutManager, diff --git a/loopd/static_loopin_status_updater.go b/loopd/static_loopin_status_updater.go new file mode 100644 index 00000000..aa0592cb --- /dev/null +++ b/loopd/static_loopin_status_updater.go @@ -0,0 +1,47 @@ +package loopd + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/staticaddr/loopin" +) + +// staticLoopInStatusUpdater publishes static-address loop-in status updates to +// the client-facing swap stream. +type staticLoopInStatusUpdater struct { + // statusChan sends updates to the client-facing swap stream. + statusChan chan<- loop.SwapInfo + + // mainCtx is canceled when loopd is shutting down. + mainCtx context.Context + + // chainParams are used to reconstruct the static loop-in HTLC address. + chainParams *chaincfg.Params +} + +// sendUpdate converts the persisted static-address loop-in into swap info and +// forwards it to the status stream unless either context is canceled. +func (u *staticLoopInStatusUpdater) sendUpdate(ctx context.Context, + swp *loopin.StaticAddressLoopIn) error { + + info, err := staticAddressLoopInSwapInfoWithChainParams( + swp, u.chainParams, + ) + if err != nil { + return fmt.Errorf("unable to notify static loop-in update: %w", err) + } + + select { + case u.statusChan <- *info: + return nil + + case <-ctx.Done(): + return ctx.Err() + + case <-u.mainCtx.Done(): + return u.mainCtx.Err() + } +} diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index 9d1acf3c..03f3d95a 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -661,6 +661,26 @@ func TestMonitorSnapshotIncludesFinalStaticAddressLoopIns(t *testing.T) { require.Len(t, completedSwaps, 1) } +// TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress protects the live-update +// invariant that static loop-in status events derive the HTLC address from the +// swap, not from reusable static address parameters. +func TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress(t *testing.T) { + ctx := t.Context() + _, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.AddressParams = nil + statusChan := make(chan loop.SwapInfo, 1) + updater := &staticLoopInStatusUpdater{ + statusChan: statusChan, + mainCtx: ctx, + chainParams: &chaincfg.TestNet3Params, + } + + err := updater.sendUpdate(ctx, staticLoopIn) + require.NoError(t, err) + swapInfo := <-statusChan + assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, swapInfo) +} + // TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate protects the // monitor race invariant that live static loop-in updates arriving during the // initial snapshot are deduplicated without dropping newer progress. diff --git a/staticaddr/loopin/actions.go b/staticaddr/loopin/actions.go index 1c432e6f..3dc1c029 100644 --- a/staticaddr/loopin/actions.go +++ b/staticaddr/loopin/actions.go @@ -341,6 +341,11 @@ func (f *FSM) InitHtlcAction(ctx context.Context, // Once the swap is stored, restart/recovery code owns invoice lifecycle. invoiceNeedsCleanup = false + err = f.sendUpdate(ctx) + if err != nil { + f.Errorf("Error sending loop-in update: %v", err) + } + event = OnHtlcInitiated return event diff --git a/staticaddr/loopin/actions_test.go b/staticaddr/loopin/actions_test.go index 20b86251..afc0085d 100644 --- a/staticaddr/loopin/actions_test.go +++ b/staticaddr/loopin/actions_test.go @@ -878,6 +878,68 @@ func TestCheckDepositsAvailableRejectsDivergentDepositOutpoints( require.Empty(t, checker.outpoints) } +// TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence protects the +// persistence-first invariant: once the loop-in is stored, a later status +// update failure must not roll back the action or state transition. +func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) { + mockLnd := test.NewMockLnd() + _, serverKey := test.CreateKey(22) + + server := &mockStaticAddressServer{ + response: testStaticAddressLoopInResponse( + serverKey.SerializeCompressed(), + ), + } + + dep := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 0, + }, + Value: 500_000, + } + + loopIn := &StaticAddressLoopIn{ + Deposits: []*deposit.Deposit{dep}, + DepositOutpoints: []string{dep.OutPoint.String()}, + SelectedAmount: dep.Value, + QuotedSwapFee: 1_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + PaymentTimeoutSeconds: 3_600, + } + + sendUpdateErr := errors.New("status channel blocked") + sendUpdateCalled := false + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + Server: server, + DepositManager: &noopDepositManager{}, + LndClient: mockLnd.Client, + WalletKit: mockLnd.WalletKit, + ChainParams: mockLnd.ChainParams, + Store: &mockStore{}, + ValidateLoopInContract: testValidateLoopInContract, + MaxStaticAddrHtlcFeePercentage: 1, + MaxStaticAddrHtlcBackupFeePercentage: 1, + SendUpdate: func(context.Context, + *StaticAddressLoopIn) error { + + sendUpdateCalled = true + + return sendUpdateErr + }, + }, + loopIn: loopIn, + } + + event := f.InitHtlcAction(t.Context(), nil) + require.Equal(t, OnHtlcInitiated, event) + require.Nil(t, f.LastActionError) + require.True(t, sendUpdateCalled) +} + // mockStaticAddressServer captures static-address loop-in requests in tests. type mockStaticAddressServer struct { swapserverrpc.StaticAddressServerClient diff --git a/staticaddr/loopin/fsm.go b/staticaddr/loopin/fsm.go index ea6e610f..eb53dada 100644 --- a/staticaddr/loopin/fsm.go +++ b/staticaddr/loopin/fsm.go @@ -273,6 +273,22 @@ func (f *FSM) updateLoopIn(ctx context.Context, notification fsm.Notification) { return } + + err = f.sendUpdate(ctx) + if err != nil { + f.Errorf("Error sending loop-in update: %v", err) + } +} + +// sendUpdate publishes the latest loop-in state after it has been persisted. +// The callback must remain lightweight because it runs synchronously with FSM +// state transitions. +func (f *FSM) sendUpdate(ctx context.Context) error { + if f.cfg.SendUpdate == nil { + return nil + } + + return f.cfg.SendUpdate(ctx, f.loopIn) } // isUpdateSkipped returns true if the loop-in should not be updated for the diff --git a/staticaddr/loopin/manager.go b/staticaddr/loopin/manager.go index 92c4a9f1..47a447fc 100644 --- a/staticaddr/loopin/manager.go +++ b/staticaddr/loopin/manager.go @@ -98,6 +98,9 @@ type Config struct { // request. ValidateLoopInContract ValidateLoopInContract + // SendUpdate publishes a loop-in status update after it is persisted. + SendUpdate func(context.Context, *StaticAddressLoopIn) error + // MaxStaticAddrHtlcFeePercentage is the percentage of the swap amount // that we allow the server to charge for the htlc transaction. // Although highly unlikely, this is a defense against the server diff --git a/staticaddr/loopin/manager_test.go b/staticaddr/loopin/manager_test.go index 564429c6..9fa8587c 100644 --- a/staticaddr/loopin/manager_test.go +++ b/staticaddr/loopin/manager_test.go @@ -245,6 +245,45 @@ func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) { require.Equal(t, selectedDeposit.Value, quoteGetter.amount) } +// TestUpdateLoopInSendsUpdateAfterSuccessfulStoreUpdate protects the +// notification contract: after a successful database update, the manager must +// publish the stored loop-in state to listeners. +func TestUpdateLoopInSendsUpdateAfterSuccessfulStoreUpdate(t *testing.T) { + ctx := t.Context() + swapHash := lntypes.Hash{1, 2, 3} + updates := make(chan *StaticAddressLoopIn, 1) + loopIn := &StaticAddressLoopIn{SwapHash: swapHash} + loopIn.SetState(SignHtlcTx) + + loopInFsm := &FSM{ + cfg: &Config{ + Store: &mockStore{stored: true}, + SendUpdate: func(_ context.Context, + updated *StaticAddressLoopIn) error { + + updates <- updated + + return nil + }, + }, + loopIn: loopIn, + } + + loopInFsm.updateLoopIn(ctx, fsm.Notification{ + PreviousState: SignHtlcTx, + NextState: MonitorInvoiceAndHtlcTx, + }) + + select { + case updated := <-updates: + require.Equal(t, swapHash, updated.SwapHash) + require.Equal(t, MonitorInvoiceAndHtlcTx, updated.GetState()) + + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } +} + // TestHandleLoopInSweepReqRejectsInvalidServerNonce ensures that a malformed // MuSig2 nonce returned by the server is rejected before it reaches the signer. func TestHandleLoopInSweepReqRejectsInvalidServerNonce(t *testing.T) { @@ -501,6 +540,7 @@ type mockStore struct { swaps []*StaticAddressLoopIn loopIns map[lntypes.Hash]*StaticAddressLoopIn mapIDs map[lntypes.Hash][]deposit.ID + stored bool } func (s *mockStore) CreateLoopIn(_ context.Context, @@ -521,7 +561,7 @@ func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context, return s.swaps, nil } func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) { - return false, nil + return s.stored, nil } // RecordStaticAddressRiskDecision implements Store for manager tests. From 4a2bcb33747256bd68cf197c74030e7f009e61b3 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 23:39:30 -0300 Subject: [PATCH 07/36] cmd/loop: display static loop-in monitor stages --- cmd/loop/main.go | 65 ++++- cmd/loop/monitor_test.go | 275 ++++++++++++++++++ .../04_loop-monitor-static-loop-in.json | 267 +++++++++++++++++ 3 files changed, 600 insertions(+), 7 deletions(-) create mode 100644 cmd/loop/monitor_test.go create mode 100644 cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 294095ca..127634d2 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -560,10 +560,7 @@ func parseAmt(text string) (btcutil.Amount, error) { func logSwap(swap *looprpc.SwapStatus) { // If our swap failed, we add our failure reason to the state. - swapState := fmt.Sprintf("%v", swap.State) - if swap.State == looprpc.SwapState_FAILED { - swapState = fmt.Sprintf("%v (%v)", swapState, swap.FailureReason) - } + swapState := monitorSwapState(swap) if swap.Type == looprpc.SwapType_LOOP_OUT { fmt.Printf("%v %v %v %v - %v", @@ -585,10 +582,32 @@ func logSwap(swap *looprpc.SwapStatus) { } } - if swap.State != looprpc.SwapState_INITIATED && - swap.State != looprpc.SwapState_HTLC_PUBLISHED && - swap.State != looprpc.SwapState_PREIMAGE_REVEALED { + showCost := shouldShowSwapCost(swap.GetState()) + if swap.Type == looprpc.SwapType_STATIC_LOOP_IN { + staticState := swap.GetStaticLoopInState() + // Static loop-ins key cost visibility off the dedicated FSM state, not + // the generic SwapState lifecycle used by traditional swaps. + switch staticState { + case looprpc.StaticAddressLoopInSwapState_INIT_HTLC, + looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX, + looprpc.StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX, + looprpc.StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT, + looprpc.StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP, + looprpc.StaticAddressLoopInSwapState_UNLOCK_DEPOSITS: + showCost = false + + case looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED, + looprpc.StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT, + looprpc.StaticAddressLoopInSwapState_SUCCEEDED, + looprpc.StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED, + looprpc.StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP: + + showCost = true + } + } + + if showCost { fmt.Printf(" (cost: server %v, onchain %v, offchain %v)", swap.CostServer, swap.CostOnchain, swap.CostOffchain, ) @@ -597,6 +616,38 @@ func logSwap(swap *looprpc.SwapStatus) { fmt.Println() } +// monitorSwapState returns the static loop-in FSM label for static swaps and +// the shared swap-state label for all others. +func monitorSwapState(swap *looprpc.SwapStatus) string { + if swap.Type == looprpc.SwapType_STATIC_LOOP_IN { + return swap.GetStaticLoopInState().String() + } + + return genericMonitorSwapState(swap) +} + +// shouldShowSwapCost reports whether a swap's generic state is terminal enough +// to include the persisted cost summary in monitor output. +func shouldShowSwapCost(loopState looprpc.SwapState) bool { + return loopState != looprpc.SwapState_INITIATED && + loopState != looprpc.SwapState_HTLC_PUBLISHED && + loopState != looprpc.SwapState_PREIMAGE_REVEALED +} + +// genericMonitorSwapState formats the shared swap-state label and failure +// reason used by non-static swaps. +func genericMonitorSwapState(swap *looprpc.SwapStatus) string { + loopState := swap.GetState() + swapState := fmt.Sprintf("%v", loopState) + if loopState == looprpc.SwapState_FAILED { + swapState = fmt.Sprintf( + "%v (%v)", swapState, swap.FailureReason, + ) + } + + return swapState +} + // getClientConn dials the loopd gRPC server with TLS and macaroon auth. func getClientConn(address, tlsCertPath, macaroonPath string) (daemonConn, func(), error) { diff --git a/cmd/loop/monitor_test.go b/cmd/loop/monitor_test.go new file mode 100644 index 00000000..9ed4841e --- /dev/null +++ b/cmd/loop/monitor_test.go @@ -0,0 +1,275 @@ +package main + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/lightninglabs/loop/looprpc" + "github.com/stretchr/testify/require" +) + +// TestMonitorSwapStateKeepsRegularLoopInFailureReason preserves the generic +// failure suffix for regular loop-in swaps so terminal errors stay visible. +func TestMonitorSwapStateKeepsRegularLoopInFailureReason(t *testing.T) { + swap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_LOOP_IN, + State: looprpc.SwapState_FAILED, + FailureReason: looprpc. + FailureReason_FAILURE_REASON_TIMEOUT, + } + + got := monitorSwapState(swap) + require.Equal(t, "FAILED (FAILURE_REASON_TIMEOUT)", got) +} + +// TestMonitorSwapStateLabelsStaticLoopInStages locks the precise static loop-in +// stage names so monitor output stays stable as the FSM evolves. +func TestMonitorSwapStateLabelsStaticLoopInStages(t *testing.T) { + tests := []struct { + name string + staticState looprpc.StaticAddressLoopInSwapState + want string + }{ + { + name: "init htlc", + staticState: looprpc.StaticAddressLoopInSwapState_INIT_HTLC, + want: "INIT_HTLC", + }, + { + name: "sign htlc", + staticState: looprpc. + StaticAddressLoopInSwapState_SIGN_HTLC_TX, + want: "SIGN_HTLC_TX", + }, + { + name: "monitor invoice and htlc", + staticState: looprpc. + StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX, + want: "MONITOR_INVOICE_HTLC_TX", + }, + { + name: "unlock deposits", + staticState: looprpc. + StaticAddressLoopInSwapState_UNLOCK_DEPOSITS, + want: "UNLOCK_DEPOSITS", + }, + { + name: "payment received", + staticState: looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED, + want: "PAYMENT_RECEIVED", + }, + { + name: "timeout sweep", + staticState: looprpc. + StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT, + want: "SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT", + }, + { + name: "monitor timeout sweep", + staticState: looprpc. + StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP, + want: "MONITOR_HTLC_TIMEOUT_SWEEP", + }, + { + name: "timeout swept", + staticState: looprpc. + StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT, + want: "HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT", + }, + { + name: "success", + staticState: looprpc.StaticAddressLoopInSwapState_SUCCEEDED, + want: "SUCCEEDED", + }, + { + name: "succeeded transitioning failed", + staticState: looprpc. + StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED, + want: "SUCCEEDED_TRANSITIONING_FAILED", + }, + { + name: "failed", + staticState: looprpc. + StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP, + want: "FAILED_STATIC_ADDRESS_SWAP", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + swap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: test.staticState, + }, + } + + got := monitorSwapState(swap) + require.Equal(t, test.want, got) + }) + } + + initSwap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_INIT_HTLC, + }, + } + signSwap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_SIGN_HTLC_TX, + }, + } + require.NotEqual(t, monitorSwapState(initSwap), monitorSwapState(signSwap)) +} + +// TestMonitorSwapStateLabelsUnknownStaticLoopIn ensures the absent static +// oneof maps to the UNKNOWN static loop-in label. +func TestMonitorSwapStateLabelsUnknownStaticLoopIn(t *testing.T) { + swap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + } + + got := monitorSwapState(swap) + require.Equal(t, "UNKNOWN_STATIC_ADDRESS_SWAP_STATE", got) +} + +// TestLogSwapHidesStaticLoopInCostForInFlightState proves in-flight static FSM +// state suppresses cost even with generic SUCCESS set deliberately. +func TestLogSwapHidesStaticLoopInCostForInFlightState(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(1, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + State: looprpc.SwapState_SUCCESS, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_INIT_HTLC, + }, + Amt: 50_000, + CostServer: 11, + CostOnchain: 22, + CostOffchain: 33, + HtlcAddressP2Wsh: "bc1qstaticinflighttestaddress", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains(t, output, "STATIC_LOOP_IN INIT_HTLC 0.00050000 BTC") + require.NotContains(t, output, "(cost:") +} + +// TestLogSwapHidesCostForUnknownStaticLoopInState ensures unknown static +// states print the UNKNOWN label without a cost summary. +func TestLogSwapHidesCostForUnknownStaticLoopInState(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(3, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE, + }, + Amt: 50_000, + CostServer: 11, + CostOnchain: 22, + CostOffchain: 33, + HtlcAddressP2Wsh: "bc1qunknownstaticterminaltestaddress", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains( + t, output, "STATIC_LOOP_IN UNKNOWN_STATIC_ADDRESS_SWAP_STATE 0.00050000 BTC", + ) + require.NotContains(t, output, "(cost:") +} + +// TestLogSwapShowsStaticLoopInCostForTerminalState preserves terminal cost +// output, including the P2WSH address, once the static loop-in is done. +func TestLogSwapShowsStaticLoopInCostForTerminalState(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(2, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + State: looprpc.SwapState_INITIATED, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_SUCCEEDED, + }, + Amt: 50_000, + CostServer: 11, + CostOnchain: 22, + CostOffchain: 33, + HtlcAddressP2Wsh: "bc1qstaticterminaltestaddress", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains( + t, output, + "STATIC_LOOP_IN SUCCEEDED 0.00050000 BTC - P2WSH: bc1qstaticterminaltestaddress", + ) + require.Contains( + t, output, + "(cost: server 11, onchain 22, offchain 33)", + ) +} + +// TestLogSwapDisplaysStaticLoopInTypeAndStage ensures the monitor output names +// the static loop-in type and active FSM stage instead of collapsing them. +func TestLogSwapDisplaysStaticLoopInTypeAndStage(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(1, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_SIGN_HTLC_TX, + }, + Amt: 50_000, + HtlcAddressP2Wsh: "tb1q5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains( + t, output, "STATIC_LOOP_IN SIGN_HTLC_TX 0.00050000 BTC", + ) + require.Contains( + t, output, "P2WSH: tb1q5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8", + ) + require.NotContains(t, output, "STATIC_LOOP_IN INIT_HTLC") +} + +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + + originalStdout := os.Stdout + reader, writer, err := os.Pipe() + require.NoError(t, err) + os.Stdout = writer + + fn() + + require.NoError(t, writer.Close()) + os.Stdout = originalStdout + + var buf bytes.Buffer + _, err = io.Copy(&buf, reader) + require.NoError(t, err) + require.NoError(t, reader.Close()) + + return strings.TrimSpace(buf.String()) +} diff --git a/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json b/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json new file mode 100644 index 00000000..451d34f3 --- /dev/null +++ b/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json @@ -0,0 +1,267 @@ +{ + "metadata": { + "args": [ + "loop", + "monitor", + "--network", + "regtest" + ], + "env": {}, + "version": "0.33.3-beta commit= commit_hash=", + "run_error": "recv: rpc error: code = Canceled desc = context canceled", + "duration": 25407490958, + "clock_start_unix": 1784150305 + }, + "events": [ + { + "time_ms": 13, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "send", + "message_type": "looprpc.MonitorRequest", + "payload": {} + } + }, + { + "time_ms": 13, + "kind": "stdout", + "data": { + "lines": [ + "Note: offchain cost may report as 0 after loopd restart during swap\n" + ] + } + }, + { + "time_ms": 16479, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "INIT_HTLC", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150321494895000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "0", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 16479, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:41-03:00 STATIC_LOOP_IN INIT_HTLC 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n" + ] + } + }, + { + "time_ms": 16482, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "SIGN_HTLC_TX", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150321499639000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "0", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 16482, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:41-03:00 STATIC_LOOP_IN SIGN_HTLC_TX 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n" + ] + } + }, + { + "time_ms": 16530, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "MONITOR_INVOICE_HTLC_TX", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150321548450000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "0", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 16530, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:41-03:00 STATIC_LOOP_IN MONITOR_INVOICE_HTLC_TX 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n" + ] + } + }, + { + "time_ms": 17027, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "PAYMENT_RECEIVED", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150322044521000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "1828", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 17027, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:42-03:00 STATIC_LOOP_IN PAYMENT_RECEIVED 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv (cost: server 1828, onchain 0, offchain 0)\n" + ] + } + }, + { + "time_ms": 17036, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "SUCCEEDED", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150322052572000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "1828", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 17036, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:42-03:00 STATIC_LOOP_IN SUCCEEDED 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv (cost: server 1828, onchain 0, offchain 0)\n" + ] + } + }, + { + "time_ms": 25406, + "kind": "signal", + "data": { + "signal": "interrupt" + } + }, + { + "time_ms": 25407, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "error", + "error": "rpc error: code = Canceled desc = context canceled", + "status": { + "code": 1, + "message": "context canceled" + } + } + }, + { + "time_ms": 25407, + "kind": "stderr", + "data": { + "lines": [ + "[loop] recv: rpc error: code = Canceled desc = context canceled\n" + ] + } + }, + { + "time_ms": 25407, + "kind": "exit", + "data": { + "run_error": "recv: rpc error: code = Canceled desc = context canceled" + } + } + ] +} From d5fb8b8da8f3972b1d2601f03719f0b0ed902b96 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Tue, 14 Jul 2026 10:17:51 -0300 Subject: [PATCH 08/36] cmd/loop: clarify traditional swap command scope --- cmd/loop/swaps.go | 15 +++++++++------ docs/loop.1 | 4 ++-- docs/loop.md | 8 ++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cmd/loop/swaps.go b/cmd/loop/swaps.go index 9323f6ce..419522b6 100644 --- a/cmd/loop/swaps.go +++ b/cmd/loop/swaps.go @@ -15,9 +15,10 @@ import ( var listSwapsCommand = &cli.Command{ Name: "listswaps", - Usage: "list all swaps in the local database", - Description: "Allows the user to get a list of all swaps that are " + - "currently stored in the database", + Usage: "list traditional Loop In and Loop Out swaps", + Description: "Lists traditional Loop In and Loop Out swaps that are " + + "currently stored in the database. Static address loop-ins are not " + + "included; use `loop static listswaps` to view them.", Action: listSwaps, Flags: []cli.Flag{ &cli.BoolFlag{ @@ -128,10 +129,12 @@ func listSwaps(ctx context.Context, cmd *cli.Command) error { var swapInfoCommand = &cli.Command{ Name: "swapinfo", - Usage: "show the status of a swap", + Usage: "show the status of a traditional swap", ArgsUsage: "id", - Description: "Allows the user to get the status of a single swap " + - "currently stored in the database", + Description: "Shows the status of a traditional Loop In or Loop Out " + + "swap currently stored in the database. Static address loop-ins " + + "must be viewed with `loop static listswaps`; there is no generic " + + "per-swap static lookup command.", Flags: []cli.Flag{ &cli.Uint64Flag{ Name: "id", diff --git a/docs/loop.1 b/docs/loop.1 index b100a83b..1e21905b 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -203,7 +203,7 @@ fetches a new L402 authentication token from the server \fB--help, -h\fP: show help .SH listswaps -list all swaps in the local database +list traditional Loop In and Loop Out swaps .PP \fB--channel\fP="": the comma-separated list of short channel IDs of the channels to loop out @@ -233,7 +233,7 @@ list all swaps in the local database \fB--start_time_ns\fP="": Unix timestamp in nanoseconds to select swaps initiated after this time (default: 0) .SH swapinfo -show the status of a swap +show the status of a traditional swap .PP \fB--help, -h\fP: show help diff --git a/docs/loop.md b/docs/loop.md index 688d3776..316cebdc 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -235,9 +235,9 @@ The following flags are supported: ### `listswaps` command -list all swaps in the local database. +list traditional Loop In and Loop Out swaps. -Allows the user to get a list of all swaps that are currently stored in the database. +Lists traditional Loop In and Loop Out swaps that are currently stored in the database. Static address loop-ins are not included; use `loop static listswaps` to view them. Usage: @@ -261,9 +261,9 @@ The following flags are supported: ### `swapinfo` command -show the status of a swap. +show the status of a traditional swap. -Allows the user to get the status of a single swap currently stored in the database. +Shows the status of a traditional Loop In or Loop Out swap currently stored in the database. Static address loop-ins must be viewed with `loop static listswaps`; there is no generic per-swap static lookup command. Usage: From 4161f5d128964be4ff0e736e5f82ff60cf14907f Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Thu, 23 Jul 2026 03:10:19 +0000 Subject: [PATCH 09/36] multi: bump google.golang.org/grpc to v1.82.1 --- go.mod | 6 +++--- go.sum | 12 ++++++------ looprpc/go.mod | 6 +++--- looprpc/go.sum | 12 ++++++------ swapserverrpc/common.pb.go | 2 +- swapserverrpc/go.mod | 4 ++-- swapserverrpc/go.sum | 28 ++++++++++++++-------------- swapserverrpc/instantout.pb.go | 2 +- swapserverrpc/reservation.pb.go | 2 +- swapserverrpc/server.pb.go | 2 +- swapserverrpc/staticaddr.pb.go | 2 +- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index d910e3d5..4a32aa7d 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/urfave/cli/v3 v3.4.1 go.etcd.io/bbolt v1.4.3 golang.org/x/sync v0.20.0 - google.golang.org/grpc v1.80.0 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 gopkg.in/macaroon-bakery.v2 v2.3.0 gopkg.in/macaroon.v2 v2.1.0 @@ -187,8 +187,8 @@ require ( golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 95bd37cf..9ba8984a 100644 --- a/go.sum +++ b/go.sum @@ -915,10 +915,10 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= -google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -932,8 +932,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/looprpc/go.mod b/looprpc/go.mod index 796e470b..16a04cad 100644 --- a/looprpc/go.mod +++ b/looprpc/go.mod @@ -6,7 +6,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/lightninglabs/loop/swapserverrpc v1.0.14 github.com/lightningnetwork/lnd v0.21.0-beta - google.golang.org/grpc v1.79.3 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 gopkg.in/macaroon-bakery.v2 v2.3.0 ) @@ -165,8 +165,8 @@ require ( golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon.v2 v2.1.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect diff --git a/looprpc/go.sum b/looprpc/go.sum index 055cb67f..e5499c1a 100644 --- a/looprpc/go.sum +++ b/looprpc/go.sum @@ -603,18 +603,18 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/swapserverrpc/common.pb.go b/swapserverrpc/common.pb.go index ae84e1a4..0ae8bb21 100644 --- a/swapserverrpc/common.pb.go +++ b/swapserverrpc/common.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: common.proto diff --git a/swapserverrpc/go.mod b/swapserverrpc/go.mod index 38664dac..2b71eea3 100644 --- a/swapserverrpc/go.mod +++ b/swapserverrpc/go.mod @@ -1,8 +1,8 @@ module github.com/lightninglabs/loop/swapserverrpc require ( - google.golang.org/grpc v1.79.3 - google.golang.org/protobuf v1.36.10 + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 ) require ( diff --git a/swapserverrpc/go.sum b/swapserverrpc/go.sum index b9da7161..2c6e462d 100644 --- a/swapserverrpc/go.sum +++ b/swapserverrpc/go.sum @@ -14,16 +14,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= @@ -34,7 +34,7 @@ golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/swapserverrpc/instantout.pb.go b/swapserverrpc/instantout.pb.go index 4593f002..96cd8e70 100644 --- a/swapserverrpc/instantout.pb.go +++ b/swapserverrpc/instantout.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: instantout.proto diff --git a/swapserverrpc/reservation.pb.go b/swapserverrpc/reservation.pb.go index 3293ee47..a1f73b43 100644 --- a/swapserverrpc/reservation.pb.go +++ b/swapserverrpc/reservation.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: reservation.proto diff --git a/swapserverrpc/server.pb.go b/swapserverrpc/server.pb.go index cbca725e..c2b1d440 100644 --- a/swapserverrpc/server.pb.go +++ b/swapserverrpc/server.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: server.proto diff --git a/swapserverrpc/staticaddr.pb.go b/swapserverrpc/staticaddr.pb.go index 77ad9799..40ab2229 100644 --- a/swapserverrpc/staticaddr.pb.go +++ b/swapserverrpc/staticaddr.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: staticaddr.proto From cb97b2a37f77fb283bf055f95b2f8c4996ffa4c1 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 6 May 2026 14:58:40 +0200 Subject: [PATCH 10/36] swapserverrpc: add multi-address proofs Carry each deposit client derivation key and an optional generated change descriptor in static-address loop-in and withdrawal requests. The server can use these fields to validate scripts it does not store directly. --- swapserverrpc/staticaddr.pb.go | 326 +++++++++++++++++++++++++-------- swapserverrpc/staticaddr.proto | 31 ++++ 2 files changed, 277 insertions(+), 80 deletions(-) diff --git a/swapserverrpc/staticaddr.pb.go b/swapserverrpc/staticaddr.pb.go index 77ad9799..11bd58d1 100644 --- a/swapserverrpc/staticaddr.pb.go +++ b/swapserverrpc/staticaddr.pb.go @@ -387,8 +387,12 @@ type ServerPsbtWithdrawRequest struct { WithdrawalPsbt []byte `protobuf:"bytes,1,opt,name=withdrawal_psbt,json=withdrawalPsbt,proto3" json:"withdrawal_psbt,omitempty"` // The map of deposit txid:idx to the nonce used by the client. DepositToNonces map[string][]byte `protobuf:"bytes,2,rep,name=deposit_to_nonces,json=depositToNonces,proto3" json:"deposit_to_nonces,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + DepositToClientPubkeys map[string]*StaticAddressDescriptor `protobuf:"bytes,3,rep,name=deposit_to_client_pubkeys,json=depositToClientPubkeys,proto3" json:"deposit_to_client_pubkeys,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ServerPsbtWithdrawRequest) Reset() { @@ -435,6 +439,13 @@ func (x *ServerPsbtWithdrawRequest) GetDepositToNonces() map[string][]byte { return nil } +func (x *ServerPsbtWithdrawRequest) GetDepositToClientPubkeys() map[string]*StaticAddressDescriptor { + if x != nil { + return x.DepositToClientPubkeys + } + return nil +} + type ServerPsbtWithdrawResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The txid of the psbt that the client wants to push the sigs for. @@ -544,6 +555,115 @@ func (x *ServerPsbtWithdrawSigningInfo) GetSig() []byte { return nil } +type StaticAddressDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The client's secp256k1 public key serialized in 33-byte compressed SEC1 + // format. This is not the 32-byte x-only BIP-340 encoding. + Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` + // The expected output script for the static address. + PkScript []byte `protobuf:"bytes,2,opt,name=pk_script,json=pkScript,proto3" json:"pk_script,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StaticAddressDescriptor) Reset() { + *x = StaticAddressDescriptor{} + mi := &file_staticaddr_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StaticAddressDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StaticAddressDescriptor) ProtoMessage() {} + +func (x *StaticAddressDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_staticaddr_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StaticAddressDescriptor.ProtoReflect.Descriptor instead. +func (*StaticAddressDescriptor) Descriptor() ([]byte, []int) { + return file_staticaddr_proto_rawDescGZIP(), []int{8} +} + +func (x *StaticAddressDescriptor) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +func (x *StaticAddressDescriptor) GetPkScript() []byte { + if x != nil { + return x.PkScript + } + return nil +} + +type StaticAddressChangeOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The descriptor for the static address change output. + StaticAddress *StaticAddressDescriptor `protobuf:"bytes,1,opt,name=static_address,json=staticAddress,proto3" json:"static_address,omitempty"` + // The expected change amount in satoshis. + Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StaticAddressChangeOutput) Reset() { + *x = StaticAddressChangeOutput{} + mi := &file_staticaddr_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StaticAddressChangeOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StaticAddressChangeOutput) ProtoMessage() {} + +func (x *StaticAddressChangeOutput) ProtoReflect() protoreflect.Message { + mi := &file_staticaddr_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StaticAddressChangeOutput.ProtoReflect.Descriptor instead. +func (*StaticAddressChangeOutput) Descriptor() ([]byte, []int) { + return file_staticaddr_proto_rawDescGZIP(), []int{9} +} + +func (x *StaticAddressChangeOutput) GetStaticAddress() *StaticAddressDescriptor { + if x != nil { + return x.StaticAddress + } + return nil +} + +func (x *StaticAddressChangeOutput) GetAmount() int64 { + if x != nil { + return x.Amount + } + return 0 +} + type ServerStaticAddressLoopInRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The client's public key for the htlc output. @@ -589,14 +709,21 @@ type ServerStaticAddressLoopInRequest struct { Amount uint64 `protobuf:"varint,9,opt,name=amount,proto3" json:"amount,omitempty"` // If set, request the server to use fast publication behavior for this // swap. - Fast bool `protobuf:"varint,10,opt,name=fast,proto3" json:"fast,omitempty"` + Fast bool `protobuf:"varint,10,opt,name=fast,proto3" json:"fast,omitempty"` + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + DepositToClientPubkeys map[string]*StaticAddressDescriptor `protobuf:"bytes,11,rep,name=deposit_to_client_pubkeys,json=depositToClientPubkeys,proto3" json:"deposit_to_client_pubkeys,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional change output metadata for fractional loop-ins that return + // funds to a newly generated static address. + ChangeOutput *StaticAddressChangeOutput `protobuf:"bytes,12,opt,name=change_output,json=changeOutput,proto3" json:"change_output,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServerStaticAddressLoopInRequest) Reset() { *x = ServerStaticAddressLoopInRequest{} - mi := &file_staticaddr_proto_msgTypes[8] + mi := &file_staticaddr_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -608,7 +735,7 @@ func (x *ServerStaticAddressLoopInRequest) String() string { func (*ServerStaticAddressLoopInRequest) ProtoMessage() {} func (x *ServerStaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[8] + mi := &file_staticaddr_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -621,7 +748,7 @@ func (x *ServerStaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerStaticAddressLoopInRequest.ProtoReflect.Descriptor instead. func (*ServerStaticAddressLoopInRequest) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{8} + return file_staticaddr_proto_rawDescGZIP(), []int{10} } func (x *ServerStaticAddressLoopInRequest) GetHtlcClientPubKey() []byte { @@ -694,6 +821,20 @@ func (x *ServerStaticAddressLoopInRequest) GetFast() bool { return false } +func (x *ServerStaticAddressLoopInRequest) GetDepositToClientPubkeys() map[string]*StaticAddressDescriptor { + if x != nil { + return x.DepositToClientPubkeys + } + return nil +} + +func (x *ServerStaticAddressLoopInRequest) GetChangeOutput() *StaticAddressChangeOutput { + if x != nil { + return x.ChangeOutput + } + return nil +} + type ServerStaticAddressLoopInResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The server's public key for the htlc output. @@ -715,7 +856,7 @@ type ServerStaticAddressLoopInResponse struct { func (x *ServerStaticAddressLoopInResponse) Reset() { *x = ServerStaticAddressLoopInResponse{} - mi := &file_staticaddr_proto_msgTypes[9] + mi := &file_staticaddr_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -727,7 +868,7 @@ func (x *ServerStaticAddressLoopInResponse) String() string { func (*ServerStaticAddressLoopInResponse) ProtoMessage() {} func (x *ServerStaticAddressLoopInResponse) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[9] + mi := &file_staticaddr_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -740,7 +881,7 @@ func (x *ServerStaticAddressLoopInResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ServerStaticAddressLoopInResponse.ProtoReflect.Descriptor instead. func (*ServerStaticAddressLoopInResponse) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{9} + return file_staticaddr_proto_rawDescGZIP(), []int{11} } func (x *ServerStaticAddressLoopInResponse) GetHtlcServerPubKey() []byte { @@ -790,7 +931,7 @@ type ServerHtlcSigningInfo struct { func (x *ServerHtlcSigningInfo) Reset() { *x = ServerHtlcSigningInfo{} - mi := &file_staticaddr_proto_msgTypes[10] + mi := &file_staticaddr_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -802,7 +943,7 @@ func (x *ServerHtlcSigningInfo) String() string { func (*ServerHtlcSigningInfo) ProtoMessage() {} func (x *ServerHtlcSigningInfo) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[10] + mi := &file_staticaddr_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -815,7 +956,7 @@ func (x *ServerHtlcSigningInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerHtlcSigningInfo.ProtoReflect.Descriptor instead. func (*ServerHtlcSigningInfo) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{10} + return file_staticaddr_proto_rawDescGZIP(), []int{12} } func (x *ServerHtlcSigningInfo) GetNonces() [][]byte { @@ -848,7 +989,7 @@ type PushStaticAddressHtlcSigsRequest struct { func (x *PushStaticAddressHtlcSigsRequest) Reset() { *x = PushStaticAddressHtlcSigsRequest{} - mi := &file_staticaddr_proto_msgTypes[11] + mi := &file_staticaddr_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -860,7 +1001,7 @@ func (x *PushStaticAddressHtlcSigsRequest) String() string { func (*PushStaticAddressHtlcSigsRequest) ProtoMessage() {} func (x *PushStaticAddressHtlcSigsRequest) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[11] + mi := &file_staticaddr_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -873,7 +1014,7 @@ func (x *PushStaticAddressHtlcSigsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushStaticAddressHtlcSigsRequest.ProtoReflect.Descriptor instead. func (*PushStaticAddressHtlcSigsRequest) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{11} + return file_staticaddr_proto_rawDescGZIP(), []int{13} } func (x *PushStaticAddressHtlcSigsRequest) GetSwapHash() []byte { @@ -916,7 +1057,7 @@ type ClientHtlcSigningInfo struct { func (x *ClientHtlcSigningInfo) Reset() { *x = ClientHtlcSigningInfo{} - mi := &file_staticaddr_proto_msgTypes[12] + mi := &file_staticaddr_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -928,7 +1069,7 @@ func (x *ClientHtlcSigningInfo) String() string { func (*ClientHtlcSigningInfo) ProtoMessage() {} func (x *ClientHtlcSigningInfo) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[12] + mi := &file_staticaddr_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -941,7 +1082,7 @@ func (x *ClientHtlcSigningInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ClientHtlcSigningInfo.ProtoReflect.Descriptor instead. func (*ClientHtlcSigningInfo) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{12} + return file_staticaddr_proto_rawDescGZIP(), []int{14} } func (x *ClientHtlcSigningInfo) GetNonces() [][]byte { @@ -966,7 +1107,7 @@ type PushStaticAddressHtlcSigsResponse struct { func (x *PushStaticAddressHtlcSigsResponse) Reset() { *x = PushStaticAddressHtlcSigsResponse{} - mi := &file_staticaddr_proto_msgTypes[13] + mi := &file_staticaddr_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -978,7 +1119,7 @@ func (x *PushStaticAddressHtlcSigsResponse) String() string { func (*PushStaticAddressHtlcSigsResponse) ProtoMessage() {} func (x *PushStaticAddressHtlcSigsResponse) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[13] + mi := &file_staticaddr_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -991,7 +1132,7 @@ func (x *PushStaticAddressHtlcSigsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use PushStaticAddressHtlcSigsResponse.ProtoReflect.Descriptor instead. func (*PushStaticAddressHtlcSigsResponse) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{13} + return file_staticaddr_proto_rawDescGZIP(), []int{15} } type PushStaticAddressSweeplessSigsRequest struct { @@ -1013,7 +1154,7 @@ type PushStaticAddressSweeplessSigsRequest struct { func (x *PushStaticAddressSweeplessSigsRequest) Reset() { *x = PushStaticAddressSweeplessSigsRequest{} - mi := &file_staticaddr_proto_msgTypes[14] + mi := &file_staticaddr_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1025,7 +1166,7 @@ func (x *PushStaticAddressSweeplessSigsRequest) String() string { func (*PushStaticAddressSweeplessSigsRequest) ProtoMessage() {} func (x *PushStaticAddressSweeplessSigsRequest) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[14] + mi := &file_staticaddr_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1038,7 +1179,7 @@ func (x *PushStaticAddressSweeplessSigsRequest) ProtoReflect() protoreflect.Mess // Deprecated: Use PushStaticAddressSweeplessSigsRequest.ProtoReflect.Descriptor instead. func (*PushStaticAddressSweeplessSigsRequest) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{14} + return file_staticaddr_proto_rawDescGZIP(), []int{16} } func (x *PushStaticAddressSweeplessSigsRequest) GetSwapHash() []byte { @@ -1082,7 +1223,7 @@ type ClientSweeplessSigningInfo struct { func (x *ClientSweeplessSigningInfo) Reset() { *x = ClientSweeplessSigningInfo{} - mi := &file_staticaddr_proto_msgTypes[15] + mi := &file_staticaddr_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1094,7 +1235,7 @@ func (x *ClientSweeplessSigningInfo) String() string { func (*ClientSweeplessSigningInfo) ProtoMessage() {} func (x *ClientSweeplessSigningInfo) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[15] + mi := &file_staticaddr_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1107,7 +1248,7 @@ func (x *ClientSweeplessSigningInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ClientSweeplessSigningInfo.ProtoReflect.Descriptor instead. func (*ClientSweeplessSigningInfo) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{15} + return file_staticaddr_proto_rawDescGZIP(), []int{17} } func (x *ClientSweeplessSigningInfo) GetNonce() []byte { @@ -1132,7 +1273,7 @@ type PushStaticAddressSweeplessSigsResponse struct { func (x *PushStaticAddressSweeplessSigsResponse) Reset() { *x = PushStaticAddressSweeplessSigsResponse{} - mi := &file_staticaddr_proto_msgTypes[16] + mi := &file_staticaddr_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1144,7 +1285,7 @@ func (x *PushStaticAddressSweeplessSigsResponse) String() string { func (*PushStaticAddressSweeplessSigsResponse) ProtoMessage() {} func (x *PushStaticAddressSweeplessSigsResponse) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[16] + mi := &file_staticaddr_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1157,7 +1298,7 @@ func (x *PushStaticAddressSweeplessSigsResponse) ProtoReflect() protoreflect.Mes // Deprecated: Use PushStaticAddressSweeplessSigsResponse.ProtoReflect.Descriptor instead. func (*PushStaticAddressSweeplessSigsResponse) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{16} + return file_staticaddr_proto_rawDescGZIP(), []int{18} } var File_staticaddr_proto protoreflect.FileDescriptor @@ -1184,13 +1325,17 @@ const file_staticaddr_proto_rawDesc = "" + "\rchange_amount\x18\x06 \x01(\x03R\fchangeAmount:\x02\x18\x01\"m\n" + "\x16ServerWithdrawResponse\x12*\n" + "\x11musig2_sweep_sigs\x18\x01 \x03(\fR\x0fmusig2SweepSigs\x12#\n" + - "\rserver_nonces\x18\x02 \x03(\fR\fserverNonces:\x02\x18\x01\"\xed\x01\n" + + "\rserver_nonces\x18\x02 \x03(\fR\fserverNonces:\x02\x18\x01\"\xd5\x03\n" + "\x19ServerPsbtWithdrawRequest\x12'\n" + "\x0fwithdrawal_psbt\x18\x01 \x01(\fR\x0ewithdrawalPsbt\x12c\n" + - "\x11deposit_to_nonces\x18\x02 \x03(\v27.looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntryR\x0fdepositToNonces\x1aB\n" + + "\x11deposit_to_nonces\x18\x02 \x03(\v27.looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntryR\x0fdepositToNonces\x12y\n" + + "\x19deposit_to_client_pubkeys\x18\x03 \x03(\v2>.looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntryR\x16depositToClientPubkeys\x1aB\n" + "\x14DepositToNoncesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\xf1\x01\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\x1ak\n" + + "\x1bDepositToClientPubkeysEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x126\n" + + "\x05value\x18\x02 \x01(\v2 .looprpc.StaticAddressDescriptorR\x05value:\x028\x01\"\xf1\x01\n" + "\x1aServerPsbtWithdrawResponse\x12\x12\n" + "\x04txid\x18\x01 \x01(\fR\x04txid\x12W\n" + "\fsigning_info\x18\x02 \x03(\v24.looprpc.ServerPsbtWithdrawResponse.SigningInfoEntryR\vsigningInfo\x1af\n" + @@ -1199,7 +1344,13 @@ const file_staticaddr_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\v2&.looprpc.ServerPsbtWithdrawSigningInfoR\x05value:\x028\x01\"G\n" + "\x1dServerPsbtWithdrawSigningInfo\x12\x14\n" + "\x05nonce\x18\x01 \x01(\fR\x05nonce\x12\x10\n" + - "\x03sig\x18\x02 \x01(\fR\x03sig\"\xae\x03\n" + + "\x03sig\x18\x02 \x01(\fR\x03sig\"N\n" + + "\x17StaticAddressDescriptor\x12\x16\n" + + "\x06pubkey\x18\x01 \x01(\fR\x06pubkey\x12\x1b\n" + + "\tpk_script\x18\x02 \x01(\fR\bpkScript\"|\n" + + "\x19StaticAddressChangeOutput\x12G\n" + + "\x0estatic_address\x18\x01 \x01(\v2 .looprpc.StaticAddressDescriptorR\rstaticAddress\x12\x16\n" + + "\x06amount\x18\x02 \x01(\x03R\x06amount\"\xe7\x05\n" + " ServerStaticAddressLoopInRequest\x12-\n" + "\x13htlc_client_pub_key\x18\x01 \x01(\fR\x10htlcClientPubKey\x12\x1b\n" + "\tswap_hash\x18\x02 \x01(\fR\bswapHash\x12+\n" + @@ -1212,7 +1363,12 @@ const file_staticaddr_proto_rawDesc = "" + "\x17payment_timeout_seconds\x18\b \x01(\rR\x15paymentTimeoutSeconds\x12\x16\n" + "\x06amount\x18\t \x01(\x04R\x06amount\x12\x12\n" + "\x04fast\x18\n" + - " \x01(\bR\x04fast\"\xe1\x02\n" + + " \x01(\bR\x04fast\x12\x80\x01\n" + + "\x19deposit_to_client_pubkeys\x18\v \x03(\v2E.looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntryR\x16depositToClientPubkeys\x12G\n" + + "\rchange_output\x18\f \x01(\v2\".looprpc.StaticAddressChangeOutputR\fchangeOutput\x1ak\n" + + "\x1bDepositToClientPubkeysEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x126\n" + + "\x05value\x18\x02 \x01(\v2 .looprpc.StaticAddressDescriptorR\x05value:\x028\x01\"\xe1\x02\n" + "!ServerStaticAddressLoopInResponse\x12-\n" + "\x13htlc_server_pub_key\x18\x01 \x01(\fR\x10htlcServerPubKey\x12\x1f\n" + "\vhtlc_expiry\x18\x02 \x01(\x05R\n" + @@ -1267,7 +1423,7 @@ func file_staticaddr_proto_rawDescGZIP() []byte { } var file_staticaddr_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_staticaddr_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_staticaddr_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_staticaddr_proto_goTypes = []any{ (StaticAddressProtocolVersion)(0), // 0: looprpc.StaticAddressProtocolVersion (*ServerNewAddressRequest)(nil), // 1: looprpc.ServerNewAddressRequest @@ -1278,53 +1434,63 @@ var file_staticaddr_proto_goTypes = []any{ (*ServerPsbtWithdrawRequest)(nil), // 6: looprpc.ServerPsbtWithdrawRequest (*ServerPsbtWithdrawResponse)(nil), // 7: looprpc.ServerPsbtWithdrawResponse (*ServerPsbtWithdrawSigningInfo)(nil), // 8: looprpc.ServerPsbtWithdrawSigningInfo - (*ServerStaticAddressLoopInRequest)(nil), // 9: looprpc.ServerStaticAddressLoopInRequest - (*ServerStaticAddressLoopInResponse)(nil), // 10: looprpc.ServerStaticAddressLoopInResponse - (*ServerHtlcSigningInfo)(nil), // 11: looprpc.ServerHtlcSigningInfo - (*PushStaticAddressHtlcSigsRequest)(nil), // 12: looprpc.PushStaticAddressHtlcSigsRequest - (*ClientHtlcSigningInfo)(nil), // 13: looprpc.ClientHtlcSigningInfo - (*PushStaticAddressHtlcSigsResponse)(nil), // 14: looprpc.PushStaticAddressHtlcSigsResponse - (*PushStaticAddressSweeplessSigsRequest)(nil), // 15: looprpc.PushStaticAddressSweeplessSigsRequest - (*ClientSweeplessSigningInfo)(nil), // 16: looprpc.ClientSweeplessSigningInfo - (*PushStaticAddressSweeplessSigsResponse)(nil), // 17: looprpc.PushStaticAddressSweeplessSigsResponse - nil, // 18: looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry - nil, // 19: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry - nil, // 20: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry - (*PrevoutInfo)(nil), // 21: looprpc.PrevoutInfo + (*StaticAddressDescriptor)(nil), // 9: looprpc.StaticAddressDescriptor + (*StaticAddressChangeOutput)(nil), // 10: looprpc.StaticAddressChangeOutput + (*ServerStaticAddressLoopInRequest)(nil), // 11: looprpc.ServerStaticAddressLoopInRequest + (*ServerStaticAddressLoopInResponse)(nil), // 12: looprpc.ServerStaticAddressLoopInResponse + (*ServerHtlcSigningInfo)(nil), // 13: looprpc.ServerHtlcSigningInfo + (*PushStaticAddressHtlcSigsRequest)(nil), // 14: looprpc.PushStaticAddressHtlcSigsRequest + (*ClientHtlcSigningInfo)(nil), // 15: looprpc.ClientHtlcSigningInfo + (*PushStaticAddressHtlcSigsResponse)(nil), // 16: looprpc.PushStaticAddressHtlcSigsResponse + (*PushStaticAddressSweeplessSigsRequest)(nil), // 17: looprpc.PushStaticAddressSweeplessSigsRequest + (*ClientSweeplessSigningInfo)(nil), // 18: looprpc.ClientSweeplessSigningInfo + (*PushStaticAddressSweeplessSigsResponse)(nil), // 19: looprpc.PushStaticAddressSweeplessSigsResponse + nil, // 20: looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry + nil, // 21: looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntry + nil, // 22: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry + nil, // 23: looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntry + nil, // 24: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry + (*PrevoutInfo)(nil), // 25: looprpc.PrevoutInfo } var file_staticaddr_proto_depIdxs = []int32{ 0, // 0: looprpc.ServerNewAddressRequest.protocol_version:type_name -> looprpc.StaticAddressProtocolVersion 3, // 1: looprpc.ServerNewAddressResponse.params:type_name -> looprpc.ServerAddressParameters - 21, // 2: looprpc.ServerWithdrawRequest.outpoints:type_name -> looprpc.PrevoutInfo - 18, // 3: looprpc.ServerPsbtWithdrawRequest.deposit_to_nonces:type_name -> looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry - 19, // 4: looprpc.ServerPsbtWithdrawResponse.signing_info:type_name -> looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry - 0, // 5: looprpc.ServerStaticAddressLoopInRequest.protocol_version:type_name -> looprpc.StaticAddressProtocolVersion - 11, // 6: looprpc.ServerStaticAddressLoopInResponse.standard_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo - 11, // 7: looprpc.ServerStaticAddressLoopInResponse.high_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo - 11, // 8: looprpc.ServerStaticAddressLoopInResponse.extreme_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo - 13, // 9: looprpc.PushStaticAddressHtlcSigsRequest.standard_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo - 13, // 10: looprpc.PushStaticAddressHtlcSigsRequest.high_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo - 13, // 11: looprpc.PushStaticAddressHtlcSigsRequest.extreme_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo - 20, // 12: looprpc.PushStaticAddressSweeplessSigsRequest.signing_info:type_name -> looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry - 8, // 13: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry.value:type_name -> looprpc.ServerPsbtWithdrawSigningInfo - 16, // 14: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry.value:type_name -> looprpc.ClientSweeplessSigningInfo - 1, // 15: looprpc.StaticAddressServer.ServerNewAddress:input_type -> looprpc.ServerNewAddressRequest - 4, // 16: looprpc.StaticAddressServer.ServerWithdrawDeposits:input_type -> looprpc.ServerWithdrawRequest - 6, // 17: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:input_type -> looprpc.ServerPsbtWithdrawRequest - 9, // 18: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:input_type -> looprpc.ServerStaticAddressLoopInRequest - 12, // 19: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:input_type -> looprpc.PushStaticAddressHtlcSigsRequest - 15, // 20: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:input_type -> looprpc.PushStaticAddressSweeplessSigsRequest - 2, // 21: looprpc.StaticAddressServer.ServerNewAddress:output_type -> looprpc.ServerNewAddressResponse - 5, // 22: looprpc.StaticAddressServer.ServerWithdrawDeposits:output_type -> looprpc.ServerWithdrawResponse - 7, // 23: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:output_type -> looprpc.ServerPsbtWithdrawResponse - 10, // 24: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:output_type -> looprpc.ServerStaticAddressLoopInResponse - 14, // 25: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:output_type -> looprpc.PushStaticAddressHtlcSigsResponse - 17, // 26: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:output_type -> looprpc.PushStaticAddressSweeplessSigsResponse - 21, // [21:27] is the sub-list for method output_type - 15, // [15:21] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 25, // 2: looprpc.ServerWithdrawRequest.outpoints:type_name -> looprpc.PrevoutInfo + 20, // 3: looprpc.ServerPsbtWithdrawRequest.deposit_to_nonces:type_name -> looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry + 21, // 4: looprpc.ServerPsbtWithdrawRequest.deposit_to_client_pubkeys:type_name -> looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntry + 22, // 5: looprpc.ServerPsbtWithdrawResponse.signing_info:type_name -> looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry + 9, // 6: looprpc.StaticAddressChangeOutput.static_address:type_name -> looprpc.StaticAddressDescriptor + 0, // 7: looprpc.ServerStaticAddressLoopInRequest.protocol_version:type_name -> looprpc.StaticAddressProtocolVersion + 23, // 8: looprpc.ServerStaticAddressLoopInRequest.deposit_to_client_pubkeys:type_name -> looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntry + 10, // 9: looprpc.ServerStaticAddressLoopInRequest.change_output:type_name -> looprpc.StaticAddressChangeOutput + 13, // 10: looprpc.ServerStaticAddressLoopInResponse.standard_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo + 13, // 11: looprpc.ServerStaticAddressLoopInResponse.high_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo + 13, // 12: looprpc.ServerStaticAddressLoopInResponse.extreme_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo + 15, // 13: looprpc.PushStaticAddressHtlcSigsRequest.standard_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo + 15, // 14: looprpc.PushStaticAddressHtlcSigsRequest.high_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo + 15, // 15: looprpc.PushStaticAddressHtlcSigsRequest.extreme_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo + 24, // 16: looprpc.PushStaticAddressSweeplessSigsRequest.signing_info:type_name -> looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry + 9, // 17: looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntry.value:type_name -> looprpc.StaticAddressDescriptor + 8, // 18: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry.value:type_name -> looprpc.ServerPsbtWithdrawSigningInfo + 9, // 19: looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntry.value:type_name -> looprpc.StaticAddressDescriptor + 18, // 20: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry.value:type_name -> looprpc.ClientSweeplessSigningInfo + 1, // 21: looprpc.StaticAddressServer.ServerNewAddress:input_type -> looprpc.ServerNewAddressRequest + 4, // 22: looprpc.StaticAddressServer.ServerWithdrawDeposits:input_type -> looprpc.ServerWithdrawRequest + 6, // 23: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:input_type -> looprpc.ServerPsbtWithdrawRequest + 11, // 24: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:input_type -> looprpc.ServerStaticAddressLoopInRequest + 14, // 25: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:input_type -> looprpc.PushStaticAddressHtlcSigsRequest + 17, // 26: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:input_type -> looprpc.PushStaticAddressSweeplessSigsRequest + 2, // 27: looprpc.StaticAddressServer.ServerNewAddress:output_type -> looprpc.ServerNewAddressResponse + 5, // 28: looprpc.StaticAddressServer.ServerWithdrawDeposits:output_type -> looprpc.ServerWithdrawResponse + 7, // 29: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:output_type -> looprpc.ServerPsbtWithdrawResponse + 12, // 30: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:output_type -> looprpc.ServerStaticAddressLoopInResponse + 16, // 31: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:output_type -> looprpc.PushStaticAddressHtlcSigsResponse + 19, // 32: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:output_type -> looprpc.PushStaticAddressSweeplessSigsResponse + 27, // [27:33] is the sub-list for method output_type + 21, // [21:27] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_staticaddr_proto_init() } @@ -1339,7 +1505,7 @@ func file_staticaddr_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_staticaddr_proto_rawDesc), len(file_staticaddr_proto_rawDesc)), NumEnums: 1, - NumMessages: 20, + NumMessages: 24, NumExtensions: 0, NumServices: 1, }, diff --git a/swapserverrpc/staticaddr.proto b/swapserverrpc/staticaddr.proto index b590f4d4..9618d013 100644 --- a/swapserverrpc/staticaddr.proto +++ b/swapserverrpc/staticaddr.proto @@ -124,6 +124,11 @@ message ServerPsbtWithdrawRequest { // The map of deposit txid:idx to the nonce used by the client. map deposit_to_nonces = 2; + + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + map deposit_to_client_pubkeys = 3; } message ServerPsbtWithdrawResponse { @@ -143,6 +148,23 @@ message ServerPsbtWithdrawSigningInfo { bytes sig = 2; } +message StaticAddressDescriptor { + // The client's secp256k1 public key serialized in 33-byte compressed SEC1 + // format. This is not the 32-byte x-only BIP-340 encoding. + bytes pubkey = 1; + + // The expected output script for the static address. + bytes pk_script = 2; +} + +message StaticAddressChangeOutput { + // The descriptor for the static address change output. + StaticAddressDescriptor static_address = 1; + + // The expected change amount in satoshis. + int64 amount = 2; +} + message ServerStaticAddressLoopInRequest { // The client's public key for the htlc output. bytes htlc_client_pub_key = 1; @@ -196,6 +218,15 @@ message ServerStaticAddressLoopInRequest { // If set, request the server to use fast publication behavior for this // swap. bool fast = 10; + + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + map deposit_to_client_pubkeys = 11; + + // Optional change output metadata for fractional loop-ins that return + // funds to a newly generated static address. + StaticAddressChangeOutput change_output = 12; } message ServerStaticAddressLoopInResponse { From 6b942941fbdb1a5ddd38a36384a2af3a3a77c4e6 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 21 Jul 2026 22:01:35 +0200 Subject: [PATCH 11/36] docs: complete historical release notes Document every published Loop release and preserve authoritative notes. Add the next-release workflow and rebuild chronological navigation. Rename the reproducible-build guide for clarity. --- .github/pull_request_template.md | 3 +- README.md | 2 +- docs/release-notes/README.md | 113 ++++++++++++ docs/release-notes/release-notes-0.1.1.md | 148 +++++++++++++++ docs/release-notes/release-notes-0.1.2.md | 27 +++ .../release-notes/release-notes-0.1.3-beta.md | 31 ++++ docs/release-notes/release-notes-0.1.3.md | 26 +++ docs/release-notes/release-notes-0.1.md | 170 ++++++++++++++++++ docs/release-notes/release-notes-0.10.0.md | 56 ++++++ docs/release-notes/release-notes-0.11.0.md | 37 ++++ docs/release-notes/release-notes-0.11.1.md | 28 +++ docs/release-notes/release-notes-0.11.2.md | 35 ++++ docs/release-notes/release-notes-0.11.3.md | 50 ++++++ docs/release-notes/release-notes-0.11.4.md | 23 +++ docs/release-notes/release-notes-0.12.0.md | 40 +++++ docs/release-notes/release-notes-0.12.1.md | 40 +++++ docs/release-notes/release-notes-0.12.2.md | 29 +++ docs/release-notes/release-notes-0.13.0.md | 25 +++ docs/release-notes/release-notes-0.14.0.md | 30 ++++ docs/release-notes/release-notes-0.14.1.md | 23 +++ docs/release-notes/release-notes-0.14.2.md | 27 +++ docs/release-notes/release-notes-0.15.0.md | 34 ++++ docs/release-notes/release-notes-0.15.1.md | 41 +++++ docs/release-notes/release-notes-0.16.0.md | 41 +++++ docs/release-notes/release-notes-0.17.0.md | 32 ++++ docs/release-notes/release-notes-0.18.0.md | 33 ++++ docs/release-notes/release-notes-0.19.1.md | 32 ++++ docs/release-notes/release-notes-0.2.0.md | 23 +++ docs/release-notes/release-notes-0.2.1.md | 38 ++++ docs/release-notes/release-notes-0.2.2.md | 26 +++ docs/release-notes/release-notes-0.2.3.md | 37 ++++ docs/release-notes/release-notes-0.2.4.md | 32 ++++ docs/release-notes/release-notes-0.2.md | 153 ++++++++++++++++ docs/release-notes/release-notes-0.20.0.md | 28 +++ docs/release-notes/release-notes-0.20.1.md | 22 +++ docs/release-notes/release-notes-0.20.2.md | 26 +++ docs/release-notes/release-notes-0.21.0.md | 30 ++++ docs/release-notes/release-notes-0.22.0.md | 26 +++ docs/release-notes/release-notes-0.22.1.md | 27 +++ docs/release-notes/release-notes-0.23.0.md | 23 +++ docs/release-notes/release-notes-0.24.0.md | 26 +++ docs/release-notes/release-notes-0.24.1.md | 23 +++ docs/release-notes/release-notes-0.25.0.md | 42 +++++ docs/release-notes/release-notes-0.25.1.md | 25 +++ docs/release-notes/release-notes-0.25.2.md | 25 +++ docs/release-notes/release-notes-0.26.0.md | 34 ++++ docs/release-notes/release-notes-0.26.1.md | 30 ++++ docs/release-notes/release-notes-0.26.2.md | 27 +++ docs/release-notes/release-notes-0.26.3.md | 33 ++++ docs/release-notes/release-notes-0.26.4.md | 32 ++++ docs/release-notes/release-notes-0.26.5.md | 28 +++ docs/release-notes/release-notes-0.26.6.md | 27 +++ docs/release-notes/release-notes-0.27.0.md | 34 ++++ docs/release-notes/release-notes-0.27.1.md | 28 +++ docs/release-notes/release-notes-0.28.0.md | 35 ++++ docs/release-notes/release-notes-0.28.1.md | 28 +++ docs/release-notes/release-notes-0.28.2.md | 36 ++++ docs/release-notes/release-notes-0.28.3.md | 31 ++++ docs/release-notes/release-notes-0.28.4.md | 33 ++++ docs/release-notes/release-notes-0.28.5.md | 24 +++ docs/release-notes/release-notes-0.28.6.md | 34 ++++ docs/release-notes/release-notes-0.28.7.md | 34 ++++ docs/release-notes/release-notes-0.28.8.md | 41 +++++ docs/release-notes/release-notes-0.28.9.md | 30 ++++ docs/release-notes/release-notes-0.29.0.md | 28 +++ docs/release-notes/release-notes-0.29.1.md | 38 ++++ docs/release-notes/release-notes-0.3.0.md | 39 ++++ docs/release-notes/release-notes-0.3.1.md | 37 ++++ docs/release-notes/release-notes-0.30.0.md | 35 ++++ docs/release-notes/release-notes-0.31.0.md | 30 ++++ docs/release-notes/release-notes-0.31.1.md | 30 ++++ docs/release-notes/release-notes-0.31.2.md | 30 ++++ docs/release-notes/release-notes-0.31.3.md | 61 +++++++ docs/release-notes/release-notes-0.31.4.md | 38 ++++ .../release-notes-0.31.5-lnd0.20.md | 26 +++ docs/release-notes/release-notes-0.31.5.md | 33 ++++ docs/release-notes/release-notes-0.31.6.md | 42 +++++ docs/release-notes/release-notes-0.31.7.md | 22 +++ docs/release-notes/release-notes-0.31.8.md | 38 ++++ docs/release-notes/release-notes-0.32.0.md | 42 +++++ docs/release-notes/release-notes-0.32.1.md | 23 +++ docs/release-notes/release-notes-0.33.0.md | 32 ++++ docs/release-notes/release-notes-0.33.1.md | 35 ++++ docs/release-notes/release-notes-0.33.2.md | 31 ++++ docs/release-notes/release-notes-0.33.3.md | 25 +++ docs/release-notes/release-notes-0.34.0.md | 23 ++- docs/release-notes/release-notes-0.4.0-rc1.md | 31 ++++ docs/release-notes/release-notes-0.4.0.md | 31 ++++ docs/release-notes/release-notes-0.4.1.md | 31 ++++ docs/release-notes/release-notes-0.5.0.md | 28 +++ docs/release-notes/release-notes-0.5.1.md | 27 +++ docs/release-notes/release-notes-0.6.0.md | 33 ++++ docs/release-notes/release-notes-0.6.1.md | 34 ++++ docs/release-notes/release-notes-0.6.2.md | 35 ++++ docs/release-notes/release-notes-0.6.3.md | 29 +++ docs/release-notes/release-notes-0.6.4.md | 32 ++++ docs/release-notes/release-notes-0.6.5.md | 29 +++ docs/release-notes/release-notes-0.7.0.md | 29 +++ docs/release-notes/release-notes-0.8.0.md | 30 ++++ docs/release-notes/release-notes-0.8.1.md | 33 ++++ docs/release-notes/release-notes-0.9.0.md | 40 +++++ docs/release-notes/release-notes-next.md | 11 ++ docs/release-notes/release-notes-template.md | 19 ++ docs/{release.md => reproducible_release.md} | 0 release_notes.md | 23 --- version.go | 3 +- 106 files changed, 3642 insertions(+), 31 deletions(-) create mode 100644 docs/release-notes/README.md create mode 100644 docs/release-notes/release-notes-0.1.1.md create mode 100644 docs/release-notes/release-notes-0.1.2.md create mode 100644 docs/release-notes/release-notes-0.1.3-beta.md create mode 100644 docs/release-notes/release-notes-0.1.3.md create mode 100644 docs/release-notes/release-notes-0.1.md create mode 100644 docs/release-notes/release-notes-0.10.0.md create mode 100644 docs/release-notes/release-notes-0.11.0.md create mode 100644 docs/release-notes/release-notes-0.11.1.md create mode 100644 docs/release-notes/release-notes-0.11.2.md create mode 100644 docs/release-notes/release-notes-0.11.3.md create mode 100644 docs/release-notes/release-notes-0.11.4.md create mode 100644 docs/release-notes/release-notes-0.12.0.md create mode 100644 docs/release-notes/release-notes-0.12.1.md create mode 100644 docs/release-notes/release-notes-0.12.2.md create mode 100644 docs/release-notes/release-notes-0.13.0.md create mode 100644 docs/release-notes/release-notes-0.14.0.md create mode 100644 docs/release-notes/release-notes-0.14.1.md create mode 100644 docs/release-notes/release-notes-0.14.2.md create mode 100644 docs/release-notes/release-notes-0.15.0.md create mode 100644 docs/release-notes/release-notes-0.15.1.md create mode 100644 docs/release-notes/release-notes-0.16.0.md create mode 100644 docs/release-notes/release-notes-0.17.0.md create mode 100644 docs/release-notes/release-notes-0.18.0.md create mode 100644 docs/release-notes/release-notes-0.19.1.md create mode 100644 docs/release-notes/release-notes-0.2.0.md create mode 100644 docs/release-notes/release-notes-0.2.1.md create mode 100644 docs/release-notes/release-notes-0.2.2.md create mode 100644 docs/release-notes/release-notes-0.2.3.md create mode 100644 docs/release-notes/release-notes-0.2.4.md create mode 100644 docs/release-notes/release-notes-0.2.md create mode 100644 docs/release-notes/release-notes-0.20.0.md create mode 100644 docs/release-notes/release-notes-0.20.1.md create mode 100644 docs/release-notes/release-notes-0.20.2.md create mode 100644 docs/release-notes/release-notes-0.21.0.md create mode 100644 docs/release-notes/release-notes-0.22.0.md create mode 100644 docs/release-notes/release-notes-0.22.1.md create mode 100644 docs/release-notes/release-notes-0.23.0.md create mode 100644 docs/release-notes/release-notes-0.24.0.md create mode 100644 docs/release-notes/release-notes-0.24.1.md create mode 100644 docs/release-notes/release-notes-0.25.0.md create mode 100644 docs/release-notes/release-notes-0.25.1.md create mode 100644 docs/release-notes/release-notes-0.25.2.md create mode 100644 docs/release-notes/release-notes-0.26.0.md create mode 100644 docs/release-notes/release-notes-0.26.1.md create mode 100644 docs/release-notes/release-notes-0.26.2.md create mode 100644 docs/release-notes/release-notes-0.26.3.md create mode 100644 docs/release-notes/release-notes-0.26.4.md create mode 100644 docs/release-notes/release-notes-0.26.5.md create mode 100644 docs/release-notes/release-notes-0.26.6.md create mode 100644 docs/release-notes/release-notes-0.27.0.md create mode 100644 docs/release-notes/release-notes-0.27.1.md create mode 100644 docs/release-notes/release-notes-0.28.0.md create mode 100644 docs/release-notes/release-notes-0.28.1.md create mode 100644 docs/release-notes/release-notes-0.28.2.md create mode 100644 docs/release-notes/release-notes-0.28.3.md create mode 100644 docs/release-notes/release-notes-0.28.4.md create mode 100644 docs/release-notes/release-notes-0.28.5.md create mode 100644 docs/release-notes/release-notes-0.28.6.md create mode 100644 docs/release-notes/release-notes-0.28.7.md create mode 100644 docs/release-notes/release-notes-0.28.8.md create mode 100644 docs/release-notes/release-notes-0.28.9.md create mode 100644 docs/release-notes/release-notes-0.29.0.md create mode 100644 docs/release-notes/release-notes-0.29.1.md create mode 100644 docs/release-notes/release-notes-0.3.0.md create mode 100644 docs/release-notes/release-notes-0.3.1.md create mode 100644 docs/release-notes/release-notes-0.30.0.md create mode 100644 docs/release-notes/release-notes-0.31.0.md create mode 100644 docs/release-notes/release-notes-0.31.1.md create mode 100644 docs/release-notes/release-notes-0.31.2.md create mode 100644 docs/release-notes/release-notes-0.31.3.md create mode 100644 docs/release-notes/release-notes-0.31.4.md create mode 100644 docs/release-notes/release-notes-0.31.5-lnd0.20.md create mode 100644 docs/release-notes/release-notes-0.31.5.md create mode 100644 docs/release-notes/release-notes-0.31.6.md create mode 100644 docs/release-notes/release-notes-0.31.7.md create mode 100644 docs/release-notes/release-notes-0.31.8.md create mode 100644 docs/release-notes/release-notes-0.32.0.md create mode 100644 docs/release-notes/release-notes-0.32.1.md create mode 100644 docs/release-notes/release-notes-0.33.0.md create mode 100644 docs/release-notes/release-notes-0.33.1.md create mode 100644 docs/release-notes/release-notes-0.33.2.md create mode 100644 docs/release-notes/release-notes-0.33.3.md create mode 100644 docs/release-notes/release-notes-0.4.0-rc1.md create mode 100644 docs/release-notes/release-notes-0.4.0.md create mode 100644 docs/release-notes/release-notes-0.4.1.md create mode 100644 docs/release-notes/release-notes-0.5.0.md create mode 100644 docs/release-notes/release-notes-0.5.1.md create mode 100644 docs/release-notes/release-notes-0.6.0.md create mode 100644 docs/release-notes/release-notes-0.6.1.md create mode 100644 docs/release-notes/release-notes-0.6.2.md create mode 100644 docs/release-notes/release-notes-0.6.3.md create mode 100644 docs/release-notes/release-notes-0.6.4.md create mode 100644 docs/release-notes/release-notes-0.6.5.md create mode 100644 docs/release-notes/release-notes-0.7.0.md create mode 100644 docs/release-notes/release-notes-0.8.0.md create mode 100644 docs/release-notes/release-notes-0.8.1.md create mode 100644 docs/release-notes/release-notes-0.9.0.md create mode 100644 docs/release-notes/release-notes-next.md create mode 100644 docs/release-notes/release-notes-template.md rename docs/{release.md => reproducible_release.md} (100%) delete mode 100644 release_notes.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 66ed7136..d68648b1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,2 +1,3 @@ #### Pull Request Checklist -- [ ] Update `release_notes.md` if your PR contains major features, breaking changes or bugfixes +- [ ] Update `docs/release-notes/release-notes-next.md` if your PR contains + major features, breaking changes or bugfixes diff --git a/README.md b/README.md index 0bba28ef..e90f858e 100644 --- a/README.md +++ b/README.md @@ -133,4 +133,4 @@ go install ./... ## Reproducible builds If you want to build release files yourself, follow -[the guide](./docs/release.md). +[the guide](./docs/reproducible_release.md). diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md new file mode 100644 index 00000000..a23154fb --- /dev/null +++ b/docs/release-notes/README.md @@ -0,0 +1,113 @@ +# Loop Client Release Notes + +Release notes are listed in chronological order and include every published +GitHub release. Previous and next links connect adjacent files in this index. +Contributor lists are derived from non-merge commit authors and co-authors +between consecutive official release tags. + +Add user-facing changes to [release-notes-next.md](release-notes-next.md). +When preparing a release, use +[release-notes-template.md](release-notes-template.md) to turn those notes into +a versioned file, then reset the next-release sections. + +| Release notes | Date | Highlights | +| --- | --- | --- | +| [v0.1-alpha](release-notes-0.1.md) | 2019-03-21 | Initial Loop Out release | +| [v0.1.1-alpha](release-notes-0.1.1.md) | 2019-04-17 | Added testnet Loop In | +| [v0.1.2-alpha](release-notes-0.1.2.md) | 2019-05-03 | Fixed macaroon, testnet, and routing fee handling | +| [v0.1.3-beta](release-notes-0.1.3-beta.md) | 2019-05-31 | Published the v0.1.3 beta tag with Docker and accounting updates | +| [v0.1.3-alpha](release-notes-0.1.3.md) | 2019-05-31 | Added Docker support and accounting improvements | +| [v0.2-alpha](release-notes-0.2.md) | 2019-06-26 | Introduced Loop In | +| [v0.2.0-alpha](release-notes-0.2.0.md) | 2019-06-26 | Aliased v0.2-alpha without source changes | +| [v0.2.1-alpha](release-notes-0.2.1.md) | 2019-07-25 | Added configurable Loop Out sweep confirmation targets | +| [v0.2.2-alpha](release-notes-0.2.2.md) | 2019-07-31 | Improved external Loop Out validation | +| [v0.2.3-alpha](release-notes-0.2.3.md) | 2019-10-03 | Fixed REST parity, fee estimation, and confirmation targets | +| [v0.2.4-alpha](release-notes-0.2.4.md) | 2019-10-11 | Prepared flexible swap fees and timing | +| [v0.3.0-alpha](release-notes-0.3.0.md) | 2019-11-21 | Added delayed, batchable Loop Out execution | +| [v0.3.1-alpha](release-notes-0.3.1.md) | 2020-01-15 | Added fee controls for Loop Out and fast quotes | +| [v0.4.0-rc1.beta](release-notes-0.4.0-rc1.md) | 2020-01-24 | Previewed authenticated requests and fixed Loop In timeouts | +| [v0.4.0-beta](release-notes-0.4.0.md) | 2020-02-04 | Added authenticated requests and REST monitoring | +| [v0.4.1-beta](release-notes-0.4.1.md) | 2020-02-11 | Added REST CORS controls and fixed delayed quote errors | +| [v0.5.0-beta](release-notes-0.5.0.md) | 2020-03-05 | Added Loop In last-hop selection | +| [v0.5.1-beta](release-notes-0.5.1.md) | 2020-03-15 | Improved Loop In quotes and fee sanity checks | +| [v0.6.0-beta](release-notes-0.6.0.md) | 2020-04-30 | Added LND v0.10 support and Loop In confirmation targets | +| [v0.6.1-beta](release-notes-0.6.1.md) | 2020-05-12 | Added native SegWit Loop In HTLCs and fixed multipart limits | +| [v0.6.2-beta](release-notes-0.6.2.md) | 2020-05-12 | Published native SegWit Loop In and multipart-limit fixes | +| [v0.6.3-beta](release-notes-0.6.3.md) | 2020-06-04 | Restored multi-channel Loop Out with LND v0.10.1 | +| [v0.6.4-beta](release-notes-0.6.4.md) | 2020-06-12 | Improved Loop Out restart behavior and liquidity speed | +| [v0.6.5-beta](release-notes-0.6.5.md) | 2020-07-02 | Added human-readable server messages | +| [v0.7.0-beta](release-notes-0.7.0.md) | 2020-07-21 | Added longer confirmation targets and server status subscriptions | +| [v0.8.0-beta](release-notes-0.8.0.md) | 2020-08-11 | Improved swap failure reporting and confirmation controls | +| [v0.8.1-beta](release-notes-0.8.1.md) | 2020-08-28 | Reduced Loop Out routing and sweep fees | +| [v0.9.0-beta](release-notes-0.9.0.md) | 2020-09-10 | Introduced liquidity management and TLS | +| [v0.10.0-beta](release-notes-0.10.0.md) | 2020-10-13 | Added multipath Loop In and fee-aware swap suggestions | +| [v0.11.0-beta](release-notes-0.11.0.md) | 2020-10-27 | Introduced Autoloop and transaction labels | +| [v0.11.1-beta](release-notes-0.11.1.md) | 2020-11-12 | Added swap initiator metadata | +| [v0.11.2-beta](release-notes-0.11.2.md) | 2020-12-08 | Added configurable Autoloop swap size limits | +| [v0.11.3-beta](release-notes-0.11.3.md) | 2021-02-09 | Added locked-LND startup waiting and single macaroon support | +| [v0.11.4-beta](release-notes-0.11.4.md) | 2021-02-11 | Defaulted to LND's admin macaroon path | +| [v0.12.0-beta](release-notes-0.12.0.md) | 2021-03-06 | Added per-peer Autoloop rules | +| [v0.12.1-beta](release-notes-0.12.1.md) | 2021-03-26 | Added verbose quote and swap output | +| [v0.12.2-beta](release-notes-0.12.2.md) | 2021-04-29 | Retried failed LSAT payments | +| [v0.13.0-beta](release-notes-0.13.0.md) | 2021-05-19 | Raised the minimum LND version to v0.11.1-beta | +| [v0.14.0-beta](release-notes-0.14.0.md) | 2021-06-01 | Reported Loop Out routing failures to the server | +| [v0.14.1-beta](release-notes-0.14.1.md) | 2021-06-09 | Removed protobuf startup warnings | +| [v0.14.2-beta](release-notes-0.14.2.md) | 2021-07-20 | Fixed Python gRPC client connectivity | +| [v0.15.0-beta](release-notes-0.15.0.md) | 2021-08-03 | Added Loop In quote probing | +| [v0.15.1-beta](release-notes-0.15.1.md) | 2021-11-18 | Updated to LND v0.14 and added WASM RPC stubs | +| [v0.16.0-beta](release-notes-0.16.0.md) | 2021-12-14 | Added private Loop In route hints | +| [v0.17.0-beta](release-notes-0.17.0.md) | 2022-02-07 | Added Loop In support to Autoloop | +| [v0.18.0-beta](release-notes-0.18.0.md) | 2022-03-30 | Added optional routing plugins | +| [v0.19.1-beta](release-notes-0.19.1.md) | 2022-06-09 | Persisted liquidity parameters | +| [v0.20.0-beta](release-notes-0.20.0.md) | 2022-07-20 | Added experimental P2TR HTLCs and MuSig2 sweeps | +| [v0.20.1-beta](release-notes-0.20.1.md) | 2022-08-01 | Improved sweep transaction logging | +| [v0.20.2-beta](release-notes-0.20.2.md) | 2022-10-26 | Raised the minimum LND version for Taproot support | +| [v0.21.0-beta](release-notes-0.21.0.md) | 2022-12-16 | Added Autoloop destination addresses | +| [v0.22.0-beta](release-notes-0.22.0.md) | 2023-03-29 | Added recurring Autoloop budgets | +| [v0.22.1-beta](release-notes-0.22.1.md) | 2023-04-24 | Corrected the HTLC script version for legacy swaps | +| [v0.23.0-beta](release-notes-0.23.0.md) | 2023-04-25 | Made MuSig2 the default swap protocol | +| [v0.24.0-beta](release-notes-0.24.0.md) | 2023-05-24 | Added Easy Autoloop | +| [v0.24.1-beta](release-notes-0.24.1.md) | 2023-05-26 | Added REST bindings for GetInfo | +| [v0.25.0-beta](release-notes-0.25.0.md) | 2023-07-03 | Added SQL stores and Easy Autoloop fee fixes | +| [v0.25.1-beta](release-notes-0.25.1.md) | 2023-07-04 | Removed Windows 386 release builds | +| [v0.25.2-beta](release-notes-0.25.2.md) | 2023-07-04 | Removed unsupported release platforms | +| [v0.26.0-beta](release-notes-0.26.0.md) | 2023-07-28 | Added xpub-backed Loop Out destinations | +| [v0.26.1-beta](release-notes-0.26.1.md) | 2023-08-08 | Recorded swap initiators and repaired faulty timestamps | +| [v0.26.2-beta](release-notes-0.26.2.md) | 2023-08-09 | Migrated faulty Loop Out timestamps | +| [v0.26.3-beta](release-notes-0.26.3.md) | 2023-09-18 | Added the FSM foundation and hardened Loop In settlement | +| [v0.26.4-beta](release-notes-0.26.4.md) | 2023-10-04 | Added Easy Autoloop destinations and Apple Silicon builds | +| [v0.26.5-beta](release-notes-0.26.5.md) | 2023-10-31 | Fixed leap-year parsing and refactored database startup | +| [v0.26.6-beta](release-notes-0.26.6.md) | 2023-11-28 | Added Loop In abandonment and specific balance failures | +| [v0.27.0-beta](release-notes-0.27.0.md) | 2024-01-30 | Added batched Loop Out sweeps | +| [v0.27.1-beta](release-notes-0.27.1.md) | 2024-02-14 | Automatically recovered incorrectly funded external Loop Ins | +| [v0.28.0-beta](release-notes-0.28.0.md) | 2024-03-05 | Expanded Instant Out listing, destinations, and fee accounting | +| [v0.28.1-beta](release-notes-0.28.1.md) | 2024-04-16 | Routed Loop Out prepayments over selected channels | +| [v0.28.2-beta](release-notes-0.28.2.md) | 2024-05-25 | Renamed LSAT options and APIs to L402 | +| [v0.28.3-beta](release-notes-0.28.3.md) | 2024-06-03 | Corrected stored Loop Out costs and batch restoration | +| [v0.28.4-beta](release-notes-0.28.4.md) | 2024-06-04 | Fixed pending-swap cost migration and added RPC timeouts | +| [v0.28.5-beta](release-notes-0.28.5.md) | 2024-06-06 | Paginated cost-migration payment lookups | +| [v0.28.6-beta](release-notes-0.28.6.md) | 2024-07-11 | Expanded sweep-batcher signing and fee controls | +| [v0.28.7-beta](release-notes-0.28.7.md) | 2024-08-08 | Improved sweep selection and stored swap metadata | +| [v0.28.8-beta](release-notes-0.28.8.md) | 2024-10-21 | Introduced the notification manager and mixed sweep batches | +| [v0.28.9-beta](release-notes-0.28.9.md) | 2024-10-30 | Added L402 retrieval and FSM context propagation | +| [v0.29.0-beta](release-notes-0.29.0.md) | 2024-12-18 | Persisted static address Loop In mode | +| [v0.29.1-beta](release-notes-0.29.1.md) | 2025-03-18 | Added Taproot Asset Loop Outs and partial static withdrawals | +| [v0.30.0-beta](release-notes-0.30.0.md) | 2025-03-27 | Added Taproot Asset Easy Autoloop and hardened recovery | +| [v0.31.0-beta](release-notes-0.31.0.md) | 2025-04-25 | Added filtering and pagination to loop listswaps | +| [v0.31.1-beta](release-notes-0.31.1.md) | 2025-05-05 | Changed Autoloop to use a slow publication deadline | +| [v0.31.2-beta](release-notes-0.31.2.md) | 2025-06-18 | Corrected commit hash and version reporting | +| [v0.31.3-beta](release-notes-0.31.3.md) | 2025-09-25 | Added partial static Loop Ins and hardened static address recovery | +| [v0.31.4-beta](release-notes-0.31.4.md) | 2025-10-16 | Added fast static Loop Ins and reproducible releases | +| [v0.31.5-beta](release-notes-0.31.5.md) | 2025-10-31 | Added swap resumption and notification versioning | +| [v0.31.5-beta-lnd0.20](release-notes-0.31.5-lnd0.20.md) | 2025-10-31 | Updated LND 0.20 compatibility and module version metadata | +| [v0.31.6-beta](release-notes-0.31.6.md) | 2025-11-26 | Added Easy Autoloop peer exclusions and loop stop | +| [v0.31.7-beta](release-notes-0.31.7.md) | 2025-11-26 | Bumped the release version after v0.31.6 | +| [v0.31.8-beta](release-notes-0.31.8.md) | 2026-02-03 | Added manual HTLC sweeps and PSBT static withdrawals | +| [v0.32.0-beta](release-notes-0.32.0.md) | 2026-03-02 | Opened channels directly from static address deposits | +| [v0.32.1-beta](release-notes-0.32.1.md) | 2026-03-02 | Updated reproducible releases to Go 1.26 | +| [v0.33.0-beta](release-notes-0.33.0.md) | 2026-04-08 | Added static Loop In fee caps and lifecycle hardening | +| [v0.33.1-beta](release-notes-0.33.1.md) | 2026-05-26 | Added Static Address Autoloop and CLI session tests | +| [v0.33.2-beta](release-notes-0.33.2.md) | 2026-06-08 | Exposed static swap details and hardened MuSig2 inputs | +| [v0.33.3-beta](release-notes-0.33.3.md) | 2026-06-21 | Raised the LND floor and updated the LND dependency | +| [v0.34.0-beta](release-notes-0.34.0.md) | 2026-07-23 | Tracked low-confirmation deposits and production Taproot channels | +| [Next release](release-notes-next.md) | Unreleased | No changes yet | diff --git a/docs/release-notes/release-notes-0.1.1.md b/docs/release-notes/release-notes-0.1.1.md new file mode 100644 index 00000000..92665ea6 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.1.md @@ -0,0 +1,148 @@ +# Loop Client Release Notes + +- **Release date:** 2019-04-17 +- **Release page:** + [v0.1.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1.1-alpha) +- **Previous release:** [v0.1-alpha](release-notes-0.1.md) +- **Next release:** [v0.1.2-alpha](release-notes-0.1.2.md) + +#### New Features + +This is a minor release of the Lightning Loop Go client. + +Includes changes to: +- Add testnet support for Loop In (on-chain to off-chain swaps) +- Add amt as a keyword argument + +The Loop In implementation can be kicked off using the `loop in ` command: +``` +⛰ loop in -h +NAME: + loop in - perform an on-chain to off-chain swap (loop in) + +USAGE: + loop in [command options] amt + +DESCRIPTION: + + Send the amount in satoshis specified by the amt argument off-chain. + +OPTIONS: + --amt value the amount in satoshis to loop in (default: 0) + --external expect htlc to be published externally +``` + +The `--external` flag is of note as it allows the HTLC to be paid by a wallet +other then `lnd`. This allows for users to do things like top-off channel from +_another wallet_, or _withdrawal_ funds from an exchange directly into one's +channel. It can also be used to obtain an address to give to someone else, which +once paid and Looped In, is credited to your channel! + +#### Breaking Changes + +#### Bug Fixes + +- Fix monitoring exception + +#### Maintenance + +- Documentation updates + +**Verifying the Release** + +In order to verify the release, you'll need to have `gpg` or `gpg2` installed on +your system. Once you've obtained a copy (and hopefully verified that as well), +you'll first need to import the keys that have signed this release if you +haven't done so already: +``` +curl https://keybase.io/roasbeef/pgp_keys.asc | gpg --import +``` + +Once you have his PGP key you can verify the release (assuming +`manifest-v0.1.1-alpha.txt` and `manifest-v0.1.1-alpha.txt.sig` are in the +current directory) with: +``` +gpg --verify manifest-v0.1.1-alpha.txt +``` + +You should see the following if the verification was successful: +``` +gpg: assuming signed data in 'manifest-v0.1.1-alpha.txt' +gpg: Signature made Wed Apr 17 15:20:49 2019 PDT +gpg: using RSA key F8037E70C12C7A263C032508CE58F7F8E20FD9A2 +gpg: Good signature from "Olaoluwa Osuntokun " [ultimate] +``` + +That will verify the signature on the main manifest page which ensures integrity +and authenticity of the binaries you've downloaded locally. Next, depending on +your operating system you should then re-calculate the `sha256` sum of the +binary, and compare that with the following hashes (which are included in the +manifest file): +``` +d3c3db1bed3ff0fb125514d9072d621492ff2b86f4a90200e93686052f18f5bd loop-darwin-386-v0.1.1-alpha.tar.gz +ebf461a604b681d8a5c82bae14a0620a38a9f65604ca4f532aedb44ddc7c4ea0 loop-darwin-amd64-v0.1.1-alpha.tar.gz +bc83eb2eefb91054858c2a9164fdf245848f2f861d01526261e65dbd2d8237e0 loop-dragonfly-amd64-v0.1.1-alpha.tar.gz +ece2bb8e4c477627a4485f809e7f0d1c88893e01ca7e33363784c392875f00b6 loop-freebsd-386-v0.1.1-alpha.tar.gz +496941befd46331a4551ff9ff60fe4dc1a49434c1593960f3ed53a255aca0783 loop-freebsd-amd64-v0.1.1-alpha.tar.gz +6e21b065178d9616e78a63bacdca4401f8ed36a82dbdfb0d915099af29ae29da loop-freebsd-arm-v0.1.1-alpha.tar.gz +3264e23ffe8994bd374c961f4973eaa6288e4dce0531967de85b23b342518824 loop-linux-386-v0.1.1-alpha.tar.gz +476f26083febb42fab15fa263048cf8bddcc86671aeaa98e1b74f5c422ebaee4 loop-linux-amd64-v0.1.1-alpha.tar.gz +9f13fb6cc9df51501e4c9560da96d96a9d0f385e600eb77fdbfdb97fb731a4a6 loop-linux-arm64-v0.1.1-alpha.tar.gz +53faaf225412c4f9dc16d437252f666243dba13f9135f07886ec019e5b109ebb loop-linux-armv6-v0.1.1-alpha.tar.gz +4e8c04d1585476a8efb02805181dde80be0a1b45816d24f004924b6756298dd5 loop-linux-armv7-v0.1.1-alpha.tar.gz +a21b05379615a6d7d4de68c3edb2f5a341f23dd0f4895b646b5272e6b99e98d2 loop-linux-mips64-v0.1.1-alpha.tar.gz +047c2a9e62b477db7eb8c4f88a5bda68bcdd10bb396d4ba66ff34328a7b9e6d8 loop-linux-mips64le-v0.1.1-alpha.tar.gz +bd25a16e27fb3140be8ae62a8df1076a80ceac443420738d6261f0c375e60bb5 loop-linux-ppc64-v0.1.1-alpha.tar.gz +ef1a37d85c8ae1eb7fba34a66613344042f26867ab838017b3995ef4d7d8cd95 loop-netbsd-386-v0.1.1-alpha.tar.gz +1962c2b174bc401c5362ca1f7282e8129b6958dae08a1fd0248de6505d4b6c4c loop-netbsd-amd64-v0.1.1-alpha.tar.gz +be27b8c84fac5c0e492aa7b395e3b344f8e0c7170525610e2df51238f8fea7de loop-openbsd-386-v0.1.1-alpha.tar.gz +31ce199daf1af10f328af3b5d8b9cfcefc980eabffd6b91481ddf8a5d2b1e235 loop-openbsd-amd64-v0.1.1-alpha.tar.gz +406d5989ec935faf85dbdaccb88a659ba88ff35ca111b71bf769871f20139ccd loop-source-v0.1.1-alpha.tar.gz +61395e480f7e1cecb9b945a7cad4a0b81186767474da5520dee48b6e89191f9b loop-windows-386-v0.1.1-alpha.zip +01848825834fb4fc1c13841ffdfdc1b59b4f352d350d5b7ffbb3252885f0330d loop-windows-amd64-v0.1.1-alpha.zip +2f0fcaca1479d04b49e85e0a3b7f70dd430cf675daaaa4b77104a83747bba3e0 vendor.tar.gz +``` + +One can use the `shasum -a 256 ` tool in order to re-compute the +`sha256` hash of the target binary for your operating system. The produced hash +should be compared with the hashes listed above and they should match *exactly*. + +Finally, you can also verify the _tag_ itself with the following command: +``` +git verify-tag v0.1.1-alpha +``` + +**Building the Contained Release** + +With this new version of `loop`, we've modified our release process to ensure +the bundled release is now _fully self contained_. As a result, with only the +attached payload with this release, users will be able to rebuild the target +release themselves without having to fetch any of the dependancies. Note that at +this stage, binaries aren't yet fully reproducible (even with `go modules` ). +This is due to the fact that by default, +[Go will include the full directory path where the binary was built in the binary itself](https://github.com/golang/go/issues/16860). +As a result, unless your file system exactly mirrors the machine used to build +the binary, you'll get a different binary, as it includes artifacts from your +local file system. This will be fixed in `go1.13`, and before then we may modify +our release system to do this automatically. + +In order to re-build from scratch, assuming that `vendor.tar.gz` and +`loop-source-v0.1.1-alpha.tar.gz` are in the current directory: +``` +tar -xvzf vendor.tar.gz +tar -xvzf loop-source-v0.1.1-alpha.tar.gz +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1.1-alpha" ./cmd/loop +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1.1-alpha" ./cmd/loopd +``` + +The `-mod=vendor` flag tells the `go build` command that it doesn't need to +fetch the dependencies, and instead, they're all enclosed in the local vendor +directory. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- conscott +- Francisco Calderón +- Joost Jager +- Olaoluwa Osuntokun diff --git a/docs/release-notes/release-notes-0.1.2.md b/docs/release-notes/release-notes-0.1.2.md new file mode 100644 index 00000000..08a90b60 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.2.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2019-05-03 +- **Release page:** + [v0.1.2-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1.2-alpha) +- **Previous release:** [v0.1.1-alpha](release-notes-0.1.1.md) +- **Next release:** [v0.1.3-beta](release-notes-0.1.3-beta.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +Patch release for Loop client with fixes: + +- No more need to regenerate macaroons +- Fixes issue that required specifying testnet remote service URL directly +- Reduce max routing fee + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Olaoluwa Osuntokun diff --git a/docs/release-notes/release-notes-0.1.3-beta.md b/docs/release-notes/release-notes-0.1.3-beta.md new file mode 100644 index 00000000..22b107cf --- /dev/null +++ b/docs/release-notes/release-notes-0.1.3-beta.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2019-05-31 +- **Release page:** + [v0.1.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.1.3-beta) +- **Previous release:** [v0.1.2-alpha](release-notes-0.1.2.md) +- **Next release:** [v0.1.3-alpha](release-notes-0.1.3.md) + +#### New Features + +* Add Docker integration. + [PR #46](https://github.com/lightninglabs/loop/pull/46) +* Persist completed swap costs and expose them through the RPC interface. + [PR #55](https://github.com/lightninglabs/loop/pull/55) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Increase the maximum Lightning gRPC receive size to accommodate larger + responses. + [PR #56](https://github.com/lightninglabs/loop/pull/56) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Geoff Taylor +- Joost Jager +- Valentine Wallace diff --git a/docs/release-notes/release-notes-0.1.3.md b/docs/release-notes/release-notes-0.1.3.md new file mode 100644 index 00000000..4f90c987 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.3.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2019-05-31 +- **Release page:** + [v0.1.3-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1.3-alpha) +- **Previous release:** [v0.1.3-beta](release-notes-0.1.3-beta.md) +- **Next release:** [v0.2-alpha](release-notes-0.2.md) + +#### New Features + +Minor changes: + +- Add Docker support +- Improve after-the-fact accounting + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Joost Jager +- Olaoluwa Osuntokun +- Valentine Wallace diff --git a/docs/release-notes/release-notes-0.1.md b/docs/release-notes/release-notes-0.1.md new file mode 100644 index 00000000..2b943411 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.md @@ -0,0 +1,170 @@ +# Loop Client Release Notes + +- **Release date:** 2019-03-21 +- **Release page:** + [v0.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1-alpha) +- **Previous release:** None +- **Next release:** [v0.1.1-alpha](release-notes-0.1.1.md) + +#### New Features + +This is the first major release of Lightning Loop! This release is the first of +many planned, and includes the initial base functionality for the system. +[Check out our blog post for a high level overview on how the Loop service works](https://blog.lightning.engineering/posts/2019/03/20/loop.html). +With this new release, users can use Loop Out to free up inbound receiving +bandwidth in their channels and also send coins on chain from their existing +channel. + +The Lightning Loop client software is similar to `lnd`. There's the primary +daemon `loopd`, and its command-line interface `loop`. +[Check out the instructions in the `README` to get started](https://github.com/lightninglabs/loop/blob/master/README.md). +The `loop` command is very simple and at initial release has the following +commands: +``` +⛰ loop -h +NAME: + loop - control plane for your loopd + +USAGE: + loop [global options] command [command options] [arguments...] + +VERSION: + 0.1.0-alpha commit=v0.1-alpha + +COMMANDS: + out perform an off-chain to on-chain swap (looping out) + terms Display the current swap terms imposed by the server. + monitor monitor progress of any active swaps + quote get a quote for the cost of a swap + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --loopd value loopd daemon address host:port (default: "localhost:11010") + --help, -h show help + --version, -v print the version +``` + +You'll most frequently be using `loop out`. A sample run looks something like: +``` +loop out 500000 --addr=bc1qnrhawnjr7fn3gd09s8gltzzjdfsxsrm2k64ze7 +``` + +By specifying the optional `addr` flag, I tell `loopd` that I'd like to have the +funds sent to that target address. This lets one do cool things like send +directly from your channel into cold storage, or even deposit directly into an +exchange or another wallet under one's control. + +Once the loop has been initiated, you can use `loop monitor` to check on its +status. + +The other primary way to +[interact with Lightning Loop is via the gRPC API](https://lightning.engineering/loop). +This lets existing lapps, services, and businesses drive Loop programmatically +in a similar fashion to how they interact with `lnd` today. We'll have more +example uses cases, documentation, and explainers in the near future. + +Loop there it is!! ⚡️🔁 + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +**Verifying the Release** + +In order to verify the release, you'll need to have `gpg` or `gpg2` installed on +your system. Once you've obtained a copy (and hopefully verified that as well), +you'll first need to import the keys that have signed this release if you +haven't done so already: +``` +curl https://keybase.io/roasbeef/pgp_keys.asc | gpg --import +``` + +Once you have his PGP key you can verify the release (assuming +`manifest-v0.1-alpha.txt` and `manifest-v0.1-alpha.txt.sig` are in the current +directory) with: +``` +gpg --verify manifest-v0.1-alpha.txt +``` + +You should see the following if the verification was successful: +``` +gpg: assuming signed data in 'manifest-v0.1-alpha.txt' +gpg: Signature made Wed Mar 20 20:22:49 2019 PDT +gpg: using RSA key F8037E70C12C7A263C032508CE58F7F8E20FD9A2 +gpg: Good signature from "Olaoluwa Osuntokun " [ultimate] +``` + +That will verify the signature on the main manifest page which ensures integrity +and authenticity of the binaries you've downloaded locally. Next, depending on +your operating system you should then re-calculate the `sha256` sum of the +binary, and compare that with the following hashes (which are included in the +manifest file): +``` +5c63d870d007d4dd9d0480a0ebc1fee83f1099acac12a4bdd121d3882b772bfb loop-darwin-386-v0.1-alpha.tar.gz +98fc299e107c89df4875af0f2fe46957b228321036c60661507ecdbb9ba1a56a loop-darwin-amd64-v0.1-alpha.tar.gz +761c5af296e62b2eaa9421e9d36a258e9f435a2bc53df33b950b41d3a9eef89d loop-dragonfly-amd64-v0.1-alpha.tar.gz +269997eba3daebeeac7edb82739b3406df3e52b3345c61124c60b145b9080c0f loop-freebsd-386-v0.1-alpha.tar.gz +38fb7a32c9c36f80cac9a47e448338dedde53554f569454202236d278654eadf loop-freebsd-amd64-v0.1-alpha.tar.gz +7226a8c5171c6e10adf754d1c3acd81dee84497d170877981ea441108368fee7 loop-freebsd-arm-v0.1-alpha.tar.gz +e07552fd11967743262a678060369f5fbe17bcde3c4ee6cc21196618248afb9c loop-linux-386-v0.1-alpha.tar.gz +816a7050c91652a8e5b881e939c1bc70e06fe23e8309ac80ef5d38fda06969b7 loop-linux-amd64-v0.1-alpha.tar.gz +75a9ce3f605359735d39d9a0fb5037676c6ac29a70d7db5bc75fc1e5e3fcc465 loop-linux-arm64-v0.1-alpha.tar.gz +b7c7668c90f1d67689a9e557346e32bf10c169ace220cd507ae4ee63f64b12d1 loop-linux-armv6-v0.1-alpha.tar.gz +8aad519ff927643c35968bc9eb4e91a5a1f063ebf60e50b12c05f745d3602540 loop-linux-armv7-v0.1-alpha.tar.gz +5b3be8f2ab4acff7e854a794ecde59abd7feed3002bb13177f89c1cd1c9419e7 loop-linux-mips64-v0.1-alpha.tar.gz +aa3da5049eb2d411912aba8386dcc241f9db2b41dd4571ddbea80cf07f3fbc20 loop-linux-mips64le-v0.1-alpha.tar.gz +1a6a8e8e93df6c02eac1e61d4e9f3a518bc2c2a096deb0b77f0e5a6c0afb553d loop-linux-ppc64-v0.1-alpha.tar.gz +85e4f4e32783aad5e7e53125ca4c2efecaf997b24e81e54db0cc380cdf876cf9 loop-netbsd-386-v0.1-alpha.tar.gz +8896df3499e9287177b5eacb2fc6b964e6dec698807f49582cbccbd3b7088878 loop-netbsd-amd64-v0.1-alpha.tar.gz +133b069ad151adeac9c62596ba507f9ed425cfe6670a03e2e5dea6316e522f18 loop-openbsd-386-v0.1-alpha.tar.gz +a9d1f80d34f698450d413844ff1a4536a4377faa6debd7c16bcfde94964cd628 loop-openbsd-amd64-v0.1-alpha.tar.gz +94c35abdcafeebb17f19850b5e88031926116cce8bd22cdf56d905716a0063d8 loop-source-v0.1-alpha.tar.gz +5cd5171f469e639ece6b37a2ff35bae3dac6d1b0a9490ed9e3f90e4f1fcadb97 loop-windows-386-v0.1-alpha.zip +b139416e426868b060ea6fd28ba840398a6e45ad492eac50297462d4391bc0ce loop-windows-amd64-v0.1-alpha.zip +e2ab0d8f7ec07468ea46e551f17521799147a01f48a3f2c5b1c2d62efd9512a5 vendor.tar.gz +``` + +One can use the `shasum -a 256 ` tool in order to re-compute the +`sha256` hash of the target binary for your operating system. The produced hash +should be compared with the hashes listed above and they should match *exactly*. + +Finally, you can also verify the _tag_ itself with the following command: +``` +git verify-tag v0.1-alpha +``` + +**Building the Contained Release** + +With this new version of `loop`, we've modified our release process to ensure +the bundled release is now _fully self contained_. As a result, with only the +attached payload with this release, users will be able to rebuild the target +release themselves without having to fetch any of the dependancies. Note that at +this stage, binaries aren't yet fully reproducible (even with `go modules` ). +This is due to the fact that by default, +[Go will include the full directory path where the binary was built in the binary itself](https://github.com/golang/go/issues/16860). +As a result, unless your file system exactly mirrors the machine used to build +the binary, you'll get a different binary, as it includes artifacts from your +local file system. This will be fixed in `go1.13`, and before then we may modify +our release system to do this automatically. + +In order to re-build from scratch, assuming that `vendor.tar.gz` and +`loop-source-v0.1-alpha.tar.gz` are in the current directory: +``` +tar -xvzf vendor.tar.gz +tar -xvzf loop-source-v0.1-alpha.tar.gz +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1-alpha" ./cmd/loop +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1-alpha" ./cmd/loopd +``` + +The `-mod=vendor` flag tells the `go build` command that it doesn't need to +fetch the dependencies, and instead, they're all enclosed in the local vendor +directory. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Olaoluwa Osuntokun +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.10.0.md b/docs/release-notes/release-notes-0.10.0.md new file mode 100644 index 00000000..a7f72675 --- /dev/null +++ b/docs/release-notes/release-notes-0.10.0.md @@ -0,0 +1,56 @@ +# Loop Client Release Notes + +- **Release date:** 2020-10-13 +- **Release page:** + [v0.10.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.10.0-beta) +- **Previous release:** [v0.9.0-beta](release-notes-0.9.0.md) +- **Next release:** [v0.11.0-beta](release-notes-0.11.0.md) + +#### New Features + +* Multi-path payment has been enabled for Loop In. This means that it is now + possible to replenish multiple channels via a single Loop In request and a + single on-chain htlc. This has to potential to greatly reduce chain fee costs. + Note that it is not yet possible to select specific peers to loop in through. +* The daemon now sends a user agent string with each swap. This allows + developers to identify their fork or custom implementation of the loop client. + +**Updated Swap Suggestions** + +* The swap suggestions endpoint has been updated to be fee-aware. Swaps that + exceed the fee limits set by the liquidity manager will no longer be suggested + (see `getparams` for the current limits, and use `setparams` to update these + values). +* Swap suggestions are now aware of ongoing and previously failed swaps. They + will not suggest swaps for channels that are currently being utilized for + swaps, and will not suggest any swaps if a swap that is not limited to a + specific peer or channel is ongoing. If a channel was part of a failed swap + within the last 24H, it will be excluded from our swap suggestions (this value + is configurable). +* The `debug` logging level is recommended if using this feature. + +#### Breaking Changes + +* Macaroon authentication has been enabled for the `loopd` gRPC and REST + connections. This makes it possible for the loop API to be exposed safely over + the internet as unauthorized access is now prevented. + +The daemon will write a default `loop.macaroon` in its main directory. For +mainnet this file will be picked up automatically by the `loop` CLI tool. For +testnet you need to specify the `--network=testnet` flag. +[More information about TLS and macaroons.](../../README.md#authentication-and-transport-security) + +* The `setparm` loopcli endpoint is renamed to `setrule` because this endpoint + is only used for setting liqudity rules (parameters can be set using the new + `setparams` endpoint). + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Joost Jager +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.0.md b/docs/release-notes/release-notes-0.11.0.md new file mode 100644 index 00000000..804e9019 --- /dev/null +++ b/docs/release-notes/release-notes-0.11.0.md @@ -0,0 +1,37 @@ +# Loop Client Release Notes + +- **Release date:** 2020-10-27 +- **Release page:** + [v0.11.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.0-beta) +- **Previous release:** [v0.10.0-beta](release-notes-0.10.0.md) +- **Next release:** [v0.11.1-beta](release-notes-0.11.1.md) + +#### New Features + +* The loop client now labels all its on-chain transactions to make them easily + identifiable in `lnd` 's `listchaintxns` output. + +**Introducing Autoloop** + +* This release includes support for opt-in automatic dispatch of loop out swaps, + based on the output of the `Suggestions` endpoint. +* To enable the autolooper, the following command can be used: + `loop setparams --autoout=true --autobudget={budget in sats} --budgetstart={start time for budget}` +* Automatically dispatched swaps are identified in the output of the `ListSwaps` + with the label `[reserved]: autoloop-out`. +* If autoloop is not enabled, the client will log the actions that the + autolooper would have taken if it was enabled, and the `Suggestions` endpoint + can be used to view the exact set of swaps that the autolooper would make if + enabled. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.1.md b/docs/release-notes/release-notes-0.11.1.md new file mode 100644 index 00000000..5d6ff9ee --- /dev/null +++ b/docs/release-notes/release-notes-0.11.1.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2020-11-12 +- **Release page:** + [v0.11.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.1-beta) +- **Previous release:** [v0.11.0-beta](release-notes-0.11.0.md) +- **Next release:** [v0.11.2-beta](release-notes-0.11.2.md) + +#### New Features + +* When requesting a swap, a new `initiator` field can be set on the gRPC/REST + interface that is appended to the user agent string that is sent to the server + to give information about Loop usage. The initiator field is meant for user + interfaces to add their name to give the full picture of the binary used + (`loopd`, LiT) and the method/interface used for triggering the swap (`loop` + CLI, autolooper, LiT UI, other 3rd party UI). + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.2.md b/docs/release-notes/release-notes-0.11.2.md new file mode 100644 index 00000000..717e93df --- /dev/null +++ b/docs/release-notes/release-notes-0.11.2.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2020-12-08 +- **Release page:** + [v0.11.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.2-beta) +- **Previous release:** [v0.11.1-beta](release-notes-0.11.1.md) +- **Next release:** [v0.11.3-beta](release-notes-0.11.3.md) + +#### New Features + +**Autoloop Swap Size** + +* Autoloop can now be configured with custom swap size limits. Previously, + autoloop would use the minimum/maximum swap amount set by the server (exposed + by the `loop terms` command) to decide on swap size. +* Setting a custom minimum swap amount is particularly useful for clients that + would like to perform fewer, larger swaps to save on fees. The trade-off when + setting a large minimum amount is that autoloop will wait until your channel + is at least the minimum amount below its incoming threshold amount before + executing a swap, which may result in channels staying under the threshold for + longer. +* These values can be set using the following command: + `loop setparams --minamt={minimum in sats} --maxamt={maximum in sats}`. +* The values set must fall within the limits set by the loop server. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen diff --git a/docs/release-notes/release-notes-0.11.3.md b/docs/release-notes/release-notes-0.11.3.md new file mode 100644 index 00000000..05e378ef --- /dev/null +++ b/docs/release-notes/release-notes-0.11.3.md @@ -0,0 +1,50 @@ +# Loop Client Release Notes + +- **Release date:** 2021-02-09 +- **Release page:** + [v0.11.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.3-beta) +- **Previous release:** [v0.11.2-beta](release-notes-0.11.2.md) +- **Next release:** [v0.11.4-beta](release-notes-0.11.4.md) + +#### New Features + +* If lnd is locked when the loop client starts up, it will wait for lnd to be + unlocked. Previous versions would exit with an error. +* Loop will no longer need all `lnd` subserver macaroons to be present in the + `--lnd.macaroondir`. Instead the new `--lnd.macaroonpath` option can be + pointed to a single macaroon, for example the `admin.macaroon` or a custom + baked one with the exact permissions needed for Loop. If the now deprecated + flag/option `--lnd.macaroondir` is used, it will fall back to use only the + `admin.macaroon` from that directory. +* The rules used for autoloop have been relaxed to allow autoloop to dispatch + swaps even if there are manually initiated swaps that are not limited to a + single channel in progress. This change was made to allow autoloop to coexist + with manual swaps. +* The `SuggestSwaps` endpoint has been updated to include reasons that indicate + why the Autolooper is not currently dispatching swaps for the set of rules + that the client is configured with. See the + [autoloop documentation](../autoloop.md) for a detailed explanations of + these reasons. + +#### Breaking Changes + +* The `AutoOut`, `AutoOutBudgetSat` and `AutoOutBudgetStartSec` fields in the + `LiquidityParameters` message used in the experimental autoloop API have been + renamed to `Autoloop`, `AutoloopBudgetSat` and `AutoloopBudgetStartSec`. +* The `autoout` flag for enabling automatic dispatch of loop out swaps has been + renamed to `autoloop` so that it can cover loop out and loop in. +* The `SuggestSwaps` rpc call will now fail with a `FailedPrecondition` grpc + error code if no rules are configured for the autolooper. Previously the rpc + would return an empty response. + +#### Bug Fixes + +- Fixed compile time compatibility with `lnd v0.12.0-beta`. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.4.md b/docs/release-notes/release-notes-0.11.4.md new file mode 100644 index 00000000..893e6c1b --- /dev/null +++ b/docs/release-notes/release-notes-0.11.4.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2021-02-11 +- **Release page:** + [v0.11.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.4-beta) +- **Previous release:** [v0.11.3-beta](release-notes-0.11.3.md) +- **Next release:** [v0.12.0-beta](release-notes-0.12.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Default for `--lnd.macaroonpath` is set to the default path for the LND + `admin.macaroon` (https://github.com/lightninglabs/loop/issues/336) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.12.0.md b/docs/release-notes/release-notes-0.12.0.md new file mode 100644 index 00000000..d8bc1014 --- /dev/null +++ b/docs/release-notes/release-notes-0.12.0.md @@ -0,0 +1,40 @@ +# Loop Client Release Notes + +- **Release date:** 2021-03-06 +- **Release page:** + [v0.12.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.12.0-beta) +- **Previous release:** [v0.11.4-beta](release-notes-0.11.4.md) +- **Next release:** [v0.12.1-beta](release-notes-0.12.1.md) + +#### New Features + +* Autoloop can now be configured on a per-peer basis, rather than only on an + individual channel level. This change allows desired liquidity thresholds to + be set for an individual peer, rather than a specific channel, and leverages + multi-loop-out to more efficiently manage liquidity. To configure peer-level + rules, provide the 'setrule' command with the peer's pubkey. +* Autoloop's fee API has been simplified to allow setting a single percentage + which will be used to limit total swap fees to a percentage of the amount + being swapped, the default budget has been updated to reflect this. Use + `loop setparams --feepercent={percentage}` to update this value. This fee + setting has been updated to the default for autoloop. +* The default confirmation target for automated loop out swap sweeps has been + increased to 100 blocks. This change will not affect the time it takes to + acquire inbound liquidity, but will decrease the cost of swaps. + +#### Breaking Changes + +#### Bug Fixes + +* The loop dockerfile has been updated to use the `make` command so that the + latest commit hash of the code being run will be included in `loopd`. +* A bug where loop in on-chain fees were not recorded properly has been + addressed. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- rockstardev diff --git a/docs/release-notes/release-notes-0.12.1.md b/docs/release-notes/release-notes-0.12.1.md new file mode 100644 index 00000000..e7a14a34 --- /dev/null +++ b/docs/release-notes/release-notes-0.12.1.md @@ -0,0 +1,40 @@ +# Loop Client Release Notes + +- **Release date:** 2021-03-26 +- **Release page:** + [v0.12.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.12.1-beta) +- **Previous release:** [v0.12.0-beta](release-notes-0.12.0.md) +- **Next release:** [v0.12.2-beta](release-notes-0.12.2.md) + +#### New Features + +* A new flag, `--verbose`, or `-v`, is added to `loop in`, `loop out` and + `loop quote`. Responses from these commands are also updated to provide more + verbose info, giving users a more intuitive view about money paid on/off-chain + and fees incurred. Use `loop in -v`, `loop out -v`, `loop quote in -v` or + `loop quote out -v` to view the details. +* A stripped down version of the Loop server is now provided as a + [Docker image](https://hub.docker.com/r/lightninglabs/loopserver). A quick + start script and example `docker-compose` environment as well as + [documentation on how to use the `regtest` Loop server](https://github.com/lightninglabs/loop/blob/master/regtest/README.md) + was added too. + +#### Breaking Changes + +#### Bug Fixes + +* A bug that would not list autoloop rules set on a per-peer basis when they + were excluded due to insufficient budget, or the number of swaps in flight has + been corrected. These rules will now be included in the output of + `suggestswaps` with other autoloop peer rules. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Elle Mouton +- Justin O'Brien +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.12.2.md b/docs/release-notes/release-notes-0.12.2.md new file mode 100644 index 00000000..a12757e7 --- /dev/null +++ b/docs/release-notes/release-notes-0.12.2.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2021-04-29 +- **Release page:** + [v0.12.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.12.2-beta) +- **Previous release:** [v0.12.1-beta](release-notes-0.12.1.md) +- **Next release:** [v0.13.0-beta](release-notes-0.13.0.md) + +#### New Features + +- If the payment for an LSAT fails, it is now automatically re-tried. + +#### Breaking Changes + +#### Bug Fixes + + - Instead of just blocking for forever without any apparent reason if another + Loop daemon process is already running, we now exit with an error after 5 + seconds if acquiring the unique lock on the Loop `bbolt` DB fails. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Elle Mouton +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.13.0.md b/docs/release-notes/release-notes-0.13.0.md new file mode 100644 index 00000000..749afcb4 --- /dev/null +++ b/docs/release-notes/release-notes-0.13.0.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2021-05-19 +- **Release page:** + [v0.13.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.13.0-beta) +- **Previous release:** [v0.12.2-beta](release-notes-0.12.2.md) +- **Next release:** [v0.14.0-beta](release-notes-0.14.0.md) + +#### New Features + +#### Breaking Changes + +A breaking change is required due to poold changes. + +- Bumped the minimum required version of `lnd` to `v0.11.1-beta`. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Carla Kirk-Cohen +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.14.0.md b/docs/release-notes/release-notes-0.14.0.md new file mode 100644 index 00000000..d36aead4 --- /dev/null +++ b/docs/release-notes/release-notes-0.14.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2021-06-01 +- **Release page:** + [v0.14.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.14.0-beta) +- **Previous release:** [v0.13.0-beta](release-notes-0.13.0.md) +- **Next release:** [v0.14.1-beta](release-notes-0.14.1.md) + +#### New Features + +- The loopd client reports off-chain routing failures for loop out swaps if it + cannot find a route to the server for the swap's prepay or invoice payment. + This allows the server to release accepted invoices, if there are any, + earlier, reducing the amount of time that funds are held off-chain. If the + swap failed on one of the loop server's channels, it will report failure + location of its off-chain failure. If the failure occurred outside of the loop + server's infrastructure, a generic failure will be used so that no information + about the client's position in the network is leaked. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Maurice Poirrier diff --git a/docs/release-notes/release-notes-0.14.1.md b/docs/release-notes/release-notes-0.14.1.md new file mode 100644 index 00000000..80e7102c --- /dev/null +++ b/docs/release-notes/release-notes-0.14.1.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2021-06-09 +- **Release page:** + [v0.14.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.14.1-beta) +- **Previous release:** [v0.14.0-beta](release-notes-0.14.0.md) +- **Next release:** [v0.14.2-beta](release-notes-0.14.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* A protobuf warning that was being logged on `loopd` startup and `loop` cli + calls has been addressed. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen diff --git a/docs/release-notes/release-notes-0.14.2.md b/docs/release-notes/release-notes-0.14.2.md new file mode 100644 index 00000000..985a2b1e --- /dev/null +++ b/docs/release-notes/release-notes-0.14.2.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2021-07-20 +- **Release page:** + [v0.14.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.14.2-beta) +- **Previous release:** [v0.14.1-beta](release-notes-0.14.1.md) +- **Next release:** [v0.15.0-beta](release-notes-0.15.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +- Certain versions of the Python gRPC library + [weren't able to connect](https://github.com/grpc/grpc/issues/23172) to + `loopd` 's gRPC interface, getting the `missing selected ALPN property` error. + A server side fix was introduced to get rid of that error message. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Olaoluwa Osuntokun +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.15.0.md b/docs/release-notes/release-notes-0.15.0.md new file mode 100644 index 00000000..13939592 --- /dev/null +++ b/docs/release-notes/release-notes-0.15.0.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2021-08-03 +- **Release page:** + [v0.15.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.15.0-beta) +- **Previous release:** [v0.14.2-beta](release-notes-0.14.2.md) +- **Next release:** [v0.15.1-beta](release-notes-0.15.1.md) + +#### New Features + +* Loop-in quote now asks the server to optionally probe the client to test + inbound liquidity. The server may use this information to give more accurate + quotes. + +#### Breaking Changes + +#### Bug Fixes + +* Grpc error codes returned by the swap server when swap initiation fails are + now surfaced to the client. Previously these error codes would be returned as + a string. + +#### Maintenance + +* Updated compile time dependencies of `lnd`, `grpc-gateway`, `protobuf` and + `grpc`. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.15.1.md b/docs/release-notes/release-notes-0.15.1.md new file mode 100644 index 00000000..feafa720 --- /dev/null +++ b/docs/release-notes/release-notes-0.15.1.md @@ -0,0 +1,41 @@ +# Loop Client Release Notes + +- **Release date:** 2021-11-18 +- **Release page:** + [v0.15.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.15.1-beta) +- **Previous release:** [v0.15.0-beta](release-notes-0.15.0.md) +- **Next release:** [v0.16.0-beta](release-notes-0.16.0.md) + +#### New Features + +* Add JSON client stubs for using the Loop RPC interface from WebAssembly. + [PR #411](https://github.com/lightninglabs/loop/pull/411) +* Export `NewListenerConfig` so callers can construct custom listeners. + [PR #432](https://github.com/lightninglabs/loop/pull/432) + +#### Breaking Changes + +#### Bug Fixes + +* Improve errors for explicitly configured files. + [PR #413](https://github.com/lightninglabs/loop/pull/413) +* Create the default macaroon only when required by the active configuration. + [PR #428](https://github.com/lightninglabs/loop/pull/428) + +#### Maintenance + +* Refactor Autoloop swap construction behind a swap-builder interface. + [PR #418](https://github.com/lightninglabs/loop/pull/418) +* Update `lndclient` and the compile-time LND dependency for LND v0.14. + [PR #425](https://github.com/lightninglabs/loop/pull/425) + [PR #426](https://github.com/lightninglabs/loop/pull/426) + [PR #438](https://github.com/lightninglabs/loop/pull/438) + +#### Contributors (Alphabetical Order) + +- Carla Kirk-Cohen +- Harsha Goli +- Martin Habovštiak +- Oliver Gugger +- Turtle +- Yong Yu diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md new file mode 100644 index 00000000..265937bf --- /dev/null +++ b/docs/release-notes/release-notes-0.16.0.md @@ -0,0 +1,41 @@ +# Loop Client Release Notes + +- **Release date:** 2021-12-14 +- **Release page:** + [v0.16.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.16.0-beta) +- **Previous release:** [v0.15.1-beta](release-notes-0.15.1.md) +- **Next release:** [v0.17.0-beta](release-notes-0.17.0.md) + +#### New Features + +* `--private` flag is now available on the `loop in` command, meaning users can + now loop in to private nodes! This was implemented in + [#415](https://github.com/lightninglabs/loop/pull/415) and has an + implementation of LND's hop hints creation feature for the time being. The + flag is also available in `loop quote in` as an extension. +* `--route_hints` has also been added on the `loop in` and `loop quote in` cli + commands, and was also include in + [#415](https://github.com/lightninglabs/loop/pull/415). While the `--private` + flag autogenerates routehints to assist the payer (Lightning Labs), + `--route_hints` allows the user to feed their own crafted versions if they so + please. + +#### Breaking Changes + +#### Bug Fixes + +* Fixed issue where loop assumes the mainnet location for lnd.macaroonpath + regardless of passed network parameters. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Harsha Goli +- Martin Habovštiak +- Oliver Gugger +- Turtle +- Yong Yu diff --git a/docs/release-notes/release-notes-0.17.0.md b/docs/release-notes/release-notes-0.17.0.md new file mode 100644 index 00000000..6b99d1a4 --- /dev/null +++ b/docs/release-notes/release-notes-0.17.0.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2022-02-07 +- **Release page:** + [v0.17.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.17.0-beta) +- **Previous release:** [v0.16.0-beta](release-notes-0.16.0.md) +- **Next release:** [v0.18.0-beta](release-notes-0.18.0.md) + +#### New Features + +* Loop in functionality has been added to AutoLoop. This feature can be enabled + to acquire outgoing capacity on your node automatically, using + `loop setrule --type=in`. At present, autoloop can only be set to loop out + *or* loop in, and cannot manage liquidity in both directions. + +* Use LND's hop hint selector when doing private loop-ins. + +#### Breaking Changes + +#### Bug Fixes + +* Close local databases when loopd daemon is stopped programmatically. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Elle Mouton +- Suhail Saqan diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md new file mode 100644 index 00000000..8f6a094d --- /dev/null +++ b/docs/release-notes/release-notes-0.18.0.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2022-03-30 +- **Release page:** + [v0.18.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.18.0-beta) +- **Previous release:** [v0.17.0-beta](release-notes-0.17.0.md) +- **Next release:** [v0.19.1-beta](release-notes-0.19.1.md) + +#### New Features + +* Loop client now supports optional routing plugins to improve off-chain payment + reliability. One such plugin that the client implements will gradually prefer + increasingly more expensive routes in case payments using cheap routes time + out. Note that with this addition the minimum required LND version is LND + 0.14.2-beta. + +#### Breaking Changes + +#### Bug Fixes + +* Loop now supports being hooked up to a remote signing pair of `lnd` nodes, as + long as `lnd` is `v0.14.3-beta` or later. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Harsha Goli +- Marnix +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.19.1.md b/docs/release-notes/release-notes-0.19.1.md new file mode 100644 index 00000000..6d4e3800 --- /dev/null +++ b/docs/release-notes/release-notes-0.19.1.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2022-06-09 +- **Release page:** + [v0.19.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.19.1-beta) +- **Previous release:** [v0.18.0-beta](release-notes-0.18.0.md) +- **Next release:** [v0.20.0-beta](release-notes-0.20.0.md) + +#### New Features + +* User-specified liquidity parameters are persisted on disk to enable the + liquidity manager's config to survive after a restart. + +#### Breaking Changes + +#### Bug Fixes + +* The `SuggestSwaps` rpc now returns the correct peer pubkeys in the + disqualified list. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Alex Miller +- Andras Banki-Horvath +- Carsten Otto +- Harsha Goli +- Oliver Gugger +- sputn1ck +- Yong Yu diff --git a/docs/release-notes/release-notes-0.2.0.md b/docs/release-notes/release-notes-0.2.0.md new file mode 100644 index 00000000..bdc485a6 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.0.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2019-06-26 +- **Release page:** + [v0.2.0-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.0-alpha) +- **Previous release:** [v0.2-alpha](release-notes-0.2.md) +- **Next release:** [v0.2.1-alpha](release-notes-0.2.1.md) + +#### New Features + +The `v0.2.0-alpha` and `v0.2-alpha` tags point to the same source commit. This +tag does not introduce additional source changes. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +No additional contributors; this tag points to the same commit as +`v0.2-alpha`. diff --git a/docs/release-notes/release-notes-0.2.1.md b/docs/release-notes/release-notes-0.2.1.md new file mode 100644 index 00000000..741cc044 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.1.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2019-07-25 +- **Release page:** + [v0.2.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.1-alpha) +- **Previous release:** [v0.2.0-alpha](release-notes-0.2.0.md) +- **Next release:** [v0.2.2-alpha](release-notes-0.2.2.md) + +#### New Features + +This release includes a new option to specify the sweep confirmation target for +Loop Out. + +``` +--conf_target=block_count +``` + +This will delay the finality of the swap in favor of a potential reduction in +chain fee expenditure. + +If you are using this feature, please note that it does become increasingly +important that you do not allow `loopd` to terminate before the swap is final, +otherwise the timeout pathway may activate in the HTLC. + +Because of the finality problem, the target will only be used at the start of +the swap preimage reveal time period and is not a guaranteed final sweep fee +rate. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.2.2.md b/docs/release-notes/release-notes-0.2.2.md new file mode 100644 index 00000000..6b9f5bb7 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.2.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2019-07-31 +- **Release page:** + [v0.2.2-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.2-alpha) +- **Previous release:** [v0.2.1-alpha](release-notes-0.2.1.md) +- **Next release:** [v0.2.3-alpha](release-notes-0.2.3.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +This release includes some important fixes for Loop Out. + +- Improved support for selecting an external Loop Out address +- Better validation for executing a Loop Out swap + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Bjorn Olav Jalborg +- Joost Jager diff --git a/docs/release-notes/release-notes-0.2.3.md b/docs/release-notes/release-notes-0.2.3.md new file mode 100644 index 00000000..ad1a4153 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.3.md @@ -0,0 +1,37 @@ +# Loop Client Release Notes + +- **Release date:** 2019-10-03 +- **Release page:** + [v0.2.3-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.3-alpha) +- **Previous release:** [v0.2.2-alpha](release-notes-0.2.2.md) +- **Next release:** [v0.2.4-alpha](release-notes-0.2.4.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +This patch release fixes the following issues: + +- Max confirmations now respects a longer confirmation target more reliably +- REST API now matches the gRPC API more closely +- Fix issue encountered when using external address with Loop In + +For more details, see: + +- https://github.com/lightninglabs/loop/pull/74 - REST API Update +- https://github.com/lightninglabs/loop/pull/86 - Loop In Fee Estimation Failure +- https://github.com/lightninglabs/loop/pull/89 - Max Confirmation Target Issue + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Nigel Christian +- Olaoluwa Osuntokun +- Oliver Gugger +- saguywalker +- William O'Beirne +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.2.4.md b/docs/release-notes/release-notes-0.2.4.md new file mode 100644 index 00000000..06ffa69a --- /dev/null +++ b/docs/release-notes/release-notes-0.2.4.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2019-10-11 +- **Release page:** + [v0.2.4-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.4-alpha) +- **Previous release:** [v0.2.3-alpha](release-notes-0.2.3.md) +- **Next release:** [v0.3.0-alpha](release-notes-0.3.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +This patch release includes: + +- Preparatory changes for upcoming server changes related to swap fees and + timing. + +In the future, the Loop service will allow more flexibility around extensions to +the swap timing in order to get a lower swap fee. + +In preparation, the API will be adjusted to provide for multiple fee rates at +different preferred swap wait times. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Johan T. Halseth +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.2.md b/docs/release-notes/release-notes-0.2.md new file mode 100644 index 00000000..9db683a4 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.md @@ -0,0 +1,153 @@ +# Loop Client Release Notes + +- **Release date:** 2019-06-26 +- **Release page:** + [v0.2-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2-alpha) +- **Previous release:** [v0.1.3-alpha](release-notes-0.1.3.md) +- **Next release:** [v0.2.0-alpha](release-notes-0.2.0.md) + +#### New Features + +This is the first second release of Lightning Loop! This new release enables +**Loop In** on mainnet, and also exposes some additional code used to build the +`loop` daemon as an external package that others can use. +[Check out our latest blog post for more details on Loop In, and the future of Lightning Loop](https://blog.lightning.engineering/announcement/2019/06/25/loop-in.html). + +**Loop In** + +At a high-level a Loop In swap can be used to: + * Refilling depleted channels with funds from cold-wallets or exchange + withdrawals + * Servicing off-chain Lightning withdrawals using on-chain payments, with no + funds in channels required + +#### Breaking Changes + +#### Bug Fixes + + * As a failsafe payment method that can be used when channel liquidity along a + route is insufficient + +A new command has been added to the `loop` cli command to allow users to drive +Loop In swaps: +``` +NAME: + loop in - perform an on-chain to off-chain swap (loop in) + +USAGE: + loop in [command options] amt + +DESCRIPTION: + + Send the amount in satoshis specified by the amt argument off-chain. + +OPTIONS: + --amt value the amount in satoshis to loop in (default: 0) + --external expect htlc to be published externally +``` + +The `--external` argument allows the on-chain HTLC transacting to be published +externally. This allows for a number of use cases like using this address to +withdraw from an exchange into your Lightning channel! + +[Additionally, the gRPC API (and REST!) docs have also been updated](https://lightning.engineering/loop). + +Loop there it is!! ⚡️🔁 + +#### Maintenance + +**Verifying the Release** + +In order to verify the release, you'll need to have `gpg` or `gpg2` installed on +your system. Once you've obtained a copy (and hopefully verified that as well), +you'll first need to import the keys that have signed this release if you +haven't done so already: +``` +curl https://keybase.io/roasbeef/pgp_keys.asc | gpg --import +``` + +Once you have his PGP key you can verify the release (assuming +`manifest-v0.2-alpha.txt` and `manifest-v0.2-alpha.txt.sig` are in the current +directory) with: +``` +gpg --verify manifest-v0.2-alpha.txt +``` + +You should see the following if the verification was successful: +``` +gpg: assuming signed data in 'manifest-v0.2-alpha.txt' +gpg: Signature made Wed Jun 26 09:56:58 2019 PDT +gpg: using RSA key F8037E70C12C7A263C032508CE58F7F8E20FD9A2 +gpg: Good signature from "Olaoluwa Osuntokun " [ultimate] +``` + +That will verify the signature on the main manifest page which ensures integrity +and authenticity of the binaries you've downloaded locally. Next, depending on +your operating system you should then re-calculate the `sha256` sum of the +binary, and compare that with the following hashes (which are included in the +manifest file): +``` +de97a2e33fcce9650911f8976ed9089d061a1d5fb374bdfd3be0d6101871585d loop-darwin-386-v0.2-alpha.tar.gz +666a5910757cb15dd2a62ac009b31359a97f4f1c83803d4028d10d23db6ee587 loop-darwin-amd64-v0.2-alpha.tar.gz +4a06d43e48e7537975a92d25ac187b78a74426976ffd36bb1efaeb83bae7aa4f loop-dragonfly-amd64-v0.2-alpha.tar.gz +f6cf356db4060d8c90b91dc1d8547472b255ceb8aae2197f531a177a285f60fc loop-freebsd-386-v0.2-alpha.tar.gz +74fdb8d3a8e0a91cfeabe5e06f2be386a68e4524fa42d9c042b9a2030499a76d loop-freebsd-amd64-v0.2-alpha.tar.gz +d500f36eefc1e1ee63b8259d4eefcd64f34b295781956559bd52389b5a37a98b loop-freebsd-arm-v0.2-alpha.tar.gz +d2d01f9a8d1483173c5f1618477ff1929e8aa5e322849091d7b86694fedbf0bb loop-linux-386-v0.2-alpha.tar.gz +da92445ec5e754da3f379e85310d697f3609a17e6639d1251a479a09fc0c16e1 loop-linux-amd64-v0.2-alpha.tar.gz +04edae2cbdb6943b70b13cbc678901d26f4f06767f60f591f7c7d791c8b5925f loop-linux-arm64-v0.2-alpha.tar.gz +5fae0185f8849364af9b998609d002f5e226d3e937dabfd56d1474415f6555db loop-linux-armv6-v0.2-alpha.tar.gz +931a43c8a936db0a3a6a3d099b67d953b69344259bfa7d37faac985341ff27e9 loop-linux-armv7-v0.2-alpha.tar.gz +757ae30b349f8fc209f7df20e1b6010814d0dd279a3717b8c7ba5cb7a092103b loop-linux-mips64-v0.2-alpha.tar.gz +900c8ce735ac94f1d35c179e4387cc9292f261be685eb57b865d772bd668d8ec loop-linux-mips64le-v0.2-alpha.tar.gz +aeb3d2f882405308827974515ea5d652d5e6d72188165ab315dbbb4c7402bc3f loop-linux-ppc64-v0.2-alpha.tar.gz +0d072fd9ed648de679f46ee959a3aef1ae9e025d756140e432df2a51bfe57221 loop-netbsd-386-v0.2-alpha.tar.gz +f26cedf2e8a38b175b47a6b6101edab99033e4776c6de21306cdc793e6dc0b0b loop-netbsd-amd64-v0.2-alpha.tar.gz +3ea2bb1482c78d2bb45358c3de7961670b5ed64a231ec9a2dc2b6ba13a3e76a6 loop-openbsd-386-v0.2-alpha.tar.gz +7a67ae9805beb8e43bd257e8355f8c471bbc740b1b2f1bbf8f834d50312299fe loop-openbsd-amd64-v0.2-alpha.tar.gz +8f15d5c5f56bd19952361ee27b1821306d634bbe8799e52ab36a09f8c151ff6d loop-source-v0.2-alpha.tar.gz +99de0a0e0464a5d7910e6ec4c5cdc881db278bf1bbb6095d5aec7c7a033aca0f loop-windows-386-v0.2-alpha.zip +fb1e4dca3728a2608dce560385bf6e07cb319d27f4d7cfeb0a4aaf00b1cc2a4c loop-windows-amd64-v0.2-alpha.zip +47b208033931c22fd6b1b23b723d49e18359709cb6b29a3f2623e6f80179bfbf vendor.tar.gz +``` + +One can use the `shasum -a 256 ` tool in order to re-compute the +`sha256` hash of the target binary for your operating system. The produced hash +should be compared with the hashes listed above and they should match *exactly*. + +Finally, you can also verify the _tag_ itself with the following command: +``` +git verify-tag v0.2-alpha +``` + +**Building the Contained Release** + +With this new version of `loop`, we've modified our release process to ensure +the bundled release is now _fully self contained_. As a result, with only the +attached payload with this release, users will be able to rebuild the target +release themselves without having to fetch any of the dependancies. Note that at +this stage, binaries aren't yet fully reproducible (even with `go modules` ). +This is due to the fact that by default, +[Go will include the full directory path where the binary was built in the binary itself](https://github.com/golang/go/issues/16860). +As a result, unless your file system exactly mirrors the machine used to build +the binary, you'll get a different binary, as it includes artifacts from your +local file system. This will be fixed in `go1.13`, and before then we may modify +our release system to do this automatically. + +In order to re-build from scratch, assuming that `vendor.tar.gz` and +`loop-source-v0.2-alpha.tar.gz` are in the current directory: +``` +tar -xvzf vendor.tar.gz +tar -xvzf loop-source-v0.2-alpha.tar.gz +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.2-alpha" ./cmd/loop +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.2-alpha" ./cmd/loopd +``` + +The `-mod=vendor` flag tells the `go build` command that it doesn't need to +fetch the dependencies, and instead, they're all enclosed in the local vendor +directory. + +#### Contributors (Alphabetical Order) + +No additional contributors; this tag points to the same commit as +`v0.1.3-alpha`. diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md new file mode 100644 index 00000000..b08ac6f2 --- /dev/null +++ b/docs/release-notes/release-notes-0.20.0.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2022-07-20 +- **Release page:** + [v0.20.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.20.0-beta) +- **Previous release:** [v0.19.1-beta](release-notes-0.19.1.md) +- **Next release:** [v0.20.1-beta](release-notes-0.20.1.md) + +#### New Features + +* P2TR HTLCs and privacy preserving and cheaper MuSig2 loopout sweeps are now + supported as an experimental feature when running `loopd` with + the`--experimental` flag. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Elle Mouton +- Oliver Gugger +- sputn1ck diff --git a/docs/release-notes/release-notes-0.20.1.md b/docs/release-notes/release-notes-0.20.1.md new file mode 100644 index 00000000..14eff49d --- /dev/null +++ b/docs/release-notes/release-notes-0.20.1.md @@ -0,0 +1,22 @@ +# Loop Client Release Notes + +- **Release date:** 2022-08-01 +- **Release page:** + [v0.20.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.20.1-beta) +- **Previous release:** [v0.20.0-beta](release-notes-0.20.0.md) +- **Next release:** [v0.20.2-beta](release-notes-0.20.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +- Improve logging for sweep transactions + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.20.2.md b/docs/release-notes/release-notes-0.20.2.md new file mode 100644 index 00000000..16196c79 --- /dev/null +++ b/docs/release-notes/release-notes-0.20.2.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2022-10-26 +- **Release page:** + [v0.20.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.20.2-beta) +- **Previous release:** [v0.20.1-beta](release-notes-0.20.1.md) +- **Next release:** [v0.21.0-beta](release-notes-0.21.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Bump minimum LND version to v0.15.1-beta to avoid failed swaps due to the + chain backend not properly supporting taproot (fixed in: + https://github.com/lightningnetwork/lnd/pull/6798). + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Evan Kaloudis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md new file mode 100644 index 00000000..e10f5121 --- /dev/null +++ b/docs/release-notes/release-notes-0.21.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2022-12-16 +- **Release page:** + [v0.21.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.21.0-beta) +- **Previous release:** [v0.20.2-beta](release-notes-0.20.2.md) +- **Next release:** [v0.22.0-beta](release-notes-0.22.0.md) + +#### New Features + +* Autoloop now has a new parameter named `destaddr` which if set to a valid + bitcoin address will direct all funds from automatically dispatched loop outs + towards that address. + +#### Breaking Changes + +* Listing loop-in swaps will not display old, nested segwit swap htlc addresses + correctly anymore since support for these htlc types is removed from the code + base. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md new file mode 100644 index 00000000..b72b804a --- /dev/null +++ b/docs/release-notes/release-notes-0.22.0.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2023-03-29 +- **Release page:** + [v0.22.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.22.0-beta) +- **Previous release:** [v0.21.0-beta](release-notes-0.21.0.md) +- **Next release:** [v0.22.1-beta](release-notes-0.22.1.md) + +#### New Features + +* Autoloop now uses a recurring budget. Users can now specify `autobudget` and + `autobudgetrefreshperiod` to specify the amount of the budget and the period + over which it will refresh. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.22.1.md b/docs/release-notes/release-notes-0.22.1.md new file mode 100644 index 00000000..84695064 --- /dev/null +++ b/docs/release-notes/release-notes-0.22.1.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2023-04-24 +- **Release page:** + [v0.22.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.22.1-beta) +- **Previous release:** [v0.22.0-beta](release-notes-0.22.0.md) +- **Next release:** [v0.23.0-beta](release-notes-0.23.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Correct the HTLC script version used when resuming very old swaps. + [PR #571](https://github.com/lightninglabs/loop/pull/571) + +#### Maintenance + +* Update the Go Docker image and remove the local `swapserverrpc` module + replacement. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.23.0.md b/docs/release-notes/release-notes-0.23.0.md new file mode 100644 index 00000000..6759b3f8 --- /dev/null +++ b/docs/release-notes/release-notes-0.23.0.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2023-04-25 +- **Release page:** + [v0.23.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.23.0-beta) +- **Previous release:** [v0.22.1-beta](release-notes-0.22.1.md) +- **Next release:** [v0.24.0-beta](release-notes-0.24.0.md) + +#### New Features + +* Set musig2 to be the default swap protocol. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.24.0.md b/docs/release-notes/release-notes-0.24.0.md new file mode 100644 index 00000000..fbcd426c --- /dev/null +++ b/docs/release-notes/release-notes-0.24.0.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2023-05-24 +- **Release page:** + [v0.24.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.24.0-beta) +- **Previous release:** [v0.23.0-beta](release-notes-0.23.0.md) +- **Next release:** [v0.24.1-beta](release-notes-0.24.1.md) + +#### New Features + +- Easy Autoloop: a new mode for autoloop which requires the user to only set a + single target balance. Autoloop will start dispatching loop outs + whenever the total channel balance of the node exceeds that target. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- George Tsagkarelis +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.24.1.md b/docs/release-notes/release-notes-0.24.1.md new file mode 100644 index 00000000..0ff93d5a --- /dev/null +++ b/docs/release-notes/release-notes-0.24.1.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2023-05-26 +- **Release page:** + [v0.24.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.24.1-beta) +- **Previous release:** [v0.24.0-beta](release-notes-0.24.0.md) +- **Next release:** [v0.25.0-beta](release-notes-0.25.0.md) + +#### New Features + +* Add REST bindings for the `GetInfo` RPC. + [PR #588](https://github.com/lightninglabs/loop/pull/588) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Slyghtning diff --git a/docs/release-notes/release-notes-0.25.0.md b/docs/release-notes/release-notes-0.25.0.md new file mode 100644 index 00000000..65d49424 --- /dev/null +++ b/docs/release-notes/release-notes-0.25.0.md @@ -0,0 +1,42 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-03 +- **Release page:** + [v0.25.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.25.0-beta) +- **Previous release:** [v0.24.1-beta](release-notes-0.24.1.md) +- **Next release:** [v0.25.1-beta](release-notes-0.25.1.md) + +#### New Features + +* Add SQLite and PostgreSQL stores, including migration from an existing bbolt + database. + [PR #585](https://github.com/lightninglabs/loop/pull/585) +* Expose the L402 token identifier through the gRPC interface. + [PR #601](https://github.com/lightninglabs/loop/pull/601) +* Distinguish swaps initiated by Easy Autoloop in their labels. + [PR #591](https://github.com/lightninglabs/loop/pull/591) + +#### Breaking Changes + +#### Bug Fixes + +* Respect the configured fee-PPM limit in Easy Autoloop. + [PR #595](https://github.com/lightninglabs/loop/pull/595) +* Read the Autoloop enabled flag directly from the active liquidity + parameters. + [PR #590](https://github.com/lightninglabs/loop/pull/590) + +#### Maintenance + +* Correct Autoloop documentation and harden its tests. + [PR #593](https://github.com/lightninglabs/loop/pull/593) + [PR #594](https://github.com/lightninglabs/loop/pull/594) + [PR #596](https://github.com/lightninglabs/loop/pull/596) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- George Tsagkarelis +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.25.1.md b/docs/release-notes/release-notes-0.25.1.md new file mode 100644 index 00000000..340ccb56 --- /dev/null +++ b/docs/release-notes/release-notes-0.25.1.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-04 +- **Release page:** + [v0.25.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.25.1-beta) +- **Previous release:** [v0.25.0-beta](release-notes-0.25.0.md) +- **Next release:** [v0.25.2-beta](release-notes-0.25.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Stop publishing Windows 386 release artifacts. + [PR #606](https://github.com/lightninglabs/loop/pull/606) +* Bump the release version to v0.25.1-beta. + [PR #607](https://github.com/lightninglabs/loop/pull/607) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.25.2.md b/docs/release-notes/release-notes-0.25.2.md new file mode 100644 index 00000000..dcffee50 --- /dev/null +++ b/docs/release-notes/release-notes-0.25.2.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-04 +- **Release page:** + [v0.25.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.25.2-beta) +- **Previous release:** [v0.25.1-beta](release-notes-0.25.1.md) +- **Next release:** [v0.26.0-beta](release-notes-0.26.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Remove unsupported release platforms, including Windows 386. + [PR #608](https://github.com/lightninglabs/loop/pull/608) +* Bump the release version to v0.25.2-beta. + [PR #609](https://github.com/lightninglabs/loop/pull/609) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.26.0.md b/docs/release-notes/release-notes-0.26.0.md new file mode 100644 index 00000000..422a90d2 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.0.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-28 +- **Release page:** + [v0.26.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.0-beta) +- **Previous release:** [v0.25.2-beta](release-notes-0.25.2.md) +- **Next release:** [v0.26.1-beta](release-notes-0.26.1.md) + +#### New Features + +This new feature enables loop out and autoloop out operations to sweep htlcs to +addresses generated from an extened public key. A precondition for using the +loop client in this fashion is onboarding a xpub account in the backing lnd +instance, e.g. +`lncli wallet accounts import xpub... my_loop_account --address_type p2tr --master_key_fingerprint 0df50`. +Loop outs can then be instructed to sweep to a new derived address from the +specified account, e.g: +`loop out --amt 10000000 --account my_loop_account --address_type p2tr`. To use +this functionality with autoloop out one has to set the backing lnd account and +address type via liquidity parameters for autoloop, e.g. +`loop --network regtest setparams --autoloop=true --account=my_loop_account --account_addr_type=p2tr...` + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- George Tsagkarelis +- Guillermo Caracuel +- Slyghtning diff --git a/docs/release-notes/release-notes-0.26.1.md b/docs/release-notes/release-notes-0.26.1.md new file mode 100644 index 00000000..5bfdd804 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.1.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2023-08-08 +- **Release page:** + [v0.26.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.1-beta) +- **Previous release:** [v0.26.0-beta](release-notes-0.26.0.md) +- **Next release:** [v0.26.2-beta](release-notes-0.26.2.md) + +#### New Features + +* Record the initiator associated with each swap. + [PR #600](https://github.com/lightninglabs/loop/pull/600) + +#### Breaking Changes + +#### Bug Fixes + +* Repair faulty Loop Out timestamps on startup. + [PR #617](https://github.com/lightninglabs/loop/pull/617) + +#### Maintenance + +* Bump the release version to v0.26.1-beta. + [PR #618](https://github.com/lightninglabs/loop/pull/618) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.2.md b/docs/release-notes/release-notes-0.26.2.md new file mode 100644 index 00000000..23a73169 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.2.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2023-08-09 +- **Release page:** + [v0.26.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.2-beta) +- **Previous release:** [v0.26.1-beta](release-notes-0.26.1.md) +- **Next release:** [v0.26.3-beta](release-notes-0.26.3.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Add a SQL migration for faulty timestamps that may have appeared for users + who were looping out through LiT v0.10.4-alpha. + [PR #619](https://github.com/lightninglabs/loop/pull/619) + +#### Maintenance + +* Bump the release version to v0.26.2-beta. + [PR #620](https://github.com/lightninglabs/loop/pull/620) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.3.md b/docs/release-notes/release-notes-0.26.3.md new file mode 100644 index 00000000..6a1bd693 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.3.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2023-09-18 +- **Release page:** + [v0.26.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.3-beta) +- **Previous release:** [v0.26.2-beta](release-notes-0.26.2.md) +- **Next release:** [v0.26.4-beta](release-notes-0.26.4.md) + +#### New Features + +* Add the reusable finite state machine module. + [PR #631](https://github.com/lightninglabs/loop/pull/631) + +#### Breaking Changes + +#### Bug Fixes + +* Treat an already-settled Loop In invoice as a successful settlement. + [PR #636](https://github.com/lightninglabs/loop/pull/636) +* Correct SQL time parsing and the faulty-year migration, and skip bbolt + migration when the SQLite database already exists. + [PR #627](https://github.com/lightninglabs/loop/pull/627) + [PR #630](https://github.com/lightninglabs/loop/pull/630) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Liongrass +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.4.md b/docs/release-notes/release-notes-0.26.4.md new file mode 100644 index 00000000..cd023afd --- /dev/null +++ b/docs/release-notes/release-notes-0.26.4.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2023-10-04 +- **Release page:** + [v0.26.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.4-beta) +- **Previous release:** [v0.26.3-beta](release-notes-0.26.3.md) +- **Next release:** [v0.26.5-beta](release-notes-0.26.5.md) + +#### New Features + +* Add destination-address support to Easy Autoloop. + [PR #644](https://github.com/lightninglabs/loop/pull/644) +* Add Apple Silicon as a release target and provide a sample `loopd` + configuration. + [PR #643](https://github.com/lightninglabs/loop/pull/643) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Update the compile-time LND dependency to v0.17.0-beta. + [PR #641](https://github.com/lightninglabs/loop/pull/641) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- dstadulis +- Oliver Gugger +- Slyghtning diff --git a/docs/release-notes/release-notes-0.26.5.md b/docs/release-notes/release-notes-0.26.5.md new file mode 100644 index 00000000..07b7aee6 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.5.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2023-10-31 +- **Release page:** + [v0.26.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.5-beta) +- **Previous release:** [v0.26.4-beta](release-notes-0.26.4.md) +- **Next release:** [v0.26.6-beta](release-notes-0.26.6.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Correct leap-year handling in database timestamp parsing. + [PR #657](https://github.com/lightninglabs/loop/pull/657) + +#### Maintenance + +* Refactor database initialization and update security-sensitive dependencies. + [PR #649](https://github.com/lightninglabs/loop/pull/649) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.6.md b/docs/release-notes/release-notes-0.26.6.md new file mode 100644 index 00000000..2045aa46 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.6.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2023-11-28 +- **Release page:** + [v0.26.6-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.6-beta) +- **Previous release:** [v0.26.5-beta](release-notes-0.26.5.md) +- **Next release:** [v0.27.0-beta](release-notes-0.27.0.md) + +#### New Features + +* Add RPC and CLI support for abandoning Loop In swaps. + [PR #661](https://github.com/lightninglabs/loop/pull/661) + +#### Breaking Changes + +#### Bug Fixes + +* Finalize Loop Ins that fail because confirmed wallet funds are insufficient + and expose a specific failure reason. + [PR #665](https://github.com/lightninglabs/loop/pull/665) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Slyghtning diff --git a/docs/release-notes/release-notes-0.27.0.md b/docs/release-notes/release-notes-0.27.0.md new file mode 100644 index 00000000..a31dd32b --- /dev/null +++ b/docs/release-notes/release-notes-0.27.0.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2024-01-30 +- **Release page:** + [v0.27.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.27.0-beta) +- **Previous release:** [v0.26.6-beta](release-notes-0.26.6.md) +- **Next release:** [v0.27.1-beta](release-notes-0.27.1.md) + +#### New Features + +* Sweep Batcher: A new sub-system was added that handles all the loopout sweeps. + Successful loopout HTLCs will no longer be swept back to the wallet via + individual transactions but will instead form a single transaction that holds + multiple inputs and pays to a single output. This will significantly reduce + chain fee costs as it's using less block space by directly consolidating all + the htlcs to a single address. Loopouts that pay to non-wallet addresses will + still use individual transactions as their output cannot be mutated. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- bitcoin-lightning +- elbandi +- George Tsagkarelis +- GoodDaisy +- Mohamed Awnallah +- shuoer86 +- sputn1ck diff --git a/docs/release-notes/release-notes-0.27.1.md b/docs/release-notes/release-notes-0.27.1.md new file mode 100644 index 00000000..825ff31d --- /dev/null +++ b/docs/release-notes/release-notes-0.27.1.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2024-02-14 +- **Release page:** + [v0.27.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.27.1-beta) +- **Previous release:** [v0.27.0-beta](release-notes-0.27.0.md) +- **Next release:** [v0.28.0-beta](release-notes-0.28.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +This release adds automatic sweeping of incorrectly deposited amounts to +external loop in addresses. Previously, a mismatch in the contract amount and +the actually deposited amount required external tools to recover the client +funds. With this release the client automatically sweeps the funds back to the +wallet upon contract expiry. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.0.md b/docs/release-notes/release-notes-0.28.0.md new file mode 100644 index 00000000..c8f093c4 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.0.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2024-03-05 +- **Release page:** + [v0.28.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.0-beta) +- **Previous release:** [v0.27.1-beta](release-notes-0.27.1.md) +- **Next release:** [v0.28.1-beta](release-notes-0.28.1.md) + +#### New Features + +* Add RPC and CLI support for listing Instant Out swaps. + [PR #708](https://github.com/lightninglabs/loop/pull/708) +* Allow Instant Out funds to be sent to a custom destination and improve the + command-line flow. + [PR #709](https://github.com/lightninglabs/loop/pull/709) + [PR #710](https://github.com/lightninglabs/loop/pull/710) + +#### Breaking Changes + +#### Bug Fixes + +* Correct per-sweep on-chain fee accounting and improve displayed server costs. + [PR #694](https://github.com/lightninglabs/loop/pull/694) + +#### Maintenance + +* Increase SQLite durability. + [PR #712](https://github.com/lightninglabs/loop/pull/712) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.1.md b/docs/release-notes/release-notes-0.28.1.md new file mode 100644 index 00000000..f386287d --- /dev/null +++ b/docs/release-notes/release-notes-0.28.1.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2024-04-16 +- **Release page:** + [v0.28.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.1-beta) +- **Previous release:** [v0.28.0-beta](release-notes-0.28.0.md) +- **Next release:** [v0.28.2-beta](release-notes-0.28.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Route the Loop Out prepayment over the selected outgoing channel set and + populate the correct HTLC in swap updates. + [PR #727](https://github.com/lightninglabs/loop/pull/727) + +#### Maintenance + +* Update SQLite, SQLC, PostgreSQL, and Docker dependencies and cache Docker + builds. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.2.md b/docs/release-notes/release-notes-0.28.2.md new file mode 100644 index 00000000..93784f22 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.2.md @@ -0,0 +1,36 @@ +# Loop Client Release Notes + +- **Release date:** 2024-05-25 +- **Release page:** + [v0.28.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.2-beta) +- **Previous release:** [v0.28.1-beta](release-notes-0.28.1.md) +- **Next release:** [v0.28.3-beta](release-notes-0.28.3.md) + +#### New Features + +#### Breaking Changes + +In loopd.conf file `maxlsatcost` and `maxlsatfee` were renamed to `maxl402cost` +and `maxl402fee` accordingly. Old versions of the options are still recognized +for backward compatibility, but a deprecation warning is printed. Users are +encouraged to change the options to new names if they have been changed locally. + +The path in looprpc "/v1/lsat/tokens" was renamed to "/v1/l402/tokens" and the +corresponding method was renamed from `GetLsatTokens` to `GetL402Tokens`. New +`loop` binary won't work with old `loopd`, because `loop listauth` is now +calling `GetL402Tokens` method, which does not exist in previous `loopd` binary. +Old `loop` binary works with new `loopd`, since `loopd` provides a wrapper for +`GetLsatTokens` which calls `GetL402Tokens`; the wrapper logs a warning and it +will be removed in a couple of releases. HTTP endpoint "/v1/l402/tokens" is now +an additional binding for API "/v1/lsat/tokens", so it still works. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.3.md b/docs/release-notes/release-notes-0.28.3.md new file mode 100644 index 00000000..dc713e46 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.3.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2024-06-03 +- **Release page:** + [v0.28.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.3-beta) +- **Previous release:** [v0.28.2-beta](release-notes-0.28.2.md) +- **Next release:** [v0.28.4-beta](release-notes-0.28.4.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Migrate incorrectly stored negative Loop Out costs and account for the + prepayment amount correctly. + [PR #764](https://github.com/lightninglabs/loop/pull/764) +* Restore sweep-batcher swaps from the Loop database and preserve their + confirmation targets. + [PR #761](https://github.com/lightninglabs/loop/pull/761) + +#### Maintenance + +* Update the compile-time LND dependency to v0.18.0-beta. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.4.md b/docs/release-notes/release-notes-0.28.4.md new file mode 100644 index 00000000..5bc6f81d --- /dev/null +++ b/docs/release-notes/release-notes-0.28.4.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2024-06-04 +- **Release page:** + [v0.28.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.4-beta) +- **Previous release:** [v0.28.3-beta](release-notes-0.28.3.md) +- **Next release:** [v0.28.5-beta](release-notes-0.28.5.md) + +#### New Features + +* Make the LND RPC timeout configurable. + [PR #771](https://github.com/lightninglabs/loop/pull/771) + +#### Breaking Changes + +#### Bug Fixes + +* Correct the cost migration for pending swaps. + [PR #771](https://github.com/lightninglabs/loop/pull/771) + +#### Maintenance + +* Factor out a sweep-batcher implementation that does not depend on the Loop + database and update LND to v0.18.0-beta.1. + [PR #766](https://github.com/lightninglabs/loop/pull/766) + [PR #770](https://github.com/lightninglabs/loop/pull/770) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.5.md b/docs/release-notes/release-notes-0.28.5.md new file mode 100644 index 00000000..513b551a --- /dev/null +++ b/docs/release-notes/release-notes-0.28.5.md @@ -0,0 +1,24 @@ +# Loop Client Release Notes + +- **Release date:** 2024-06-06 +- **Release page:** + [v0.28.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.5-beta) +- **Previous release:** [v0.28.4-beta](release-notes-0.28.4.md) +- **Next release:** [v0.28.6-beta](release-notes-0.28.6.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Paginate LND payment lookups while migrating stored Loop Out costs. + [PR #773](https://github.com/lightninglabs/loop/pull/773) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- jinjingroad diff --git a/docs/release-notes/release-notes-0.28.6.md b/docs/release-notes/release-notes-0.28.6.md new file mode 100644 index 00000000..7025a1f6 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.6.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2024-07-11 +- **Release page:** + [v0.28.6-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.6-beta) +- **Previous release:** [v0.28.5-beta](release-notes-0.28.5.md) +- **Next release:** [v0.28.7-beta](release-notes-0.28.7.md) + +#### New Features + +* Add sweep-batcher options for custom MuSig2 signing and disabling fee + bumping. + [PR #783](https://github.com/lightninglabs/loop/pull/783) + [PR #784](https://github.com/lightninglabs/loop/pull/784) +* Track a minimum fee rate for sweeps and log transaction weight. + [PR #785](https://github.com/lightninglabs/loop/pull/785) + +#### Breaking Changes + +#### Bug Fixes + +* Fix sweep-batcher shutdown and database transaction races. + [PR #779](https://github.com/lightninglabs/loop/pull/779) + [PR #780](https://github.com/lightninglabs/loop/pull/780) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- chengehe +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.7.md b/docs/release-notes/release-notes-0.28.7.md new file mode 100644 index 00000000..da77ab61 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.7.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2024-08-08 +- **Release page:** + [v0.28.7-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.7-beta) +- **Previous release:** [v0.28.6-beta](release-notes-0.28.6.md) +- **Next release:** [v0.28.8-beta](release-notes-0.28.8.md) + +#### New Features + +* Return the last hop when fetching stored swaps. + [PR #806](https://github.com/lightninglabs/loop/pull/806) +* Add greedy sweep-batch selection. + [PR #787](https://github.com/lightninglabs/loop/pull/787) + +#### Breaking Changes + +#### Bug Fixes + +* Correct command path handling, sweep fee estimates, and Instant Out SQL + network configuration. + [PR #803](https://github.com/lightninglabs/loop/pull/803) + [PR #793](https://github.com/lightninglabs/loop/pull/793) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- crystalstall +- longxiangqiao +- Slyghtning +- yingshanghuangqiao diff --git a/docs/release-notes/release-notes-0.28.8.md b/docs/release-notes/release-notes-0.28.8.md new file mode 100644 index 00000000..e1007636 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.8.md @@ -0,0 +1,41 @@ +# Loop Client Release Notes + +- **Release date:** 2024-10-21 +- **Release page:** + [v0.28.8-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.8-beta) +- **Previous release:** [v0.28.7-beta](release-notes-0.28.7.md) +- **Next release:** [v0.28.9-beta](release-notes-0.28.9.md) + +#### New Features + +* Add a persistent notification manager and generalize the server notification + stream. + [PR #826](https://github.com/lightninglabs/loop/pull/826) +* Add mixed sweep batches, configurable publication delays, transaction labels, + and a maximum batch size. + [PR #791](https://github.com/lightninglabs/loop/pull/791) + [PR #801](https://github.com/lightninglabs/loop/pull/801) + [PR #809](https://github.com/lightninglabs/loop/pull/809) + [PR #813](https://github.com/lightninglabs/loop/pull/813) + +#### Breaking Changes + +#### Bug Fixes + +* Prevent sweep-batcher fee rates from decreasing and improve publication error + handling. + [PR #815](https://github.com/lightninglabs/loop/pull/815) + [PR #833](https://github.com/lightninglabs/loop/pull/833) + +#### Maintenance + +* Publish `looprpc` as a separately versioned Go module. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- drawdrop +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.9.md b/docs/release-notes/release-notes-0.28.9.md new file mode 100644 index 00000000..e48e7d5c --- /dev/null +++ b/docs/release-notes/release-notes-0.28.9.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2024-10-30 +- **Release page:** + [v0.28.9-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.9-beta) +- **Previous release:** [v0.28.8-beta](release-notes-0.28.8.md) +- **Next release:** [v0.29.0-beta](release-notes-0.29.0.md) + +#### New Features + +* Add the `FetchL402` RPC and `loop fetchl402` command. + [PR #841](https://github.com/lightninglabs/loop/pull/841) +* Propagate event contexts through the FSM, reservation, and Instant Out + packages. + [PR #839](https://github.com/lightninglabs/loop/pull/839) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Update the compile-time LND dependency to v0.18.4-beta. + [PR #828](https://github.com/lightninglabs/loop/pull/828) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Oliver Gugger +- sputn1ck diff --git a/docs/release-notes/release-notes-0.29.0.md b/docs/release-notes/release-notes-0.29.0.md new file mode 100644 index 00000000..e2229fec --- /dev/null +++ b/docs/release-notes/release-notes-0.29.0.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2024-12-18 +- **Release page:** + [v0.29.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.29.0-beta) +- **Previous release:** [v0.28.9-beta](release-notes-0.28.9.md) +- **Next release:** [v0.29.1-beta](release-notes-0.29.1.md) + +#### New Features + +Add support for persisting address loop in mode +https://docs.lightning.engineering/lightning-network-tools/loop/static-loop-in-addresses + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- pinglanlu +- Slyghtning +- sputn1ck +- ziggie diff --git a/docs/release-notes/release-notes-0.29.1.md b/docs/release-notes/release-notes-0.29.1.md new file mode 100644 index 00000000..5129fb44 --- /dev/null +++ b/docs/release-notes/release-notes-0.29.1.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2025-03-18 +- **Release page:** + [v0.29.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.29.1-beta) +- **Previous release:** [v0.29.0-beta](release-notes-0.29.0.md) +- **Next release:** [v0.30.0-beta](release-notes-0.30.0.md) + +#### New Features + +* Add Taproot Asset Loop Outs, including asset-aware quotes, accounting, and + command-line options. + [PR #872](https://github.com/lightninglabs/loop/pull/872) + [PR #876](https://github.com/lightninglabs/loop/pull/876) +* Allow a specific amount to be withdrawn from static address deposits. + [PR #860](https://github.com/lightninglabs/loop/pull/860) + +#### Breaking Changes + +#### Bug Fixes + +* Fix Loop Out fee estimation when the confirmation target is one. + [PR #899](https://github.com/lightninglabs/loop/pull/899) +* Add notification backoff and pending-L402 handling to avoid busy looping. + [PR #879](https://github.com/lightninglabs/loop/pull/879) + +#### Maintenance + +* Update the compile-time LND dependency to v0.19.0. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Oliver Gugger +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.3.0.md b/docs/release-notes/release-notes-0.3.0.md new file mode 100644 index 00000000..cda614a1 --- /dev/null +++ b/docs/release-notes/release-notes-0.3.0.md @@ -0,0 +1,39 @@ +# Loop Client Release Notes + +- **Release date:** 2019-11-21 +- **Release page:** + [v0.3.0-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.3.0-alpha) +- **Previous release:** [v0.2.4-alpha](release-notes-0.2.4.md) +- **Next release:** [v0.3.1-alpha](release-notes-0.3.1.md) + +#### New Features + +This minor version release includes: + +- Change the default behavior of Loop Out swaps to use the new Loop Out delay +- New `--fast` flag to execute Loop Out swaps immediately +- Add delay configuration to the Loop Out client API + +The new delay option is related to an on-going effort to minimize the on-chain +footprint of Lightning Loop, which will deliver increased privacy and lower +chain fees. + +If multiple execution delayed Loop Outs are present at the time of Loop Out +on-chain funding, the Loop service will now batch those funding outputs together +into a single transaction, reducing the number of change outputs and required +inputs used for each swap. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Corey Phillips +- Johan T. Halseth +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.3.1.md b/docs/release-notes/release-notes-0.3.1.md new file mode 100644 index 00000000..275f89c3 --- /dev/null +++ b/docs/release-notes/release-notes-0.3.1.md @@ -0,0 +1,37 @@ +# Loop Client Release Notes + +- **Release date:** 2020-01-15 +- **Release page:** + [v0.3.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.3.1-alpha) +- **Previous release:** [v0.3.0-alpha](release-notes-0.3.0.md) +- **Next release:** [v0.4.0-rc1.beta](release-notes-0.4.0-rc1.md) + +#### New Features + +In this patch version release: + +- `loop out` now supports an optional `--max_swap_routing_fee` flag to specify a + max routing fee +- `loop quote` now supports an optional `--fast` flag to specify a quote for an + immediate swap + +This version is recommended for all users to improve the fidelity of fee +estimation. In a previous update the swap quote may have returned a higher +estimated fee than would have been ultimately paid. + +Max swap routing fee is also recommended for users who want to adjust the fee +ceiling of their Loop Out Lightning Network fees. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- FreeThinker +- Johan T. Halseth +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.30.0.md b/docs/release-notes/release-notes-0.30.0.md new file mode 100644 index 00000000..446b9627 --- /dev/null +++ b/docs/release-notes/release-notes-0.30.0.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2025-03-27 +- **Release page:** + [v0.30.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.30.0-beta) +- **Previous release:** [v0.29.1-beta](release-notes-0.29.1.md) +- **Next release:** [v0.31.0-beta](release-notes-0.31.0.md) + +#### New Features + +* Add Taproot Asset support to Easy Autoloop Loop Outs. + [PR #886](https://github.com/lightninglabs/loop/pull/886) + +#### Breaking Changes + +#### Bug Fixes + +* Make static address deposit recovery non-concurrent and correct deposit FSM + shutdown and locking behavior. + [PR #908](https://github.com/lightninglabs/loop/pull/908) +* Fix data races across Loop Out payment, reservations, static addresses, + logging, and sweep batching. + [PR #890](https://github.com/lightninglabs/loop/pull/890) + [PR #902](https://github.com/lightninglabs/loop/pull/902) + +#### Maintenance + +* Run unit tests with the Go race detector. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.31.0.md b/docs/release-notes/release-notes-0.31.0.md new file mode 100644 index 00000000..ff847787 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2025-04-25 +- **Release page:** + [v0.31.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.0-beta) +- **Previous release:** [v0.30.0-beta](release-notes-0.30.0.md) +- **Next release:** [v0.31.1-beta](release-notes-0.31.1.md) + +#### New Features + +* [Enhance](https://github.com/lightninglabs/loop/pull/912) the `loop listswaps` + command by improving the ability to filter the response. Use + `--start_timestamp_ns` to return only swaps after that timestamp. Use + `--max_swaps` to limit total swap outputs. Paging is enabled using the + `next_start_time` field in the response. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Guillermo Caracuel +- Sam Korn +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.1.md b/docs/release-notes/release-notes-0.31.1.md new file mode 100644 index 00000000..b1a9313f --- /dev/null +++ b/docs/release-notes/release-notes-0.31.1.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2025-05-05 +- **Release page:** + [v0.31.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.1-beta) +- **Previous release:** [v0.31.0-beta](release-notes-0.31.0.md) +- **Next release:** [v0.31.2-beta](release-notes-0.31.2.md) + +#### New Features + +#### Breaking Changes + +* Autoloop now defaults to a slow publication deadline (30 minutes after + initiation) to reduce fees. To revert to the previous behavior, users can run + `loop setparams --fast`, which causes swap HTLCs to be published immediately + after initiation. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- ffranr +- Oliver Gugger +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.31.2.md b/docs/release-notes/release-notes-0.31.2.md new file mode 100644 index 00000000..bb0e37b4 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.2.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2025-06-18 +- **Release page:** + [v0.31.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.2-beta) +- **Previous release:** [v0.31.1-beta](release-notes-0.31.1.md) +- **Next release:** [v0.31.3-beta](release-notes-0.31.3.md) + +#### New Features + +#### Breaking Changes + +* The content of the `commit_hash` field of the `GetInfo` response has been + updated so that it contains the Git commit hash the Loop binary build was + based on. If the build had uncommited changes, this field will contain the + most recent commit hash, suffixed by "-dirty". +* The `Commit` part of the `--version` command output has been updated to + contain the most recent git commit tag. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Oliver Gugger +- Slyghtning +- Viktor Tigerström diff --git a/docs/release-notes/release-notes-0.31.3.md b/docs/release-notes/release-notes-0.31.3.md new file mode 100644 index 00000000..4b54d0f1 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.3.md @@ -0,0 +1,61 @@ +# Loop Client Release Notes + +- **Release date:** 2025-09-25 +- **Release page:** + [v0.31.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.3-beta) +- **Previous release:** [v0.31.2-beta](release-notes-0.31.2.md) +- **Next release:** [v0.31.4-beta](release-notes-0.31.4.md) + +#### New Features + +* Allow static address Loop Ins to swap an amount smaller than the selected + deposits and expose arbitrary static swap amounts through the server RPC. + [PR #887](https://github.com/lightninglabs/loop/pull/887) + [PR #951](https://github.com/lightninglabs/loop/pull/951) +* Add deposit indices, outpoint-based deposit lookups, and CLI output for + deposits and their swap hashes. + [PR #959](https://github.com/lightninglabs/loop/pull/959) + [PR #990](https://github.com/lightninglabs/loop/pull/990) + [PR #991](https://github.com/lightninglabs/loop/pull/991) +* Add sweep-batcher change outputs. + [PR #976](https://github.com/lightninglabs/loop/pull/976) + +#### Breaking Changes + +#### Bug Fixes + +* Harden static address deposit recovery, withdrawal handling, block polling, + expiry tracking, and deposit availability filtering. + [PR #954](https://github.com/lightninglabs/loop/pull/954) + [PR #956](https://github.com/lightninglabs/loop/pull/956) + [PR #995](https://github.com/lightninglabs/loop/pull/995) + [PR #996](https://github.com/lightninglabs/loop/pull/996) + [PR #1003](https://github.com/lightninglabs/loop/pull/1003) + [PR #1004](https://github.com/lightninglabs/loop/pull/1004) +* Avoid creating a Loop In timeout transaction after its invoice has already + been paid. + [PR #998](https://github.com/lightninglabs/loop/pull/998) +* Fix sweep-batcher reorg detection and presigned-sweep handling. + [PR #952](https://github.com/lightninglabs/loop/pull/952) + [PR #975](https://github.com/lightninglabs/loop/pull/975) +* Avoid a startup panic when the Taproot Asset client is unavailable. + [PR #970](https://github.com/lightninglabs/loop/pull/970) + +#### Maintenance + +* Update LND through v0.19.3, Taproot Assets through v0.6.1, Go, and lint + tooling. + [PR #985](https://github.com/lightninglabs/loop/pull/985) + [PR #989](https://github.com/lightninglabs/loop/pull/989) + [PR #993](https://github.com/lightninglabs/loop/pull/993) + [PR #1001](https://github.com/lightninglabs/loop/pull/1001) +* Build static release binaries and fix release-version matching. + [PR #965](https://github.com/lightninglabs/loop/pull/965) + [PR #978](https://github.com/lightninglabs/loop/pull/978) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- Boris Nagaev +- George Tsagkarelis +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.4.md b/docs/release-notes/release-notes-0.31.4.md new file mode 100644 index 00000000..142786b1 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.4.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2025-10-16 +- **Release page:** + [v0.31.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.4-beta) +- **Previous release:** [v0.31.3-beta](release-notes-0.31.3.md) +- **Next release:** [v0.31.5-beta](release-notes-0.31.5.md) + +#### New Features + +* Add fast publication support to traditional and static Loop Ins. + [PR #1009](https://github.com/lightninglabs/loop/pull/1009) +* Add reproducible host and Docker release builds. + [PR #1007](https://github.com/lightninglabs/loop/pull/1007) + +#### Breaking Changes + +* Migrate the command-line client to `urfave/cli/v3`. + [PR #1013](https://github.com/lightninglabs/loop/pull/1013) + +#### Bug Fixes + +* Correct sweep-batcher change-output fee accounting and publish fee-rate + calculation. + [PR #1020](https://github.com/lightninglabs/loop/pull/1020) +* Harden static deposit recovery, withdrawal, and swap-state handling. + +#### Maintenance + +* Presign sweep transactions in parallel. + [PR #1022](https://github.com/lightninglabs/loop/pull/1022) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- George Tsagkarelis +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.5-lnd0.20.md b/docs/release-notes/release-notes-0.31.5-lnd0.20.md new file mode 100644 index 00000000..b41906a7 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.5-lnd0.20.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2025-10-31 +- **Release page:** + [v0.31.5-beta-lnd0.20](https://github.com/lightninglabs/loop/releases/tag/v0.31.5-beta-lnd0.20) +- **Previous release:** [v0.31.5-beta](release-notes-0.31.5.md) +- **Next release:** [v0.31.6-beta](release-notes-0.31.6.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Restore the LND v0.20 release dependency set, including the corresponding + `lndclient` and Taproot Assets versions. +* Restore CLI documentation generation and related release tooling that had + been removed by the dependency downgrade. +* Correct the `looprpc` and `swapserverrpc` module versions used by this + compatibility release. + +#### Contributors (Alphabetical Order) + +- sputn1ck diff --git a/docs/release-notes/release-notes-0.31.5.md b/docs/release-notes/release-notes-0.31.5.md new file mode 100644 index 00000000..cea88b6f --- /dev/null +++ b/docs/release-notes/release-notes-0.31.5.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2025-10-31 +- **Release page:** + [v0.31.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.5-beta) +- **Previous release:** [v0.31.4-beta](release-notes-0.31.4.md) +- **Next release:** + [v0.31.5-beta-lnd0.20](release-notes-0.31.5-lnd0.20.md) + +#### New Features + +* Add notification versioning and a manager for resuming swaps. + [PR #1029](https://github.com/lightninglabs/loop/pull/1029) +* Add Instant Out and static address RPCs to `client.yaml`. + [PR #1026](https://github.com/lightninglabs/loop/pull/1026) + +#### Breaking Changes + +* Standardize static address amount flags on `--amt`. + [PR #1023](https://github.com/lightninglabs/loop/pull/1023) + +#### Bug Fixes + +* Avoid an Instant Out startup crash when experimental features are disabled. + [PR #1030](https://github.com/lightninglabs/loop/pull/1030) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev diff --git a/docs/release-notes/release-notes-0.31.6.md b/docs/release-notes/release-notes-0.31.6.md new file mode 100644 index 00000000..1324a083 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.6.md @@ -0,0 +1,42 @@ +# Loop Client Release Notes + +- **Release date:** 2025-11-26 +- **Release page:** + [v0.31.6-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.6-beta) +- **Previous release:** + [v0.31.5-beta-lnd0.20](release-notes-0.31.5-lnd0.20.md) +- **Next release:** [v0.31.7-beta](release-notes-0.31.7.md) + +#### New Features + +* Add Easy Autoloop peer exclusion and an option to include all peers. + [PR #1016](https://github.com/lightninglabs/loop/pull/1016) +* Add the `loop stop` command. + [PR #1048](https://github.com/lightninglabs/loop/pull/1048) + +#### Breaking Changes + +#### Bug Fixes + +* Wait for the chain notifier and a positive block height during startup. + [PR #1046](https://github.com/lightninglabs/loop/pull/1046) +* Correct a sweep-batcher race while re-adding sweeps during shutdown. + [PR #1042](https://github.com/lightninglabs/loop/pull/1042) + +#### Maintenance + +* Update LND to v0.20.0-beta and Taproot Assets to v0.7.0. + [PR #1045](https://github.com/lightninglabs/loop/pull/1045) +* Update `lndclient`, Go, and security-sensitive dependencies. + [PR #1034](https://github.com/lightninglabs/loop/pull/1034) + [PR #1041](https://github.com/lightninglabs/loop/pull/1041) + [PR #1047](https://github.com/lightninglabs/loop/pull/1047) + [PR #1049](https://github.com/lightninglabs/loop/pull/1049) +* Add Lightning Terminal integration tests to CI. + [PR #1050](https://github.com/lightninglabs/loop/pull/1050) + +#### Contributors (Alphabetical Order) + +- Boris Nagaev +- ffranr +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.7.md b/docs/release-notes/release-notes-0.31.7.md new file mode 100644 index 00000000..57064af3 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.7.md @@ -0,0 +1,22 @@ +# Loop Client Release Notes + +- **Release date:** 2025-11-26 +- **Release page:** + [v0.31.7-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.7-beta) +- **Previous release:** [v0.31.6-beta](release-notes-0.31.6.md) +- **Next release:** [v0.31.8-beta](release-notes-0.31.8.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Bump the release version to v0.31.7-beta. + [PR #1051](https://github.com/lightninglabs/loop/pull/1051) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth diff --git a/docs/release-notes/release-notes-0.31.8.md b/docs/release-notes/release-notes-0.31.8.md new file mode 100644 index 00000000..a4b432c6 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.8.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2026-02-03 +- **Release page:** + [v0.31.8-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.8-beta) +- **Previous release:** [v0.31.7-beta](release-notes-0.31.7.md) +- **Next release:** [v0.32.0-beta](release-notes-0.32.0.md) + +#### New Features + +* Add the `SweepHtlc` RPC and `loop out sweephtlc` command for manually + sweeping a Loop Out HTLC. + [PR #1066](https://github.com/lightninglabs/loop/pull/1066) +* Add PSBT-based static address withdrawals. + [PR #1043](https://github.com/lightninglabs/loop/pull/1043) + +#### Breaking Changes + +* Deprecate the previous server-driven static withdrawal RPC in favor of the + PSBT-based endpoint. + [PR #1043](https://github.com/lightninglabs/loop/pull/1043) + +#### Bug Fixes + +* Re-register static address HTLC notifications after confirmation errors. +* Persist confirmed sweep batches atomically. + [PR #1044](https://github.com/lightninglabs/loop/pull/1044) + +#### Maintenance + +* Replace deprecated gRPC dial helpers and remove unused client code. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning +- Viktor Torstensson diff --git a/docs/release-notes/release-notes-0.32.0.md b/docs/release-notes/release-notes-0.32.0.md new file mode 100644 index 00000000..80f5cf78 --- /dev/null +++ b/docs/release-notes/release-notes-0.32.0.md @@ -0,0 +1,42 @@ +# Loop Client Release Notes + +- **Release date:** 2026-03-02 +- **Release page:** + [v0.32.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.32.0-beta) +- **Previous release:** [v0.31.8-beta](release-notes-0.31.8.md) +- **Next release:** [v0.32.1-beta](release-notes-0.32.1.md) + +#### New Features + +Static Address Open Channel — This release introduces the ability to open +Lightning channels directly from static address deposits. Instead of performing +a loop-in to get funds into your Lightning node and then opening a channel as +separate steps, you can now combine both into a single operation using the new +loop openchannel command. + + - Consolidate deposits into channels: If you have one or more static address + deposits sitting on-chain, you can open a channel to any peer using those + funds directly, skipping the loop-in swap and its associated fees. + - Flexible funding: Use the --utxo flag to select specific deposit outpoints, + or use --fundmax to sweep all selected UTXOs into a single channel. + - Full channel configuration: The command supports the same options as lncli + openchannel — channel type (tweakless, anchors, taproot), private channels, + push amounts, fee rates, zero-conf, and more. + +Examples: + +```shell +loop openchannel --node_key --local_amt 1000000 +loop openchannel --node_key --utxo txid:0 --fundmax +``` + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Slyghtning diff --git a/docs/release-notes/release-notes-0.32.1.md b/docs/release-notes/release-notes-0.32.1.md new file mode 100644 index 00000000..4fcaee65 --- /dev/null +++ b/docs/release-notes/release-notes-0.32.1.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2026-03-02 +- **Release page:** + [v0.32.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.32.1-beta) +- **Previous release:** [v0.32.0-beta](release-notes-0.32.0.md) +- **Next release:** [v0.33.0-beta](release-notes-0.33.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Update release builds to Go 1.26.0. + [PR #1083](https://github.com/lightninglabs/loop/pull/1083) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev diff --git a/docs/release-notes/release-notes-0.33.0.md b/docs/release-notes/release-notes-0.33.0.md new file mode 100644 index 00000000..08ce105a --- /dev/null +++ b/docs/release-notes/release-notes-0.33.0.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2026-04-08 +- **Release page:** + [v0.33.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.0-beta) +- **Previous release:** [v0.32.1-beta](release-notes-0.32.1.md) +- **Next release:** [v0.33.1-beta](release-notes-0.33.1.md) + +#### New Features + +* Add maximum swap-fee controls to static address Loop Ins. + [PR #1114](https://github.com/lightninglabs/loop/pull/1114) + +#### Breaking Changes + +#### Bug Fixes + +* Harden static address startup, withdrawal notification registration, Loop In + recovery, HTLC timeout handling, and sweep output validation. + [PR #1078](https://github.com/lightninglabs/loop/pull/1078) +* Ensure cleanup operations use live contexts and fix a Loop Out test goroutine + leak. + +#### Maintenance + +* Regenerate the command-line reference and update build dependencies. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.33.1.md b/docs/release-notes/release-notes-0.33.1.md new file mode 100644 index 00000000..596236cc --- /dev/null +++ b/docs/release-notes/release-notes-0.33.1.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2026-05-26 +- **Release page:** + [v0.33.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.1-beta) +- **Previous release:** [v0.33.0-beta](release-notes-0.33.0.md) +- **Next release:** [v0.33.2-beta](release-notes-0.33.2.md) + +#### New Features + +* Add Static Address Loop Ins to Autoloop planning and execution. + [PR #1119](https://github.com/lightninglabs/loop/pull/1119) + +#### Breaking Changes + +#### Bug Fixes + +* Validate static address server parameters and prevent panics when displaying + malformed or missing Instant Out reservation identifiers. + [PR #1137](https://github.com/lightninglabs/loop/pull/1137) +* Harden sweep-batcher shutdown and route-hint preservation. + +#### Maintenance + +* Add record-and-replay integration tests for CLI sessions. + [PR #1072](https://github.com/lightninglabs/loop/pull/1072) +* Add the project security policy. + +#### Contributors (Alphabetical Order) + +- 0xfandom +- Alex Bosworth +- Boris Nagaev +- Olexandr88 +- Slyghtning diff --git a/docs/release-notes/release-notes-0.33.2.md b/docs/release-notes/release-notes-0.33.2.md new file mode 100644 index 00000000..84e8ff44 --- /dev/null +++ b/docs/release-notes/release-notes-0.33.2.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2026-06-08 +- **Release page:** + [v0.33.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.2-beta) +- **Previous release:** [v0.33.1-beta](release-notes-0.33.1.md) +- **Next release:** [v0.33.3-beta](release-notes-0.33.3.md) + +#### New Features + +* Expose static address swap timing and cost details through the RPC and CLI. + [PR #1150](https://github.com/lightninglabs/loop/pull/1150) +* Handle server notifications that a static address HTLC has confirmed. + [PR #1120](https://github.com/lightninglabs/loop/pull/1120) + +#### Breaking Changes + +#### Bug Fixes + +* Reject malformed server keys and MuSig2 signing data. + [PR #1148](https://github.com/lightninglabs/loop/pull/1148) +* Recover confirmed HTLCs through the direct sweep path and reuse their stored + destinations. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.33.3.md b/docs/release-notes/release-notes-0.33.3.md new file mode 100644 index 00000000..53abbffa --- /dev/null +++ b/docs/release-notes/release-notes-0.33.3.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2026-06-21 +- **Release page:** + [v0.33.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.3-beta) +- **Previous release:** [v0.33.2-beta](release-notes-0.33.2.md) +- **Next release:** [v0.34.0-beta](release-notes-0.34.0.md) + +#### New Features + +#### Breaking Changes + +* Raise the minimum supported LND version to v0.18.4-beta. + +#### Bug Fixes + +#### Maintenance + +* Update the compile-time LND dependency to v0.21. + [PR #1153](https://github.com/lightninglabs/loop/pull/1153) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev diff --git a/docs/release-notes/release-notes-0.34.0.md b/docs/release-notes/release-notes-0.34.0.md index 293c46e6..19fa587f 100644 --- a/docs/release-notes/release-notes-0.34.0.md +++ b/docs/release-notes/release-notes-0.34.0.md @@ -1,13 +1,19 @@ # Loop Client Release Notes +- **Release date:** 2026-07-23 +- **Release page:** + [v0.34.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.34.0-beta) +- **Previous release:** [v0.33.3-beta](release-notes-0.33.3.md) +- **Next release:** [Next release](release-notes-next.md) + #### New Features * Static address deposits are now tracked and shown as soon as they appear in - the wallet, including while they are still in the mempool. Static loop-ins - can select low-confirmation deposits, with a CLI warning that payment may - wait for more confirmations under the server's confirmation-risk policy. - Withdrawals and channel opens continue to require confirmed deposits. The - selected risk decision and payment deadline are persisted across restarts. + the wallet, including while they are still in the mempool. Static loop-ins can + select low-confirmation deposits, with a CLI warning that payment may wait for + more confirmations under the server's confirmation-risk policy. Withdrawals + and channel opens continue to require confirmed deposits. The selected risk + decision and payment deadline are persisted across restarts. [PR #1141](https://github.com/lightninglabs/loop/pull/1141) * `loop openchannel` now supports LND's production `TAPROOT` commitment type while retaining explicit support for the legacy `SIMPLE_TAPROOT` type. @@ -67,3 +73,10 @@ * FSM diagram generation is now deterministic. Static address deposit and loop-in diagrams are generated and checked in CI to prevent stale diagrams. [PR #1179](https://github.com/lightninglabs/loop/pull/1179) + +#### Contributors (Alphabetical Order) + +- Boris Nagaev +- Chanda Chewe +- Gustavo Stingelin +- Slyghtning diff --git a/docs/release-notes/release-notes-0.4.0-rc1.md b/docs/release-notes/release-notes-0.4.0-rc1.md new file mode 100644 index 00000000..5230fd92 --- /dev/null +++ b/docs/release-notes/release-notes-0.4.0-rc1.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2020-01-24 +- **Release page:** + [v0.4.0-rc1.beta](https://github.com/lightninglabs/loop/releases/tag/v0.4.0-rc1.beta) +- **Previous release:** [v0.3.1-alpha](release-notes-0.3.1.md) +- **Next release:** [v0.4.0-beta](release-notes-0.4.0.md) + +#### New Features + +#### Breaking Changes + +This is a release candidate for the minor version update to v0.4.0 + +In this minor version release candidate: + +- Default ports are switched to authenticated port +- Requests now require payment for an auth token + +#### Bug Fixes + +- Loop In timeouts are fixed for overfunded cases + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.4.0.md b/docs/release-notes/release-notes-0.4.0.md new file mode 100644 index 00000000..261fdb0c --- /dev/null +++ b/docs/release-notes/release-notes-0.4.0.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2020-02-04 +- **Release page:** + [v0.4.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.4.0-beta) +- **Previous release:** [v0.4.0-rc1.beta](release-notes-0.4.0-rc1.md) +- **Next release:** [v0.4.1-beta](release-notes-0.4.1.md) + +#### New Features + +- REST endpoints are now available for monitoring the client daemon + +Upgrading is recommended for all users + +#### Breaking Changes + +In this update: + +- Default ports are switched to the authenticated port +- Swap requests will now require payment for an auth token + +#### Bug Fixes + +- Loop In refund timeouts are now fixed for overfunded cases + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.4.1.md b/docs/release-notes/release-notes-0.4.1.md new file mode 100644 index 00000000..1621f215 --- /dev/null +++ b/docs/release-notes/release-notes-0.4.1.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2020-02-11 +- **Release page:** + [v0.4.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.4.1-beta) +- **Previous release:** [v0.4.0-beta](release-notes-0.4.0.md) +- **Next release:** [v0.5.0-beta](release-notes-0.5.0.md) + +#### New Features + +In this patch release: + +- REST clients can now + [adjust their CORS origin headers](https://github.com/lightninglabs/loop/pull/148) + +#### Breaking Changes + +#### Bug Fixes + +- [An error was fixed](https://github.com/lightninglabs/loop/pull/149) when + getting price quote with delayed conf target but fast execution + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Olaoluwa Osuntokun +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.5.0.md b/docs/release-notes/release-notes-0.5.0.md new file mode 100644 index 00000000..896228e9 --- /dev/null +++ b/docs/release-notes/release-notes-0.5.0.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2020-03-05 +- **Release page:** + [v0.5.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.5.0-beta) +- **Previous release:** [v0.4.1-beta](release-notes-0.4.1.md) +- **Next release:** [v0.5.1-beta](release-notes-0.5.1.md) + +#### New Features + +In this minor release: + +- Add support for specifying the last hop of a Loop In + +#### Breaking Changes + +- Non-working REST streaming status endpoint is removed + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.5.1.md b/docs/release-notes/release-notes-0.5.1.md new file mode 100644 index 00000000..aefee8d2 --- /dev/null +++ b/docs/release-notes/release-notes-0.5.1.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2020-03-15 +- **Release page:** + [v0.5.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.5.1-beta) +- **Previous release:** [v0.5.0-beta](release-notes-0.5.0.md) +- **Next release:** [v0.6.0-beta](release-notes-0.6.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +In this patch release: + +- Allow calling for a Loop In quote when there are insufficient funds in the + wallet +- Increase the level of the sanity check on miner fee maximum to account for + high fee variance + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.6.0.md b/docs/release-notes/release-notes-0.6.0.md new file mode 100644 index 00000000..d00ebc9e --- /dev/null +++ b/docs/release-notes/release-notes-0.6.0.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2020-04-30 +- **Release page:** + [v0.6.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.0-beta) +- **Previous release:** [v0.5.1-beta](release-notes-0.5.1.md) +- **Next release:** [v0.6.1-beta](release-notes-0.6.1.md) + +#### New Features + +- Loop In configuration target can now be specified to reduce on-chain fee costs +- Loop Out now uses LND 0.10.0 upgraded payment APIs to allow for improved + payment success + +#### Breaking Changes + +This release is designed to support LND 0.10.0 - please make sure you have +updated your node + +In this minor release: + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.6.1.md b/docs/release-notes/release-notes-0.6.1.md new file mode 100644 index 00000000..f7716b8b --- /dev/null +++ b/docs/release-notes/release-notes-0.6.1.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2020-05-12 +- **Release page:** + [v0.6.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.1-beta) +- **Previous release:** [v0.6.0-beta](release-notes-0.6.0.md) +- **Next release:** [v0.6.2-beta](release-notes-0.6.2.md) + +#### New Features + +* Add native SegWit P2WSH addresses for Loop In HTLCs while retaining support + for externally funded nested SegWit addresses. + [PR #184](https://github.com/lightninglabs/loop/pull/184) + +#### Breaking Changes + +#### Bug Fixes + +* Return the correct HTLC address in the Loop In swap response. + [PR #198](https://github.com/lightninglabs/loop/pull/198) +* Enforce the configured maximum number of payment parts for multi-channel + Loop Outs. + [PR #196](https://github.com/lightninglabs/loop/pull/196) + +#### Maintenance + +* Configure CI to clone the full repository history. + [PR #192](https://github.com/lightninglabs/loop/pull/192) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager diff --git a/docs/release-notes/release-notes-0.6.2.md b/docs/release-notes/release-notes-0.6.2.md new file mode 100644 index 00000000..12c9e390 --- /dev/null +++ b/docs/release-notes/release-notes-0.6.2.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2020-05-12 +- **Release page:** + [v0.6.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.2-beta) +- **Previous release:** [v0.6.1-beta](release-notes-0.6.1.md) +- **Next release:** [v0.6.3-beta](release-notes-0.6.3.md) + +#### New Features + +* Switch Loop In to use native SegWit P2WSH HTLC addresses. Externally funded + Loop Ins can use either nested SegWit or native SegWit addresses. + [PR #184](https://github.com/lightninglabs/loop/pull/184) + +#### Breaking Changes + +#### Bug Fixes + +* Fix the maximum number of payment parts for Multi-Loop Out so that the + configured limit takes effect. + [PR #196](https://github.com/lightninglabs/loop/pull/196) + +#### Maintenance + +* Apply follow-up version updates for v0.6.1-beta and v0.6.2-beta. + [PR #199](https://github.com/lightninglabs/loop/pull/199) + [PR #200](https://github.com/lightninglabs/loop/pull/200) + +The user-facing changes highlighted in the published v0.6.2-beta release were +already present in the v0.6.1-beta tag history. The tag-to-tag source delta for +v0.6.2-beta contains the version metadata updates above. + +#### Contributors (Alphabetical Order) + +- Joost Jager diff --git a/docs/release-notes/release-notes-0.6.3.md b/docs/release-notes/release-notes-0.6.3.md new file mode 100644 index 00000000..a89ba49b --- /dev/null +++ b/docs/release-notes/release-notes-0.6.3.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2020-06-04 +- **Release page:** + [v0.6.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.3-beta) +- **Previous release:** [v0.6.2-beta](release-notes-0.6.2.md) +- **Next release:** [v0.6.4-beta](release-notes-0.6.4.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +In this patch release: + +- Loop Out using the LND v0.10.1 release as the backing LND can now select + multiple channels to Loop Out + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager +- Olaoluwa Osuntokun +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.6.4.md b/docs/release-notes/release-notes-0.6.4.md new file mode 100644 index 00000000..a020023c --- /dev/null +++ b/docs/release-notes/release-notes-0.6.4.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2020-06-12 +- **Release page:** + [v0.6.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.4-beta) +- **Previous release:** [v0.6.3-beta](release-notes-0.6.3.md) +- **Next release:** [v0.6.5-beta](release-notes-0.6.5.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +In this patch release: + +- A loopd error will now terminate with exit code 1 instead of exit code 0 to + assist auto-restarting +- Loop Out will now increase inbound liquidity more quickly in situations where + low chain fees were specified + +If you have time to wait for the funds to return to you on-chain, consider +increasing `--conf_target` from the default of 6 when running the `loop out` +command to achieve fee savings at the cost of slower swap finality. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.6.5.md b/docs/release-notes/release-notes-0.6.5.md new file mode 100644 index 00000000..2be491dc --- /dev/null +++ b/docs/release-notes/release-notes-0.6.5.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2020-07-02 +- **Release page:** + [v0.6.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.5-beta) +- **Previous release:** [v0.6.4-beta](release-notes-0.6.4.md) +- **Next release:** [v0.7.0-beta](release-notes-0.7.0.md) + +#### New Features + +In this patch release: + +* Added support for a human-readable message from the server. The message is + received after initiation of a swap and printed to the log. It is also exposed + as a field on the client RPC interface. + +#### Breaking Changes + +#### Bug Fixes + +* Several bugs fixes and security improvements. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Carla Kirk-Cohen +- Joost Jager +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.7.0.md b/docs/release-notes/release-notes-0.7.0.md new file mode 100644 index 00000000..c1c907ae --- /dev/null +++ b/docs/release-notes/release-notes-0.7.0.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2020-07-21 +- **Release page:** + [v0.7.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.7.0-beta) +- **Previous release:** [v0.6.5-beta](release-notes-0.6.5.md) +- **Next release:** [v0.8.0-beta](release-notes-0.8.0.md) + +#### New Features + +In this minor release: + +- Loop Out confirmation targets can now be much longer, resulting in potentially + much cheaper Loop Out on-chain costs +- The Loop server has been upgraded to supply a state subscription service to + supply indications of server-side swap status + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Gabriel Comte +- Joost Jager diff --git a/docs/release-notes/release-notes-0.8.0.md b/docs/release-notes/release-notes-0.8.0.md new file mode 100644 index 00000000..53ab1bbe --- /dev/null +++ b/docs/release-notes/release-notes-0.8.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2020-08-11 +- **Release page:** + [v0.8.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.8.0-beta) +- **Previous release:** [v0.7.0-beta](release-notes-0.7.0.md) +- **Next release:** [v0.8.1-beta](release-notes-0.8.1.md) + +#### New Features + +In this minor release: + +- The new LND 0.11.0 chain transaction labeling feature is used to record in the + LND db which txs are Loop-related +- You can now specify the number of confirmations to wait before considering the + swap service's on-chain send to you to be confirmed + +#### Breaking Changes + +#### Bug Fixes + +- More details into swap failure causes are surfaced to help better understand + how to fix issues encountered with swap execution + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen diff --git a/docs/release-notes/release-notes-0.8.1.md b/docs/release-notes/release-notes-0.8.1.md new file mode 100644 index 00000000..e9984db7 --- /dev/null +++ b/docs/release-notes/release-notes-0.8.1.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2020-08-28 +- **Release page:** + [v0.8.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.8.1-beta) +- **Previous release:** [v0.8.0-beta](release-notes-0.8.0.md) +- **Next release:** [v0.9.0-beta](release-notes-0.9.0.md) + +#### New Features + +In this patch release: + +- The Loop Out pathfinding timeout is increased to help discover lower fee + routes for larger amounts +- The Loop Out default sweep confirmation target is extended to help lower + on-chain fees for Loop Outs +- Support is added to choose your own directory for Loop's database and log + files + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +- Preparatory support is included for an upcoming update to the on-chain script + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Kartik Shah diff --git a/docs/release-notes/release-notes-0.9.0.md b/docs/release-notes/release-notes-0.9.0.md new file mode 100644 index 00000000..5d67f1aa --- /dev/null +++ b/docs/release-notes/release-notes-0.9.0.md @@ -0,0 +1,40 @@ +# Loop Client Release Notes + +- **Release date:** 2020-09-10 +- **Release page:** + [v0.9.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.9.0-beta) +- **Previous release:** [v0.8.1-beta](release-notes-0.8.1.md) +- **Next release:** [v0.10.0-beta](release-notes-0.10.0.md) + +#### New Features + +- A liquidity management system is introduced to drive automated Loop actions + based on desired liquidity + - Fully automated Loop actions are not yet included, this release introduces + suggested actions that can be executed using the API, or in the future can + be directed to be executed automatically. +- A rewrite of the on-chain script is introduced that is a bit cheaper on-chain + in the successful case + +#### Breaking Changes + +In this patch release: + +- TLS encryption is introduced and is now required to communicate with the Loop + daemon API +- Support is discontinued in binary releases for Darwin 386 (32 bit Mac) + +#### Bug Fixes + +- The Docker image Go version is increased to Go 1.13 + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager +- Oliver Gugger +- Tom Dickman diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md new file mode 100644 index 00000000..4de57fdb --- /dev/null +++ b/docs/release-notes/release-notes-next.md @@ -0,0 +1,11 @@ +# Loop Client Release Notes + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) diff --git a/docs/release-notes/release-notes-template.md b/docs/release-notes/release-notes-template.md new file mode 100644 index 00000000..f7c15829 --- /dev/null +++ b/docs/release-notes/release-notes-template.md @@ -0,0 +1,19 @@ +# Loop Client Release Notes + +- **Release date:** YYYY-MM-DD +- **Release page:** + [vX.Y.Z-beta](https://github.com/lightninglabs/loop/releases/tag/vX.Y.Z-beta) +- **Previous release:** [vX.Y.W-beta](release-notes-X.Y.W.md) +- **Next release:** None + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Contributor Name diff --git a/docs/release.md b/docs/reproducible_release.md similarity index 100% rename from docs/release.md rename to docs/reproducible_release.md diff --git a/release_notes.md b/release_notes.md deleted file mode 100644 index 38afc603..00000000 --- a/release_notes.md +++ /dev/null @@ -1,23 +0,0 @@ -# Loop Client Release Notes -This file tracks release notes for the loop client. - -### Developers: -* When new features are added to the repo, a short description of the feature should be added under the "Next Release" heading. -* This should be done in the same PR as the change so that our release notes stay in sync! - -### Release Manager: -* All of the items under the "Next Release" heading should be included in the release notes. -* As part of the PR that bumps the client version, cut everything below the 'Next Release' heading. -* These notes can either be pasted in a temporary doc, or you can get them from the PR diff once it is merged. -* The notes are just a guideline as to the changes that have been made since the last release, they can be updated. -* Once the version bump PR is merged and tagged, add the release notes to the tag on GitHub. - -## Next release - -#### New Features - -#### Breaking Changes - -#### Bug Fixes - -#### Maintenance diff --git a/version.go b/version.go index 6b80b2d4..88d2cc4b 100644 --- a/version.go +++ b/version.go @@ -35,7 +35,8 @@ const semanticAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr // These constants define the application version and follow the semantic // versioning 2.0.0 spec (http://semver.org/). const ( - // Note: please update release_notes.md when you change these values. + // Note: please update docs/release-notes/release-notes-next.md when you + // change these values. appMajor uint = 0 appMinor uint = 34 appPatch uint = 0 From f51148a3a09d9af4f2ad750c6182f3b4ccefcb93 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 27 Jul 2026 13:25:29 +0200 Subject: [PATCH 12/36] github: add gateway code-review workflow --- .github/workflows/gateway.yml | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/gateway.yml diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml new file mode 100644 index 00000000..5e22aa11 --- /dev/null +++ b/.github/workflows/gateway.yml @@ -0,0 +1,66 @@ +name: gateway + +# Opt-in code-review bot. Triggered by a `/gateway ` comment on a PR +# (e.g. `/gateway review`); review/approve commands are gated to maintainers. +# Comment-commands only — no pull_request triggers — so fork PRs (which receive +# no secrets) never spawn failing runs. v0.5.0 adds the +# pull_request_review_comment trigger: /gateway dismiss, promote, and explain +# now also work as replies on a finding's inline thread (finding id inferred +# from the thread when omitted). Also a comment event — same fork-PR safety +# profile as issue_comment. +# +# Thin shim: the public lightninglabs/gateway-action mints an App token and +# checks out the private gateway runtime at execution time. The runtime stays +# private; only this entry point is public. + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +permissions: + # The action mints an App installation token internally; the GITHUB_TOKEN + # handed to this shim is unused, so we minimise it. + contents: read + +jobs: + review: + # issue_comment fires for all issues and every PR comment. Filter to PR + # comments that look like a /gateway command so unrelated comments don't + # spin up a no-op runner. `contains` (not `startsWith`) because the runtime + # accepts the command at column 0 of any line, including multi-line bodies. + if: >- + ${{ + (github.event_name == 'issue_comment' + && github.event.issue.pull_request != null + && contains(github.event.comment.body, '/gateway')) || + (github.event_name == 'pull_request_review_comment' + && contains(github.event.comment.body, '/gateway')) + }} + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + GATEWAY_REVIEW_MODE: multi + steps: + - uses: lightninglabs/gateway-action@3a31b86adf442852801a04ddb9c6bc0f12d363da # v0.5.0 + with: + # Pin the private runtime to an immutable commit (matches the action + # SHA-pin above) so runtime upgrades go through an lnd PR, not a moved + # tag. Without this, runtime_ref defaults to the v0.5.0 tag. + runtime_ref: b7490e68db31b391becfe9534e947b8004fc518b # gateway v0.5.0 + event_name: ${{ github.event_name }} + event_action: ${{ github.event.action }} + repo: ${{ github.repository }} + pr_number: ${{ github.event.issue.number || github.event.pull_request.number }} + actor: ${{ github.event.sender.login }} + comment_body: ${{ github.event.comment.body }} + comment_id: ${{ github.event.comment.id }} + comment_in_reply_to: ${{ github.event.comment.in_reply_to_id }} + # installation_id intentionally omitted: as of gateway v0.4.4 the + # runtime resolves the App installation covering this repo from + # app_id/private_key, so a hardcoded (and easily wrong-org) id is no + # longer needed. + app_id: ${{ secrets.GATEWAY_APP_ID }} + private_key: ${{ secrets.GATEWAY_PRIVATE_KEY }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} From 2dda48266012a602314d822e414c72480bf40481 Mon Sep 17 00:00:00 2001 From: saubyk <39208279+saubyk@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:46:33 -0700 Subject: [PATCH 13/36] github: pin gateway v0.6.0 (Opus 5, /gateway check) Bumps both pins together: the gateway-action SHA and the runtime_ref it resolves. runtime_ref is pinned explicitly rather than left to the action's default, so bumping only the action would leave the job on the v0.5.0 runtime. v0.6.0 adds no trigger and no input, so the rest of the shim is unchanged. Also corrects a copy-paste artifact in the adjacent comment, which said runtime upgrades go through an lnd PR. --- .github/workflows/gateway.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml index 5e22aa11..1462b01f 100644 --- a/.github/workflows/gateway.yml +++ b/.github/workflows/gateway.yml @@ -3,7 +3,7 @@ name: gateway # Opt-in code-review bot. Triggered by a `/gateway ` comment on a PR # (e.g. `/gateway review`); review/approve commands are gated to maintainers. # Comment-commands only — no pull_request triggers — so fork PRs (which receive -# no secrets) never spawn failing runs. v0.5.0 adds the +# no secrets) never spawn failing runs. v0.5.0 added the # pull_request_review_comment trigger: /gateway dismiss, promote, and explain # now also work as replies on a finding's inline thread (finding id inferred # from the thread when omitted). Also a comment event — same fork-PR safety @@ -43,12 +43,12 @@ jobs: env: GATEWAY_REVIEW_MODE: multi steps: - - uses: lightninglabs/gateway-action@3a31b86adf442852801a04ddb9c6bc0f12d363da # v0.5.0 + - uses: lightninglabs/gateway-action@334a8455ee316e40668ae3ac85249150c62704ec # v0.6.0 with: # Pin the private runtime to an immutable commit (matches the action - # SHA-pin above) so runtime upgrades go through an lnd PR, not a moved - # tag. Without this, runtime_ref defaults to the v0.5.0 tag. - runtime_ref: b7490e68db31b391becfe9534e947b8004fc518b # gateway v0.5.0 + # SHA-pin above) so runtime upgrades go through a loop PR, not a moved + # tag. Without this, runtime_ref defaults to the v0.6.0 tag. + runtime_ref: 75f6e67deac362bdcfc10d10629ddcf69c0e2615 # gateway v0.6.0 event_name: ${{ github.event_name }} event_action: ${{ github.event.action }} repo: ${{ github.repository }} From 514c3f06ada69f5c6ae6395f84bfcc95a0a0882b Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Fri, 31 Jul 2026 21:16:37 +0200 Subject: [PATCH 14/36] loopd: close tapd client connections Close the TapdClient when daemon initialization fails, during normal shutdown, and after the view command completes. This prevents gRPC transport resources from leaking across embedded daemon lifecycles and error paths. --- loopd/daemon.go | 21 +++++++++++++++++---- loopd/view.go | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/loopd/daemon.go b/loopd/daemon.go index f625aa27..319709d8 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -158,6 +158,15 @@ func (d *Daemon) Start() error { if err != nil { return err } + + defer func() { + if err == nil || d.assetClient == nil { + return + } + + d.assetClient.Close() + d.assetClient = nil + }() } // With lnd connected, initialize everything else, such as the swap @@ -178,15 +187,15 @@ func (d *Daemon) Start() error { // If we get here, we already have started several goroutines. So if // anything goes wrong now, we need to cleanly shut down again. - startErr := d.startWebServers() - if startErr != nil { - errorf("Error while starting daemon: %v", startErr) + err = d.startWebServers() + if err != nil { + errorf("Error while starting daemon: %v", err) d.Stop() stopErr := <-d.ErrChan if stopErr != nil { errorf("Error while stopping daemon: %v", stopErr) } - return startErr + return err } return nil @@ -1145,6 +1154,10 @@ func (d *Daemon) stop() { if d.clientCleanup != nil { d.clientCleanup() } + if d.assetClient != nil { + d.assetClient.Close() + d.assetClient = nil + } // Everything should be shutting down now, wait for completion. d.wg.Wait() diff --git a/loopd/view.go b/loopd/view.go index 73b401d8..86b9604a 100644 --- a/loopd/view.go +++ b/loopd/view.go @@ -44,6 +44,7 @@ func view(config *Config, lisCfg *ListenerCfg) error { if err != nil { return err } + defer assetClient.Close() } swapClient, cleanup, err := getClient( From c7d5e466cdd2f05ea1a32c986008f1f6dd934f86 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Fri, 31 Jul 2026 21:17:25 +0200 Subject: [PATCH 15/36] assets: validate RFQ timeout conversion Convert the configured duration once during client creation. Round positive fractional durations up to the whole seconds accepted by tapd. Reject zero, negative, and overflowing values, and cover the conversion boundaries with unit tests. --- assets/client.go | 40 +++++++++++++++++++++++------ assets/client_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/assets/client.go b/assets/client.go index 6b8b514f..2d963bb0 100644 --- a/assets/client.go +++ b/assets/client.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "fmt" + "math" "os" "path/filepath" "sync" @@ -78,14 +79,19 @@ type TapdClient struct { rfqrpc.RfqClient universerpc.UniverseClient - cfg *TapdConfig - assetNameCache map[string]string - assetNameMutex sync.Mutex - cc *grpc.ClientConn + rfqTimeoutSeconds uint32 + assetNameCache map[string]string + assetNameMutex sync.Mutex + cc *grpc.ClientConn } // NewTapdClient returns a new taproot assets client. func NewTapdClient(config *TapdConfig) (*TapdClient, error) { + rfqTimeoutSeconds, err := getRfqTimeoutSeconds(config.RFQtimeout) + if err != nil { + return nil, err + } + // Create the client connection to the server. conn, err := getClientConn(config) if err != nil { @@ -96,7 +102,7 @@ func NewTapdClient(config *TapdConfig) (*TapdClient, error) { client := &TapdClient{ assetNameCache: make(map[string]string), cc: conn, - cfg: config, + rfqTimeoutSeconds: rfqTimeoutSeconds, TaprootAssetsClient: taprpc.NewTaprootAssetsClient(conn), TaprootAssetChannelsClient: tapchannelrpc.NewTaprootAssetChannelsClient(conn), PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn), @@ -139,7 +145,7 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context, PeerPubKey: peerPubkey, PaymentMaxAmt: uint64(paymentMaxAmt), Expiry: uint64(expiry), - TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), + TimeoutSeconds: c.rfqTimeoutSeconds, }) if err != nil { return nil, err @@ -220,7 +226,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, }, PaymentMaxAmt: uint64(msatAmt), Expiry: uint64(rfqExpiry), - TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), + TimeoutSeconds: c.rfqTimeoutSeconds, PeerPubKey: peerPubkey, }) if err != nil { @@ -288,6 +294,26 @@ func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( ) } +// getRfqTimeoutSeconds converts the configured RFQ timeout to the whole +// seconds accepted by tapd. Fractional seconds are rounded up so tapd's +// timeout is never shorter than the configured duration. +func getRfqTimeoutSeconds(timeout time.Duration) (uint32, error) { + if timeout <= 0 { + return 0, fmt.Errorf("RFQ timeout must be greater than zero") + } + + seconds := timeout / time.Second + if timeout%time.Second != 0 { + seconds++ + } + if seconds > time.Duration(math.MaxUint32) { + return 0, fmt.Errorf("RFQ timeout exceeds maximum of %v seconds", + uint64(math.MaxUint32)) + } + + return uint32(seconds), nil +} + func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) { // Load the specified TLS certificate and build transport credentials. creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "") diff --git a/assets/client_test.go b/assets/client_test.go index 8fa79092..c23ab7f2 100644 --- a/assets/client_test.go +++ b/assets/client_test.go @@ -2,11 +2,13 @@ package assets import ( "encoding/pem" + "math" "net/http" "net/http/httptest" "os" "path/filepath" "testing" + "time" "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" @@ -141,6 +143,62 @@ func TestGetPaymentMaxAmount(t *testing.T) { } } +// TestGetRfqTimeoutSeconds verifies that configured durations are safely +// converted to tapd's whole-second timeout field. +func TestGetRfqTimeoutSeconds(t *testing.T) { + tests := []struct { + name string + timeout time.Duration + expectedSeconds uint32 + expectError bool + }{ + { + name: "whole seconds", + timeout: 60 * time.Second, + expectedSeconds: 60, + }, + { + name: "sub-second rounded up", + timeout: time.Millisecond, + expectedSeconds: 1, + }, + { + name: "fractional second rounded up", + timeout: time.Second + time.Nanosecond, + expectedSeconds: 2, + }, + { + name: "zero", + timeout: 0, + expectError: true, + }, + { + name: "negative", + timeout: -time.Second, + expectError: true, + }, + { + name: "overflow", + timeout: time.Duration(math.MaxUint32)*time.Second + + time.Nanosecond, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + seconds, err := getRfqTimeoutSeconds(test.timeout) + if test.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, test.expectedSeconds, seconds) + }) + } +} + func TestGetSatsFromAssetAmt(t *testing.T) { tests := []struct { assetAmt uint64 From dc39d63d8db4f8a1b919b771eaa9c53bdddb9208 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Fri, 31 Jul 2026 21:19:02 +0200 Subject: [PATCH 16/36] assets: avoid locking cache during RPC Restrict the asset-name cache mutex to map access so a slow QueryAssetStats call cannot block cached readers. Use an RWMutex for independent cache reads and add a concurrent regression test. --- assets/client.go | 25 ++++++++++--- assets/client_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/assets/client.go b/assets/client.go index 2d963bb0..ef320af8 100644 --- a/assets/client.go +++ b/assets/client.go @@ -81,7 +81,7 @@ type TapdClient struct { rfqTimeoutSeconds uint32 assetNameCache map[string]string - assetNameMutex sync.Mutex + assetNameMutex sync.RWMutex cc *grpc.ClientConn } @@ -169,10 +169,8 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context, func (c *TapdClient) GetAssetName(ctx context.Context, assetId []byte) (string, error) { - c.assetNameMutex.Lock() - defer c.assetNameMutex.Unlock() assetIdStr := hex.EncodeToString(assetId) - if name, ok := c.assetNameCache[assetIdStr]; ok { + if name, ok := c.getCachedAssetName(assetIdStr); ok { return name, nil } @@ -198,11 +196,28 @@ func (c *TapdClient) GetAssetName(ctx context.Context, assetName = assetStats.AssetStats[0].Asset.AssetName } - c.assetNameCache[assetIdStr] = assetName + c.cacheAssetName(assetIdStr, assetName) return assetName, nil } +// getCachedAssetName returns an asset name from the cache. +func (c *TapdClient) getCachedAssetName(assetID string) (string, bool) { + c.assetNameMutex.RLock() + defer c.assetNameMutex.RUnlock() + + name, ok := c.assetNameCache[assetID] + return name, ok +} + +// cacheAssetName adds an asset name to the cache. +func (c *TapdClient) cacheAssetName(assetID, name string) { + c.assetNameMutex.Lock() + defer c.assetNameMutex.Unlock() + + c.assetNameCache[assetID] = name +} + // GetAssetPrice returns the price of an asset in satoshis. NOTE: this currently // uses the rfq process for the asset price. A future implementation should // use a price oracle to not spam a peer. diff --git a/assets/client_test.go b/assets/client_test.go index c23ab7f2..5fd2b2e1 100644 --- a/assets/client_test.go +++ b/assets/client_test.go @@ -1,6 +1,8 @@ package assets import ( + "context" + "encoding/hex" "encoding/pem" "math" "net/http" @@ -12,11 +14,38 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" + "google.golang.org/grpc" "gopkg.in/macaroon.v2" ) +type blockingUniverseClient struct { + universerpc.UniverseClient + + queryStarted chan struct{} + releaseQuery chan struct{} +} + +func (b *blockingUniverseClient) QueryAssetStats(context.Context, + *universerpc.AssetStatsQuery, ...grpc.CallOption) ( + *universerpc.UniverseAssetStats, error) { + + close(b.queryStarted) + <-b.releaseQuery + + return &universerpc.UniverseAssetStats{ + AssetStats: []*universerpc.AssetStatsSnapshot{ + { + Asset: &universerpc.AssetStatsAsset{ + AssetName: "queried asset", + }, + }, + }, + }, nil +} + // TestDefaultTapdConfig tests that the default tapd connection paths match // tapd's mainnet defaults. func TestDefaultTapdConfig(t *testing.T) { @@ -84,6 +113,61 @@ func TestTapdConfigClientConn(t *testing.T) { ) } +// TestGetAssetNameCachedLookupNotBlocked verifies that a slow universe query +// for one asset does not prevent another caller from reading a cached name. +func TestGetAssetNameCachedLookupNotBlocked(t *testing.T) { + const cachedName = "cached asset" + + cachedAssetID := []byte{1} + queryStarted := make(chan struct{}) + releaseQuery := make(chan struct{}) + client := &TapdClient{ + UniverseClient: &blockingUniverseClient{ + queryStarted: queryStarted, + releaseQuery: releaseQuery, + }, + assetNameCache: map[string]string{ + hex.EncodeToString(cachedAssetID): cachedName, + }, + } + + queryResult := make(chan error, 1) + go func() { + _, err := client.GetAssetName(context.Background(), []byte{2}) + queryResult <- err + }() + + select { + case <-queryStarted: + case <-time.After(time.Second): + t.Fatal("universe query did not start") + } + + type nameResult struct { + name string + err error + } + cachedResult := make(chan nameResult, 1) + go func() { + name, err := client.GetAssetName( + context.Background(), cachedAssetID, + ) + cachedResult <- nameResult{name: name, err: err} + }() + + select { + case result := <-cachedResult: + require.NoError(t, result.err) + require.Equal(t, cachedName, result.name) + case <-time.After(time.Second): + close(releaseQuery) + t.Fatal("cached lookup blocked behind universe query") + } + + close(releaseQuery) + require.NoError(t, <-queryResult) +} + func TestGetPaymentMaxAmount(t *testing.T) { tests := []struct { satAmount btcutil.Amount From cfbade12394c4c6b6434b0d627da4ff90f2ce0cb Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Fri, 31 Jul 2026 21:19:37 +0200 Subject: [PATCH 17/36] assets: reject malformed RFQ asset rates Validate the rate pointer and decimal coefficient before converting asset units. Return errors for nil, malformed, non-positive, and oversized-scale rates instead of allowing nil dereferences or division-by-zero panics. Add regression tests for each case. --- assets/client.go | 31 +++++++++++++++++++++++++++++-- assets/client_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/assets/client.go b/assets/client.go index ef320af8..39090afc 100644 --- a/assets/client.go +++ b/assets/client.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "math" + "math/big" "os" "path/filepath" "sync" @@ -12,7 +13,6 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/taproot-assets/rfqmath" - "github.com/lightninglabs/taproot-assets/rpcutils" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" @@ -275,7 +275,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( btcutil.Amount, error) { - rateFP, err := rpcutils.UnmarshalRfqFixedPoint(assetRate) + rateFP, err := unmarshalAssetRate(assetRate) if err != nil { return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err) } @@ -287,6 +287,33 @@ func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( return msatAmt.ToSatoshis(), nil } +// unmarshalAssetRate validates and converts an RPC asset rate to the fixed +// point representation used for RFQ arithmetic. +func unmarshalAssetRate(assetRate *rfqrpc.FixedPoint) ( + *rfqmath.BigIntFixedPoint, error) { + + if assetRate == nil { + return nil, fmt.Errorf("asset rate cannot be nil") + } + if assetRate.Scale > math.MaxUint8 { + return nil, fmt.Errorf("scale value overflow: %v", assetRate.Scale) + } + + coefficient, ok := new(big.Int).SetString(assetRate.Coefficient, 10) + if !ok { + return nil, fmt.Errorf("invalid asset rate coefficient: %q", + assetRate.Coefficient) + } + if coefficient.Sign() <= 0 { + return nil, fmt.Errorf("asset rate coefficient must be positive") + } + + return &rfqmath.BigIntFixedPoint{ + Coefficient: rfqmath.NewBigInt(coefficient), + Scale: uint8(assetRate.Scale), + }, nil +} + // getPaymentMaxAmount returns the milisat amount we are willing to pay for the // payment. func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( diff --git a/assets/client_test.go b/assets/client_test.go index 5fd2b2e1..af3f671e 100644 --- a/assets/client_test.go +++ b/assets/client_test.go @@ -308,6 +308,39 @@ func TestGetSatsFromAssetAmt(t *testing.T) { expected: btcutil.Amount(0), expectError: false, }, + { + assetAmt: 1000, + assetRate: nil, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "not-a-number", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "0", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "-1", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "1", Scale: 256, + }, + expectError: true, + }, } for _, test := range tests { From 9e97992e184cd09eba1e7d2d322f06bda5848c3a Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 3 Aug 2026 10:01:35 +0200 Subject: [PATCH 18/36] assets: validate accepted RFQ asset rates Validate the bid rate before returning an accepted asset sell quote. This prevents malformed rates from reaching downstream quote arithmetic, where nil or non-positive values can panic. Cover valid and malformed responses with table-driven tests. --- assets/client.go | 13 ++++-- assets/client_test.go | 96 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/assets/client.go b/assets/client.go index 39090afc..7eef98b0 100644 --- a/assets/client.go +++ b/assets/client.go @@ -158,11 +158,18 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context, rfq.GetRejectedQuote()) } - if rfq.GetAcceptedQuote() != nil { - return rfq.GetAcceptedQuote(), nil + acceptedQuote := rfq.GetAcceptedQuote() + if acceptedQuote == nil { + return nil, fmt.Errorf("no accepted quote") } - return nil, fmt.Errorf("no accepted quote") + _, err = unmarshalAssetRate(acceptedQuote.BidAssetRate) + if err != nil { + return nil, fmt.Errorf("invalid accepted quote asset rate: %w", + err) + } + + return acceptedQuote, nil } // GetAssetName returns the human-readable name of the asset. diff --git a/assets/client_test.go b/assets/client_test.go index af3f671e..5941ae44 100644 --- a/assets/client_test.go +++ b/assets/client_test.go @@ -46,6 +46,19 @@ func (b *blockingUniverseClient) QueryAssetStats(context.Context, }, nil } +type staticRfqClient struct { + rfqrpc.RfqClient + + response *rfqrpc.AddAssetSellOrderResponse +} + +func (s *staticRfqClient) AddAssetSellOrder(context.Context, + *rfqrpc.AddAssetSellOrderRequest, ...grpc.CallOption) ( + *rfqrpc.AddAssetSellOrderResponse, error) { + + return s.response, nil +} + // TestDefaultTapdConfig tests that the default tapd connection paths match // tapd's mainnet defaults. func TestDefaultTapdConfig(t *testing.T) { @@ -168,6 +181,89 @@ func TestGetAssetNameCachedLookupNotBlocked(t *testing.T) { require.NoError(t, <-queryResult) } +// TestGetRfqForAssetValidatesRate verifies that malformed accepted quote rates +// are rejected before they reach downstream RFQ arithmetic. +func TestGetRfqForAssetValidatesRate(t *testing.T) { + tests := []struct { + name string + assetRate *rfqrpc.FixedPoint + expectError bool + }{ + { + name: "valid", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "100000", Scale: 0, + }, + }, + { + name: "nil", + assetRate: nil, + expectError: true, + }, + { + name: "malformed coefficient", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "not-a-number", Scale: 0, + }, + expectError: true, + }, + { + name: "zero coefficient", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "0", Scale: 0, + }, + expectError: true, + }, + { + name: "negative coefficient", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "-1", Scale: 0, + }, + expectError: true, + }, + { + name: "scale overflow", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "1", Scale: 256, + }, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + acceptedQuote := &rfqrpc.PeerAcceptedSellQuote{ + BidAssetRate: test.assetRate, + } + acceptedResponse := + &rfqrpc.AddAssetSellOrderResponse_AcceptedQuote{ + AcceptedQuote: acceptedQuote, + } + client := &TapdClient{ + RfqClient: &staticRfqClient{ + response: &rfqrpc.AddAssetSellOrderResponse{ + Response: acceptedResponse, + }, + }, + rfqTimeoutSeconds: 60, + } + + quote, err := client.GetRfqForAsset( + context.Background(), 1000, []byte{1}, []byte{2}, + time.Now().Add(time.Minute).Unix(), 1, + ) + if test.expectError { + require.Error(t, err) + require.Nil(t, quote) + return + } + + require.NoError(t, err) + require.Same(t, acceptedQuote, quote) + }) + } +} + func TestGetPaymentMaxAmount(t *testing.T) { tests := []struct { satAmount btcutil.Amount From b686d97445918e321f61c605c40c2749eb79ebb2 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 3 Aug 2026 10:04:45 +0200 Subject: [PATCH 19/36] docs: note tapd client hardening Document the RFQ validation, cache responsiveness, and tapd connection lifecycle fixes in the rolling release notes. --- docs/release-notes/release-notes-next.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index 4de57fdb..7c9d492e 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -6,6 +6,11 @@ #### Bug Fixes +* Taproot Asset Loop Out handling now validates RFQ timeouts and asset rates, + keeps cached asset-name lookups responsive during slow `tapd` queries, and + closes `tapd` connections cleanly during shutdown and startup failures. + [PR #1189](https://github.com/lightninglabs/loop/pull/1189) + #### Maintenance #### Contributors (Alphabetical Order) From 414760c607f20cf81b42c382d1accc4b8d965f6b Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 3 Aug 2026 11:17:54 +0200 Subject: [PATCH 20/36] github: require release notes for pull requests --- .github/pull_request_template.md | 4 +-- .github/workflows/main.yml | 36 ++++++++++++++++++++++++ docs/release-notes/release-notes-next.md | 3 ++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d68648b1..c1f965a1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,3 @@ #### Pull Request Checklist -- [ ] Update `docs/release-notes/release-notes-next.md` if your PR contains - major features, breaking changes or bugfixes +- [ ] Add an entry to `docs/release-notes/release-notes-next.md` (required by + CI) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3250faf5..fba07721 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,42 @@ env: LITD_ITEST_BRANCH: master jobs: + ######################## + # release notes + ######################## + release-notes: + name: Release Notes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: git checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for a release note + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + RELEASE_NOTES_FILE: docs/release-notes/release-notes-next.md + run: | + if git diff --unified=0 "$BASE_SHA...$HEAD_SHA" -- \ + "$RELEASE_NOTES_FILE" | + awk ' + /^@@/ { in_hunk = 1; next } + in_hunk && /^\+/ { + line = substr($0, 2) + if (line ~ /[^[:space:]]/) found = 1 + } + END { exit found ? 0 : 1 } + ' + then + exit 0 + fi + + echo "::error file=$RELEASE_NOTES_FILE::Add a release note for this PR." + exit 1 + ######################## # commit message lint ######################## diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index 7c9d492e..ed23b7c1 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -13,4 +13,7 @@ #### Maintenance +* Added a CI gate requiring every pull request to include a non-empty entry in + the next release notes. + #### Contributors (Alphabetical Order) From 4016f4a8f2109f84582c3aebb6cbbdadc767029d Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 3 Aug 2026 11:18:14 +0200 Subject: [PATCH 21/36] docs: instruct agents to add release notes --- AGENTS.md | 9 +++++++++ docs/release-notes/release-notes-next.md | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 194b4b85..175d1a01 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,3 +72,12 @@ the binding dependencies are the asset loop-out fields `routerrpc.SendPaymentRequest.first_hop_custom_records` and `lnrpc.Route.custom_channel_data` (lnd v0.18.4-beta), plus the sweep-batcher fee floor `walletrpc.EstimateFeeResponse.min_relay_fee_sat_per_kw` (lnd v0.18.3-beta). + +**6. Pull Request Release Notes:** + +Every pull request must add at least one non-empty line to +`docs/release-notes/release-notes-next.md`. Before finishing any change intended +for a pull request, add a concise entry under the appropriate section (`New +Features`, `Breaking Changes`, `Bug Fixes`, or `Maintenance`). This applies to +all pull requests, including internal maintenance and documentation changes, and +is enforced by CI. diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index ed23b7c1..414aa11c 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -13,7 +13,7 @@ #### Maintenance -* Added a CI gate requiring every pull request to include a non-empty entry in - the next release notes. +* Added a CI gate and repository agent guidance requiring every pull request to + include a non-empty entry in the next release notes. #### Contributors (Alphabetical Order) From 6f0d817db4e7b9ae902f6bb586fa18400ea02059 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 4 Aug 2026 11:02:22 +0200 Subject: [PATCH 22/36] github: allow no-changelog to skip release notes --- .github/workflows/main.yml | 36 ------------------ .github/workflows/release-notes.yml | 58 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/release-notes.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fba07721..3250faf5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,42 +24,6 @@ env: LITD_ITEST_BRANCH: master jobs: - ######################## - # release notes - ######################## - release-notes: - name: Release Notes - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: git checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check for a release note - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - RELEASE_NOTES_FILE: docs/release-notes/release-notes-next.md - run: | - if git diff --unified=0 "$BASE_SHA...$HEAD_SHA" -- \ - "$RELEASE_NOTES_FILE" | - awk ' - /^@@/ { in_hunk = 1; next } - in_hunk && /^\+/ { - line = substr($0, 2) - if (line ~ /[^[:space:]]/) found = 1 - } - END { exit found ? 0 : 1 } - ' - then - exit 0 - fi - - echo "::error file=$RELEASE_NOTES_FILE::Add a release note for this PR." - exit 1 - ######################## # commit message lint ######################## diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 00000000..818ccbf4 --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,58 @@ +name: Release Notes + +on: + pull_request: + branches: + - "*" + types: + - opened + - reopened + - synchronize + - labeled + - unlabeled + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + release-notes: + name: Release Notes + runs-on: ubuntu-latest + steps: + - name: Skip release notes check + if: contains(github.event.pull_request.labels.*.name, 'no-changelog') + run: echo "Release notes check skipped by the no-changelog label." + + - name: git checkout + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for a release note + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }} + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + RELEASE_NOTES_FILE: docs/release-notes/release-notes-next.md + run: | + if git diff --unified=0 "$BASE_SHA...$HEAD_SHA" -- \ + "$RELEASE_NOTES_FILE" | + awk ' + /^@@/ { in_hunk = 1; next } + in_hunk && /^\+/ { + line = substr($0, 2) + if (line ~ /[^[:space:]]/) found = 1 + } + END { exit found ? 0 : 1 } + ' + then + exit 0 + fi + + echo "::error file=$RELEASE_NOTES_FILE::Add a release note or apply the no-changelog label." + exit 1 From 57fb007f95a4ed494621333071c93f03bdd68ea3 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 4 Aug 2026 11:02:33 +0200 Subject: [PATCH 23/36] docs: document no-changelog release note opt-out --- .github/pull_request_template.md | 4 ++-- AGENTS.md | 12 ++++++------ docs/release-notes/release-notes-next.md | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c1f965a1..a6d279b5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,3 @@ #### Pull Request Checklist -- [ ] Add an entry to `docs/release-notes/release-notes-next.md` (required by - CI) +- [ ] Add an entry to `docs/release-notes/release-notes-next.md`, or apply the + `no-changelog` label (required by CI) diff --git a/AGENTS.md b/AGENTS.md index 175d1a01..22f10a06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,9 +75,9 @@ floor `walletrpc.EstimateFeeResponse.min_relay_fee_sat_per_kw` (lnd v0.18.3-beta **6. Pull Request Release Notes:** -Every pull request must add at least one non-empty line to -`docs/release-notes/release-notes-next.md`. Before finishing any change intended -for a pull request, add a concise entry under the appropriate section (`New -Features`, `Breaking Changes`, `Bug Fixes`, or `Maintenance`). This applies to -all pull requests, including internal maintenance and documentation changes, and -is enforced by CI. +Every pull request must either add at least one non-empty line to +`docs/release-notes/release-notes-next.md` or carry the `no-changelog` label. +Before finishing any change intended for a pull request, add a concise entry +under the appropriate section (`New Features`, `Breaking Changes`, `Bug Fixes`, +or `Maintenance`). For a change that does not warrant a release note, ensure the +pull request uses the `no-changelog` label instead. This is enforced by CI. diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index 414aa11c..f0ca7972 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -14,6 +14,7 @@ #### Maintenance * Added a CI gate and repository agent guidance requiring every pull request to - include a non-empty entry in the next release notes. + include a non-empty entry in the next release notes unless it carries the + `no-changelog` label. #### Contributors (Alphabetical Order) From 95e184a40fa3d61adc8f2074232e0002aaaff99e Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Mon, 10 Aug 2026 17:30:38 -0500 Subject: [PATCH 24/36] loopd: account for channel reserves in loop out validation Use each channel's local balance minus its reserve when simulating Loop Out payment bandwidth, clamping the result at zero. This prevents swaps from starting when the gross balance covers the amount and routing fee but the spendable balance does not. Add one-sat boundary tests and a bug-fix release note. Leave a TODO for server-fee and prepay capacity because enforcing their maximum fee caps could reject viable swaps whose actual routing fees are lower. --- docs/release-notes/release-notes-next.md | 4 +++ loopd/swapclient_server.go | 30 ++++++++++++---- loopd/swapclient_server_test.go | 44 ++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index f0ca7972..fb302d1e 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -6,6 +6,10 @@ #### Bug Fixes +* Loop Out requests now account for channel reserves when checking outbound + capacity, preventing swaps from starting when their off-chain payment cannot + be funded. + * Taproot Asset Loop Out handling now validates RFQ timeouts and asset rates, keeps cached asset-name lookups responsive during slow `tapd` queries, and closes `tapd` connections cleanly during shutdown and startup failures. diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 28b4f722..a0559d41 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -3013,6 +3013,13 @@ func validateLoopOutRequest(ctx context.Context, lnd lndclient.LightningClient, // the amount requested, the maximum possible routing fees, // the available channel set and the fact that equal splitting is // used for MPP. + // + // TODO: Also account for the quoted server fee and the concurrent prepay + // payment. The CLI and autoloop obtain the fee and prepay amounts from a + // quote, but this RPC only carries maximum limits, which direct callers may + // set higher than the quoted amounts. Treating those limits as exact can + // reject a viable swap, while the actual routing fees are only known when + // the invoices are paid. requiredBalance := btcutil.Amount(req.Amt + req.MaxSwapRoutingFee) isRoutable, _ := hasBandwidth(activeChannelSet, requiredBalance, int(maxParts)) @@ -3047,11 +3054,24 @@ func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount, localBalances := make([]btcutil.Amount, len(channels)) var totalBandwidth btcutil.Amount for i, channel := range channels { - tracef("Channel %v: local=%v remote=%v", channel.ChannelID, - channel.LocalBalance, channel.RemoteBalance) + localBalance := channel.LocalBalance + var reserve btcutil.Amount + if channel.LocalConstraints != nil { + reserve = channel.LocalConstraints.Reserve + } - localBalances[i] = channel.LocalBalance - totalBandwidth += channel.LocalBalance + if reserve >= localBalance { + localBalance = 0 + } else { + localBalance -= reserve + } + + tracef("Channel %v: local=%v reserve=%v available=%v "+ + "remote=%v", channel.ChannelID, channel.LocalBalance, + reserve, localBalance, channel.RemoteBalance) + + localBalances[i] = localBalance + totalBandwidth += localBalance } tracef("Total bandwidth: %v", totalBandwidth) @@ -3073,8 +3093,6 @@ func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount, paid := false for i := range len(localBalances) { - // TODO(hieblmi): Consider channel reserves because the - // channel can't send its full local balance. if localBalances[i] >= split { tracef("len(shards)=%v: Local channel "+ "balance %v can pay %v sats", diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index 03f3d95a..1171389c 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -1378,6 +1378,50 @@ func TestValidateLoopOutRequest(t *testing.T) { err: errBalanceTooLow, expectedTarget: 0, }, + { + name: "channel reserve leaves balance one sat short", + chain: chaincfg.MainNetParams, + destAddr: mainnetAddr, + label: "label ok", + confTarget: 2, + channels: []lndclient.ChannelInfo{ + { + Active: true, + ChannelID: chanID2.ToUint64(), + LocalBalance: 10100, + LocalConstraints: &lndclient.ChannelConstraints{ + Reserve: 100, + }, + }, + }, + amount: 10000, + maxRoutingFee: 1, + maxParts: 1, + err: errBalanceTooLow, + expectedTarget: 0, + }, + { + name: "channel reserve leaves exact balance", + chain: chaincfg.MainNetParams, + destAddr: mainnetAddr, + label: "label ok", + confTarget: 2, + channels: []lndclient.ChannelInfo{ + { + Active: true, + ChannelID: chanID2.ToUint64(), + LocalBalance: 10101, + LocalConstraints: &lndclient.ChannelConstraints{ + Reserve: 100, + }, + }, + }, + amount: 10000, + maxRoutingFee: 1, + maxParts: 1, + err: nil, + expectedTarget: 2, + }, { name: "can split between channels", chain: chaincfg.MainNetParams, From 7cab89d45b4b86165cf02eee578a028ec621953d Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:30:10 +0200 Subject: [PATCH 25/36] looprpc: align instant out permissions with loop out Apply the loop:out permission to Instant Out and reservation RPCs so their authorization requirements match the rest of the Loop Out API. --- looprpc/perms.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/looprpc/perms.go b/looprpc/perms.go index d646f667..9187920a 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -177,18 +177,30 @@ var RequiredPermissions = map[string][]bakery.Op{ "/looprpc.SwapClient/ListReservations": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/InstantOut": {{ Entity: "swap", Action: "execute", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/InstantOutQuote": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/ListInstantOuts": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/StopDaemon": {{ Entity: "loop", From 806c1f9356594243d67f8e8cad69ca50ce23ab5b Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:31:08 +0200 Subject: [PATCH 26/36] reservation: keep processing after notification errors Log individual reservation initialization failures and continue consuming later notifications instead of stopping the manager. --- instantout/reservation/manager.go | 3 +- instantout/reservation/manager_test.go | 45 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 600febfe..b5ff0443 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -80,7 +80,8 @@ func (m *Manager) Run(ctx context.Context, height int32, runCtx, uint32(currentHeight), reservationRes, ) if err != nil { - return err + log.Errorf("Unable to create reservation %x: %v", + reservationRes.ReservationId, err) } case err := <-newBlockErrChan: diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index 79455750..4b01fcfa 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -99,6 +99,51 @@ func TestManager(t *testing.T) { require.NoError(t, err) } +// TestManagerContinuesAfterInvalidNotification verifies that a malformed +// server notification doesn't stop the reservation manager from processing +// later notifications. +func TestManagerContinuesAfterInvalidNotification(t *testing.T) { + testContext := newManagerTestContext(t) + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + initChan := make(chan struct{}) + errChan := make(chan error, 1) + go func() { + errChan <- testContext.manager.Run( + ctx, testContext.mockLnd.Height, initChan, + ) + }() + + <-initChan + + // A malformed ID is rejected by newReservation. The manager should log + // the error and continue processing the stream. + testContext.reservationNotificationChan <- &swapserverrpc.ServerReservationNotification{ + ReservationId: []byte{1}, + } + + testContext.reservationNotificationChan <- &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + } + + select { + case <-testContext.mockLnd.RegisterConfChannel: + case err := <-errChan: + require.NoError(t, err) + t.Fatal("reservation manager stopped after malformed notification") + case <-time.After(5 * time.Second): + t.Fatal("valid reservation notification was not processed") + } + + cancel() + require.NoError(t, <-errChan) +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { From 2989ceee428dd216c5434b98ab82dbd909787e11 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:31:55 +0200 Subject: [PATCH 27/36] reservation: isolate asynchronous initialization errors Use a goroutine-local result for event dispatch so observer errors remain independent and initialization outcomes stay deterministic. --- instantout/reservation/manager.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index b5ff0443..0a902c05 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -131,9 +131,11 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, // Send the init event to the state machine. go func() { - err = reservationFSM.SendEvent(ctx, OnServerRequest, initContext) - if err != nil { - log.Errorf("Error sending init event: %v", err) + sendErr := reservationFSM.SendEvent( + ctx, OnServerRequest, initContext, + ) + if sendErr != nil { + log.Errorf("Error sending init event: %v", sendErr) } }() From 41b884f610fce2a298f7e5f9660f1bd8b8fa2582 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:32:52 +0200 Subject: [PATCH 28/36] reservation: reject duplicate reservation entries Check active and persisted reservations before creating a new state machine, preserving the existing reservation when a duplicate arrives. --- instantout/reservation/manager.go | 24 ++++++++++++++++++++- instantout/reservation/manager_test.go | 29 ++++++++++++++++++++++++++ instantout/reservation/store.go | 4 ++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 0a902c05..930dff7b 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -2,6 +2,7 @@ package reservation import ( "context" + "errors" "fmt" "strings" "sync" @@ -111,13 +112,28 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, return nil, err } + _, err = m.cfg.Store.GetReservation(ctx, reservationID) + switch { + case err == nil: + return nil, ErrReservationAlreadyExists + + case !errors.Is(err, ErrReservationNotFound): + return nil, err + } + // Create the reservation state machine. We need to pass in the runCtx // of the reservation manager so that the state machine will keep on // running even if the grpc conte reservationFSM := NewFSM(m.cfg) - // Add the reservation to the active reservations map. + // Add the reservation to the active reservations map. Check the map while + // holding the lock as concurrent callers may both have completed the store + // lookup above. m.Lock() + if _, ok := m.activeReservations[reservationID]; ok { + m.Unlock() + return nil, ErrReservationAlreadyExists + } m.activeReservations[reservationID] = reservationFSM m.Unlock() @@ -146,6 +162,12 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, fsm.WithWaitForStateOption(time.Second), ) if err != nil { + m.Lock() + if m.activeReservations[reservationID] == reservationFSM { + delete(m.activeReservations, reservationID) + } + m.Unlock() + if reservationFSM.LastActionError != nil { return nil, fmt.Errorf("error waiting for "+ "state: %v, last action error: %v", diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index 4b01fcfa..0437955e 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -144,6 +144,35 @@ func TestManagerContinuesAfterInvalidNotification(t *testing.T) { require.NoError(t, <-errChan) } +// TestManagerRejectsDuplicateReservation verifies that a duplicate server +// notification cannot replace the active FSM for an existing reservation. +func TestManagerRejectsDuplicateReservation(t *testing.T) { + testContext := newManagerTestContext(t) + ctx := t.Context() + req := &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + } + + firstFSM, err := testContext.manager.newReservation( + ctx, uint32(testContext.mockLnd.Height), req, + ) + require.NoError(t, err) + + secondFSM, err := testContext.manager.newReservation( + ctx, uint32(testContext.mockLnd.Height), req, + ) + require.ErrorIs(t, err, ErrReservationAlreadyExists) + require.Nil(t, secondFSM) + require.Same( + t, firstFSM, + testContext.manager.activeReservations[defaultReservationId], + ) +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { diff --git a/instantout/reservation/store.go b/instantout/reservation/store.go index 117d02c6..72613f9c 100644 --- a/instantout/reservation/store.go +++ b/instantout/reservation/store.go @@ -180,6 +180,10 @@ func (r *SQLStore) GetReservation(ctx context.Context, return nil }) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrReservationNotFound + } + return nil, err } From c75d7e3e81149e0c2dfd435b1e41a80fa36d3d3a Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:34:36 +0200 Subject: [PATCH 29/36] reservation: bound and prune active state machines Limit newly accepted reservation state machines, resume all persisted reservations, remove terminal entries from memory, and make cleanup resilient to observer-driven eviction. --- instantout/actions.go | 25 ++-- instantout/cleanup_test.go | 77 ++++++++++++ instantout/reservation/interfaces.go | 14 ++- instantout/reservation/manager.go | 71 +++++++++-- instantout/reservation/manager_test.go | 166 +++++++++++++++++++++++++ 5 files changed, 331 insertions(+), 22 deletions(-) create mode 100644 instantout/cleanup_test.go diff --git a/instantout/actions.go b/instantout/actions.go index d1c405fd..25842652 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -617,20 +617,23 @@ func (f *FSM) WaitForHtlcSweepConfirmedAction(ctx context.Context, // handleErrorAndUnlockReservations handles an error and unlocks the // reservations. func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, - err error) fsm.EventType { + actionErr error) fsm.EventType { // We might get here from a canceled context, we create a new context // with a timeout to unlock the reservations. - ctx, cancel := context.WithTimeout(ctx, time.Second*30) + cleanupCtx, cancel := context.WithTimeout( + context.WithoutCancel(ctx), time.Second*30, + ) defer cancel() // Unlock the reservations. + var unlockErr error for _, reservation := range f.InstantOut.Reservations { err := f.cfg.ReservationManager.UnlockReservation( - ctx, reservation.ID, + cleanupCtx, reservation.ID, ) if err != nil { f.Errorf("error unlocking reservation: %v", err) - return f.HandleError(err) + unlockErr = errors.Join(unlockErr, err) } } @@ -638,10 +641,12 @@ func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, // release the reservations. This can be done in a goroutine as we // wan't to fail the fsm early. go func() { - ctx, cancel := context.WithTimeout(ctx, time.Second*30) + cancelCtx, cancel := context.WithTimeout( + context.WithoutCancel(ctx), time.Second*30, + ) defer cancel() _, cancelErr := f.cfg.InstantOutClient.CancelInstantSwap( - ctx, &swapserverrpc.CancelInstantSwapRequest{ + cancelCtx, &swapserverrpc.CancelInstantSwapRequest{ SwapHash: f.InstantOut.SwapHash[:], }, ) @@ -652,7 +657,13 @@ func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, } }() - return f.HandleError(err) + // Preserve the action failure when cleanup also fails. If cleanup was + // the only failure, report it to the state machine. + if actionErr != nil { + return f.HandleError(actionErr) + } + + return f.HandleError(unlockErr) } func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { diff --git a/instantout/cleanup_test.go b/instantout/cleanup_test.go new file mode 100644 index 00000000..45640079 --- /dev/null +++ b/instantout/cleanup_test.go @@ -0,0 +1,77 @@ +package instantout + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/instantout/reservation" + "github.com/lightninglabs/loop/swapserverrpc" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type cleanupTestReservationManager struct { + ReservationManager + + unlockErr error +} + +func (m *cleanupTestReservationManager) UnlockReservation(context.Context, + reservation.ID) error { + + return m.unlockErr +} + +type cleanupTestInstantOutClient struct { + swapserverrpc.InstantSwapServerClient + + canceled chan struct{} +} + +func (c *cleanupTestInstantOutClient) CancelInstantSwap(context.Context, + *swapserverrpc.CancelInstantSwapRequest, ...grpc.CallOption) ( + *swapserverrpc.CancelInstantSwapResponse, error) { + + close(c.canceled) + return &swapserverrpc.CancelInstantSwapResponse{}, nil +} + +// TestCleanupPreservesActionError verifies that an unlock failure doesn't +// replace the action failure or prevent the cancellation notification. +func TestCleanupPreservesActionError(t *testing.T) { + actionErr := errors.New("action failed") + cancelClient := &cleanupTestInstantOutClient{ + canceled: make(chan struct{}), + } + instantOutFSM := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + ReservationManager: &cleanupTestReservationManager{ + unlockErr: errors.New("unlock failed"), + }, + InstantOutClient: cancelClient, + }, + InstantOut: &InstantOut{ + Reservations: []*reservation.Reservation{ + {ID: reservation.ID{1}}, + }, + }, + } + + event := instantOutFSM.handleErrorAndUnlockReservations( + t.Context(), actionErr, + ) + require.Equal(t, fsm.OnError, event) + require.ErrorIs(t, instantOutFSM.LastActionError, actionErr) + require.Eventually(t, func() bool { + select { + case <-cancelClient.canceled: + return true + default: + return false + } + }, time.Second, time.Millisecond) +} diff --git a/instantout/reservation/interfaces.go b/instantout/reservation/interfaces.go index 04bf830d..23658d5a 100644 --- a/instantout/reservation/interfaces.go +++ b/instantout/reservation/interfaces.go @@ -8,14 +8,18 @@ import ( ) var ( - ErrReservationAlreadyExists = fmt.Errorf("reservation already exists") - ErrReservationNotFound = fmt.Errorf("reservation not found") + ErrReservationAlreadyExists = fmt.Errorf("reservation already exists") + ErrReservationNotFound = fmt.Errorf("reservation not found") + ErrTooManyActiveReservations = fmt.Errorf( + "too many active reservations", + ) ) const ( - KeyFamily = int32(42068) - DefaultConfTarget = int32(3) - IdLength = 32 + KeyFamily = int32(42068) + DefaultConfTarget = int32(3) + IdLength = 32 + maxActiveReservations = 1000 ) // Store is the interface that stores the reservations. diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 930dff7b..bccdfa1e 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -14,6 +14,11 @@ import ( reservationrpc "github.com/lightninglabs/loop/swapserverrpc" ) +var ( + reservationStateWaitTimeout = 5 * time.Second + reservationStatePollDelay = time.Second +) + // Manager manages the reservation state machines. type Manager struct { sync.Mutex @@ -26,6 +31,28 @@ type Manager struct { activeReservations map[ID]*FSM } +// finalStateObserver removes a reservation FSM from the active set once it +// reaches a terminal state. +type finalStateObserver struct { + manager *Manager + id ID + fsm *FSM +} + +// Notify implements the fsm.Observer interface. +func (o *finalStateObserver) Notify(notification fsm.Notification) { + if !isFinalState(notification.NextState) { + return + } + + o.manager.Lock() + defer o.manager.Unlock() + + if o.manager.activeReservations[o.id] == o.fsm { + delete(o.manager.activeReservations, o.id) + } +} + // NewManager creates a new reservation manager. func NewManager(cfg *Config) *Manager { return &Manager{ @@ -134,9 +161,19 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, m.Unlock() return nil, ErrReservationAlreadyExists } + if len(m.activeReservations) >= maxActiveReservations { + m.Unlock() + return nil, ErrTooManyActiveReservations + } m.activeReservations[reservationID] = reservationFSM m.Unlock() + reservationFSM.RegisterObserver(&finalStateObserver{ + manager: m, + id: reservationID, + fsm: reservationFSM, + }) + initContext := &InitReservationContext{ reservationID: reservationID, serverPubkey: serverKey, @@ -158,16 +195,10 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, // We'll now wait for the reservation to be in the state where it is // waiting to be confirmed. err = reservationFSM.DefaultObserver.WaitForState( - ctx, 5*time.Second, WaitForConfirmation, - fsm.WithWaitForStateOption(time.Second), + ctx, reservationStateWaitTimeout, WaitForConfirmation, + fsm.WithWaitForStateOption(reservationStatePollDelay), ) if err != nil { - m.Lock() - if m.activeReservations[reservationID] == reservationFSM { - delete(m.activeReservations, reservationID) - } - m.Unlock() - if reservationFSM.LastActionError != nil { return nil, fmt.Errorf("error waiting for "+ "state: %v, last action error: %v", @@ -198,7 +229,14 @@ func (m *Manager) RecoverReservations(ctx context.Context) error { reservationFSM := NewFSMFromReservation(m.cfg, reservation) + m.Lock() m.activeReservations[reservation.ID] = reservationFSM + m.Unlock() + reservationFSM.RegisterObserver(&finalStateObserver{ + manager: m, + id: reservation.ID, + fsm: reservationFSM, + }) // As SendEvent can block, we'll start a goroutine to process // the event. @@ -236,7 +274,7 @@ func (m *Manager) LockReservation(ctx context.Context, id ID) error { m.Unlock() if !ok { - return fmt.Errorf("reservation not found") + return ErrReservationNotFound } // Try to send the lock event to the reservation. @@ -256,7 +294,20 @@ func (m *Manager) UnlockReservation(ctx context.Context, id ID) error { m.Unlock() if !ok { - return fmt.Errorf("reservation not found") + storedReservation, err := m.cfg.Store.GetReservation(ctx, id) + if err != nil { + return err + } + + // Terminal reservations are removed from the active set. Treat an + // unlock after that removal as idempotent, while still surfacing a + // missing active FSM for reservations that should be running. + if isFinalState(storedReservation.State) { + return nil + } + + return fmt.Errorf("%w: reservation %x is in state %v", + ErrReservationNotFound, id, storedReservation.State) } // Try to send the unlock event to the reservation. diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index 0437955e..ca3dec45 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -12,6 +12,7 @@ import ( "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/keychain" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -97,6 +98,11 @@ func TestManager(t *testing.T) { // We'll now expect the reservation to be expired. err = reservationFSM.DefaultObserver.WaitForState(ctxb, 5*time.Second, Spent) require.NoError(t, err) + + testContext.manager.Lock() + _, ok := testContext.manager.activeReservations[defaultReservationId] + testContext.manager.Unlock() + require.False(t, ok) } // TestManagerContinuesAfterInvalidNotification verifies that a malformed @@ -173,6 +179,166 @@ func TestManagerRejectsDuplicateReservation(t *testing.T) { ) } +// TestManagerLimitsActiveReservations verifies that server notifications +// cannot grow the active FSM set without bound. +func TestManagerLimitsActiveReservations(t *testing.T) { + testContext := newManagerTestContext(t) + + for i := range maxActiveReservations { + var id ID + id[0] = byte(i) + id[1] = byte(i >> 8) + testContext.manager.activeReservations[id] = NewFSM( + testContext.manager.cfg, + ) + } + + reservationFSM, err := testContext.manager.newReservation( + t.Context(), uint32(testContext.mockLnd.Height), + &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + }, + ) + require.ErrorIs(t, err, ErrTooManyActiveReservations) + require.Nil(t, reservationFSM) + require.Len( + t, testContext.manager.activeReservations, + maxActiveReservations, + ) +} + +// TestManagerKeepsReservationAfterWaitTimeout verifies that a caller-side +// wait timeout doesn't evict a reservation FSM that is still initializing. +func TestManagerKeepsReservationAfterWaitTimeout(t *testing.T) { + testContext := newManagerTestContext(t) + + originalWaitTimeout := reservationStateWaitTimeout + originalPollDelay := reservationStatePollDelay + reservationStateWaitTimeout = 20 * time.Millisecond + reservationStatePollDelay = time.Millisecond + t.Cleanup(func() { + reservationStateWaitTimeout = originalWaitTimeout + reservationStatePollDelay = originalPollDelay + }) + + releaseOpen := make(chan struct{}) + testContext.mockReservationClient.ExpectedCalls = nil + testContext.mockReservationClient.On( + "OpenReservation", mock.Anything, mock.Anything, mock.Anything, + ).Run(func(mock.Arguments) { + <-releaseOpen + }).Return( + &swapserverrpc.ServerOpenReservationResponse{}, nil, + ) + + reservationFSM, err := testContext.manager.newReservation( + t.Context(), uint32(testContext.mockLnd.Height), + &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + }, + ) + require.Error(t, err) + require.Nil(t, reservationFSM) + + testContext.manager.Lock() + activeFSM := testContext.manager.activeReservations[defaultReservationId] + testContext.manager.Unlock() + require.NotNil(t, activeFSM) + + close(releaseOpen) + require.NoError(t, activeFSM.DefaultObserver.WaitForState( + t.Context(), 5*time.Second, WaitForConfirmation, + )) +} + +// TestManagerRecoversAllPersistedReservations verifies that the cap applied to +// new notifications doesn't prevent the manager from resuming obligations +// already recorded in the database. The terminal transitions also exercise +// concurrent observer-driven removal from the active map. +func TestManagerRecoversAllPersistedReservations(t *testing.T) { + reservations := make([]*Reservation, maxActiveReservations+1) + for i := range reservations { + reservations[i] = &Reservation{ + ID: ID{ + byte(i), byte(i >> 8), byte(i >> 16), + }, + State: Init, + ProtocolVersion: ProtocolVersionServerInitiated, + } + } + + manager := NewManager(&Config{ + Store: &recoveryStore{reservations: reservations}, + }) + require.NoError(t, manager.RecoverReservations(t.Context())) + require.Eventually(t, func() bool { + manager.Lock() + defer manager.Unlock() + + return len(manager.activeReservations) == 0 + }, 5*time.Second, time.Millisecond) +} + +// TestUnlockTerminalReservationIsIdempotent verifies that cleanup can safely +// race with terminal-state eviction without masking the original swap result. +func TestUnlockTerminalReservationIsIdempotent(t *testing.T) { + testContext := newManagerTestContext(t) + storedReservation := &Reservation{ + ID: defaultReservationId, + State: Init, + ClientPubkey: defaultPubkey, + ServerPubkey: defaultPubkey, + Value: defaultValue, + Expiry: defaultExpiry, + ProtocolVersion: ProtocolVersionServerInitiated, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(KeyFamily), + Index: 1, + }, + } + + require.NoError(t, testContext.manager.cfg.Store.CreateReservation( + t.Context(), storedReservation, + )) + storedReservation.State = TimedOut + require.NoError(t, testContext.manager.cfg.Store.UpdateReservation( + t.Context(), storedReservation, + )) + + require.NoError(t, testContext.manager.UnlockReservation( + t.Context(), defaultReservationId, + )) + require.ErrorIs(t, testContext.manager.UnlockReservation( + t.Context(), ID{1}, + ), ErrReservationNotFound) +} + +type recoveryStore struct { + Store + + reservations []*Reservation +} + +func (s *recoveryStore) ListReservations(context.Context) ([]*Reservation, + error) { + + return s.reservations, nil +} + +func (s *recoveryStore) UpdateReservation(context.Context, + *Reservation) error { + + return nil +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { From 51eb8f410aa79b585bf8aa290b025de055c94e1f Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:35:33 +0200 Subject: [PATCH 30/36] reservation: validate confirmed output amounts Compare each confirmed transaction output with the expected reservation amount before advancing the state machine. --- instantout/reservation/actions_test.go | 10 +++++++++- instantout/reservation/manager_test.go | 1 + instantout/reservation/reservation.go | 11 +++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/instantout/reservation/actions_test.go b/instantout/reservation/actions_test.go index 40e6509b..643b8ce3 100644 --- a/instantout/reservation/actions_test.go +++ b/instantout/reservation/actions_test.go @@ -203,6 +203,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) { blockHeight int32 blockErr error sendTxConf bool + outputValue btcutil.Amount confErr error expectedEvent fsm.EventType }{ @@ -210,8 +211,15 @@ func TestSubscribeToConfirmationAction(t *testing.T) { name: "success", blockHeight: 0, sendTxConf: true, + outputValue: defaultValue, expectedEvent: OnConfirmed, }, + { + name: "reservation value mismatch", + sendTxConf: true, + outputValue: defaultValue - 1, + expectedEvent: fsm.OnError, + }, { name: "expired", blockHeight: 100, @@ -273,7 +281,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) { TxIn: []*wire.TxIn{}, TxOut: []*wire.TxOut{ { - Value: int64(defaultValue), + Value: int64(tc.outputValue), PkScript: pkScript, }, }, diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index ca3dec45..af014926 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -58,6 +58,7 @@ func TestManager(t *testing.T) { confTx := &wire.MsgTx{ TxOut: []*wire.TxOut{ { + Value: int64(defaultValue), PkScript: pkScript, }, }, diff --git a/instantout/reservation/reservation.go b/instantout/reservation/reservation.go index 5a167d2e..8b83ae33 100644 --- a/instantout/reservation/reservation.go +++ b/instantout/reservation/reservation.go @@ -142,8 +142,14 @@ func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint, return nil, err } + var foundScript bool for i, txOut := range tx.TxOut { if bytes.Equal(txOut.PkScript, pkScript) { + foundScript = true + if txOut.Value != int64(r.Value) { + continue + } + return &wire.OutPoint{ Hash: tx.TxHash(), Index: uint32(i), @@ -151,6 +157,11 @@ func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint, } } + if foundScript { + return nil, fmt.Errorf("reservation output value mismatch: "+ + "expected %d", r.Value) + } + return nil, errors.New("reservation output not found") } From b3776e92f45ff1c9cb61b7f2a3261e47eb3c7066 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:36:27 +0200 Subject: [PATCH 31/36] instantout: validate MuSig2 response dimensions Check nonce, signature, session, and transaction input counts before indexing signing vectors, returning clear errors for incomplete data. --- instantout/instantout.go | 38 ++++++++++++++++++++++++++++ instantout/instantout_test.go | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 instantout/instantout_test.go diff --git a/instantout/instantout.go b/instantout/instantout.go index f8c89eb0..40cbf12e 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -263,12 +263,31 @@ func (i *InstantOut) signMusig2Tx(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2sessions)) + } + if len(counterPartyNonces) != len(inputs) { + return nil, fmt.Errorf("invalid number of server nonces: "+ + "expected %d, got %d", len(inputs), len(counterPartyNonces)) + } prevOutFetcher := inputs.GetPrevoutFetcher() sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) sigs := make([][]byte, len(inputs)) for idx, reservation := range inputs { + if musig2sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint, reservation.Outpoint) { @@ -329,8 +348,27 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2Sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2Sessions)) + } + if len(serverSigs) != len(inputs) { + return nil, fmt.Errorf("invalid number of server signatures: "+ + "expected %d, got %d", len(inputs), len(serverSigs)) + } for idx := range inputs { + if musig2Sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + haveAllSigs, finalSig, err := signer.MuSig2CombineSig( ctx, musig2Sessions[idx].SessionID, [][]byte{serverSigs[idx]}, diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go new file mode 100644 index 00000000..6b0e7cb5 --- /dev/null +++ b/instantout/instantout_test.go @@ -0,0 +1,47 @@ +package instantout + +import ( + "context" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/instantout/reservation" + "github.com/lightningnetwork/lnd/input" + "github.com/stretchr/testify/require" +) + +// TestMuSig2VectorLengthValidation verifies that malformed server-controlled +// vectors are rejected before they can be indexed. +func TestMuSig2VectorLengthValidation(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{ + { + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + }, + }, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{}) + sessions := []*input.MuSig2SessionInfo{{}} + + require.NotPanics(t, func() { + _, err := instantOut.signMusig2Tx( + context.Background(), nil, tx, sessions, nil, + ) + require.ErrorContains(t, err, "server nonces") + }) + + require.NotPanics(t, func() { + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), nil, sessions, tx, nil, + ) + require.ErrorContains(t, err, "server signatures") + }) +} From 6853e69a05c7c253d1324ac616ccbe5ba4985668 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:36:57 +0200 Subject: [PATCH 32/36] instantout: verify finalized MuSig2 witnesses Run script validation for every combined signature before accepting a finalized transaction, surfacing invalid witnesses immediately. --- instantout/instantout.go | 17 ++++++++++++++++ instantout/instantout_test.go | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/instantout/instantout.go b/instantout/instantout.go index 40cbf12e..16f91d45 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -364,6 +364,9 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, "expected %d, got %d", len(inputs), len(serverSigs)) } + prevOutFetcher := inputs.GetPrevoutFetcher() + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) + for idx := range inputs { if musig2Sessions[idx] == nil { return nil, fmt.Errorf("MuSig2 session %d is nil", idx) @@ -382,6 +385,20 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, } tx.TxIn[idx].Witness = wire.TxWitness{finalSig} + + vm, err := txscript.NewEngine( + inputs[idx].PkScript, tx, idx, + txscript.StandardVerifyFlags, nil, sigHashes, + int64(inputs[idx].Value), prevOutFetcher, + ) + if err != nil { + return nil, fmt.Errorf("unable to verify final MuSig2 "+ + "signature for input %d: %w", idx, err) + } + if err := vm.Execute(); err != nil { + return nil, fmt.Errorf("invalid final MuSig2 signature "+ + "for input %d: %w", idx, err) + } } return tx, nil diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index 6b0e7cb5..f2ba3241 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -7,11 +7,22 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightningnetwork/lnd/input" "github.com/stretchr/testify/require" ) +type invalidFinalSigSigner struct { + lndclient.SignerClient +} + +func (s *invalidFinalSigSigner) MuSig2CombineSig(context.Context, [32]byte, + [][]byte) (bool, []byte, error) { + + return true, make([]byte, 64), nil +} + // TestMuSig2VectorLengthValidation verifies that malformed server-controlled // vectors are rejected before they can be indexed. func TestMuSig2VectorLengthValidation(t *testing.T) { @@ -45,3 +56,29 @@ func TestMuSig2VectorLengthValidation(t *testing.T) { require.ErrorContains(t, err, "server signatures") }) } + +// TestFinalizeMuSig2TransactionVerifiesSignature verifies that a combined +// signature is validated locally before the transaction can be used as the +// instant-out safety net. +func TestFinalizeMuSig2TransactionVerifiesSignature(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + res := &reservation.Reservation{ + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + } + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{res}, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{PreviousOutPoint: *res.Outpoint}) + tx.AddTxOut(&wire.TxOut{Value: 90_000}) + + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), &invalidFinalSigSigner{}, + []*input.MuSig2SessionInfo{{}}, tx, [][]byte{{1}}, + ) + require.ErrorContains(t, err, "invalid final MuSig2 signature") +} From b689e361c15acbe46f6836456df2e11288fb4b4c Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:37:46 +0200 Subject: [PATCH 33/36] instantout: close unfinished MuSig2 sessions Clean up abandoned signing sessions on error paths while leaving completed sessions to lnd. --- instantout/actions.go | 18 ++++++++++++++++ instantout/instantout.go | 39 ++++++++++++++++++++++++++++++++++- instantout/instantout_test.go | 33 ++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/instantout/actions.go b/instantout/actions.go index 25842652..fffe987c 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -293,6 +293,15 @@ func (f *FSM) BuildHTLCAction(ctx context.Context, } f.htlcMusig2Sessions = htlcSessions + defer func() { + err := cleanupMuSig2Sessions( + ctx, f.cfg.Signer, f.htlcMusig2Sessions, + ) + if err != nil { + f.Errorf("unable to clean up HTLC MuSig2 sessions: %v", err) + } + f.htlcMusig2Sessions = nil + }() // Send the server the client nonces. htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig( @@ -382,6 +391,15 @@ func (f *FSM) PushPreimageAction(ctx context.Context, } f.sweeplessSweepSessions = coopSessions + defer func() { + err := cleanupMuSig2Sessions( + ctx, f.cfg.Signer, f.sweeplessSweepSessions, + ) + if err != nil { + f.Errorf("unable to clean up sweep MuSig2 sessions: %v", err) + } + f.sweeplessSweepSessions = nil + }() // Get the feerate for the coop sweep. feeRate, err := f.cfg.Wallet.EstimateFeeRate(ctx, normalConfTarget) diff --git a/instantout/instantout.go b/instantout/instantout.go index 16f91d45..eade26e0 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" @@ -25,6 +26,8 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +const muSig2CleanupTimeout = 5 * time.Second + // InstantOut holds the necessary information to execute an instant out swap. type InstantOut struct { // SwapHash is the hash of the swap. @@ -112,7 +115,10 @@ func (i *InstantOut) createMusig2Session(ctx context.Context, for idx, reservation := range i.Reservations { session, err := reservation.Musig2CreateSession(ctx, signer) if err != nil { - return nil, nil, err + cleanupErr := cleanupMuSig2Sessions( + ctx, signer, musig2Sessions[:idx], + ) + return nil, nil, errors.Join(err, cleanupErr) } musig2Sessions[idx] = session @@ -122,6 +128,32 @@ func (i *InstantOut) createMusig2Session(ctx context.Context, return musig2Sessions, clientNonces, nil } +// cleanupMuSig2Sessions removes completed or abandoned MuSig2 sessions from +// lnd. Cleanup uses a bounded context that survives cancellation of the swap +// action that created the sessions. +func cleanupMuSig2Sessions(ctx context.Context, signer lndclient.SignerClient, + sessions []*input.MuSig2SessionInfo) error { + + cleanupCtx, cancel := context.WithTimeout( + context.WithoutCancel(ctx), muSig2CleanupTimeout, + ) + defer cancel() + + var cleanupErr error + for _, session := range sessions { + if session == nil { + continue + } + + err := signer.MuSig2Cleanup(cleanupCtx, session.SessionID) + if err != nil { + cleanupErr = errors.Join(cleanupErr, err) + } + } + + return cleanupErr +} + // getInputReservations returns the input reservations for the instant out. func (i *InstantOut) getInputReservations() (InputReservations, error) { if len(i.Reservations) == 0 { @@ -384,6 +416,11 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, return nil, fmt.Errorf("missing sigs") } + // lnd removes a MuSig2 session automatically once all signatures + // have been combined. Clear the local entry so the caller's deferred + // cleanup only targets sessions abandoned on an error path. + musig2Sessions[idx] = nil + tx.TxIn[idx].Witness = wire.TxWitness{finalSig} vm, err := txscript.NewEngine( diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index f2ba3241..fd45b788 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -23,6 +23,19 @@ func (s *invalidFinalSigSigner) MuSig2CombineSig(context.Context, [32]byte, return true, make([]byte, 64), nil } +type cleanupTrackingSigner struct { + lndclient.SignerClient + + cleaned [][32]byte +} + +func (s *cleanupTrackingSigner) MuSig2Cleanup(_ context.Context, + sessionID [32]byte) error { + + s.cleaned = append(s.cleaned, sessionID) + return nil +} + // TestMuSig2VectorLengthValidation verifies that malformed server-controlled // vectors are rejected before they can be indexed. func TestMuSig2VectorLengthValidation(t *testing.T) { @@ -76,9 +89,27 @@ func TestFinalizeMuSig2TransactionVerifiesSignature(t *testing.T) { tx.AddTxIn(&wire.TxIn{PreviousOutPoint: *res.Outpoint}) tx.AddTxOut(&wire.TxOut{Value: 90_000}) + sessions := []*input.MuSig2SessionInfo{{}} _, err := instantOut.finalizeMusig2Transaction( context.Background(), &invalidFinalSigSigner{}, - []*input.MuSig2SessionInfo{{}}, tx, [][]byte{{1}}, + sessions, tx, [][]byte{{1}}, ) require.ErrorContains(t, err, "invalid final MuSig2 signature") + require.Nil(t, sessions[0]) +} + +// TestCleanupMuSig2Sessions verifies that all allocated sessions are released +// while nil entries from partial session creation are skipped. +func TestCleanupMuSig2Sessions(t *testing.T) { + firstID := [32]byte{1} + secondID := [32]byte{2} + signer := &cleanupTrackingSigner{} + + err := cleanupMuSig2Sessions( + t.Context(), signer, []*input.MuSig2SessionInfo{ + {SessionID: firstID}, nil, {SessionID: secondID}, + }, + ) + require.NoError(t, err) + require.Equal(t, [][32]byte{firstID, secondID}, signer.cleaned) } From b596eabab3e27c927534ad100f2c359faccf1e4e Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:38:27 +0200 Subject: [PATCH 34/36] instantout: recheck reservation timing during recovery Refresh the chain height when a swap resumes, verify both the reservation and HTLC windows, and select the HTLC path when either remaining window is too short. --- instantout/actions.go | 47 ++++++++++++++++++++++ instantout/instantout_test.go | 76 +++++++++++++++++++++++++++++++++++ instantout/manager.go | 3 +- 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/instantout/actions.go b/instantout/actions.go index fffe987c..2d623811 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -51,6 +51,10 @@ const ( // htlcExpiryDelta is the delta in blocks we require between the htlc // expiry and reservation expiry. htlcExpiryDelta = int32(40) + + // htlcRecoverySafetyDelta leaves one urgent confirmation target for + // the HTLC and another for its preimage sweep after recovery. + htlcRecoverySafetyDelta = 2 * urgentConfTarget ) // InitInstantOutCtx contains the context for the InitInstantOutAction. @@ -63,6 +67,9 @@ type InitInstantOutCtx struct { sweepAddress btcutil.Address } +// RecoverInstantOutCtx marks an action as being resumed after restart. +type RecoverInstantOutCtx struct{} + // InitInstantOutAction is the first action that is executed when the instant // out FSM is started. It will send the instant out request to the server. func (f *FSM) InitInstantOutAction(ctx context.Context, @@ -382,6 +389,46 @@ func (f *FSM) BuildHTLCAction(ctx context.Context, func (f *FSM) PushPreimageAction(ctx context.Context, eventCtx fsm.EventContext) fsm.EventType { + // A recovered swap may have been offline long enough that the server's + // reservation timeout is now close. Fall back to the already finalized + // HTLC instead of revealing the preimage without enough time to publish + // that safety transaction. + if _, ok := eventCtx.(*RecoverInstantOutCtx); ok { + info, err := f.cfg.LndClient.GetInfo(ctx) + if err != nil { + f.LastActionError = fmt.Errorf( + "unable to get recovery chain height: %w", err, + ) + + return OnErrorPublishHtlc + } + + currentHeight := int64(info.BlockHeight) + minHtlcExpiry := currentHeight + + int64(htlcRecoverySafetyDelta) + if int64(f.InstantOut.CltvExpiry) < minHtlcExpiry { + f.LastActionError = fmt.Errorf("instant out HTLC expires at "+ + "height %d, before recovery safety height %d", + f.InstantOut.CltvExpiry, minHtlcExpiry) + + return OnErrorPublishHtlc + } + + minReservationExpiry := currentHeight + + int64(htlcExpiryDelta) + for _, res := range f.InstantOut.Reservations { + if int64(res.Expiry) >= minReservationExpiry { + continue + } + + f.LastActionError = fmt.Errorf("reservation %x expires at "+ + "height %d, before recovery safety height %d", + res.ID, res.Expiry, minReservationExpiry) + + return OnErrorPublishHtlc + } + } + // First we'll create the musig2 context. coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session( ctx, f.cfg.Signer, diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index fd45b788..48195c32 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightningnetwork/lnd/input" "github.com/stretchr/testify/require" @@ -29,6 +30,23 @@ type cleanupTrackingSigner struct { cleaned [][32]byte } +type fixedHeightLightningClient struct { + lndclient.LightningClient + + height uint32 + err error +} + +func (c *fixedHeightLightningClient) GetInfo(context.Context) ( + *lndclient.Info, error) { + + if c.err != nil { + return nil, c.err + } + + return &lndclient.Info{BlockHeight: c.height}, nil +} + func (s *cleanupTrackingSigner) MuSig2Cleanup(_ context.Context, sessionID [32]byte) error { @@ -113,3 +131,61 @@ func TestCleanupMuSig2Sessions(t *testing.T) { require.NoError(t, err) require.Equal(t, [][32]byte{firstID, secondID}, signer.cleaned) } + +// TestPushPreimageRejectsExpiringReservation verifies that recovery takes the +// on-chain fallback before revealing the preimage when a reservation is too +// close to its server-controlled timeout. +func TestPushPreimageRejectsExpiringReservation(t *testing.T) { + instantOutFSM := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + LndClient: &fixedHeightLightningClient{height: 100}, + }, + InstantOut: &InstantOut{ + CltvExpiry: 200, + Reservations: []*reservation.Reservation{ + { + ID: reservation.ID{1}, + Expiry: 139, + }, + }, + }, + } + + event := instantOutFSM.PushPreimageAction( + t.Context(), &RecoverInstantOutCtx{}, + ) + require.Equal(t, OnErrorPublishHtlc, event) + require.ErrorContains( + t, instantOutFSM.LastActionError, "before recovery safety height", + ) +} + +// TestPushPreimageRejectsExpiringHtlc verifies that recovery uses a fresh +// chain height and leaves time to confirm both fallback transactions. +func TestPushPreimageRejectsExpiringHtlc(t *testing.T) { + instantOutFSM := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + LndClient: &fixedHeightLightningClient{height: 100}, + }, + InstantOut: &InstantOut{ + CltvExpiry: 105, + Reservations: []*reservation.Reservation{ + { + ID: reservation.ID{1}, + Expiry: 200, + }, + }, + }, + } + + event := instantOutFSM.PushPreimageAction( + t.Context(), &RecoverInstantOutCtx{}, + ) + require.Equal(t, OnErrorPublishHtlc, event) + require.ErrorContains( + t, instantOutFSM.LastActionError, + "instant out HTLC expires at height 105", + ) +} diff --git a/instantout/manager.go b/instantout/manager.go index 37ccb681..0d806251 100644 --- a/instantout/manager.go +++ b/instantout/manager.go @@ -119,8 +119,9 @@ func (m *Manager) recoverInstantOuts(ctx context.Context) error { // As SendEvent can block, we'll start a goroutine to process // the event. + recoverCtx := &RecoverInstantOutCtx{} go func() { - err := instantOutFSM.SendEvent(ctx, OnRecover, nil) + err := instantOutFSM.SendEvent(ctx, OnRecover, recoverCtx) if err != nil { log.Errorf("FSM %v Error sending recover "+ "event %v, state: %v", From e772f8ccfa204e8882278d447e8e638fb40565e0 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:43:16 +0200 Subject: [PATCH 35/36] instantout: enforce the accepted swap fee Carry the accepted quote into each request, persist it, and reject invoices above that limit. Preserve compatibility for requests that omit the cap while distinguishing an explicit zero. --- cmd/loop/instantout.go | 5 ++ .../instantout/02_loop-instantout.json | 3 +- .../07_loop-instantout-channel.json | 3 +- .../08_loop-instantout-select-index.json | 3 +- instantout/actions.go | 50 ++++++++++++- instantout/instantout.go | 3 + instantout/instantout_test.go | 70 +++++++++++++++++++ instantout/manager.go | 31 +++++++- instantout/store.go | 3 +- loopd/swapclient_server.go | 9 ++- looprpc/client.pb.go | 45 +++++++++++- looprpc/client.proto | 10 +++ looprpc/client.swagger.json | 5 ++ looprpc/client_test.go | 32 +++++++++ 14 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 looprpc/client_test.go diff --git a/cmd/loop/instantout.go b/cmd/loop/instantout.go index 9783f005..3cbcda08 100644 --- a/cmd/loop/instantout.go +++ b/cmd/loop/instantout.go @@ -183,6 +183,8 @@ func instantOut(ctx context.Context, cmd *cli.Command) error { fmt.Println("Starting instant swap out") + maxSwapFee := quote.ServiceFeeSat + // Now we can request the instant out swap. instantOutRes, err := client.InstantOut( ctx, @@ -190,6 +192,9 @@ func instantOut(ctx context.Context, cmd *cli.Command) error { ReservationIds: selectedReservations, OutgoingChanSet: outgoingChanSet, DestAddr: cmd.String("addr"), + MaxSwapFee: &looprpc.InstantOutRequest_MaxSwapFeeSat{ + MaxSwapFeeSat: maxSwapFee, + }, }, ) if err != nil { diff --git a/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json b/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json index 6dae20b1..257f27c7 100644 --- a/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json +++ b/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json @@ -139,7 +139,8 @@ "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=" ], "outgoing_chan_set": [], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "4800" } } }, diff --git a/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json b/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json index 205ae43e..07038f8e 100644 --- a/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json +++ b/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json @@ -148,7 +148,8 @@ "outgoing_chan_set": [ "125344325763072" ], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "3200" } } }, diff --git a/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json b/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json index 9a6d103b..89f13c31 100644 --- a/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json +++ b/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json @@ -162,7 +162,8 @@ "cSfKVONNmsK9+p4Uc5nc3ZtE+37uOODHeq1vprhh/x4=" ], "outgoing_chan_set": [], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "1600" } } }, diff --git a/instantout/actions.go b/instantout/actions.go index 2d623811..329add5e 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" ) const ( @@ -65,6 +66,7 @@ type InitInstantOutCtx struct { outgoingChanSet loopdb.ChannelSet protocolVersion ProtocolVersion sweepAddress btcutil.Address + maxSwapFee *btcutil.Amount } // RecoverInstantOutCtx marks an action as being resumed after restart. @@ -85,7 +87,7 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, } var ( - reservationAmt uint64 + reservationAmt btcutil.Amount reservationIds = make([][]byte, 0, len(initCtx.reservations)) reservations = make( []*reservation.Reservation, 0, len(initCtx.reservations), @@ -106,7 +108,7 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, "locked", reservationId)) } - reservationAmt += uint64(res.Value) + reservationAmt += res.Value reservationIds = append(reservationIds, resId[:]) reservations = append(reservations, res) @@ -168,6 +170,11 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+ "expected %x got %x", preimage.Hash(), payReq.Hash)) } + if err := validateInstantOutInvoiceAmount( + payReq.Value, reservationAmt, initCtx.maxSwapFee, + ); err != nil { + return f.HandleError(err) + } serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey) if err != nil { return f.HandleError(err) @@ -186,6 +193,11 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, } // Now we can create the instant out. + var maxSwapFee btcutil.Amount + if initCtx.maxSwapFee != nil { + maxSwapFee = *initCtx.maxSwapFee + } + instantOut := &InstantOut{ SwapHash: swapHash, swapPreimage: preimage, @@ -195,7 +207,8 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, CltvExpiry: initCtx.cltvExpiry, clientPubkey: keyRes.PubKey, serverPubkey: serverPubkey, - Value: btcutil.Amount(reservationAmt), + Value: reservationAmt, + MaxSwapFee: maxSwapFee, htlcFeeRate: feeRate, swapInvoice: instantOutResponse.SwapInvoice, Reservations: reservations, @@ -213,6 +226,37 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, return OnInit } +// validateInstantOutInvoiceAmount verifies that the server invoice doesn't +// charge more than the client-approved swap fee. Sub-satoshi fees are rounded +// up so the cap cannot be bypassed with millisatoshi precision. +func validateInstantOutInvoiceAmount(invoiceAmount lnwire.MilliSatoshi, + swapAmount btcutil.Amount, maxSwapFee *btcutil.Amount) error { + + // Omitting the cap preserves the behavior of clients that predate this + // field. In-tree callers set it explicitly after accepting a quote. + if maxSwapFee == nil { + return nil + } + + if *maxSwapFee < 0 { + return fmt.Errorf("maximum swap fee must not be negative") + } + + swapAmountMsat := lnwire.NewMSatFromSatoshis(swapAmount) + if invoiceAmount <= swapAmountMsat { + return nil + } + + swapFeeMsat := invoiceAmount - swapAmountMsat + swapFeeSat := btcutil.Amount((int64(swapFeeMsat)-1)/1000 + 1) + if swapFeeSat > *maxSwapFee { + return fmt.Errorf("instant out swap fee %d exceeds maximum %d", + swapFeeSat, *maxSwapFee) + } + + return nil +} + // PollPaymentAcceptedAction locks the reservations, sends the payment to the // server and polls the server for the payment status. func (f *FSM) PollPaymentAcceptedAction(ctx context.Context, diff --git a/instantout/instantout.go b/instantout/instantout.go index eade26e0..c700ee91 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -60,6 +60,9 @@ type InstantOut struct { // Value is the amount that is swapped. Value btcutil.Amount + // MaxSwapFee is the maximum off-chain swap fee accepted by the client. + MaxSwapFee btcutil.Amount + // keyLocator is the key locator that is used for the swap. keyLocator keychain.KeyLocator diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index 48195c32..66301a22 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -11,6 +11,7 @@ import ( "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -189,3 +190,72 @@ func TestPushPreimageRejectsExpiringHtlc(t *testing.T) { "instant out HTLC expires at height 105", ) } + +// TestValidateInstantOutInvoiceAmount verifies enforcement of the fee cap at +// millisatoshi precision. +func TestValidateInstantOutInvoiceAmount(t *testing.T) { + const swapAmount = btcutil.Amount(100_000) + + maxSwapFee := btcutil.Amount(200) + zeroSwapFee := btcutil.Amount(0) + negativeSwapFee := btcutil.Amount(-1) + + tests := []struct { + name string + invoiceAmount lnwire.MilliSatoshi + maxSwapFee *btcutil.Amount + expectErr bool + }{ + { + name: "exact fee cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount + maxSwapFee, + ), + maxSwapFee: &maxSwapFee, + }, + { + name: "one millisatoshi over fee cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount+maxSwapFee, + ) + 1, + maxSwapFee: &maxSwapFee, + expectErr: true, + }, + { + name: "discounted invoice", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount - 1, + ), + maxSwapFee: &zeroSwapFee, + }, + { + name: "negative cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount, + ), + maxSwapFee: &negativeSwapFee, + expectErr: true, + }, + { + name: "omitted cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount + maxSwapFee + 1, + ), + maxSwapFee: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validateInstantOutInvoiceAmount( + tc.invoiceAmount, swapAmount, tc.maxSwapFee, + ) + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} diff --git a/instantout/manager.go b/instantout/manager.go index 0d806251..e9ecb352 100644 --- a/instantout/manager.go +++ b/instantout/manager.go @@ -20,6 +20,20 @@ var ( ErrSwapDoesNotExist = errors.New("swap does not exist") ) +type newInstantOutOptions struct { + maxSwapFee *btcutil.Amount +} + +// NewInstantOutOption customizes an instant out request. +type NewInstantOutOption func(*newInstantOutOptions) + +// WithMaxSwapFee limits the off-chain fee accepted for an instant out. +func WithMaxSwapFee(maxSwapFee btcutil.Amount) NewInstantOutOption { + return func(options *newInstantOutOptions) { + options.maxSwapFee = &maxSwapFee + } +} + // Manager manages the instantout state machines. type Manager struct { sync.Mutex @@ -136,7 +150,21 @@ func (m *Manager) recoverInstantOuts(ctx context.Context) error { // NewInstantOut creates a new instantout. func (m *Manager) NewInstantOut(ctx context.Context, - reservations []reservation.ID, sweepAddress string) (*FSM, error) { + reservations []reservation.ID, sweepAddress string, + options ...NewInstantOutOption) (*FSM, error) { + + requestOptions := &newInstantOutOptions{} + for _, option := range options { + if option != nil { + option(requestOptions) + } + } + + if requestOptions.maxSwapFee != nil && + *requestOptions.maxSwapFee < 0 { + + return nil, fmt.Errorf("maximum swap fee must not be negative") + } var ( sweepAddr btcutil.Address @@ -159,6 +187,7 @@ func (m *Manager) NewInstantOut(ctx context.Context, initationHeight: m.currentHeight, protocolVersion: CurrentProtocolVersion(), sweepAddress: sweepAddr, + maxSwapFee: requestOptions.maxSwapFee, } instantOut, err := NewFSM(m.cfg, ProtocolVersionFullReservation) diff --git a/instantout/store.go b/instantout/store.go index 25d7fe70..0e2a5066 100644 --- a/instantout/store.go +++ b/instantout/store.go @@ -104,7 +104,7 @@ func (s *SQLStore) CreateInstantLoopOut(ctx context.Context, AmountRequested: int64(instantOut.Value), CltvExpiry: instantOut.CltvExpiry, MaxMinerFee: 0, - MaxSwapFee: 0, + MaxSwapFee: int64(instantOut.MaxSwapFee), InitiationHeight: instantOut.initiationHeight, ProtocolVersion: int32(instantOut.protocolVersion), Label: "", @@ -368,6 +368,7 @@ func (s *SQLStore) sqlInstantOutToInstantOut(ctx context.Context, protocolVersion: ProtocolVersion(row.ProtocolVersion), initiationHeight: row.InitiationHeight, Value: btcutil.Amount(row.AmountRequested), + MaxSwapFee: btcutil.Amount(row.MaxSwapFee), keyLocator: keychain.KeyLocator{ Family: keychain.KeyFamily(row.ClientKeyFamily), Index: uint32(row.ClientKeyIndex), diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index a0559d41..497fe3a8 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -1763,8 +1763,15 @@ func (s *swapClientServer) InstantOut(ctx context.Context, reservationIds[i] = resId } + var options []instantout.NewInstantOutOption + if req.GetMaxSwapFee() != nil { + options = append(options, instantout.WithMaxSwapFee( + btcutil.Amount(req.GetMaxSwapFeeSat()), + )) + } + instantOutFsm, err := s.instantOutManager.NewInstantOut( - ctx, reservationIds, req.DestAddr, + ctx, reservationIds, req.DestAddr, options..., ) if err != nil { return nil, err diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index ec324648..04b35f1e 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -4467,7 +4467,11 @@ type InstantOutRequest struct { OutgoingChanSet []uint64 `protobuf:"varint,2,rep,packed,name=outgoing_chan_set,json=outgoingChanSet,proto3" json:"outgoing_chan_set,omitempty"` // An optional address to sweep the onchain funds to. If not set, the funds // will be swept to the wallet's internal address. - DestAddr string `protobuf:"bytes,3,opt,name=dest_addr,json=destAddr,proto3" json:"dest_addr,omitempty"` + DestAddr string `protobuf:"bytes,3,opt,name=dest_addr,json=destAddr,proto3" json:"dest_addr,omitempty"` + // Types that are valid to be assigned to MaxSwapFee: + // + // *InstantOutRequest_MaxSwapFeeSat + MaxSwapFee isInstantOutRequest_MaxSwapFee `protobuf_oneof:"max_swap_fee"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4523,6 +4527,36 @@ func (x *InstantOutRequest) GetDestAddr() string { return "" } +func (x *InstantOutRequest) GetMaxSwapFee() isInstantOutRequest_MaxSwapFee { + if x != nil { + return x.MaxSwapFee + } + return nil +} + +func (x *InstantOutRequest) GetMaxSwapFeeSat() int64 { + if x != nil { + if x, ok := x.MaxSwapFee.(*InstantOutRequest_MaxSwapFeeSat); ok { + return x.MaxSwapFeeSat + } + } + return 0 +} + +type isInstantOutRequest_MaxSwapFee interface { + isInstantOutRequest_MaxSwapFee() +} + +type InstantOutRequest_MaxSwapFeeSat struct { + // The maximum off-chain swap fee that may be charged for the swap. If + // this field is omitted, no fee cap is applied for compatibility with + // clients that predate this field. An explicitly set value of zero + // rejects any positive swap fee. + MaxSwapFeeSat int64 `protobuf:"varint,4,opt,name=max_swap_fee_sat,json=maxSwapFeeSat,proto3,oneof"` +} + +func (*InstantOutRequest_MaxSwapFeeSat) isInstantOutRequest_MaxSwapFee() {} + type InstantOutResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The hash of the swap preimage. @@ -6964,11 +6998,13 @@ const file_client_proto_rawDesc = "" + "\x06amount\x18\x03 \x01(\x04R\x06amount\x12\x13\n" + "\x05tx_id\x18\x04 \x01(\tR\x04txId\x12\x12\n" + "\x04vout\x18\x05 \x01(\rR\x04vout\x12\x16\n" + - "\x06expiry\x18\x06 \x01(\rR\x06expiry\"\x85\x01\n" + + "\x06expiry\x18\x06 \x01(\rR\x06expiry\"\xc0\x01\n" + "\x11InstantOutRequest\x12'\n" + "\x0freservation_ids\x18\x01 \x03(\fR\x0ereservationIds\x12*\n" + "\x11outgoing_chan_set\x18\x02 \x03(\x04R\x0foutgoingChanSet\x12\x1b\n" + - "\tdest_addr\x18\x03 \x01(\tR\bdestAddr\"t\n" + + "\tdest_addr\x18\x03 \x01(\tR\bdestAddr\x12)\n" + + "\x10max_swap_fee_sat\x18\x04 \x01(\x03H\x00R\rmaxSwapFeeSatB\x0e\n" + + "\fmax_swap_fee\"t\n" + "\x12InstantOutResponse\x12(\n" + "\x10instant_out_hash\x18\x01 \x01(\fR\x0einstantOutHash\x12\x1e\n" + "\vsweep_tx_id\x18\x02 \x01(\tR\tsweepTxId\x12\x14\n" + @@ -7490,6 +7526,9 @@ func file_client_proto_init() { (*SweepHtlcResponse_Published)(nil), (*SweepHtlcResponse_Failed)(nil), } + file_client_proto_msgTypes[48].OneofWrappers = []any{ + (*InstantOutRequest_MaxSwapFeeSat)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/looprpc/client.proto b/looprpc/client.proto index 844dade7..f2a592f1 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -1685,6 +1685,16 @@ message InstantOutRequest { will be swept to the wallet's internal address. */ string dest_addr = 3; + + oneof max_swap_fee { + /* + The maximum off-chain swap fee that may be charged for the swap. If + this field is omitted, no fee cap is applied for compatibility with + clients that predate this field. An explicitly set value of zero + rejects any positive swap fee. + */ + int64 max_swap_fee_sat = 4; + } } message InstantOutResponse { diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 1c94febb..57d6dfd6 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -1917,6 +1917,11 @@ "dest_addr": { "type": "string", "description": "An optional address to sweep the onchain funds to. If not set, the funds\nwill be swept to the wallet's internal address." + }, + "max_swap_fee_sat": { + "type": "string", + "format": "int64", + "description": "The maximum off-chain swap fee that may be charged for the swap. If\nthis field is omitted, no fee cap is applied for compatibility with\nclients that predate this field. An explicitly set value of zero\nrejects any positive swap fee." } } }, diff --git a/looprpc/client_test.go b/looprpc/client_test.go new file mode 100644 index 00000000..4a19c68f --- /dev/null +++ b/looprpc/client_test.go @@ -0,0 +1,32 @@ +package looprpc + +import ( + "testing" + + "google.golang.org/protobuf/proto" +) + +// TestInstantOutMaxSwapFeePresence verifies that an omitted fee cap remains +// distinguishable from an explicitly encoded zero while retaining the scalar +// field's original wire representation. +func TestInstantOutMaxSwapFeePresence(t *testing.T) { + request := &InstantOutRequest{} + if err := proto.Unmarshal(nil, request); err != nil { + t.Fatalf("unable to unmarshal omitted cap: %v", err) + } + if request.GetMaxSwapFee() != nil { + t.Fatal("omitted cap unexpectedly has presence") + } + + // Field four, encoded as a varint with value zero. This is the same wire + // representation used before the field gained presence semantics. + if err := proto.Unmarshal([]byte{0x20, 0x00}, request); err != nil { + t.Fatalf("unable to unmarshal explicit zero cap: %v", err) + } + if request.GetMaxSwapFee() == nil { + t.Fatal("explicit zero cap lost presence") + } + if request.GetMaxSwapFeeSat() != 0 { + t.Fatalf("expected zero cap, got %d", request.GetMaxSwapFeeSat()) + } +} From f826260266a6e064c26795551f17518a91864431 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:43:54 +0200 Subject: [PATCH 36/36] docs: document instant out reliability improvements Record the Instant Out and reservation validation, recovery, fee-limit, lifecycle, and custom macaroon updates in the next release notes. --- docs/release-notes/release-notes-next.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index fb302d1e..1f889227 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -4,12 +4,21 @@ #### Breaking Changes +* Instant Out and reservation RPCs now require the `loop:out` permission. + Operators using custom scoped macaroons must rebake them before calling + `ListReservations`, `InstantOut`, `InstantOutQuote`, or `ListInstantOuts`. + [PR #1194](https://github.com/lightninglabs/loop/pull/1194) + #### Bug Fixes * Loop Out requests now account for channel reserves when checking outbound capacity, preventing swaps from starting when their off-chain payment cannot be funded. +* Improved Instant Out and reservation validation, lifecycle cleanup, recovery + timing, fee limits, and macaroon permissions. + [PR #1194](https://github.com/lightninglabs/loop/pull/1194) + * Taproot Asset Loop Out handling now validates RFQ timeouts and asset rates, keeps cached asset-name lookups responsive during slow `tapd` queries, and closes `tapd` connections cleanly during shutdown and startup failures.