loop/staticaddr/address/manager_test.go
Boris Nagaev d554b42ef6
multi: migrate to btcd v2 modules
Update LND, Aperture, and Taproot Assets to revisions using the
btcd v2 modules, and update lndclient to v0.21.0-3. Migrate Loop
chain, transaction, and address types to their corresponding v2
packages.

The lndclient release includes the migration from:
https://github.com/lightninglabs/lndclient/pull/280

Taproot Assets is temporarily replaced with its btcd v2 revision
because the v0.8 release branch has not adopted the new modules.

This raises the minimum Go version to 1.26 and changes exported
address types.
2026-08-12 23:39:26 +00:00

331 lines
9.3 KiB
Go

package address
import (
"context"
"encoding/hex"
"testing"
btcaddr "github.com/btcsuite/btcd/address/v2"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
var (
defaultServerPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d")
defaultServerPubkey, _ = btcec.ParsePubKey(defaultServerPubkeyBytes)
defaultExpiry = uint32(100)
)
type mockStaticAddressClient struct {
mock.Mock
}
func (m *mockStaticAddressClient) ServerStaticAddressLoopIn(ctx context.Context,
in *swapserverrpc.ServerStaticAddressLoopInRequest,
opts ...grpc.CallOption) (
*swapserverrpc.ServerStaticAddressLoopInResponse, error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.ServerStaticAddressLoopInResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) PushStaticAddressSweeplessSigs(ctx context.Context,
in *swapserverrpc.PushStaticAddressSweeplessSigsRequest,
opts ...grpc.CallOption) (
*swapserverrpc.PushStaticAddressSweeplessSigsResponse, error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.PushStaticAddressSweeplessSigsResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) PushStaticAddressHtlcSigs(ctx context.Context,
in *swapserverrpc.PushStaticAddressHtlcSigsRequest,
opts ...grpc.CallOption) (
*swapserverrpc.PushStaticAddressHtlcSigsResponse, error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.PushStaticAddressHtlcSigsResponse),
args.Error(1)
}
// ServerWithdrawDeposits implements the deprecated RPC required by the
// generated client interface. Production code uses ServerPsbtWithdrawDeposits.
//
//nolint:staticcheck
func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context,
in *swapserverrpc.ServerWithdrawRequest,
opts ...grpc.CallOption) (*swapserverrpc.ServerWithdrawResponse,
error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.ServerWithdrawResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) ServerPsbtWithdrawDeposits(ctx context.Context,
in *swapserverrpc.ServerPsbtWithdrawRequest,
opts ...grpc.CallOption) (*swapserverrpc.ServerPsbtWithdrawResponse,
error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.ServerPsbtWithdrawResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) (
*swapserverrpc.ServerNewAddressResponse, error) {
args := m.Called(ctx, in, opts)
resp, _ := args.Get(0).(*swapserverrpc.ServerNewAddressResponse)
return resp, args.Error(1)
}
// TestManager tests the static address manager generates the corerct static
// taproot address from the given test parameters.
func TestManager(t *testing.T) {
ctxb := t.Context()
testContext := NewAddressManagerTestContext(t)
// Start the manager.
initChan := make(chan struct{})
go func() {
err := testContext.manager.Run(ctxb, initChan)
require.ErrorIs(t, err, context.Canceled)
}()
<-initChan
// Create the expected static address.
expectedAddress, err := GenerateExpectedTaprootAddress(testContext)
require.NoError(t, err)
// Create a new static address.
taprootAddress, expiry, err := testContext.manager.NewAddress(ctxb)
require.NoError(t, err)
// The addresses have to match.
require.Equal(t, expectedAddress.String(), taprootAddress.String())
// The expiry has to match.
require.EqualValues(t, defaultExpiry, expiry)
}
// TestNewAddressValidatesServerResponse tests that the untrusted
// ServerNewAddress response is validated before the address script is created.
func TestNewAddressValidatesServerResponse(t *testing.T) {
tests := []struct {
name string
resp *swapserverrpc.ServerNewAddressResponse
expected string
}{
{
name: "nil response",
expected: "missing server new address response",
},
{
name: "nil params",
resp: &swapserverrpc.ServerNewAddressResponse{},
expected: "missing server address parameters",
},
{
name: "missing server key",
resp: &swapserverrpc.ServerNewAddressResponse{
Params: &swapserverrpc.ServerAddressParameters{
Expiry: defaultExpiry,
},
},
expected: "missing server public key",
},
{
name: "uncompressed server key",
resp: &swapserverrpc.ServerNewAddressResponse{
Params: &swapserverrpc.ServerAddressParameters{
ServerKey: []byte{0x04},
Expiry: defaultExpiry,
},
},
expected: "server public key is not a compressed",
},
{
name: "zero expiry",
resp: newServerNewAddressResponse(0),
expected: "static address CSV expiry must be non-zero",
},
{
name: "seconds flag",
resp: newServerNewAddressResponse(
wire.SequenceLockTimeIsSeconds | 1,
),
expected: "static address expiry does not fit into CSV",
},
{
name: "disabled flag",
resp: newServerNewAddressResponse(
wire.SequenceLockTimeDisabled | 1,
),
expected: "static address expiry does not fit into CSV",
},
{
name: "reserved flag",
resp: newServerNewAddressResponse(
wire.SequenceLockTimeMask + 1,
),
expected: "static address expiry does not fit into CSV",
},
{
name: "too large",
resp: newServerNewAddressResponse(
maxStaticAddressCSVExpiry + 1,
),
expected: "exceeds maximum",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testContext := NewAddressManagerTestContextWithResponse(
t, test.resp,
)
_, _, err := testContext.manager.NewAddress(t.Context())
require.ErrorContains(t, err, test.expected)
})
}
}
// TestNewAddressAcceptsMaxCSVExpiry tests the upper valid CSV boundary.
func TestNewAddressAcceptsMaxCSVExpiry(t *testing.T) {
testContext := NewAddressManagerTestContextWithResponse(
t, newServerNewAddressResponse(maxStaticAddressCSVExpiry),
)
_, expiry, err := testContext.manager.NewAddress(t.Context())
require.NoError(t, err)
require.EqualValues(t, maxStaticAddressCSVExpiry, expiry)
}
// GenerateExpectedTaprootAddress generates the expected taproot address that
// the predefined parameters are supposed to generate.
func GenerateExpectedTaprootAddress(t *ManagerTestContext) (
*btcaddr.AddressTaproot, error) {
keyIndex := int32(0)
_, pubKey := test.CreateKey(keyIndex)
keyDescriptor := &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(swap.StaticAddressKeyFamily),
Index: uint32(keyIndex),
},
PubKey: pubKey,
}
staticAddress, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(defaultExpiry),
keyDescriptor.PubKey, defaultServerPubkey,
)
if err != nil {
return nil, err
}
return btcaddr.NewAddressTaproot(
schnorr.SerializePubKey(staticAddress.TaprootKey),
t.manager.cfg.ChainParams,
)
}
// ManagerTestContext is a helper struct that contains all the necessary
// components to test the static address manager.
type ManagerTestContext struct {
manager *Manager
context test.Context
mockLnd *test.LndMockServices
mockStaticAddressClient *mockStaticAddressClient
}
// NewAddressManagerTestContext creates a new test context for the static
// address manager.
func NewAddressManagerTestContext(t *testing.T) *ManagerTestContext {
return NewAddressManagerTestContextWithResponse(
t, newServerNewAddressResponse(defaultExpiry),
)
}
// NewAddressManagerTestContextWithResponse creates a new test context with a
// custom ServerNewAddress response.
func NewAddressManagerTestContextWithResponse(t *testing.T,
resp *swapserverrpc.ServerNewAddressResponse) *ManagerTestContext {
ctxb, cancel := context.WithCancel(context.Background())
defer cancel()
mockLnd := test.NewMockLnd()
lndContext := test.NewContext(t, mockLnd)
dbFixture := loopdb.NewTestDB(t)
store := NewSqlStore(dbFixture.BaseDB)
mockStaticAddressClient := new(mockStaticAddressClient)
mockStaticAddressClient.On(
"ServerNewAddress", mock.Anything, mock.Anything, mock.Anything,
).Return(resp, nil)
cfg := &ManagerConfig{
Store: store,
WalletKit: mockLnd.WalletKit,
ChainParams: mockLnd.ChainParams,
AddressClient: mockStaticAddressClient,
ChainNotifier: mockLnd.ChainNotifier,
FetchL402: func(context.Context) error { return nil },
}
getInfo, err := mockLnd.Client.GetInfo(ctxb)
require.NoError(t, err)
manager, err := NewManager(cfg, int32(getInfo.BlockHeight))
require.NoError(t, err)
return &ManagerTestContext{
manager: manager,
context: lndContext,
mockLnd: mockLnd,
mockStaticAddressClient: mockStaticAddressClient,
}
}
// newServerNewAddressResponse returns a valid server response with the given
// CSV expiry.
func newServerNewAddressResponse(expiry uint32) *swapserverrpc.ServerNewAddressResponse {
return &swapserverrpc.ServerNewAddressResponse{
Params: &swapserverrpc.ServerAddressParameters{
ServerKey: defaultServerPubkeyBytes,
Expiry: expiry,
},
}
}