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:
Boris Nagaev 2026-04-13 00:09:14 -05:00
parent ac58336104
commit 562416e99b
No known key found for this signature in database
4 changed files with 119 additions and 10 deletions

View file

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

View file

@ -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) {

View file

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

View file

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