diff --git a/staticaddr/address/manager_test.go b/staticaddr/address/manager_test.go new file mode 100644 index 0000000..9a7daaa --- /dev/null +++ b/staticaddr/address/manager_test.go @@ -0,0 +1,147 @@ +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/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) ServerNewAddress(ctx context.Context, + in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) ( + *swapserverrpc.ServerNewAddressResponse, error) { + + args := m.Called(ctx, in, opts) + + return args.Get(0).(*swapserverrpc.ServerNewAddressResponse), + args.Error(1) +} + +func TestManager(t *testing.T) { + ctxb, cancel := context.WithCancel(context.Background()) + defer cancel() + + testContext := NewAddressManagerTestContext(t) + + // Start the manager. + go func() { + err := testContext.manager.Run(ctxb) + require.NoError(t, err) + }() + + // Create the expected static address. + expectedAddress, err := GenerateExpectedTaprootAddress(testContext) + require.NoError(t, err) + + // Create a new static address. + taprootAddress, err := testContext.manager.NewAddress(ctxb) + require.NoError(t, err) + + // The addresses have to match. + require.Equal(t, expectedAddress.String(), taprootAddress.String()) +} + +// 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 { + 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( + &swapserverrpc.ServerNewAddressResponse{ + Params: &swapserverrpc.ServerAddressParameters{ + ServerKey: defaultServerPubkeyBytes, + Expiry: defaultExpiry, + }, + }, nil, + ) + + cfg := &ManagerConfig{ + Store: store, + WalletKit: mockLnd.WalletKit, + ChainParams: mockLnd.ChainParams, + AddressClient: mockStaticAddressClient, + FetchL402: func(context.Context) error { return nil }, + } + + manager := NewManager(cfg) + + return &ManagerTestContext{ + manager: manager, + context: lndContext, + mockLnd: mockLnd, + mockStaticAddressClient: mockStaticAddressClient, + } +} diff --git a/staticaddr/deposit/manager_test.go b/staticaddr/deposit/manager_test.go new file mode 100644 index 0000000..ab91620 --- /dev/null +++ b/staticaddr/deposit/manager_test.go @@ -0,0 +1,314 @@ +package deposit + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/staticaddr/address" + "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/chainntnfs" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "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) + + defaultDepositConfirmations = uint32(3) + + confChan = make(chan *chainntnfs.TxConfirmation) + + confErrChan = make(chan error) + + blockChan = make(chan int32) + + blockErrChan = make(chan error) + + initChan = make(chan struct{}) + + finalizedDepositChan = make(chan wire.OutPoint) +) + +type mockStaticAddressClient struct { + mock.Mock +} + +func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context, + in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) ( + *swapserverrpc.ServerNewAddressResponse, error) { + + args := m.Called(ctx, in, opts) + + return args.Get(0).(*swapserverrpc.ServerNewAddressResponse), + args.Error(1) +} + +type mockAddressManager struct { + mock.Mock +} + +func (m *mockAddressManager) GetStaticAddressParameters(ctx context.Context) ( + *address.Parameters, error) { + + args := m.Called(ctx) + + return args.Get(0).(*address.Parameters), + args.Error(1) +} + +func (m *mockAddressManager) GetStaticAddress(ctx context.Context) ( + *script.StaticAddress, error) { + + args := m.Called(ctx) + + return args.Get(0).(*script.StaticAddress), + args.Error(1) +} + +func (m *mockAddressManager) ListUnspent(ctx context.Context, + minConfs, maxConfs int32) ([]*lnwallet.Utxo, error) { + + args := m.Called(ctx, minConfs, maxConfs) + + return args.Get(0).([]*lnwallet.Utxo), + args.Error(1) +} + +type mockStore struct { + mock.Mock +} + +func (s *mockStore) CreateDeposit(ctx context.Context, deposit *Deposit) error { + args := s.Called(ctx, deposit) + return args.Error(0) +} + +func (s *mockStore) UpdateDeposit(ctx context.Context, deposit *Deposit) error { + args := s.Called(ctx, deposit) + return args.Error(0) +} + +func (s *mockStore) GetDeposit(ctx context.Context, depositID ID) (*Deposit, + error) { + + args := s.Called(ctx, depositID) + return args.Get(0).(*Deposit), args.Error(1) +} + +func (s *mockStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { + args := s.Called(ctx) + return args.Get(0).([]*Deposit), args.Error(1) +} + +type MockChainNotifier struct { + mock.Mock +} + +func (m *MockChainNotifier) RegisterConfirmationsNtfn(ctx context.Context, + txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32, + _ ...lndclient.NotifierOption) (chan *chainntnfs.TxConfirmation, + chan error, error) { + + args := m.Called(ctx, txid, pkScript, numConfs, heightHint) + return args.Get(0).(chan *chainntnfs.TxConfirmation), + args.Get(1).(chan error), args.Error(2) +} + +func (m *MockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) ( + chan int32, chan error, error) { + + args := m.Called(ctx) + return args.Get(0).(chan int32), args.Get(1).(chan error), args.Error(2) +} + +func (m *MockChainNotifier) RegisterSpendNtfn(ctx context.Context, + outpoint *wire.OutPoint, pkScript []byte, heightHint int32) ( + chan *chainntnfs.SpendDetail, chan error, error) { + + args := m.Called(ctx, pkScript, heightHint) + return args.Get(0).(chan *chainntnfs.SpendDetail), + args.Get(1).(chan error), args.Error(2) +} + +// TestManager checks that the manager processes the right channel notifications +// while a deposit is expiring. +func TestManager(t *testing.T) { + ctxb, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create the test context with required mocks. + testContext := newManagerTestContext(t) + + // Start the deposit manager. + go func() { + err := testContext.manager.Run( + ctxb, uint32(testContext.mockLnd.Height), + ) + require.NoError(t, err) + }() + + // Ensure that the manager has been initialized. + <-initChan + + // Notify about the last block before the expiry. + blockChan <- int32(defaultDepositConfirmations + defaultExpiry) + + // Ensure that the deposit state machine didn't sign for the expiry tx. + select { + case <-testContext.mockLnd.SignOutputRawChannel: + t.Fatal("received unexpected sign request") + + default: + } + + // Mine the expiry tx height. + blockChan <- int32(defaultDepositConfirmations + defaultExpiry) + + // Ensure that the deposit state machine didn't sign for the expiry tx. + <-testContext.mockLnd.SignOutputRawChannel + + // Ensure that the signed expiry transaction is published. + expiryTx := <-testContext.mockLnd.TxPublishChannel + + // Ensure that the deposit is waiting for a confirmation notification. + confChan <- &chainntnfs.TxConfirmation{ + BlockHeight: defaultDepositConfirmations + defaultExpiry + 3, + Tx: expiryTx, + } +} + +// ManagerTestContext is a helper struct that contains all the necessary +// components to test the reservation manager. +type ManagerTestContext struct { + manager *Manager + context test.Context + mockLnd *test.LndMockServices + mockStaticAddressClient *mockStaticAddressClient + mockAddressManager *mockAddressManager +} + +// newManagerTestContext creates a new test context for the reservation manager. +func newManagerTestContext(t *testing.T) *ManagerTestContext { + mockLnd := test.NewMockLnd() + lndContext := test.NewContext(t, mockLnd) + + mockStaticAddressClient := new(mockStaticAddressClient) + mockAddressManager := new(mockAddressManager) + mockStore := new(mockStore) + mockChainNotifier := new(MockChainNotifier) + + ID, err := GetRandomDepositID() + utxo := &lnwallet.Utxo{ + AddressType: lnwallet.TaprootPubkey, + Value: btcutil.Amount(100000), + Confirmations: int64(defaultDepositConfirmations), + PkScript: []byte("pkscript"), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + } + require.NoError(t, err) + storedDeposits := []*Deposit{ + { + ID: ID, + State: Deposited, + OutPoint: utxo.OutPoint, + Value: utxo.Value, + ConfirmationHeight: 3, + TimeOutSweepPkScript: []byte{0x42, 0x21, 0x69}, + }, + } + + mockStore.On( + "AllDeposits", mock.Anything, + ).Return(storedDeposits, nil) + + mockStore.On( + "UpdateDeposit", mock.Anything, mock.Anything, + ).Return(nil) + + mockAddressManager.On( + "GetStaticAddressParameters", mock.Anything, + ).Return(&address.Parameters{ + Expiry: defaultExpiry, + }, nil) + + mockAddressManager.On( + "ListUnspent", mock.Anything, mock.Anything, mock.Anything, + ).Return([]*lnwallet.Utxo{utxo}, nil) + + // Define the expected return values for the mocks. + mockChainNotifier.On( + "RegisterConfirmationsNtfn", mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, + ).Return(confChan, confErrChan, nil) + + mockChainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return( + blockChan, blockErrChan, nil, + ) + + cfg := &ManagerConfig{ + AddressClient: mockStaticAddressClient, + AddressManager: mockAddressManager, + Store: mockStore, + WalletKit: mockLnd.WalletKit, + ChainParams: mockLnd.ChainParams, + ChainNotifier: mockChainNotifier, + Signer: mockLnd.Signer, + } + + manager := NewManager(cfg) + manager.initChan = initChan + manager.finalizedDepositChan = finalizedDepositChan + + testContext := &ManagerTestContext{ + manager: manager, + context: lndContext, + mockLnd: mockLnd, + mockStaticAddressClient: mockStaticAddressClient, + mockAddressManager: mockAddressManager, + } + + staticAddress := generateStaticAddress( + context.Background(), testContext, + ) + mockAddressManager.On( + "GetStaticAddress", mock.Anything, + ).Return(staticAddress, nil) + + return testContext +} + +func generateStaticAddress(ctx context.Context, + t *ManagerTestContext) *script.StaticAddress { + + keyDescriptor, err := t.mockLnd.WalletKit.DeriveNextKey( + ctx, swap.StaticAddressKeyFamily, + ) + require.NoError(t.context.T, err) + + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(defaultExpiry), + keyDescriptor.PubKey, defaultServerPubkey, + ) + require.NoError(t.context.T, err) + + return staticAddress +} diff --git a/test/walletkit_mock.go b/test/walletkit_mock.go index c26f7b4..c8f9c11 100644 --- a/test/walletkit_mock.go +++ b/test/walletkit_mock.go @@ -266,5 +266,5 @@ func (m *mockWalletKit) ImportPublicKey(ctx context.Context, func (m *mockWalletKit) ImportTaprootScript(ctx context.Context, tapscript *waddrmgr.Tapscript) (btcutil.Address, error) { - return nil, fmt.Errorf("unimplemented") + return nil, nil }