mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Treat lnd wallet view as the source of spendable static-address outputs while keeping historical deposit records in the DB. Reconcile active FSMs against the current wallet view and reactivate known deposits when their outpoints are visible again. Refresh deposits before selection, withdrawal, loop-in, and channel-open paths, and filter list and summary responses through the live active set so stale Deposited records are not exposed as available funds.
703 lines
19 KiB
Go
703 lines
19 KiB
Go
package deposit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/staticaddr/script"
|
|
"github.com/lightninglabs/loop/staticaddr/version"
|
|
"github.com/lightninglabs/loop/test"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// expectStableBestBlock configures two stable best-block lookups.
|
|
func expectStableBestBlock(mockChainKit *MockChainKit, height int32) {
|
|
mockChainKit.On(
|
|
"GetBestBlock", mock.Anything,
|
|
).Return(chainhash.Hash{}, height, nil).Twice()
|
|
}
|
|
|
|
// TestReconcileDepositsSerialized verifies reconciliation is serialized across
|
|
// concurrent callers.
|
|
func TestReconcileDepositsSerialized(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockLnd := test.NewMockLnd()
|
|
utxo := &lnwallet.Utxo{
|
|
AddressType: lnwallet.TaprootPubkey,
|
|
Value: btcutil.Amount(100_000),
|
|
Confirmations: 0,
|
|
OutPoint: wire.OutPoint{
|
|
Hash: chainhash.Hash{1},
|
|
Index: 1,
|
|
},
|
|
}
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{utxo}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddressParameters", mock.Anything,
|
|
).Return((*script.Parameters)(nil), errors.New("fsm init failed"))
|
|
|
|
mockStore := new(mockStore)
|
|
var createCalls atomic.Int32
|
|
createEntered := make(chan struct{})
|
|
releaseCreate := make(chan struct{})
|
|
mockStore.On(
|
|
"CreateDeposit", mock.Anything, mock.Anything,
|
|
).Return(nil).Run(func(mock.Arguments) {
|
|
if createCalls.Add(1) == 1 {
|
|
close(createEntered)
|
|
}
|
|
|
|
<-releaseCreate
|
|
})
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
Store: mockStore,
|
|
WalletKit: mockLnd.WalletKit,
|
|
Signer: mockLnd.Signer,
|
|
})
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
|
|
errs := make(chan error, 2)
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- manager.reconcileDeposits(ctx)
|
|
}()
|
|
|
|
<-createEntered
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- manager.reconcileDeposits(ctx)
|
|
}()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
close(releaseCreate)
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
var gotErrs []error
|
|
for err := range errs {
|
|
gotErrs = append(gotErrs, err)
|
|
}
|
|
|
|
require.EqualValues(t, 1, createCalls.Load())
|
|
require.Len(t, manager.deposits, 1)
|
|
require.Empty(t, manager.activeDeposits)
|
|
require.Len(t, gotErrs, 2)
|
|
|
|
var errCount int
|
|
for _, err := range gotErrs {
|
|
if err == nil {
|
|
continue
|
|
}
|
|
|
|
errCount++
|
|
errMsg := err.Error()
|
|
require.True(
|
|
t,
|
|
strings.Contains(
|
|
errMsg, "unable to start new deposit FSM",
|
|
) || strings.Contains(
|
|
errMsg, "unable to sync active deposits",
|
|
),
|
|
"unexpected error: %v", err,
|
|
)
|
|
}
|
|
require.Equal(t, 2, errCount)
|
|
}
|
|
|
|
// TestReconcileConfirmedDepositUsesBestBlockHeight verifies confirmation
|
|
// heights are derived from a stable chain tip.
|
|
func TestReconcileConfirmedDepositUsesBestBlockHeight(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockLnd := test.NewMockLnd()
|
|
utxo := &lnwallet.Utxo{
|
|
AddressType: lnwallet.TaprootPubkey,
|
|
Value: btcutil.Amount(100_000),
|
|
Confirmations: 3,
|
|
OutPoint: wire.OutPoint{
|
|
Hash: chainhash.Hash{8},
|
|
Index: 1,
|
|
},
|
|
}
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{utxo}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddressParameters", mock.Anything,
|
|
).Return((*script.Parameters)(nil), errors.New("fsm init failed"))
|
|
|
|
mockChainKit := new(MockChainKit)
|
|
expectStableBestBlock(mockChainKit, 100)
|
|
|
|
mockStore := new(mockStore)
|
|
mockStore.On(
|
|
"CreateDeposit", mock.Anything, mock.Anything,
|
|
).Return(nil).Run(func(args mock.Arguments) {
|
|
createdDeposit := args.Get(1).(*Deposit)
|
|
require.EqualValues(t, 98, createdDeposit.ConfirmationHeight)
|
|
})
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
ChainKit: mockChainKit,
|
|
Store: mockStore,
|
|
WalletKit: mockLnd.WalletKit,
|
|
Signer: mockLnd.Signer,
|
|
})
|
|
|
|
err := manager.reconcileDeposits(ctx)
|
|
require.ErrorContains(t, err, "unable to start new deposit FSM")
|
|
}
|
|
|
|
// TestReconcileConfirmedDepositRelistsOnBlockChange verifies that a block
|
|
// arriving while listing deposits does not fail reconciliation. Instead we
|
|
// relist deposits and use the latest height.
|
|
func TestReconcileConfirmedDepositRelistsOnBlockChange(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockLnd := test.NewMockLnd()
|
|
initialUtxo := &lnwallet.Utxo{
|
|
AddressType: lnwallet.TaprootPubkey,
|
|
Value: btcutil.Amount(100_000),
|
|
Confirmations: 1,
|
|
OutPoint: wire.OutPoint{
|
|
Hash: chainhash.Hash{18},
|
|
Index: 1,
|
|
},
|
|
}
|
|
latestUtxo := &lnwallet.Utxo{
|
|
AddressType: lnwallet.TaprootPubkey,
|
|
Value: btcutil.Amount(100_000),
|
|
Confirmations: 2,
|
|
OutPoint: wire.OutPoint{
|
|
Hash: chainhash.Hash{18},
|
|
Index: 1,
|
|
},
|
|
}
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{initialUtxo}, nil).Once()
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{initialUtxo}, nil).Once()
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{latestUtxo}, nil).Once()
|
|
mockAddressManager.On(
|
|
"GetStaticAddressParameters", mock.Anything,
|
|
).Return((*script.Parameters)(nil), errors.New("fsm init failed"))
|
|
|
|
mockChainKit := new(MockChainKit)
|
|
mockChainKit.On(
|
|
"GetBestBlock", mock.Anything,
|
|
).Return(chainhash.Hash{}, int32(100), nil).Once()
|
|
mockChainKit.On(
|
|
"GetBestBlock", mock.Anything,
|
|
).Return(chainhash.Hash{}, int32(101), nil).Once()
|
|
|
|
mockStore := new(mockStore)
|
|
mockStore.On(
|
|
"CreateDeposit", mock.Anything, mock.Anything,
|
|
).Return(nil).Run(func(args mock.Arguments) {
|
|
createdDeposit := args.Get(1).(*Deposit)
|
|
require.EqualValues(t, 100, createdDeposit.ConfirmationHeight)
|
|
})
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
ChainKit: mockChainKit,
|
|
Store: mockStore,
|
|
WalletKit: mockLnd.WalletKit,
|
|
Signer: mockLnd.Signer,
|
|
})
|
|
|
|
err := manager.reconcileDeposits(ctx)
|
|
require.ErrorContains(t, err, "unable to start new deposit FSM")
|
|
mockAddressManager.AssertExpectations(t)
|
|
mockChainKit.AssertExpectations(t)
|
|
}
|
|
|
|
// TestUpdateDepositConfirmationsResetsReorgedDeposit verifies that a deposit
|
|
// which remains wallet-visible but loses confirmations has its confirmation
|
|
// height reset. This can happen if a confirmed transaction is reorged back into
|
|
// the mempool.
|
|
func TestUpdateDepositConfirmationsResetsReorgedDeposit(t *testing.T) {
|
|
ctx := context.Background()
|
|
outpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{7},
|
|
Index: 2,
|
|
}
|
|
|
|
deposit := &Deposit{
|
|
OutPoint: outpoint,
|
|
ConfirmationHeight: 99,
|
|
}
|
|
deposit.SetState(Deposited)
|
|
|
|
utxo := &lnwallet.Utxo{
|
|
OutPoint: outpoint,
|
|
Confirmations: 0,
|
|
}
|
|
|
|
mockStore := new(mockStore)
|
|
mockStore.On(
|
|
"UpdateDeposit", mock.Anything, mock.Anything,
|
|
).Return(nil).Run(func(args mock.Arguments) {
|
|
updatedDeposit := args.Get(1).(*Deposit)
|
|
require.Zero(t, updatedDeposit.ConfirmationHeight)
|
|
})
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
Store: mockStore,
|
|
})
|
|
manager.deposits[outpoint] = deposit
|
|
|
|
err := manager.updateDepositConfirmations(ctx, []*lnwallet.Utxo{utxo}, 0)
|
|
require.NoError(t, err)
|
|
require.Zero(t, deposit.ConfirmationHeight)
|
|
mockStore.AssertExpectations(t)
|
|
}
|
|
|
|
// TestReconcileDepositsDeactivatesVanishedUnconfirmedDeposit verifies that a
|
|
// missing wallet outpoint is removed from the live active set without mutating
|
|
// its historical DB state.
|
|
func TestReconcileDepositsDeactivatesVanishedUnconfirmedDeposit(t *testing.T) {
|
|
ctx := context.Background()
|
|
outpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{2},
|
|
Index: 7,
|
|
}
|
|
|
|
deposit := &Deposit{
|
|
OutPoint: outpoint,
|
|
}
|
|
deposit.SetState(Deposited)
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{}, nil)
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
Store: new(mockStore),
|
|
})
|
|
manager.deposits[outpoint] = deposit
|
|
fsm := &FSM{
|
|
deposit: deposit,
|
|
stopChan: make(chan struct{}),
|
|
quitChan: make(chan struct{}),
|
|
}
|
|
go func() {
|
|
<-fsm.stopChan
|
|
close(fsm.quitChan)
|
|
}()
|
|
manager.activeDeposits[outpoint] = fsm
|
|
|
|
require.NoError(t, manager.reconcileDeposits(ctx))
|
|
require.Equal(t, Deposited, deposit.GetState())
|
|
require.Empty(t, manager.activeDeposits)
|
|
select {
|
|
case <-fsm.quitChan:
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal("fsm did not stop after deposit vanished")
|
|
}
|
|
}
|
|
|
|
// TestReconcileDepositsDeactivatesVanishedConfirmedDeposit verifies that a
|
|
// previously confirmed deposit is also removed from the live active set if it
|
|
// vanishes from the wallet view.
|
|
func TestReconcileDepositsDeactivatesVanishedConfirmedDeposit(t *testing.T) {
|
|
ctx := context.Background()
|
|
outpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{9},
|
|
Index: 4,
|
|
}
|
|
|
|
deposit := &Deposit{
|
|
OutPoint: outpoint,
|
|
ConfirmationHeight: 123,
|
|
}
|
|
deposit.SetState(Deposited)
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{}, nil)
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
Store: new(mockStore),
|
|
})
|
|
manager.deposits[outpoint] = deposit
|
|
fsm := &FSM{
|
|
deposit: deposit,
|
|
stopChan: make(chan struct{}),
|
|
quitChan: make(chan struct{}),
|
|
}
|
|
go func() {
|
|
<-fsm.stopChan
|
|
close(fsm.quitChan)
|
|
}()
|
|
manager.activeDeposits[outpoint] = fsm
|
|
|
|
require.NoError(t, manager.reconcileDeposits(ctx))
|
|
require.Equal(t, Deposited, deposit.GetState())
|
|
require.EqualValues(t, 123, deposit.ConfirmationHeight)
|
|
require.Empty(t, manager.activeDeposits)
|
|
select {
|
|
case <-fsm.quitChan:
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal("fsm did not stop after confirmed deposit vanished")
|
|
}
|
|
}
|
|
|
|
// TestAllOutpointsActiveDepositsRejectsDuplicateOutpoints verifies that a
|
|
// duplicated selection is rejected before the manager tries to lock the same
|
|
// deposit twice.
|
|
func TestAllOutpointsActiveDepositsRejectsDuplicateOutpoints(t *testing.T) {
|
|
outpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{12},
|
|
Index: 6,
|
|
}
|
|
|
|
deposit := &Deposit{
|
|
OutPoint: outpoint,
|
|
}
|
|
deposit.SetState(Deposited)
|
|
|
|
manager := NewManager(&ManagerConfig{})
|
|
manager.deposits[outpoint] = deposit
|
|
manager.activeDeposits[outpoint] = &FSM{
|
|
deposit: deposit,
|
|
}
|
|
|
|
deposits, ok := manager.AllOutpointsActiveDeposits(
|
|
[]wire.OutPoint{outpoint, outpoint}, Deposited,
|
|
)
|
|
require.False(t, ok)
|
|
require.Nil(t, deposits)
|
|
}
|
|
|
|
// TestTransitionDepositsRejectsDuplicateOutpoints verifies that transition
|
|
// callers cannot deadlock the manager by passing the same deposit twice.
|
|
func TestTransitionDepositsRejectsDuplicateOutpoints(t *testing.T) {
|
|
outpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{13},
|
|
Index: 6,
|
|
}
|
|
|
|
deposit := &Deposit{
|
|
OutPoint: outpoint,
|
|
}
|
|
deposit.SetState(Deposited)
|
|
|
|
manager := NewManager(&ManagerConfig{})
|
|
err := manager.TransitionDeposits(
|
|
t.Context(), []*Deposit{deposit, deposit}, OnLoopInInitiated,
|
|
LoopingIn,
|
|
)
|
|
require.ErrorContains(t, err, "duplicate deposit outpoint")
|
|
require.Equal(t, Deposited, deposit.GetState())
|
|
}
|
|
|
|
// TestReconcileDepositsReactivatesReappearedDeposit verifies that the same
|
|
// outpoint can become active again if lnd reports it after a prior wallet-view
|
|
// miss.
|
|
func TestReconcileDepositsReactivatesReappearedDeposit(t *testing.T) {
|
|
ctx := context.Background()
|
|
outpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{3},
|
|
Index: 5,
|
|
}
|
|
|
|
deposit := &Deposit{
|
|
OutPoint: outpoint,
|
|
Value: btcutil.Amount(100_000),
|
|
ConfirmationHeight: 77,
|
|
}
|
|
deposit.SetState(Deposited)
|
|
|
|
utxo := &lnwallet.Utxo{
|
|
OutPoint: outpoint,
|
|
Value: deposit.Value,
|
|
Confirmations: 0,
|
|
}
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{utxo}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddressParameters", mock.Anything,
|
|
).Return(&script.Parameters{
|
|
ProtocolVersion: version.ProtocolVersion_V0,
|
|
}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddress", mock.Anything,
|
|
).Return((*script.StaticAddress)(nil), nil)
|
|
|
|
mockStore := new(mockStore)
|
|
var updateStates []fsm.StateType
|
|
mockStore.On(
|
|
"UpdateDeposit", mock.Anything, mock.Anything,
|
|
).Return(nil).Run(func(args mock.Arguments) {
|
|
updatedDeposit := args.Get(1).(*Deposit)
|
|
updateStates = append(updateStates, updatedDeposit.state)
|
|
if updatedDeposit.IsInStateNoLock(Deposited) {
|
|
require.Zero(t, updatedDeposit.ConfirmationHeight)
|
|
}
|
|
})
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
Store: mockStore,
|
|
})
|
|
manager.deposits[outpoint] = deposit
|
|
|
|
// Reconciliation should reactivate the existing record instead of
|
|
// creating a second deposit entry for the same outpoint.
|
|
require.NoError(t, manager.reconcileDeposits(ctx))
|
|
require.Equal(t, Deposited, deposit.GetState())
|
|
require.Zero(t, deposit.ConfirmationHeight)
|
|
require.Len(t, manager.activeDeposits, 1)
|
|
require.Equal(t, []fsm.StateType{Deposited}, updateStates)
|
|
}
|
|
|
|
// TestReconcileDepositsKeepsInactiveOnFSMStartFailure verifies that a failed
|
|
// reactivation does not leave memory saying a deposit is active without an FSM.
|
|
func TestReconcileDepositsKeepsInactiveOnFSMStartFailure(t *testing.T) {
|
|
ctx := context.Background()
|
|
outpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{11},
|
|
Index: 5,
|
|
}
|
|
|
|
deposit := &Deposit{
|
|
OutPoint: outpoint,
|
|
Value: btcutil.Amount(100_000),
|
|
ConfirmationHeight: 77,
|
|
}
|
|
deposit.SetState(Deposited)
|
|
|
|
utxo := &lnwallet.Utxo{
|
|
OutPoint: outpoint,
|
|
Value: deposit.Value,
|
|
Confirmations: 0,
|
|
}
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{utxo}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddressParameters", mock.Anything,
|
|
).Return((*script.Parameters)(nil), errors.New("fsm init failed"))
|
|
|
|
var (
|
|
updateStates []fsm.StateType
|
|
updateHeights []int64
|
|
)
|
|
mockStore := new(mockStore)
|
|
mockStore.On(
|
|
"UpdateDeposit", mock.Anything, mock.Anything,
|
|
).Return(nil).Run(func(args mock.Arguments) {
|
|
updatedDeposit := args.Get(1).(*Deposit)
|
|
updateStates = append(updateStates, updatedDeposit.state)
|
|
updateHeights = append(
|
|
updateHeights, updatedDeposit.ConfirmationHeight,
|
|
)
|
|
})
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
Store: mockStore,
|
|
})
|
|
manager.deposits[outpoint] = deposit
|
|
|
|
err := manager.reconcileDeposits(ctx)
|
|
require.ErrorContains(t, err, "unable to sync active deposits")
|
|
require.Equal(t, Deposited, deposit.GetState())
|
|
require.Zero(t, deposit.ConfirmationHeight)
|
|
require.Empty(t, manager.activeDeposits)
|
|
require.Equal(t, []fsm.StateType{Deposited}, updateStates)
|
|
require.EqualValues(t, []int64{0}, updateHeights)
|
|
}
|
|
|
|
// TestReconcileDepositsDeactivatesBeforeActivationFailure verifies that a
|
|
// failed reactivation of one visible deposit does not leave another vanished
|
|
// deposit in the live active set.
|
|
func TestReconcileDepositsDeactivatesBeforeActivationFailure(t *testing.T) {
|
|
ctx := context.Background()
|
|
visibleOutpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{21},
|
|
Index: 5,
|
|
}
|
|
vanishedOutpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{22},
|
|
Index: 6,
|
|
}
|
|
|
|
visibleDeposit := &Deposit{
|
|
OutPoint: visibleOutpoint,
|
|
Value: btcutil.Amount(100_000),
|
|
}
|
|
visibleDeposit.SetState(Deposited)
|
|
|
|
vanishedDeposit := &Deposit{
|
|
OutPoint: vanishedOutpoint,
|
|
Value: btcutil.Amount(100_000),
|
|
}
|
|
vanishedDeposit.SetState(Deposited)
|
|
|
|
utxo := &lnwallet.Utxo{
|
|
OutPoint: visibleOutpoint,
|
|
Value: visibleDeposit.Value,
|
|
Confirmations: 0,
|
|
}
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{utxo}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddressParameters", mock.Anything,
|
|
).Return((*script.Parameters)(nil), errors.New("fsm init failed"))
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
Store: new(mockStore),
|
|
})
|
|
manager.deposits[visibleOutpoint] = visibleDeposit
|
|
manager.deposits[vanishedOutpoint] = vanishedDeposit
|
|
|
|
vanishedFsm := &FSM{
|
|
deposit: vanishedDeposit,
|
|
stopChan: make(chan struct{}),
|
|
quitChan: make(chan struct{}),
|
|
}
|
|
go func() {
|
|
<-vanishedFsm.stopChan
|
|
close(vanishedFsm.quitChan)
|
|
}()
|
|
manager.activeDeposits[vanishedOutpoint] = vanishedFsm
|
|
|
|
err := manager.reconcileDeposits(ctx)
|
|
require.ErrorContains(t, err, "unable to sync active deposits")
|
|
require.Empty(t, manager.activeDeposits)
|
|
|
|
select {
|
|
case <-vanishedFsm.quitChan:
|
|
|
|
case <-time.After(time.Second):
|
|
t.Fatal("vanished deposit fsm did not stop")
|
|
}
|
|
}
|
|
|
|
// TestReconcileReplacementDepositCreatesNewDeposit ensures that a replacement
|
|
// UTXO is retained as a new deposit while an in-flight deposit remains tied to
|
|
// the outpoint selected by a loop-in.
|
|
func TestReconcileReplacementDepositCreatesNewDeposit(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockLnd := test.NewMockLnd()
|
|
oldOutpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{4},
|
|
Index: 8,
|
|
}
|
|
newOutpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{5},
|
|
Index: 9,
|
|
}
|
|
|
|
depositID, err := GetRandomDepositID()
|
|
require.NoError(t, err)
|
|
|
|
deposit := &Deposit{
|
|
ID: depositID,
|
|
OutPoint: oldOutpoint,
|
|
Value: btcutil.Amount(100_000),
|
|
}
|
|
deposit.SetState(LoopingIn)
|
|
|
|
utxo := &lnwallet.Utxo{
|
|
OutPoint: newOutpoint,
|
|
Value: deposit.Value,
|
|
Confirmations: 0,
|
|
}
|
|
|
|
mockAddressManager := new(mockAddressManager)
|
|
mockAddressManager.On(
|
|
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
|
|
).Return([]*lnwallet.Utxo{utxo}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddressParameters", mock.Anything,
|
|
).Return(&script.Parameters{
|
|
ProtocolVersion: version.ProtocolVersion_V0,
|
|
}, nil)
|
|
mockAddressManager.On(
|
|
"GetStaticAddress", mock.Anything,
|
|
).Return((*script.StaticAddress)(nil), nil)
|
|
|
|
mockStore := new(mockStore)
|
|
var createdDeposit *Deposit
|
|
mockStore.On(
|
|
"CreateDeposit", mock.Anything, mock.Anything,
|
|
).Return(nil).Run(func(args mock.Arguments) {
|
|
createdDeposit = args.Get(1).(*Deposit)
|
|
})
|
|
|
|
manager := NewManager(&ManagerConfig{
|
|
AddressManager: mockAddressManager,
|
|
Store: mockStore,
|
|
WalletKit: mockLnd.WalletKit,
|
|
Signer: mockLnd.Signer,
|
|
})
|
|
manager.deposits[oldOutpoint] = deposit
|
|
fsm := &FSM{}
|
|
manager.activeDeposits[oldOutpoint] = fsm
|
|
|
|
require.NoError(t, manager.reconcileDeposits(ctx))
|
|
|
|
require.Same(t, deposit, manager.deposits[oldOutpoint])
|
|
require.Equal(t, oldOutpoint, deposit.OutPoint)
|
|
require.Equal(t, LoopingIn, deposit.GetState())
|
|
|
|
replacement, ok := manager.deposits[newOutpoint]
|
|
require.True(t, ok)
|
|
require.Same(t, createdDeposit, replacement)
|
|
require.NotEqual(t, depositID, replacement.ID)
|
|
require.Equal(t, newOutpoint, replacement.OutPoint)
|
|
require.Equal(t, Deposited, replacement.GetState())
|
|
require.Zero(t, replacement.ConfirmationHeight)
|
|
|
|
require.Same(t, fsm, manager.activeDeposits[oldOutpoint])
|
|
require.NotSame(t, fsm, manager.activeDeposits[newOutpoint])
|
|
|
|
mockStore.AssertNotCalled(
|
|
t, "UpdateDeposit", mock.Anything, mock.Anything,
|
|
)
|
|
}
|