Skip to content

Commit 13bad2c

Browse files
Chinwendu20yyforyongyu
authored andcommitted
sweep: Add selectUtxos to CraftSweepAllTx args
1 parent 99339f7 commit 13bad2c

File tree

3 files changed

+117
-23
lines changed

3 files changed

+117
-23
lines changed

rpcserver.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,7 +1363,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
13631363
sweepTxPkg, err := sweep.CraftSweepAllTx(
13641364
feePerKw, maxFeeRate, uint32(bestHeight), nil,
13651365
targetAddr, wallet, wallet, wallet.WalletController,
1366-
r.server.cc.Signer, minConfs,
1366+
r.server.cc.Signer, minConfs, nil,
13671367
)
13681368
if err != nil {
13691369
return nil, err
@@ -1417,7 +1417,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
14171417
feePerKw, maxFeeRate, uint32(bestHeight),
14181418
outputs, targetAddr, wallet, wallet,
14191419
wallet.WalletController,
1420-
r.server.cc.Signer, minConfs,
1420+
r.server.cc.Signer, minConfs, nil,
14211421
)
14221422
if err != nil {
14231423
return nil, err

sweep/walletsweep.go

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"github.com/btcsuite/btcd/txscript"
1111
"github.com/btcsuite/btcd/wire"
1212
"github.com/btcsuite/btcwallet/wtxmgr"
13+
"github.com/lightningnetwork/lnd/fn"
1314
"github.com/lightningnetwork/lnd/input"
1415
"github.com/lightningnetwork/lnd/lnwallet"
1516
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
1617
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
18+
"golang.org/x/exp/maps"
1719
)
1820

1921
var (
@@ -24,6 +26,10 @@ var (
2426
// ErrFeePreferenceConflict is returned when both a fee rate and a conf
2527
// target is set for a fee preference.
2628
ErrFeePreferenceConflict = errors.New("fee preference conflict")
29+
30+
// ErrUnknownUTXO is returned when creating a sweeping tx using an UTXO
31+
// that's unknown to the wallet.
32+
ErrUnknownUTXO = errors.New("unknown utxo")
2733
)
2834

2935
// FeePreference defines an interface that allows the caller to specify how the
@@ -181,11 +187,11 @@ type OutputLeaser interface {
181187
}
182188

183189
// WalletSweepPackage is a package that gives the caller the ability to sweep
184-
// ALL funds from a wallet in a single transaction. We also package a function
185-
// closure that allows one to abort the operation.
190+
// relevant funds from a wallet in a single transaction. We also package a
191+
// function closure that allows one to abort the operation.
186192
type WalletSweepPackage struct {
187193
// SweepTx is a fully signed, and valid transaction that is broadcast,
188-
// will sweep ALL confirmed coins in the wallet with a single
194+
// will sweep ALL relevant confirmed coins in the wallet with a single
189195
// transaction.
190196
SweepTx *wire.MsgTx
191197

@@ -208,27 +214,28 @@ type DeliveryAddr struct {
208214
}
209215

210216
// CraftSweepAllTx attempts to craft a WalletSweepPackage which will allow the
211-
// caller to sweep ALL outputs within the wallet to a list of outputs. Any
212-
// leftover amount after these outputs and transaction fee, is sent to a single
213-
// output, as specified by the change address. The sweep transaction will be
214-
// crafted with the target fee rate, and will use the utxoSource and
215-
// outputLeaser as sources for wallet funds.
217+
// caller to sweep ALL funds in ALL or SELECT outputs within the wallet to a
218+
// list of outputs. Any leftover amount after these outputs and transaction fee,
219+
// is sent to a single output, as specified by the change address. The sweep
220+
// transaction will be crafted with the target fee rate, and will use the
221+
// utxoSource and outputLeaser as sources for wallet funds.
216222
func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
217223
blockHeight uint32, deliveryAddrs []DeliveryAddr,
218224
changeAddr btcutil.Address, coinSelectLocker CoinSelectionLocker,
219225
utxoSource UtxoSource, outputLeaser OutputLeaser,
220-
signer input.Signer, minConfs int32) (*WalletSweepPackage, error) {
226+
signer input.Signer, minConfs int32,
227+
selectUtxos fn.Set[wire.OutPoint]) (*WalletSweepPackage, error) {
221228

222229
// TODO(roasbeef): turn off ATPL as well when available?
223230

224-
var allOutputs []*lnwallet.Utxo
231+
var outputsForSweep []*lnwallet.Utxo
225232

226233
// We'll make a function closure up front that allows us to unlock all
227234
// selected outputs to ensure that they become available again in the
228235
// case of an error after the outputs have been locked, but before we
229236
// can actually craft a sweeping transaction.
230237
unlockOutputs := func() {
231-
for _, utxo := range allOutputs {
238+
for _, utxo := range outputsForSweep {
232239
// Log the error but continue since we're already
233240
// handling an error.
234241
err := outputLeaser.ReleaseOutput(
@@ -242,9 +249,9 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
242249
}
243250

244251
// Next, we'll use the coinSelectLocker to ensure that no coin
245-
// selection takes place while we fetch and lock all outputs the wallet
246-
// knows of. Otherwise, it may be possible for a new funding flow to
247-
// lock an output while we fetch the set of unspent witnesses.
252+
// selection takes place while we fetch and lock outputs in the
253+
// wallet. Otherwise, it may be possible for a new funding flow to lock
254+
// an output while we fetch the set of unspent witnesses.
248255
err := coinSelectLocker.WithCoinSelectLock(func() error {
249256
log.Trace("[WithCoinSelectLock] entered the lock")
250257

@@ -260,6 +267,16 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
260267

261268
log.Trace("[WithCoinSelectLock] finished fetching UTXOs")
262269

270+
// Use select utxos, if provided.
271+
if len(selectUtxos) > 0 {
272+
utxos, err = fetchUtxosFromOutpoints(
273+
utxos, selectUtxos.ToSlice(),
274+
)
275+
if err != nil {
276+
return err
277+
}
278+
}
279+
263280
// We'll now lock each UTXO to ensure that other callers don't
264281
// attempt to use these UTXOs in transactions while we're
265282
// crafting out sweep all transaction.
@@ -278,7 +295,7 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
278295

279296
log.Trace("[WithCoinSelectLock] exited the lock")
280297

281-
allOutputs = append(allOutputs, utxos...)
298+
outputsForSweep = append(outputsForSweep, utxos...)
282299

283300
return nil
284301
})
@@ -287,15 +304,15 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
287304
// in case we had any lingering outputs.
288305
unlockOutputs()
289306

290-
return nil, fmt.Errorf("unable to fetch+lock wallet "+
291-
"utxos: %v", err)
307+
return nil, fmt.Errorf("unable to fetch+lock wallet utxos: %w",
308+
err)
292309
}
293310

294311
// Now that we've locked all the potential outputs to sweep, we'll
295312
// assemble an input for each of them, so we can hand it off to the
296313
// sweeper to generate and sign a transaction for us.
297314
var inputsToSweep []input.Input
298-
for _, output := range allOutputs {
315+
for _, output := range outputsForSweep {
299316
// As we'll be signing for outputs under control of the wallet,
300317
// we only need to populate the output value and output script.
301318
// The rest of the items will be populated internally within
@@ -390,3 +407,24 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
390407
CancelSweepAttempt: unlockOutputs,
391408
}, nil
392409
}
410+
411+
// fetchUtxosFromOutpoints returns UTXOs for given outpoints. Errors if any
412+
// outpoint is not in the passed slice of utxos.
413+
func fetchUtxosFromOutpoints(utxos []*lnwallet.Utxo,
414+
outpoints []wire.OutPoint) ([]*lnwallet.Utxo, error) {
415+
416+
lookup := fn.SliceToMap(utxos, func(utxo *lnwallet.Utxo) wire.OutPoint {
417+
return utxo.OutPoint
418+
}, func(utxo *lnwallet.Utxo) *lnwallet.Utxo {
419+
return utxo
420+
})
421+
422+
subMap, err := fn.NewSubMap(lookup, outpoints)
423+
if err != nil {
424+
return nil, fmt.Errorf("%w: %v", ErrUnknownUTXO, err.Error())
425+
}
426+
427+
fetchedUtxos := maps.Values(subMap)
428+
429+
return fetchedUtxos, nil
430+
}

sweep/walletsweep_test.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/btcsuite/btcd/txscript"
1313
"github.com/btcsuite/btcd/wire"
1414
"github.com/btcsuite/btcwallet/wtxmgr"
15+
"github.com/lightningnetwork/lnd/fn"
1516
"github.com/lightningnetwork/lnd/lntest/mock"
1617
"github.com/lightningnetwork/lnd/lnwallet"
1718
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -339,7 +340,7 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
339340

340341
_, err := CraftSweepAllTx(
341342
0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLeaser,
342-
nil, 0,
343+
nil, 0, nil,
343344
)
344345

345346
// Since we instructed the coin select locker to fail above, we should
@@ -365,7 +366,7 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
365366

366367
_, err := CraftSweepAllTx(
367368
0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLeaser,
368-
nil, 0,
369+
nil, 0, nil,
369370
)
370371

371372
// Since passed in a p2wsh output, which is unknown, we should fail to
@@ -399,7 +400,7 @@ func TestCraftSweepAllTx(t *testing.T) {
399400

400401
sweepPkg, err := CraftSweepAllTx(
401402
0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
402-
utxoLeaser, signer, 0,
403+
utxoLeaser, signer, 0, nil,
403404
)
404405
require.NoError(t, err, "unable to make sweep tx")
405406

@@ -440,3 +441,58 @@ func TestCraftSweepAllTx(t *testing.T) {
440441
sweepPkg.CancelSweepAttempt()
441442
assertUtxosReleased(t, utxoLeaser, testUtxos[:2])
442443
}
444+
445+
// TestCraftSweepAllTxWithSelectedUTXO tests that we'll properly lock the
446+
// selected outputs within the wallet, and craft a single sweep transaction
447+
// that pays to the target output.
448+
func TestCraftSweepAllTxWithSelectedUTXO(t *testing.T) {
449+
t.Parallel()
450+
451+
// First, we'll make a mock signer along with a fee estimator, We'll
452+
// use zero fees to we can assert a precise output value.
453+
signer := &mock.DummySigner{}
454+
455+
// Grab the first UTXO from the test UTXOs.
456+
utxo1 := testUtxos[0]
457+
utxoSource := newMockUtxoSource([]*lnwallet.Utxo{utxo1})
458+
coinSelectLocker := &mockCoinSelectionLocker{}
459+
utxoLeaser := newMockOutputLeaser()
460+
461+
// Create an unknown utxo.
462+
outpointUknown := wire.OutPoint{Index: 4}
463+
464+
// Sweep using the uknnown utxo and expect an error.
465+
sweepPkg, err := CraftSweepAllTx(
466+
0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
467+
utxoLeaser, signer, 0, fn.NewSet(outpointUknown),
468+
)
469+
require.ErrorIs(t, err, ErrUnknownUTXO)
470+
require.Nil(t, sweepPkg)
471+
472+
// Sweep again using the known utxo and expect no error.
473+
sweepPkg, err = CraftSweepAllTx(
474+
0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
475+
utxoLeaser, signer, 0, fn.NewSet(utxo1.OutPoint),
476+
)
477+
require.NoError(t, err)
478+
479+
// At this point utxo1 should be locked.
480+
assertUtxosLeased(t, utxoLeaser, []*lnwallet.Utxo{utxo1})
481+
assertNoUtxosReleased(t, utxoLeaser, []*lnwallet.Utxo{utxo1})
482+
483+
// Validate the sweeping tx has the expected shape.
484+
sweepTx := sweepPkg.SweepTx
485+
require.Len(t, sweepTx.TxIn, 1)
486+
require.Len(t, sweepTx.TxOut, 1)
487+
488+
// We should have a single output that pays to our sweep script
489+
// generated above.
490+
expectedSweepValue := utxo1.Value
491+
require.Equal(t, sweepScript, sweepTx.TxOut[0].PkScript)
492+
require.EqualValues(t, expectedSweepValue, sweepTx.TxOut[0].Value)
493+
494+
// If we cancel the sweep attempt, then we should find utxo1 to be
495+
// unlocked.
496+
sweepPkg.CancelSweepAttempt()
497+
assertUtxosReleased(t, utxoLeaser, []*lnwallet.Utxo{utxo1})
498+
}

0 commit comments

Comments
 (0)