mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/loopin: add lnd txout checker
Add a TxOutChecker interface for testing whether an original deposit outpoint is still available before a static loop-in signs its HTLC transaction. Implement the checker using lnd's wallet transaction view so wallet-known spends, including mempool spends when requested, cause the outpoint to be reported unavailable.
This commit is contained in:
parent
cee30f29ae
commit
e02e5bc258
3 changed files with 181 additions and 0 deletions
|
|
@ -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"
|
||||
|
|
@ -108,6 +109,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
|
||||
|
|
|
|||
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
|
||||
}
|
||||
98
staticaddr/loopin/txout_checker_test.go
Normal file
98
staticaddr/loopin/txout_checker_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLndTxOutChecker(t *testing.T) {
|
||||
fundingTx := wire.NewMsgTx(2)
|
||||
fundingTx.AddTxOut(wire.NewTxOut(1000, []byte{0x01}))
|
||||
fundingTx.AddTxOut(wire.NewTxOut(2000, []byte{0x02}))
|
||||
|
||||
outpoint := wire.OutPoint{
|
||||
Hash: fundingTx.TxHash(),
|
||||
Index: 1,
|
||||
}
|
||||
|
||||
t.Run("returns live tx output", func(t *testing.T) {
|
||||
client := &mockTxListLightningClient{
|
||||
txs: []lndclient.Transaction{{
|
||||
Tx: fundingTx,
|
||||
}},
|
||||
}
|
||||
|
||||
checker := NewLndTxOutChecker(client)
|
||||
txOut, err := checker.GetTxOut(t.Context(), outpoint, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fundingTx.TxOut[outpoint.Index], txOut)
|
||||
require.Equal(t, []txListCall{{
|
||||
startHeight: 0,
|
||||
endHeight: 0,
|
||||
}}, client.calls)
|
||||
})
|
||||
|
||||
t.Run("returns nil for known spend", func(t *testing.T) {
|
||||
client := &mockTxListLightningClient{
|
||||
txs: []lndclient.Transaction{{
|
||||
Tx: fundingTx,
|
||||
}, {
|
||||
PreviousOutpoints: []*lnrpc.PreviousOutPoint{{
|
||||
Outpoint: outpoint.String(),
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
checker := NewLndTxOutChecker(client)
|
||||
txOut, err := checker.GetTxOut(t.Context(), outpoint, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, txOut)
|
||||
require.Equal(t, []txListCall{{
|
||||
startHeight: 0,
|
||||
endHeight: -1,
|
||||
}}, client.calls)
|
||||
})
|
||||
|
||||
t.Run("returns error", func(t *testing.T) {
|
||||
expectedErr := errors.New("list transactions failed")
|
||||
client := &mockTxListLightningClient{
|
||||
err: expectedErr,
|
||||
}
|
||||
|
||||
checker := NewLndTxOutChecker(client)
|
||||
txOut, err := checker.GetTxOut(t.Context(), outpoint, false)
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
require.Nil(t, txOut)
|
||||
})
|
||||
}
|
||||
|
||||
type txListCall struct {
|
||||
startHeight int32
|
||||
endHeight int32
|
||||
}
|
||||
|
||||
type mockTxListLightningClient struct {
|
||||
lndclient.LightningClient
|
||||
|
||||
txs []lndclient.Transaction
|
||||
err error
|
||||
calls []txListCall
|
||||
}
|
||||
|
||||
func (m *mockTxListLightningClient) ListTransactions(_ context.Context,
|
||||
startHeight, endHeight int32, _ ...lndclient.ListTransactionsOption) (
|
||||
[]lndclient.Transaction, error) {
|
||||
|
||||
m.calls = append(m.calls, txListCall{
|
||||
startHeight: startHeight,
|
||||
endHeight: endHeight,
|
||||
})
|
||||
|
||||
return m.txs, m.err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue