Merge pull request #8496 from aakselrod/locks-to-leases

multi: replace `LockOutpoint` with `LeaseOutput`
This commit is contained in:
Oliver Gugger authored and GitHub committed 2024-03-18 03:34:56 -06:00
commit 0bc3d29413
21 files changed
+225 -148

No files matched your search

+2 -1
View File
@@ -22,6 +22,7 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@@ -1537,7 +1538,7 @@ func releaseOutput(ctx *cli.Context) error {
return fmt.Errorf("error parsing outpoint: %w", err) return fmt.Errorf("error parsing outpoint: %w", err)
} }
lockID := walletrpc.LndInternalLockID[:] lockID := chanfunding.LndInternalLockID[:]
lockIDStr := ctx.String("lockid") lockIDStr := ctx.String("lockid")
if lockIDStr != "" { if lockIDStr != "" {
var err error var err error
@@ -90,6 +90,11 @@
precision issue when querying payments and invoices using the start and end precision issue when querying payments and invoices using the start and end
date filters. date filters.
* [Fixed](https://github.com/lightningnetwork/lnd/pull/8496) an issue where
`locked_balance` is not updated in `WalletBalanceResponse` when outputs are
reserved for `OpenChannel` by using non-volatile leases instead of volatile
locks.
# New Features # New Features
## Functional Enhancements ## Functional Enhancements
@@ -364,6 +369,7 @@
# Contributors (Alphabetical Order) # Contributors (Alphabetical Order)
* Alex Akselrod
* Amin Bashiri * Amin Bashiri
* Andras Banki-Horvath * Andras Banki-Horvath
* BitcoinerCoderBob * BitcoinerCoderBob
+4
View File
@@ -574,4 +574,8 @@ var allTestCases = []*lntest.TestCase{
Name: "coop close with htlcs", Name: "coop close with htlcs",
TestFunc: testCoopCloseWithHtlcs, TestFunc: testCoopCloseWithHtlcs,
}, },
{
Name: "open channel locked balance",
TestFunc: testOpenChannelLockedBalance,
},
} }
+57
View File
@@ -14,6 +14,7 @@ import (
"github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntest/rpc"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -822,3 +823,59 @@ func testSimpleTaprootChannelActivation(ht *lntest.HarnessTest) {
// Our test is done and Alice closes her channel to Bob. // Our test is done and Alice closes her channel to Bob.
ht.CloseChannel(alice, chanPoint) ht.CloseChannel(alice, chanPoint)
} }
// testOpenChannelLockedBalance tests that when a funding reservation is
// made for opening a channel, the balance of the required outputs shows
// up as locked balance in the WalletBalance response.
func testOpenChannelLockedBalance(ht *lntest.HarnessTest) {
var (
alice = ht.Alice
bob = ht.Bob
req *lnrpc.ChannelAcceptRequest
err error
)
// We first make sure Alice has no locked wallet balance.
balance := alice.RPC.WalletBalance()
require.EqualValues(ht, 0, balance.LockedBalance)
// Next, we register a ChannelAcceptor on Bob. This way, we can get
// Alice's wallet balance after coin selection is done and outpoints
// are locked.
stream, cancel := bob.RPC.ChannelAcceptor()
defer cancel()
// Then, we request creation of a channel from Alice to Bob. We don't
// use OpenChannelSync since we want to receive Bob's message in the
// same goroutine.
openChannelReq := &lnrpc.OpenChannelRequest{
NodePubkey: bob.PubKey[:],
LocalFundingAmount: int64(funding.MaxBtcFundingAmount),
}
_ = alice.RPC.OpenChannel(openChannelReq)
// After that, we receive the request on Bob's side, get the wallet
// balance from Alice, and ensure the locked balance is non-zero.
err = wait.NoError(func() error {
req, err = stream.Recv()
return err
}, defaultTimeout)
require.NoError(ht, err)
balance = alice.RPC.WalletBalance()
require.NotEqualValues(ht, 0, balance.LockedBalance)
// Next, we let Bob deny the request.
resp := &lnrpc.ChannelAcceptResponse{
Accept: false,
PendingChanId: req.PendingChanId,
}
err = wait.NoError(func() error {
return stream.Send(resp)
}, defaultTimeout)
require.NoError(ht, err)
// Finally, we check to make sure the balance is unlocked again.
balance = alice.RPC.WalletBalance()
require.EqualValues(ht, 0, balance.LockedBalance)
}
-4
View File
@@ -69,10 +69,6 @@ const (
// closure. // closure.
DefaultOutgoingCltvRejectDelta = DefaultOutgoingBroadcastDelta + 3 DefaultOutgoingCltvRejectDelta = DefaultOutgoingBroadcastDelta + 3
// DefaultReservationTimeout is the default time we wait until we remove
// an unfinished (zombiestate) open channel flow from memory.
DefaultReservationTimeout = 10 * time.Minute
// DefaultZombieSweeperInterval is the default time interval at which // DefaultZombieSweeperInterval is the default time interval at which
// unfinished (zombiestate) open channel flows are purged from memory. // unfinished (zombiestate) open channel flows are purged from memory.
DefaultZombieSweeperInterval = 1 * time.Minute DefaultZombieSweeperInterval = 1 * time.Minute
+3 -1
View File
@@ -4,6 +4,8 @@ package lncfg
import ( import (
"time" "time"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
) )
// IsDevBuild returns a bool to indicate whether we are in a development // IsDevBuild returns a bool to indicate whether we are in a development
@@ -37,7 +39,7 @@ func (d *DevConfig) GetUnsafeDisconnect() bool {
// GetReservationTimeout returns the config value for `ReservationTimeout`. // GetReservationTimeout returns the config value for `ReservationTimeout`.
func (d *DevConfig) GetReservationTimeout() time.Duration { func (d *DevConfig) GetReservationTimeout() time.Duration {
return DefaultReservationTimeout return chanfunding.DefaultReservationTimeout
} }
// GetZombieSweeperInterval returns the config value for`ZombieSweeperInterval`. // GetZombieSweeperInterval returns the config value for`ZombieSweeperInterval`.
+3 -1
View File
@@ -4,6 +4,8 @@ package lncfg
import ( import (
"time" "time"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
) )
// IsDevBuild returns a bool to indicate whether we are in a development // IsDevBuild returns a bool to indicate whether we are in a development
@@ -33,7 +35,7 @@ func (d *DevConfig) ChannelReadyWait() time.Duration {
// GetReservationTimeout returns the config value for `ReservationTimeout`. // GetReservationTimeout returns the config value for `ReservationTimeout`.
func (d *DevConfig) GetReservationTimeout() time.Duration { func (d *DevConfig) GetReservationTimeout() time.Duration {
if d.ReservationTimeout == 0 { if d.ReservationTimeout == 0 {
return DefaultReservationTimeout return chanfunding.DefaultReservationTimeout
} }
return d.ReservationTimeout return d.ReservationTimeout
+5 -9
View File
@@ -6,23 +6,18 @@ package walletrpc
import ( import (
"fmt" "fmt"
"math" "math"
"time"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
base "github.com/btcsuite/btcwallet/wallet" base "github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
) )
const ( const (
defaultMaxConf = math.MaxInt32 defaultMaxConf = math.MaxInt32
) )
var (
// DefaultLockDuration is the default duration used to lock outputs.
DefaultLockDuration = 10 * time.Minute
)
// verifyInputsUnspent checks that all inputs are contained in the list of // verifyInputsUnspent checks that all inputs are contained in the list of
// known, non-locked UTXOs given. // known, non-locked UTXOs given.
func verifyInputsUnspent(inputs []*wire.TxIn, utxos []*lnwallet.Utxo) error { func verifyInputsUnspent(inputs []*wire.TxIn, utxos []*lnwallet.Utxo) error {
@@ -56,13 +51,14 @@ func lockInputs(w lnwallet.WalletController,
for idx := range outpoints { for idx := range outpoints {
lock := &base.ListLeasedOutputResult{ lock := &base.ListLeasedOutputResult{
LockedOutput: &wtxmgr.LockedOutput{ LockedOutput: &wtxmgr.LockedOutput{
LockID: LndInternalLockID, LockID: chanfunding.LndInternalLockID,
Outpoint: outpoints[idx], Outpoint: outpoints[idx],
}, },
} }
expiration, pkScript, value, err := w.LeaseOutput( expiration, pkScript, value, err := w.LeaseOutput(
lock.LockID, lock.Outpoint, DefaultLockDuration, lock.LockID, lock.Outpoint,
chanfunding.DefaultLockDuration,
) )
if err != nil { if err != nil {
// If we run into a problem with locking one output, we // If we run into a problem with locking one output, we
@@ -72,7 +68,7 @@ func lockInputs(w lnwallet.WalletController,
for i := 0; i < idx; i++ { for i := 0; i < idx; i++ {
op := locks[i].Outpoint op := locks[i].Outpoint
if err := w.ReleaseOutput( if err := w.ReleaseOutput(
LndInternalLockID, op, chanfunding.LndInternalLockID, op,
); err != nil { ); err != nil {
log.Errorf("could not release the "+ log.Errorf("could not release the "+
"lock on %v: %v", op, err) "lock on %v: %v", op, err)
+2 -14
View File
@@ -184,18 +184,6 @@ var (
// configuration file in this package. // configuration file in this package.
DefaultWalletKitMacFilename = "walletkit.macaroon" DefaultWalletKitMacFilename = "walletkit.macaroon"
// LndInternalLockID is the binary representation of the SHA256 hash of
// the string "lnd-internal-lock-id" and is used for UTXO lock leases to
// identify that we ourselves are locking an UTXO, for example when
// giving out a funded PSBT. The ID corresponds to the hex value of
// ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98.
LndInternalLockID = wtxmgr.LockID{
0xed, 0xe1, 0x9a, 0x92, 0xed, 0x32, 0x1a, 0x47,
0x05, 0xf8, 0xa1, 0xcc, 0xcc, 0x1d, 0x4f, 0x61,
0x82, 0x54, 0x5d, 0x4b, 0xb4, 0xfa, 0xe0, 0x8b,
0xd5, 0x93, 0x78, 0x31, 0xb7, 0xe3, 0x8f, 0x98,
}
// allWitnessTypes is a mapping between the witness types defined in the // allWitnessTypes is a mapping between the witness types defined in the
// `input` package, and the witness types in the protobuf definition. // `input` package, and the witness types in the protobuf definition.
// This map is necessary because the native enum and the protobuf enum // This map is necessary because the native enum and the protobuf enum
@@ -482,7 +470,7 @@ func (w *WalletKit) LeaseOutput(ctx context.Context,
// Don't allow our internal ID to be used externally for locking. Only // Don't allow our internal ID to be used externally for locking. Only
// unlocking is allowed. // unlocking is allowed.
if lockID == LndInternalLockID { if lockID == chanfunding.LndInternalLockID {
return nil, errors.New("reserved id cannot be used") return nil, errors.New("reserved id cannot be used")
} }
@@ -492,7 +480,7 @@ func (w *WalletKit) LeaseOutput(ctx context.Context,
} }
// Use the specified lock duration or fall back to the default. // Use the specified lock duration or fall back to the default.
duration := DefaultLockDuration duration := chanfunding.DefaultLockDuration
if req.ExpirationSeconds != 0 { if req.ExpirationSeconds != 0 {
duration = time.Duration(req.ExpirationSeconds) * time.Second duration = time.Duration(req.ExpirationSeconds) * time.Second
} }
-6
View File
@@ -186,12 +186,6 @@ func (w *WalletController) ListTransactionDetails(int32, int32,
return nil, nil return nil, nil
} }
// LockOutpoint currently does nothing.
func (w *WalletController) LockOutpoint(o wire.OutPoint) {}
// UnlockOutpoint currently does nothing.
func (w *WalletController) UnlockOutpoint(o wire.OutPoint) {}
// LeaseOutput returns the current time and a nil error. // LeaseOutput returns the current time and a nil error.
func (w *WalletController) LeaseOutput(wtxmgr.LockID, wire.OutPoint, func (w *WalletController) LeaseOutput(wtxmgr.LockID, wire.OutPoint,
time.Duration) (time.Time, []byte, btcutil.Amount, error) { time.Duration) (time.Time, []byte, btcutil.Amount, error) {
+1 -1
View File
@@ -20,7 +20,7 @@ const (
// DefaultTimeout is a timeout that will be used for various wait // DefaultTimeout is a timeout that will be used for various wait
// scenarios where no custom timeout value is defined. // scenarios where no custom timeout value is defined.
DefaultTimeout = time.Second * 30 DefaultTimeout = time.Second * 45
// AsyncBenchmarkTimeout is the timeout used when running the async // AsyncBenchmarkTimeout is the timeout used when running the async
// payments benchmark. // payments benchmark.
-22
View File
@@ -1053,28 +1053,6 @@ func (b *BtcWallet) CreateSimpleTx(outputs []*wire.TxOut,
) )
} }
// LockOutpoint marks an outpoint as locked meaning it will no longer be deemed
// as eligible for coin selection. Locking outputs are utilized in order to
// avoid race conditions when selecting inputs for usage when funding a
// channel.
//
// NOTE: This method requires the global coin selection lock to be held.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) LockOutpoint(o wire.OutPoint) {
b.wallet.LockOutpoint(o)
}
// UnlockOutpoint unlocks a previously locked output, marking it eligible for
// coin selection.
//
// NOTE: This method requires the global coin selection lock to be held.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) UnlockOutpoint(o wire.OutPoint) {
b.wallet.UnlockOutpoint(o)
}
// LeaseOutput locks an output to the given ID, preventing it from being // LeaseOutput locks an output to the given ID, preventing it from being
// available for any future coin selection attempts. The absolute time of the // available for any future coin selection attempts. The absolute time of the
// lock's expiration is returned. The expiration of the lock can be extended by // lock's expiration is returned. The expiration of the lock can be extended by
+12 -9
View File
@@ -1,9 +1,12 @@
package chanfunding package chanfunding
import ( import (
"time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
@@ -34,18 +37,18 @@ type CoinSelectionLocker interface {
WithCoinSelectLock(func() error) error WithCoinSelectLock(func() error) error
} }
// OutpointLocker allows a caller to lock/unlock an outpoint. When locked, the // OutputLeaser allows a caller to lease/release an output. When leased, the
// outpoints shouldn't be used for any sort of channel funding of coin // outputs shouldn't be used for any sort of channel funding or coin selection.
// selection. Locked outpoints are not expected to be persisted between // Leased outputs are expected to be persisted between restarts.
// restarts. type OutputLeaser interface {
type OutpointLocker interface { // LeaseOutput leases a target output, rendering it unusable for coin
// LockOutpoint locks a target outpoint, rendering it unusable for coin
// selection. // selection.
LockOutpoint(o wire.OutPoint) LeaseOutput(i wtxmgr.LockID, o wire.OutPoint, d time.Duration) (
time.Time, []byte, btcutil.Amount, error)
// UnlockOutpoint unlocks a target outpoint, allowing it to be used for // ReleaseOutput releases a target output, allowing it to be used for
// coin selection once again. // coin selection once again.
UnlockOutpoint(o wire.OutPoint) ReleaseOutput(i wtxmgr.LockID, o wire.OutPoint) error
} }
// Request is a new request for funding a channel. The items in the struct // Request is a new request for funding a channel. The items in the struct
+44 -7
View File
@@ -3,6 +3,7 @@ package chanfunding
import ( import (
"fmt" "fmt"
"math" "math"
"time"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
@@ -10,10 +11,34 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
) )
const (
// DefaultReservationTimeout is the default time we wait until we remove
// an unfinished (zombiestate) open channel flow from memory.
DefaultReservationTimeout = 10 * time.Minute
// DefaultLockDuration is the default duration used to lock outputs.
DefaultLockDuration = 10 * time.Minute
)
var (
// LndInternalLockID is the binary representation of the SHA256 hash of
// the string "lnd-internal-lock-id" and is used for UTXO lock leases to
// identify that we ourselves are locking an UTXO, for example when
// giving out a funded PSBT. The ID corresponds to the hex value of
// ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98.
LndInternalLockID = wtxmgr.LockID{
0xed, 0xe1, 0x9a, 0x92, 0xed, 0x32, 0x1a, 0x47,
0x05, 0xf8, 0xa1, 0xcc, 0xcc, 0x1d, 0x4f, 0x61,
0x82, 0x54, 0x5d, 0x4b, 0xb4, 0xfa, 0xe0, 0x8b,
0xd5, 0x93, 0x78, 0x31, 0xb7, 0xe3, 0x8f, 0x98,
}
)
// FullIntent is an intent that is fully backed by the internal wallet. This // FullIntent is an intent that is fully backed by the internal wallet. This
// intent differs from the ShimIntent, in that the funding transaction will be // intent differs from the ShimIntent, in that the funding transaction will be
// constructed internally, and will consist of only inputs we wholly control. // constructed internally, and will consist of only inputs we wholly control.
@@ -37,9 +62,9 @@ type FullIntent struct {
// change from the main funding transaction. // change from the main funding transaction.
ChangeOutputs []*wire.TxOut ChangeOutputs []*wire.TxOut
// coinLocker is the Assembler's instance of the OutpointLocker // coinLeaser is the Assembler's instance of the OutputLeaser
// interface. // interface.
coinLocker OutpointLocker coinLeaser OutputLeaser
// coinSource is the Assembler's instance of the CoinSource interface. // coinSource is the Assembler's instance of the CoinSource interface.
coinSource CoinSource coinSource CoinSource
@@ -194,7 +219,13 @@ func (f *FullIntent) Outputs() []*wire.TxOut {
// NOTE: Part of the chanfunding.Intent interface. // NOTE: Part of the chanfunding.Intent interface.
func (f *FullIntent) Cancel() { func (f *FullIntent) Cancel() {
for _, coin := range f.InputCoins { for _, coin := range f.InputCoins {
f.coinLocker.UnlockOutpoint(coin.OutPoint) err := f.coinLeaser.ReleaseOutput(
LndInternalLockID, coin.OutPoint,
)
if err != nil {
log.Warnf("Failed to release UTXO %s (%v))",
coin.OutPoint, err)
}
} }
f.ShimIntent.Cancel() f.ShimIntent.Cancel()
@@ -216,9 +247,9 @@ type WalletConfig struct {
// access to the current set of coins returned by the CoinSource. // access to the current set of coins returned by the CoinSource.
CoinSelectLocker CoinSelectionLocker CoinSelectLocker CoinSelectionLocker
// CoinLocker is what the WalletAssembler uses to lock coins that may // CoinLeaser is what the WalletAssembler uses to lease coins that may
// be used as inputs for a new funding transaction. // be used as inputs for a new funding transaction.
CoinLocker OutpointLocker CoinLeaser OutputLeaser
// Signer allows the WalletAssembler to sign inputs on any potential // Signer allows the WalletAssembler to sign inputs on any potential
// funding transactions. // funding transactions.
@@ -493,7 +524,13 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
outpoint := coin.OutPoint outpoint := coin.OutPoint
w.cfg.CoinLocker.LockOutpoint(outpoint) _, _, _, err = w.cfg.CoinLeaser.LeaseOutput(
LndInternalLockID, outpoint,
DefaultReservationTimeout,
)
if err != nil {
return err
}
} }
newIntent := &FullIntent{ newIntent := &FullIntent{
@@ -503,7 +540,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
musig2: r.Musig2, musig2: r.Musig2,
}, },
InputCoins: selectedCoins, InputCoins: selectedCoins,
coinLocker: w.cfg.CoinLocker, coinLeaser: w.cfg.CoinLeaser,
coinSource: w.cfg.CoinSource, coinSource: w.cfg.CoinSource,
signer: w.cfg.Signer, signer: w.cfg.Signer,
} }
-14
View File
@@ -393,20 +393,6 @@ type WalletController interface {
ListTransactionDetails(startHeight, endHeight int32, ListTransactionDetails(startHeight, endHeight int32,
accountFilter string) ([]*TransactionDetail, error) accountFilter string) ([]*TransactionDetail, error)
// LockOutpoint marks an outpoint as locked meaning it will no longer
// be deemed as eligible for coin selection. Locking outputs are
// utilized in order to avoid race conditions when selecting inputs for
// usage when funding a channel.
//
// NOTE: This method requires the global coin selection lock to be held.
LockOutpoint(o wire.OutPoint)
// UnlockOutpoint unlocks a previously locked output, marking it
// eligible for coin selection.
//
// NOTE: This method requires the global coin selection lock to be held.
UnlockOutpoint(o wire.OutPoint)
// LeaseOutput locks an output to the given ID, preventing it from being // LeaseOutput locks an output to the given ID, preventing it from being
// available for any future coin selection attempts. The absolute time // available for any future coin selection attempts. The absolute time
// of the lock's expiration is returned. The expiration of the lock can // of the lock's expiration is returned. The expiration of the lock can
-6
View File
@@ -192,12 +192,6 @@ func (w *mockWalletController) ListTransactionDetails(int32, int32,
return nil, nil return nil, nil
} }
// LockOutpoint currently does nothing.
func (w *mockWalletController) LockOutpoint(o wire.OutPoint) {}
// UnlockOutpoint currently does nothing.
func (w *mockWalletController) UnlockOutpoint(o wire.OutPoint) {}
// LeaseOutput returns the current time and a nil error. // LeaseOutput returns the current time and a nil error.
func (w *mockWalletController) LeaseOutput(wtxmgr.LockID, wire.OutPoint, func (w *mockWalletController) LeaseOutput(wtxmgr.LockID, wire.OutPoint,
time.Duration) (time.Time, []byte, btcutil.Amount, error) { time.Duration) (time.Time, []byte, btcutil.Amount, error) {
+1 -1
View File
@@ -2990,7 +2990,7 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness,
chanfunding.WalletConfig{ chanfunding.WalletConfig{
CoinSource: lnwallet.NewCoinSource(alice), CoinSource: lnwallet.NewCoinSource(alice),
CoinSelectLocker: alice, CoinSelectLocker: alice,
CoinLocker: alice, CoinLeaser: alice,
Signer: alice.Cfg.Signer, Signer: alice.Cfg.Signer,
DustLimit: 600, DustLimit: 600,
CoinSelectionStrategy: wallet.CoinSelectionLargest, CoinSelectionStrategy: wallet.CoinSelectionLargest,
+6 -3
View File
@@ -588,7 +588,7 @@ func (l *LightningWallet) ResetReservations() {
l.reservationIDs = make(map[[32]byte]uint64) l.reservationIDs = make(map[[32]byte]uint64)
for outpoint := range l.lockedOutPoints { for outpoint := range l.lockedOutPoints {
l.UnlockOutpoint(outpoint) _ = l.ReleaseOutput(chanfunding.LndInternalLockID, outpoint)
} }
l.lockedOutPoints = make(map[wire.OutPoint]struct{}) l.lockedOutPoints = make(map[wire.OutPoint]struct{})
} }
@@ -851,7 +851,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
cfg := chanfunding.WalletConfig{ cfg := chanfunding.WalletConfig{
CoinSource: &CoinSource{l}, CoinSource: &CoinSource{l},
CoinSelectLocker: l, CoinSelectLocker: l,
CoinLocker: l, CoinLeaser: l,
Signer: l.Cfg.Signer, Signer: l.Cfg.Signer,
DustLimit: DustLimitForSize( DustLimit: DustLimitForSize(
input.P2WSHSize, input.P2WSHSize,
@@ -1425,7 +1425,10 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
// requests. // requests.
for _, unusedInput := range pendingReservation.ourContribution.Inputs { for _, unusedInput := range pendingReservation.ourContribution.Inputs {
delete(l.lockedOutPoints, unusedInput.PreviousOutPoint) delete(l.lockedOutPoints, unusedInput.PreviousOutPoint)
l.UnlockOutpoint(unusedInput.PreviousOutPoint) _ = l.ReleaseOutput(
chanfunding.LndInternalLockID,
unusedInput.PreviousOutPoint,
)
} }
// TODO(roasbeef): is it even worth it to keep track of unused keys? // TODO(roasbeef): is it even worth it to keep track of unused keys?
+2 -1
View File
@@ -53,6 +53,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/nat" "github.com/lightningnetwork/lnd/nat"
@@ -1279,7 +1280,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
// For the reservationTimeout and the zombieSweeperInterval different // For the reservationTimeout and the zombieSweeperInterval different
// values are set in case we are in a dev environment so enhance test // values are set in case we are in a dev environment so enhance test
// capacilities. // capacilities.
reservationTimeout := lncfg.DefaultReservationTimeout reservationTimeout := chanfunding.DefaultReservationTimeout
zombieSweeperInterval := lncfg.DefaultZombieSweeperInterval zombieSweeperInterval := lncfg.DefaultZombieSweeperInterval
// Get the development config for funding manager. If we are not in // Get the development config for funding manager. If we are not in
+30 -12
View File
@@ -3,13 +3,16 @@ package sweep
import ( import (
"fmt" "fmt"
"math" "math"
"time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
) )
const ( const (
@@ -134,17 +137,18 @@ type CoinSelectionLocker interface {
WithCoinSelectLock(func() error) error WithCoinSelectLock(func() error) error
} }
// OutpointLocker allows a caller to lock/unlock an outpoint. When locked, the // OutputLeaser allows a caller to lease/release an output. When leased, the
// outpoints shouldn't be used for any sort of channel funding of coin // outputs shouldn't be used for any sort of channel funding or coin selection.
// selection. Locked outpoints are not expected to be persisted between restarts. // Leased outputs are expected to be persisted between restarts.
type OutpointLocker interface { type OutputLeaser interface {
// LockOutpoint locks a target outpoint, rendering it unusable for coin // LeaseOutput leases a target output, rendering it unusable for coin
// selection. // selection.
LockOutpoint(o wire.OutPoint) LeaseOutput(i wtxmgr.LockID, o wire.OutPoint, d time.Duration) (
time.Time, []byte, btcutil.Amount, error)
// UnlockOutpoint unlocks a target outpoint, allowing it to be used for // ReleaseOutput releases a target output, allowing it to be used for
// coin selection once again. // coin selection once again.
UnlockOutpoint(o wire.OutPoint) ReleaseOutput(i wtxmgr.LockID, o wire.OutPoint) error
} }
// WalletSweepPackage is a package that gives the caller the ability to sweep // WalletSweepPackage is a package that gives the caller the ability to sweep
@@ -179,11 +183,11 @@ type DeliveryAddr struct {
// leftover amount after these outputs and transaction fee, is sent to a single // leftover amount after these outputs and transaction fee, is sent to a single
// output, as specified by the change address. The sweep transaction will be // output, as specified by the change address. The sweep transaction will be
// crafted with the target fee rate, and will use the utxoSource and // crafted with the target fee rate, and will use the utxoSource and
// outpointLocker as sources for wallet funds. // outputLeaser as sources for wallet funds.
func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight, func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
blockHeight uint32, deliveryAddrs []DeliveryAddr, blockHeight uint32, deliveryAddrs []DeliveryAddr,
changeAddr btcutil.Address, coinSelectLocker CoinSelectionLocker, changeAddr btcutil.Address, coinSelectLocker CoinSelectionLocker,
utxoSource UtxoSource, outpointLocker OutpointLocker, utxoSource UtxoSource, outputLeaser OutputLeaser,
signer input.Signer, minConfs int32) (*WalletSweepPackage, error) { signer input.Signer, minConfs int32) (*WalletSweepPackage, error) {
// TODO(roasbeef): turn off ATPL as well when available? // TODO(roasbeef): turn off ATPL as well when available?
@@ -196,7 +200,15 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
// can actually craft a sweeping transaction. // can actually craft a sweeping transaction.
unlockOutputs := func() { unlockOutputs := func() {
for _, utxo := range allOutputs { for _, utxo := range allOutputs {
outpointLocker.UnlockOutpoint(utxo.OutPoint) // Log the error but continue since we're already
// handling an error.
err := outputLeaser.ReleaseOutput(
chanfunding.LndInternalLockID, utxo.OutPoint,
)
if err != nil {
log.Warnf("Failed to release UTXO %s (%v))",
utxo.OutPoint, err)
}
} }
} }
@@ -219,7 +231,13 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
// attempt to use these UTXOs in transactions while we're // attempt to use these UTXOs in transactions while we're
// crafting out sweep all transaction. // crafting out sweep all transaction.
for _, utxo := range utxos { for _, utxo := range utxos {
outpointLocker.LockOutpoint(utxo.OutPoint) _, _, _, err = outputLeaser.LeaseOutput(
chanfunding.LndInternalLockID, utxo.OutPoint,
chanfunding.DefaultLockDuration,
)
if err != nil {
return err
}
} }
allOutputs = append(allOutputs, utxos...) allOutputs = append(allOutputs, utxos...)
+47 -36
View File
@@ -4,11 +4,13 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -151,25 +153,34 @@ func (m *mockCoinSelectionLocker) WithCoinSelectLock(f func() error) error {
} }
type mockOutpointLocker struct { type mockOutputLeaser struct {
lockedOutpoints map[wire.OutPoint]struct{} leasedOutputs map[wire.OutPoint]struct{}
unlockedOutpoints map[wire.OutPoint]struct{} releasedOutputs map[wire.OutPoint]struct{}
} }
func newMockOutpointLocker() *mockOutpointLocker { func newMockOutputLeaser() *mockOutputLeaser {
return &mockOutpointLocker{ return &mockOutputLeaser{
lockedOutpoints: make(map[wire.OutPoint]struct{}), leasedOutputs: make(map[wire.OutPoint]struct{}),
unlockedOutpoints: make(map[wire.OutPoint]struct{}), releasedOutputs: make(map[wire.OutPoint]struct{}),
} }
} }
func (m *mockOutpointLocker) LockOutpoint(o wire.OutPoint) { func (m *mockOutputLeaser) LeaseOutput(_ wtxmgr.LockID, o wire.OutPoint,
m.lockedOutpoints[o] = struct{}{} t time.Duration) (time.Time, []byte, btcutil.Amount, error) {
m.leasedOutputs[o] = struct{}{}
return time.Now().Add(t), nil, 0, nil
} }
func (m *mockOutpointLocker) UnlockOutpoint(o wire.OutPoint) {
m.unlockedOutpoints[o] = struct{}{} func (m *mockOutputLeaser) ReleaseOutput(_ wtxmgr.LockID,
o wire.OutPoint) error {
m.releasedOutputs[o] = struct{}{}
return nil
} }
var sweepScript = []byte{ var sweepScript = []byte{
@@ -234,53 +245,53 @@ var testUtxos = []*lnwallet.Utxo{
}, },
} }
func assertUtxosLocked(t *testing.T, utxoLocker *mockOutpointLocker, func assertUtxosLeased(t *testing.T, utxoLeaser *mockOutputLeaser,
utxos []*lnwallet.Utxo) { utxos []*lnwallet.Utxo) {
t.Helper() t.Helper()
for _, utxo := range utxos { for _, utxo := range utxos {
if _, ok := utxoLocker.lockedOutpoints[utxo.OutPoint]; !ok { if _, ok := utxoLeaser.leasedOutputs[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never locked", utxo.OutPoint) t.Fatalf("utxo %v was never leased", utxo.OutPoint)
} }
} }
} }
func assertNoUtxosUnlocked(t *testing.T, utxoLocker *mockOutpointLocker, func assertNoUtxosReleased(t *testing.T, utxoLeaser *mockOutputLeaser,
utxos []*lnwallet.Utxo) { utxos []*lnwallet.Utxo) {
t.Helper() t.Helper()
if len(utxoLocker.unlockedOutpoints) != 0 { if len(utxoLeaser.releasedOutputs) != 0 {
t.Fatalf("outputs have been locked, but shouldn't have been") t.Fatalf("outputs have been released, but shouldn't have been")
} }
} }
func assertUtxosUnlocked(t *testing.T, utxoLocker *mockOutpointLocker, func assertUtxosReleased(t *testing.T, utxoLeaser *mockOutputLeaser,
utxos []*lnwallet.Utxo) { utxos []*lnwallet.Utxo) {
t.Helper() t.Helper()
for _, utxo := range utxos { for _, utxo := range utxos {
if _, ok := utxoLocker.unlockedOutpoints[utxo.OutPoint]; !ok { if _, ok := utxoLeaser.releasedOutputs[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never unlocked", utxo.OutPoint) t.Fatalf("utxo %v was never released", utxo.OutPoint)
} }
} }
} }
func assertUtxosLockedAndUnlocked(t *testing.T, utxoLocker *mockOutpointLocker, func assertUtxosLeasedAndReleased(t *testing.T, utxoLeaser *mockOutputLeaser,
utxos []*lnwallet.Utxo) { utxos []*lnwallet.Utxo) {
t.Helper() t.Helper()
for _, utxo := range utxos { for _, utxo := range utxos {
if _, ok := utxoLocker.lockedOutpoints[utxo.OutPoint]; !ok { if _, ok := utxoLeaser.leasedOutputs[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never locked", utxo.OutPoint) t.Fatalf("utxo %v was never leased", utxo.OutPoint)
} }
if _, ok := utxoLocker.unlockedOutpoints[utxo.OutPoint]; !ok { if _, ok := utxoLeaser.releasedOutputs[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never unlocked", utxo.OutPoint) t.Fatalf("utxo %v was never released", utxo.OutPoint)
} }
} }
} }
@@ -294,10 +305,10 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
coinSelectLocker := &mockCoinSelectionLocker{ coinSelectLocker := &mockCoinSelectionLocker{
fail: true, fail: true,
} }
utxoLocker := newMockOutpointLocker() utxoLeaser := newMockOutputLeaser()
_, err := CraftSweepAllTx( _, err := CraftSweepAllTx(
0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLocker, 0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLeaser,
nil, 0, nil, 0,
) )
@@ -309,7 +320,7 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
// At this point, we'll now verify that all outputs were initially // At this point, we'll now verify that all outputs were initially
// locked, and then also unlocked due to the failure. // locked, and then also unlocked due to the failure.
assertUtxosLockedAndUnlocked(t, utxoLocker, testUtxos) assertUtxosLeasedAndReleased(t, utxoLeaser, testUtxos)
} }
// TestCraftSweepAllTxUnknownWitnessType tests that if one of the inputs we // TestCraftSweepAllTxUnknownWitnessType tests that if one of the inputs we
@@ -320,10 +331,10 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
utxoSource := newMockUtxoSource(testUtxos) utxoSource := newMockUtxoSource(testUtxos)
coinSelectLocker := &mockCoinSelectionLocker{} coinSelectLocker := &mockCoinSelectionLocker{}
utxoLocker := newMockOutpointLocker() utxoLeaser := newMockOutputLeaser()
_, err := CraftSweepAllTx( _, err := CraftSweepAllTx(
0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLocker, 0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLeaser,
nil, 0, nil, 0,
) )
@@ -336,7 +347,7 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
// At this point, we'll now verify that all outputs were initially // At this point, we'll now verify that all outputs were initially
// locked, and then also unlocked since we weren't able to find a // locked, and then also unlocked since we weren't able to find a
// witness type for the last output. // witness type for the last output.
assertUtxosLockedAndUnlocked(t, utxoLocker, testUtxos) assertUtxosLeasedAndReleased(t, utxoLeaser, testUtxos)
} }
// TestCraftSweepAllTx tests that we'll properly lock all available outputs // TestCraftSweepAllTx tests that we'll properly lock all available outputs
@@ -354,18 +365,18 @@ func TestCraftSweepAllTx(t *testing.T) {
targetUTXOs := testUtxos[:2] targetUTXOs := testUtxos[:2]
utxoSource := newMockUtxoSource(targetUTXOs) utxoSource := newMockUtxoSource(targetUTXOs)
coinSelectLocker := &mockCoinSelectionLocker{} coinSelectLocker := &mockCoinSelectionLocker{}
utxoLocker := newMockOutpointLocker() utxoLeaser := newMockOutputLeaser()
sweepPkg, err := CraftSweepAllTx( sweepPkg, err := CraftSweepAllTx(
0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource, 0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
utxoLocker, signer, 0, utxoLeaser, signer, 0,
) )
require.NoError(t, err, "unable to make sweep tx") require.NoError(t, err, "unable to make sweep tx")
// At this point, all of the UTXOs that we made above should be locked // At this point, all of the UTXOs that we made above should be locked
// and none of them unlocked. // and none of them unlocked.
assertUtxosLocked(t, utxoLocker, testUtxos[:2]) assertUtxosLeased(t, utxoLeaser, testUtxos[:2])
assertNoUtxosUnlocked(t, utxoLocker, testUtxos[:2]) assertNoUtxosReleased(t, utxoLeaser, testUtxos[:2])
// Now that we have our sweep transaction, we should find that we have // Now that we have our sweep transaction, we should find that we have
// a UTXO for each input, and also that our final output value is the // a UTXO for each input, and also that our final output value is the
@@ -397,5 +408,5 @@ func TestCraftSweepAllTx(t *testing.T) {
// If we cancel the sweep attempt, then we should find that all the // If we cancel the sweep attempt, then we should find that all the
// UTXOs within the sweep transaction are now unlocked. // UTXOs within the sweep transaction are now unlocked.
sweepPkg.CancelSweepAttempt() sweepPkg.CancelSweepAttempt()
assertUtxosUnlocked(t, utxoLocker, testUtxos[:2]) assertUtxosReleased(t, utxoLeaser, testUtxos[:2])
} }