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 checking whether a selected deposit outpoint is still available before signing the HTLC transaction. Back the implementation with lnd wallet transaction data so known confirmed and mempool spends mark the outpoint unavailable.
This commit is contained in:
parent
508bf90a1c
commit
f8e9d11d04
3 changed files with 201 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"
|
||||
|
|
@ -105,6 +106,15 @@ type QuoteGetter interface {
|
|||
numDeposits uint32, fast bool) (*loop.LoopInQuote, error)
|
||||
}
|
||||
|
||||
// TxOutChecker checks whether outpoints are still available in the chain
|
||||
// backend's UTXO view.
|
||||
type TxOutChecker interface {
|
||||
// GetTxOuts returns entries for the requested outpoints that are
|
||||
// available and unspent. Missing entries are unavailable or spent.
|
||||
GetTxOuts(ctx context.Context, outpoints []wire.OutPoint) (
|
||||
map[wire.OutPoint]*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
|
||||
|
|
|
|||
79
staticaddr/loopin/txout_checker.go
Normal file
79
staticaddr/loopin/txout_checker.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
)
|
||||
|
||||
// lndTxOutChecker checks outpoint availability using lnd's wallet transaction
|
||||
// view. It omits 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,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTxOuts returns all requested tx outputs that lnd's transaction view still
|
||||
// reports as unspent.
|
||||
func (c *lndTxOutChecker) GetTxOuts(ctx context.Context,
|
||||
outpoints []wire.OutPoint) (map[wire.OutPoint]*wire.TxOut, error) {
|
||||
|
||||
outpointByString := make(map[string]wire.OutPoint, len(outpoints))
|
||||
outpointsByHash := make(map[string][]wire.OutPoint, len(outpoints))
|
||||
for _, outpoint := range outpoints {
|
||||
outpointByString[outpoint.String()] = outpoint
|
||||
outpointsByHash[outpoint.Hash.String()] = append(
|
||||
outpointsByHash[outpoint.Hash.String()], outpoint,
|
||||
)
|
||||
}
|
||||
|
||||
// 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. Use endHeight=-1 so
|
||||
// lnd includes unconfirmed transactions and mempool spends.
|
||||
txs, err := c.client.ListTransactions(ctx, 0, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txOuts := make(map[wire.OutPoint]*wire.TxOut, len(outpoints))
|
||||
spent := make(map[wire.OutPoint]struct{}, len(outpoints))
|
||||
for _, tx := range txs {
|
||||
for _, prevOutpoint := range tx.PreviousOutpoints {
|
||||
outpoint, ok := outpointByString[prevOutpoint.GetOutpoint()]
|
||||
if ok {
|
||||
spent[outpoint] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if tx.Tx == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
txHash := tx.TxHash
|
||||
if txHash == "" {
|
||||
txHash = tx.Tx.TxHash().String()
|
||||
}
|
||||
|
||||
for _, outpoint := range outpointsByHash[txHash] {
|
||||
if int(outpoint.Index) >= len(tx.Tx.TxOut) {
|
||||
continue
|
||||
}
|
||||
|
||||
txOuts[outpoint] = tx.Tx.TxOut[outpoint.Index]
|
||||
}
|
||||
}
|
||||
|
||||
for outpoint := range spent {
|
||||
delete(txOuts, outpoint)
|
||||
}
|
||||
|
||||
return txOuts, nil
|
||||
}
|
||||
112
staticaddr/loopin/txout_checker_test.go
Normal file
112
staticaddr/loopin/txout_checker_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
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 outputs", func(t *testing.T) {
|
||||
otherOutpoint := wire.OutPoint{
|
||||
Hash: fundingTx.TxHash(),
|
||||
Index: 0,
|
||||
}
|
||||
client := &mockTxListLightningClient{
|
||||
txs: []lndclient.Transaction{{
|
||||
Tx: fundingTx,
|
||||
}},
|
||||
}
|
||||
|
||||
checker := NewLndTxOutChecker(client)
|
||||
txOuts, err := checker.GetTxOuts(
|
||||
t.Context(), []wire.OutPoint{outpoint, otherOutpoint},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fundingTx.TxOut[outpoint.Index], txOuts[outpoint])
|
||||
require.Equal(
|
||||
t, fundingTx.TxOut[otherOutpoint.Index],
|
||||
txOuts[otherOutpoint],
|
||||
)
|
||||
require.Equal(t, []txListCall{{
|
||||
startHeight: 0,
|
||||
endHeight: -1,
|
||||
}}, 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)
|
||||
txOuts, err := checker.GetTxOuts(
|
||||
t.Context(), []wire.OutPoint{outpoint},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, txOuts[outpoint])
|
||||
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)
|
||||
txOuts, err := checker.GetTxOuts(
|
||||
t.Context(), []wire.OutPoint{outpoint},
|
||||
)
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
require.Nil(t, txOuts)
|
||||
})
|
||||
}
|
||||
|
||||
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