mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr: cancel loop-ins when deposit inputs vanish
Keep replacement UTXOs as fresh deposits while preserving the original deposit record and selected outpoint snapshot for pending swaps. Before signing a static loop-in HTLC, check each original selected outpoint with GetTxOut(..., includeMempool=true). Cancel the pending invoice only when that check reports an original outpoint unavailable; lookup errors fail the action without canceling so transient chain backend errors do not incorrectly abandon the swap. Keep recovered loop-ins using their stored outpoint snapshot and cover replacement discovery and cancellation in tests.
This commit is contained in:
parent
6d70f0fe47
commit
858066dd0c
13 changed files with 509 additions and 35 deletions
|
|
@ -700,6 +700,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
|
|||
Store: staticAddressLoopInStore,
|
||||
WalletKit: d.lnd.WalletKit,
|
||||
ChainNotifier: d.lnd.ChainNotifier,
|
||||
TxOutChecker: loopin.NewLndTxOutChecker(d.lnd.Client),
|
||||
NotificationManager: notificationManager,
|
||||
ChainParams: d.lnd.ChainParams,
|
||||
Signer: d.lnd.Signer,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ const (
|
|||
//
|
||||
// A single miss can happen during a transient wallet-view gap while lnd is
|
||||
// processing a replacement or reorg. Requiring two misses keeps that narrow
|
||||
// race recoverable without leaving vanished deposits selectable forever.
|
||||
// race recoverable without leaving vanished deposits selectable forever. At
|
||||
// the default PollInterval, this means a vanished deposit can remain active
|
||||
// for up to roughly 20 seconds.
|
||||
vanishedDepositThreshold = 2
|
||||
)
|
||||
|
||||
|
|
@ -416,6 +418,7 @@ func (m *Manager) listUnspentWithBestHeight(ctx context.Context) (
|
|||
return nil, 0, errors.New("unable to get stable best block while " +
|
||||
"listing deposits")
|
||||
}
|
||||
|
||||
// createNewDeposit transforms the wallet utxo into a deposit struct and stores
|
||||
// it in our database and manager memory.
|
||||
func (m *Manager) createNewDeposit(ctx context.Context,
|
||||
|
|
|
|||
|
|
@ -348,3 +348,89 @@ func TestReconcileDepositsReactivatesReappearedReplacedDeposit(t *testing.T) {
|
|||
require.Zero(t, deposit.ConfirmationHeight)
|
||||
require.Len(t, manager.activeDeposits, 1)
|
||||
}
|
||||
|
||||
// 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
|
||||
manager.missingDeposits[oldOutpoint] = 1
|
||||
|
||||
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])
|
||||
require.Empty(t, manager.missingDeposits)
|
||||
|
||||
mockStore.AssertNotCalled(
|
||||
t, "UpdateDeposit", mock.Anything, mock.Anything,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcwallet/chain"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop"
|
||||
|
|
@ -337,6 +338,68 @@ func (f *FSM) cancelSwapInvoice(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// handleInvoiceUpdate applies the monitor state's invoice-update semantics and
|
||||
// reports whether the update produced a terminal event.
|
||||
func (f *FSM) handleInvoiceUpdate(update lndclient.InvoiceUpdate) (
|
||||
fsm.EventType, bool) {
|
||||
|
||||
switch update.State {
|
||||
case invoices.ContractOpen:
|
||||
return fsm.NoOp, false
|
||||
|
||||
case invoices.ContractAccepted:
|
||||
return fsm.NoOp, false
|
||||
|
||||
case invoices.ContractSettled:
|
||||
f.Debugf("received off-chain payment update %v", update.State)
|
||||
return OnPaymentReceived, true
|
||||
|
||||
case invoices.ContractCanceled:
|
||||
// If the invoice was canceled we only log here since we still need
|
||||
// to monitor until the htlc timed out.
|
||||
log.Warnf("invoice for swap hash %v canceled", f.loopIn.SwapHash)
|
||||
return fsm.NoOp, false
|
||||
|
||||
default:
|
||||
err := fmt.Errorf("unexpected invoice state %v for swap hash %v "+
|
||||
"canceled", update.State, f.loopIn.SwapHash)
|
||||
return f.HandleError(err), true
|
||||
}
|
||||
}
|
||||
|
||||
// originalDepositOutpointUnavailable checks the original selected deposit
|
||||
// outpoints against the chain backend's UTXO view.
|
||||
func (f *FSM) originalDepositOutpointUnavailable(ctx context.Context) (
|
||||
bool, error) {
|
||||
|
||||
if f.cfg.TxOutChecker == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
const includeMempool = true
|
||||
for _, outpointStr := range f.loopIn.DepositOutpoints {
|
||||
outpoint, err := wire.NewOutPointFromString(outpointStr)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid deposit outpoint %q: %w",
|
||||
outpointStr, err)
|
||||
}
|
||||
|
||||
txOut, err := f.cfg.TxOutChecker.GetTxOut(
|
||||
ctx, *outpoint, includeMempool,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to get txout %v: %w",
|
||||
outpoint, err)
|
||||
}
|
||||
|
||||
if txOut == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SignHtlcTxAction is called if the htlc was initialized and the server
|
||||
// provided the necessary information to construct the htlc tx. We sign the htlc
|
||||
// tx and send the signatures to the server.
|
||||
|
|
@ -345,6 +408,18 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
|
|||
|
||||
var err error
|
||||
|
||||
outpointUnavailable, err := f.originalDepositOutpointUnavailable(ctx)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
if outpointUnavailable {
|
||||
err = errors.New("original deposit outpoint no longer available")
|
||||
f.Warnf("%v, canceling swap invoice", err)
|
||||
f.cancelSwapInvoice(ctx)
|
||||
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
f.loopIn.AddressParams, err =
|
||||
f.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
||||
|
||||
|
|
@ -714,32 +789,22 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
|
||||
return f.HandleError(err)
|
||||
|
||||
case update := <-invoiceUpdateChan:
|
||||
switch update.State {
|
||||
case invoices.ContractOpen:
|
||||
case invoices.ContractAccepted:
|
||||
case invoices.ContractSettled:
|
||||
f.Debugf("received off-chain payment update "+
|
||||
"%v", update.State)
|
||||
|
||||
return OnPaymentReceived
|
||||
|
||||
case invoices.ContractCanceled:
|
||||
// If the invoice was canceled we only log here
|
||||
// since we still need to monitor until the htlc
|
||||
// timed out.
|
||||
log.Warnf("invoice for swap hash %v canceled",
|
||||
f.loopIn.SwapHash)
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unexpected invoice state %v "+
|
||||
"for swap hash %v canceled",
|
||||
update.State, f.loopIn.SwapHash)
|
||||
|
||||
return f.HandleError(err)
|
||||
case update, ok := <-invoiceUpdateChan:
|
||||
if !ok {
|
||||
invoiceUpdateChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if event, done := f.handleInvoiceUpdate(update); done {
|
||||
return event
|
||||
}
|
||||
|
||||
case err, ok := <-invoiceErrChan:
|
||||
if !ok {
|
||||
invoiceErrChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
case err = <-invoiceErrChan:
|
||||
f.Errorf("invoice subscription error: %v", err)
|
||||
|
||||
case <-ctx.Done():
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) {
|
|||
loopIn.SetState(MonitorInvoiceAndHtlcTx)
|
||||
|
||||
// Seed the mock invoice store so LookupInvoice succeeds.
|
||||
mockLnd.Invoices[swapHash] = &lndclient.Invoice{
|
||||
mockLnd.SetInvoice(&lndclient.Invoice{
|
||||
Hash: swapHash,
|
||||
State: invoices.ContractOpen,
|
||||
}
|
||||
})
|
||||
|
||||
cfg := &Config{
|
||||
AddressManager: &mockAddressManager{
|
||||
|
|
@ -270,6 +270,133 @@ func testValidateLoopInContract(_ int32, _ int32) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TestOriginalDepositOutpointUnavailableRequiresMissingTxOut verifies that a
|
||||
// present txout does not trigger the RBF cancellation path.
|
||||
func TestOriginalDepositOutpointUnavailableRequiresMissingTxOut(t *testing.T) {
|
||||
originalOutpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{1},
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
txOutChecker := &testTxOutChecker{
|
||||
txOut: &wire.TxOut{Value: 10_000},
|
||||
}
|
||||
f := &FSM{
|
||||
cfg: &Config{
|
||||
TxOutChecker: txOutChecker,
|
||||
},
|
||||
loopIn: &StaticAddressLoopIn{
|
||||
DepositOutpoints: []string{originalOutpoint.String()},
|
||||
},
|
||||
}
|
||||
|
||||
unavailable, err := f.originalDepositOutpointUnavailable(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.False(t, unavailable)
|
||||
require.Equal(t, []wire.OutPoint{originalOutpoint}, txOutChecker.outpoints)
|
||||
require.Equal(t, []bool{true}, txOutChecker.includeMempool)
|
||||
}
|
||||
|
||||
// TestSignHtlcTxActionCancelsWhenOriginalOutpointUnavailable verifies that a
|
||||
// pending loop-in is canceled before HTLC signing if GetTxOut with mempool
|
||||
// awareness reports that one of the originally selected outpoints is gone.
|
||||
func TestSignHtlcTxActionCancelsWhenOriginalOutpointUnavailable(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mockLnd := test.NewMockLnd()
|
||||
|
||||
swapHash := lntypes.Hash{9, 8, 7}
|
||||
originalOutpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{1},
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
DepositOutpoints: []string{originalOutpoint.String()},
|
||||
}
|
||||
|
||||
txOutChecker := &testTxOutChecker{}
|
||||
cfg := &Config{
|
||||
AddressManager: &mockAddressManager{
|
||||
params: &script.Parameters{
|
||||
ProtocolVersion: version.ProtocolVersion_V0,
|
||||
},
|
||||
},
|
||||
InvoicesClient: mockLnd.LndServices.Invoices,
|
||||
TxOutChecker: txOutChecker,
|
||||
}
|
||||
|
||||
f, err := NewFSM(ctx, loopIn, cfg, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
event := f.SignHtlcTxAction(ctx, nil)
|
||||
require.Equal(t, fsm.OnError, event)
|
||||
require.ErrorContains(
|
||||
t, f.LastActionError, "original deposit outpoint no longer available",
|
||||
)
|
||||
|
||||
select {
|
||||
case hash := <-mockLnd.FailInvoiceChannel:
|
||||
require.Equal(t, swapHash, hash)
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("invoice was not canceled: %v", ctx.Err())
|
||||
}
|
||||
|
||||
require.Equal(t, []wire.OutPoint{originalOutpoint}, txOutChecker.outpoints)
|
||||
require.Equal(t, []bool{true}, txOutChecker.includeMempool)
|
||||
}
|
||||
|
||||
// TestSignHtlcTxActionDoesNotCancelOnTxOutLookupError verifies that lookup
|
||||
// failures are treated as errors, but do not cancel the invoice. The invoice is
|
||||
// only canceled when GetTxOut explicitly returns nil for an original outpoint.
|
||||
func TestSignHtlcTxActionDoesNotCancelOnTxOutLookupError(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mockLnd := test.NewMockLnd()
|
||||
|
||||
swapHash := lntypes.Hash{9, 8, 6}
|
||||
originalOutpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{3},
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
DepositOutpoints: []string{originalOutpoint.String()},
|
||||
}
|
||||
|
||||
txOutChecker := &testTxOutChecker{
|
||||
err: errors.New("backend unavailable"),
|
||||
}
|
||||
cfg := &Config{
|
||||
AddressManager: &mockAddressManager{
|
||||
params: &script.Parameters{
|
||||
ProtocolVersion: version.ProtocolVersion_V0,
|
||||
},
|
||||
},
|
||||
InvoicesClient: mockLnd.LndServices.Invoices,
|
||||
TxOutChecker: txOutChecker,
|
||||
}
|
||||
|
||||
f, err := NewFSM(ctx, loopIn, cfg, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
event := f.SignHtlcTxAction(ctx, nil)
|
||||
require.Equal(t, fsm.OnError, event)
|
||||
require.ErrorContains(
|
||||
t, f.LastActionError, "unable to get txout",
|
||||
)
|
||||
|
||||
select {
|
||||
case hash := <-mockLnd.FailInvoiceChannel:
|
||||
t.Fatalf("invoice should not have been canceled: %x", hash)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitHtlcActionCancelsInvoiceOnServerError verifies that an invoice
|
||||
// created before a server-side rejection is canceled immediately.
|
||||
func TestInitHtlcActionCancelsInvoiceOnServerError(t *testing.T) {
|
||||
|
|
@ -446,6 +573,24 @@ func (n *noopDepositManager) GetActiveDepositsInState(fsm.StateType) (
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
type testTxOutChecker struct {
|
||||
txOut *wire.TxOut
|
||||
err error
|
||||
|
||||
outpoints []wire.OutPoint
|
||||
includeMempool []bool
|
||||
}
|
||||
|
||||
// GetTxOut records lookup parameters and returns the configured result.
|
||||
func (t *testTxOutChecker) GetTxOut(_ context.Context,
|
||||
outpoint wire.OutPoint, includeMempool bool) (*wire.TxOut, error) {
|
||||
|
||||
t.outpoints = append(t.outpoints, outpoint)
|
||||
t.includeMempool = append(t.includeMempool, includeMempool)
|
||||
|
||||
return t.txOut, t.err
|
||||
}
|
||||
|
||||
// initHtlcTestServer lets InitHtlcAction tests inject a deterministic server
|
||||
// response without standing up the full gRPC client.
|
||||
type initHtlcTestServer struct {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
|
|
@ -105,6 +106,16 @@ type QuoteGetter interface {
|
|||
numDeposits uint32, fast bool) (*loop.LoopInQuote, error)
|
||||
}
|
||||
|
||||
// TxOutChecker checks whether an outpoint is still available in the chain
|
||||
// backend's UTXO view.
|
||||
type TxOutChecker interface {
|
||||
// GetTxOut returns nil if the outpoint is unavailable or spent. The
|
||||
// includeMempool flag must be passed through to the underlying chain
|
||||
// backend.
|
||||
GetTxOut(ctx context.Context, outpoint wire.OutPoint,
|
||||
includeMempool bool) (*wire.TxOut, error)
|
||||
}
|
||||
|
||||
type NotificationManager interface {
|
||||
// SubscribeStaticLoopInSweepRequests subscribes to the static loop in
|
||||
// sweep requests. These are sent by the server to the client to request
|
||||
|
|
|
|||
|
|
@ -93,8 +93,6 @@ type StaticAddressLoopIn struct {
|
|||
|
||||
// The outpoints in the format txid:vout that are part of the loop-in
|
||||
// swap.
|
||||
// TODO(hieblmi): Replace this with a getter method that fetches the
|
||||
// outpoints from the deposits.
|
||||
DepositOutpoints []string
|
||||
|
||||
// SelectedAmount is the amount that the user selected for the swap. If
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ type Config struct {
|
|||
// blocks.
|
||||
ChainNotifier lndclient.ChainNotifierClient
|
||||
|
||||
// TxOutChecker checks whether selected deposit outpoints are still
|
||||
// available before we sign an HTLC transaction for them.
|
||||
TxOutChecker TxOutChecker
|
||||
|
||||
// Signer is the signer client that is used to sign transactions.
|
||||
Signer lndclient.SignerClient
|
||||
|
||||
|
|
@ -755,8 +759,10 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
|
|||
}
|
||||
|
||||
swap := &StaticAddressLoopIn{
|
||||
SelectedAmount: req.SelectedAmount,
|
||||
DepositOutpoints: selectedOutpoints,
|
||||
SelectedAmount: req.SelectedAmount,
|
||||
DepositOutpoints: append(
|
||||
[]string(nil), selectedOutpoints...,
|
||||
),
|
||||
Deposits: selectedDeposits,
|
||||
Label: req.Label,
|
||||
Initiator: req.Initiator,
|
||||
|
|
|
|||
|
|
@ -507,9 +507,12 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
|
|||
}
|
||||
}
|
||||
|
||||
depositOutpoints := strings.Split(
|
||||
swap.DepositOutpoints, OutpointSeparator,
|
||||
)
|
||||
var depositOutpoints []string
|
||||
if swap.DepositOutpoints != "" {
|
||||
depositOutpoints = strings.Split(
|
||||
swap.DepositOutpoints, OutpointSeparator,
|
||||
)
|
||||
}
|
||||
|
||||
timeoutAddressString := swap.HtlcTimeoutSweepAddress
|
||||
var timeoutAddress btcutil.Address
|
||||
|
|
|
|||
|
|
@ -286,3 +286,76 @@ func TestCreateLoopIn(t *testing.T) {
|
|||
time.Microsecond,
|
||||
)
|
||||
}
|
||||
|
||||
// TestGetLoopInByHashPreservesStoredDepositOutpoints ensures recovered loop-ins
|
||||
// keep the original outpoint snapshot stored when the swap was created.
|
||||
func TestGetLoopInByHashPreservesStoredDepositOutpoints(t *testing.T) {
|
||||
ctxb := context.Background()
|
||||
testDb := loopdb.NewTestDB(t)
|
||||
testClock := clock.NewTestClock(time.Now())
|
||||
defer testDb.Close()
|
||||
|
||||
depositStore := deposit.NewSqlStore(testDb.BaseDB)
|
||||
swapStore := NewSqlStore(
|
||||
loopdb.NewTypedStore[Querier](testDb), testClock,
|
||||
&chaincfg.RegressionNetParams,
|
||||
)
|
||||
|
||||
depositID, err := deposit.GetRandomDepositID()
|
||||
require.NoError(t, err)
|
||||
|
||||
oldOutpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d},
|
||||
Index: 0,
|
||||
}
|
||||
currentOutpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{0x5a, 0x6b, 0x7c, 0x8d},
|
||||
Index: 1,
|
||||
}
|
||||
|
||||
d := &deposit.Deposit{
|
||||
ID: depositID,
|
||||
OutPoint: oldOutpoint,
|
||||
Value: btcutil.Amount(100_000),
|
||||
TimeOutSweepPkScript: []byte{
|
||||
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
|
||||
},
|
||||
}
|
||||
require.NoError(t, depositStore.CreateDeposit(ctxb, d))
|
||||
|
||||
d.SetState(deposit.LoopingIn)
|
||||
require.NoError(t, depositStore.UpdateDeposit(ctxb, d))
|
||||
|
||||
_, clientPubKey := test.CreateKey(1)
|
||||
_, serverPubKey := test.CreateKey(2)
|
||||
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
|
||||
swap := StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
|
||||
DepositOutpoints: []string{oldOutpoint.String()},
|
||||
Deposits: []*deposit.Deposit{d},
|
||||
ClientPubkey: clientPubKey,
|
||||
ServerPubkey: serverPubKey,
|
||||
HtlcTimeoutSweepAddress: addr,
|
||||
}
|
||||
swap.SetState(SignHtlcTx)
|
||||
|
||||
require.NoError(t, swapStore.CreateLoopIn(ctxb, &swap))
|
||||
|
||||
d.OutPoint = currentOutpoint
|
||||
d.ConfirmationHeight = 42
|
||||
require.NoError(t, depositStore.UpdateDeposit(ctxb, d))
|
||||
|
||||
storedSwap, err := swapStore.GetLoopInByHash(ctxb, swapHash)
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t, []string{oldOutpoint.String()},
|
||||
storedSwap.DepositOutpoints,
|
||||
)
|
||||
require.Len(t, storedSwap.Deposits, 1)
|
||||
require.Equal(t, currentOutpoint, storedSwap.Deposits[0].OutPoint)
|
||||
require.Equal(t, int64(42), storedSwap.Deposits[0].ConfirmationHeight)
|
||||
}
|
||||
|
|
|
|||
72
staticaddr/loopin/txout_checker.go
Normal file
72
staticaddr/loopin/txout_checker.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
)
|
||||
|
||||
// lndTxOutChecker checks outpoint availability using lnd's wallet transaction
|
||||
// view. It returns nil for outputs already spent by a wallet-known transaction.
|
||||
type lndTxOutChecker struct {
|
||||
client lndclient.LightningClient
|
||||
}
|
||||
|
||||
// NewLndTxOutChecker creates a TxOutChecker backed by lnd.
|
||||
func NewLndTxOutChecker(client lndclient.LightningClient) TxOutChecker {
|
||||
return &lndTxOutChecker{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTxOut returns the tx output if lnd's transaction view still reports the
|
||||
// outpoint as unspent.
|
||||
func (c *lndTxOutChecker) GetTxOut(ctx context.Context,
|
||||
outpoint wire.OutPoint, includeMempool bool) (*wire.TxOut, error) {
|
||||
|
||||
endHeight := int32(0)
|
||||
if includeMempool {
|
||||
endHeight = -1
|
||||
}
|
||||
|
||||
// We need lnd's wallet transaction view rather than only the funding
|
||||
// transaction: a matching previous outpoint tells us the deposit has
|
||||
// already been spent by a wallet-known transaction. When mempool spends
|
||||
// matter, lnd exposes them through ListTransactions with endHeight=-1.
|
||||
txs, err := c.client.ListTransactions(ctx, 0, endHeight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outpointStr := outpoint.String()
|
||||
for _, tx := range txs {
|
||||
for _, prevOutpoint := range tx.PreviousOutpoints {
|
||||
if prevOutpoint.GetOutpoint() == outpointStr {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tx := range txs {
|
||||
if tx.Tx == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
txHash := tx.TxHash
|
||||
if txHash == "" {
|
||||
txHash = tx.Tx.TxHash().String()
|
||||
}
|
||||
if txHash != outpoint.Hash.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
if int(outpoint.Index) >= len(tx.Tx.TxOut) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return tx.Tx.TxOut[outpoint.Index], nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -170,7 +170,9 @@ func (h *mockLightningClient) LookupInvoice(_ context.Context,
|
|||
return nil, fmt.Errorf("invoice: %x not found", hash)
|
||||
}
|
||||
|
||||
return inv, nil
|
||||
invoiceCopy := *inv
|
||||
|
||||
return &invoiceCopy, nil
|
||||
}
|
||||
|
||||
// ListTransactions returns all known transactions of the backing lnd node.
|
||||
|
|
|
|||
|
|
@ -218,6 +218,15 @@ func (s *LndMockServices) AddTx(tx *wire.MsgTx) {
|
|||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
// SetInvoice stores a copy of the given invoice in the mock invoice store.
|
||||
func (s *LndMockServices) SetInvoice(invoice *lndclient.Invoice) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
invoiceCopy := *invoice
|
||||
s.Invoices[invoice.Hash] = &invoiceCopy
|
||||
}
|
||||
|
||||
// IsDone checks whether all channels have been fully emptied. If not this may
|
||||
// indicate unexpected behaviour of the code under test.
|
||||
func (s *LndMockServices) IsDone() error {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue