mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
loopd: validate static loop-in labels at rpc
Move static loop-in label validation to the rpc boundary and remove the same check from the internal manager path. This keeps external requests aligned with the existing swap rpc surface while allowing internal autoloop callers to keep using reserved labels for automated swaps. The tests cover both sides of that contract: rpc requests still reject reserved labels, and the manager path accepts them.
This commit is contained in:
parent
ac58336104
commit
562416e99b
4 changed files with 119 additions and 10 deletions
|
|
@ -2097,6 +2097,13 @@ func (s *swapClientServer) StaticAddressLoopIn(ctx context.Context,
|
|||
Fast: in.Fast,
|
||||
}
|
||||
|
||||
// External callers must not be able to use reserved autoloop labels.
|
||||
// Internal autoloop dispatch bypasses this RPC and can still use the
|
||||
// reserved labels needed to attribute automated swaps correctly.
|
||||
if err := labels.Validate(req.Label); err != nil {
|
||||
return nil, fmt.Errorf("invalid label: %w", err)
|
||||
}
|
||||
|
||||
if in.LastHop != nil {
|
||||
lastHop, err := route.NewVertexFromBytes(in.LastHop)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -262,6 +262,24 @@ func TestValidateLoopInRequest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestStaticAddressLoopInRejectsReservedLabel verifies that external static
|
||||
// loop-in requests still reject reserved autoloop labels at the RPC boundary.
|
||||
func TestStaticAddressLoopInRejectsReservedLabel(t *testing.T) {
|
||||
logger := btclog.NewSLogger(
|
||||
btclog.NewDefaultHandler(os.Stdout),
|
||||
)
|
||||
setLogger(logger.SubSystem(Subsystem))
|
||||
|
||||
server := &swapClientServer{}
|
||||
|
||||
_, err := server.StaticAddressLoopIn(
|
||||
t.Context(), &looprpc.StaticAddressLoopInRequest{
|
||||
Label: labels.AutoloopLabel(swap.TypeIn),
|
||||
},
|
||||
)
|
||||
require.ErrorContains(t, err, labels.ErrReservedPrefix.Error())
|
||||
}
|
||||
|
||||
// TestSwapClientServerStopDaemon ensures that calling StopDaemon triggers the
|
||||
// daemon shutdown.
|
||||
func TestSwapClientServerStopDaemon(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/labels"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/staticaddr/staticutil"
|
||||
|
|
@ -692,12 +691,6 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
|
|||
err)
|
||||
}
|
||||
|
||||
// Check that the label is valid.
|
||||
err = labels.Validate(req.Label)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid label: %w", err)
|
||||
}
|
||||
|
||||
// Private and route hints are mutually exclusive as setting private
|
||||
// means we retrieve our own route hints from the connected node.
|
||||
if len(req.RouteHints) != 0 && req.Private {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,21 @@ package loopin
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/labels"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -176,8 +182,46 @@ func TestSelectDeposits(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestInitiateLoopInAllowsReservedAutoloopLabel verifies that the internal
|
||||
// loop-in manager path does not reject reserved autoloop labels. The RPC
|
||||
// boundary owns that validation, while internal autoloop dispatch must be able
|
||||
// to reuse the reserved labels directly.
|
||||
func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
selectedDeposit := makeDeposit(1, 0, 9_000)
|
||||
selectedOutpoint := selectedDeposit.OutPoint.String()
|
||||
quoteErr := errors.New("quote failed")
|
||||
quoteGetter := &mockQuoteGetter{
|
||||
err: quoteErr,
|
||||
}
|
||||
|
||||
manager, err := NewManager(&Config{
|
||||
DepositManager: &mockDepositManager{
|
||||
byOutpoint: map[string]*deposit.Deposit{
|
||||
selectedOutpoint: selectedDeposit,
|
||||
},
|
||||
},
|
||||
QuoteGetter: quoteGetter,
|
||||
NodePubkey: route.Vertex{2},
|
||||
}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = manager.initiateLoopIn(ctx, &loop.StaticAddressLoopInRequest{
|
||||
DepositOutpoints: []string{selectedOutpoint},
|
||||
SelectedAmount: selectedDeposit.Value,
|
||||
MaxSwapFee: 1_000,
|
||||
Label: labels.AutoloopLabel(swap.TypeIn),
|
||||
Initiator: "autoloop",
|
||||
})
|
||||
require.ErrorIs(t, err, quoteErr)
|
||||
require.NotContains(t, err.Error(), labels.ErrReservedPrefix.Error())
|
||||
require.Equal(t, selectedDeposit.Value, quoteGetter.amount)
|
||||
}
|
||||
|
||||
// mockDepositManager implements DepositManager for tests.
|
||||
type mockDepositManager struct {
|
||||
// byOutpoint maps outpoint strings to deposits for direct lookups.
|
||||
byOutpoint map[string]*deposit.Deposit
|
||||
}
|
||||
|
||||
|
|
@ -187,10 +231,28 @@ func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockDepositManager) AllStringOutpointsActiveDeposits(_ []string,
|
||||
_ fsm.StateType) ([]*deposit.Deposit, bool) {
|
||||
func (m *mockDepositManager) AllStringOutpointsActiveDeposits(outpoints []string,
|
||||
state fsm.StateType) ([]*deposit.Deposit, bool) {
|
||||
|
||||
return nil, false
|
||||
if state != deposit.Deposited {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if m.byOutpoint == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
res := make([]*deposit.Deposit, 0, len(outpoints))
|
||||
for _, outpoint := range outpoints {
|
||||
selectedDeposit, ok := m.byOutpoint[outpoint]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
res = append(res, selectedDeposit)
|
||||
}
|
||||
|
||||
return res, true
|
||||
}
|
||||
|
||||
func (m *mockDepositManager) TransitionDeposits(_ context.Context,
|
||||
|
|
@ -217,6 +279,35 @@ func (m *mockDepositManager) GetActiveDepositsInState(_ fsm.StateType) (
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// mockQuoteGetter returns either a configured quote or a configured error and
|
||||
// records the quoted amount for assertions.
|
||||
type mockQuoteGetter struct {
|
||||
// err is the optional error returned from GetLoopInQuote.
|
||||
err error
|
||||
|
||||
// amount records the quoted amount.
|
||||
amount btcutil.Amount
|
||||
}
|
||||
|
||||
// GetLoopInQuote returns the configured quote result for tests.
|
||||
func (m *mockQuoteGetter) GetLoopInQuote(_ context.Context,
|
||||
amt btcutil.Amount, _ route.Vertex, lastHop *route.Vertex,
|
||||
_ [][]zpay32.HopHint, initiator string, numDeposits uint32,
|
||||
fast bool) (*loop.LoopInQuote, error) {
|
||||
|
||||
m.amount = amt
|
||||
_ = lastHop
|
||||
_ = initiator
|
||||
_ = numDeposits
|
||||
_ = fast
|
||||
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
return &loop.LoopInQuote{}, nil
|
||||
}
|
||||
|
||||
// mockStore implements StaticAddressLoopInStore for tests.
|
||||
type mockStore struct {
|
||||
loopIns map[lntypes.Hash]*StaticAddressLoopIn
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue