mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-15 12:50:28 +02:00
327 lines
9.1 KiB
Go
327 lines
9.1 KiB
Go
package address
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"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)
|
|
}
|
|
|
|
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) (
|
|
*btcutil.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 btcutil.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,
|
|
},
|
|
}
|
|
}
|