Skip to content

Commit 4f2ff5d

Browse files
committed
all withdrawal amount changes
1 parent 5b68e42 commit 4f2ff5d

File tree

8 files changed

+765
-611
lines changed

8 files changed

+765
-611
lines changed

cmd/loop/staticaddr.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ var withdrawalCommand = cli.Command{
143143
Usage: "withdraws all static address deposits.",
144144
},
145145
cli.StringFlag{
146-
Name: "addr",
146+
Name: "dest_addr",
147147
Usage: "the optional address that the withdrawn " +
148148
"funds should be sent to, if let blank the " +
149149
"funds will go to lnd's wallet",
@@ -154,6 +154,12 @@ var withdrawalCommand = cli.Command{
154154
"sat/vbyte that should be used when crafting " +
155155
"the transaction",
156156
},
157+
cli.IntFlag{
158+
Name: "amount",
159+
Usage: "the number of satoshis that should be " +
160+
"withdrawn from the selected deposits. The " +
161+
"change is sent back to the static address",
162+
},
157163
},
158164
Action: withdraw,
159165
}
@@ -193,8 +199,8 @@ func withdraw(ctx *cli.Context) error {
193199
return fmt.Errorf("unknown withdrawal request")
194200
}
195201

196-
if ctx.IsSet("addr") {
197-
destAddr = ctx.String("addr")
202+
if ctx.IsSet("dest_addr") {
203+
destAddr = ctx.String("dest_addr")
198204
}
199205

200206
resp, err := client.WithdrawDeposits(ctxb,
@@ -203,6 +209,7 @@ func withdraw(ctx *cli.Context) error {
203209
All: isAllSelected,
204210
DestAddr: destAddr,
205211
SatPerVbyte: int64(ctx.Uint64("sat_per_vbyte")),
212+
Amount: ctx.Int64("amount"),
206213
})
207214
if err != nil {
208215
return err

loopd/swapclient_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1447,7 +1447,7 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
14471447
}
14481448

14491449
txhash, pkScript, err := s.withdrawalManager.DeliverWithdrawalRequest(
1450-
ctx, outpoints, req.DestAddr, req.SatPerVbyte,
1450+
ctx, outpoints, req.DestAddr, req.SatPerVbyte, req.Amount,
14511451
)
14521452
if err != nil {
14531453
return nil, err

looprpc/client.pb.go

Lines changed: 408 additions & 393 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

looprpc/client.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,15 @@ message WithdrawDepositsRequest {
15921592
The fee rate in sat/vbyte to use for the withdrawal transaction.
15931593
*/
15941594
int64 sat_per_vbyte = 4;
1595+
1596+
/*
1597+
The amount in satoshis that should be withdrawn from the selected deposits.
1598+
If there is change, it will be sent back to the static address. The fees for
1599+
the transaction are taken from the change output. If the change is below
1600+
the dust limit, there won't be a change output and the dust goes towards
1601+
fees.
1602+
*/
1603+
int64 amount = 5;
15951604
}
15961605

15971606
message WithdrawDepositsResponse {

staticaddr/withdraw/interface.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package withdraw
33
import (
44
"context"
55

6+
"github.com/btcsuite/btcd/btcec/v2"
7+
"github.com/btcsuite/btcd/btcutil"
68
"github.com/btcsuite/btcd/wire"
79
"github.com/lightninglabs/loop/fsm"
810
"github.com/lightninglabs/loop/staticaddr/address"
@@ -24,6 +26,11 @@ type AddressManager interface {
2426
// ListUnspent returns a list of utxos at the static address.
2527
ListUnspent(ctx context.Context, minConfs,
2628
maxConfs int32) ([]*lnwallet.Utxo, error)
29+
30+
// GetTaprootAddress returns a taproot address for the given client and
31+
// server public keys and expiry.
32+
GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey,
33+
expiry int64) (*btcutil.AddressTaproot, error)
2734
}
2835

2936
type DepositManager interface {

staticaddr/withdraw/manager.go

Lines changed: 141 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type newWithdrawalRequest struct {
7575
respChan chan *newWithdrawalResponse
7676
destAddr string
7777
satPerVbyte int64
78+
amount int64
7879
}
7980

8081
// newWithdrawalResponse is used to return withdrawal info and error to the
@@ -156,10 +157,10 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
156157
err)
157158
}
158159

159-
case request := <-m.newWithdrawalRequestChan:
160+
case req := <-m.newWithdrawalRequestChan:
160161
txHash, pkScript, err = m.WithdrawDeposits(
161-
ctx, request.outpoints, request.destAddr,
162-
request.satPerVbyte,
162+
ctx, req.outpoints, req.destAddr,
163+
req.satPerVbyte, req.amount,
163164
)
164165
if err != nil {
165166
log.Errorf("Error withdrawing deposits: %v",
@@ -174,7 +175,7 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
174175
err: err,
175176
}
176177
select {
177-
case request.respChan <- resp:
178+
case req.respChan <- resp:
178179

179180
case <-ctx.Done():
180181
// Notify subroutines that the main loop has
@@ -261,8 +262,8 @@ func (m *Manager) WaitInitComplete() {
261262

262263
// WithdrawDeposits starts a deposits withdrawal flow.
263264
func (m *Manager) WithdrawDeposits(ctx context.Context,
264-
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64) (string,
265-
string, error) {
265+
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
266+
amount int64) (string, string, error) {
266267

267268
if len(outpoints) == 0 {
268269
return "", "", fmt.Errorf("no outpoints selected to " +
@@ -272,7 +273,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
272273
// Ensure that the deposits are in a state in which they can be
273274
// withdrawn.
274275
deposits, allActive := m.cfg.DepositManager.AllOutpointsActiveDeposits(
275-
outpoints, deposit.Deposited)
276+
outpoints, deposit.Deposited,
277+
)
276278

277279
if !allActive {
278280
return "", "", ErrWithdrawingInactiveDeposits
@@ -303,7 +305,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
303305
}
304306

305307
finalizedTx, err := m.createFinalizedWithdrawalTx(
306-
ctx, deposits, withdrawalAddress, satPerVbyte,
308+
ctx, deposits, withdrawalAddress, satPerVbyte, amount,
307309
)
308310
if err != nil {
309311
return "", "", err
@@ -355,7 +357,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
355357

356358
func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
357359
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address,
358-
satPerVbyte int64) (*wire.MsgTx, error) {
360+
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx,
361+
error) {
359362

360363
// Create a musig2 session for each deposit.
361364
withdrawalSessions, clientNonces, err := m.createMusig2Sessions(
@@ -380,32 +383,49 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
380383
).FeePerKWeight()
381384
}
382385

383-
outpoints := toOutpoints(deposits)
384-
resp, err := m.cfg.StaticAddressServerClient.ServerWithdrawDeposits(
385-
ctx, &staticaddressrpc.ServerWithdrawRequest{
386-
Outpoints: toPrevoutInfo(outpoints),
387-
ClientNonces: clientNonces,
388-
ClientSweepAddr: withdrawalAddress.String(),
389-
TxFeeRate: uint64(withdrawalSweepFeeRate),
390-
},
386+
params, err := m.cfg.AddressManager.GetStaticAddressParameters(
387+
ctx,
391388
)
392389
if err != nil {
393-
return nil, err
390+
return nil, fmt.Errorf("couldn't get confirmation height for "+
391+
"deposit, %w", err)
394392
}
395393

396-
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
397-
ctx,
394+
// Send change back to the static address.
395+
changeAddress, err := m.cfg.AddressManager.GetTaprootAddress(
396+
params.ClientPubkey, params.ServerPubkey, int64(params.Expiry),
398397
)
399398
if err != nil {
400-
return nil, fmt.Errorf("couldn't get confirmation height for "+
401-
"deposit, %w", err)
399+
log.Warnf("error retrieving taproot address %w", err)
400+
401+
return nil, fmt.Errorf("withdrawal failed")
402402
}
403403

404-
prevOuts := m.toPrevOuts(deposits, addressParams.PkScript)
404+
outpoints := toOutpoints(deposits)
405+
prevOuts := m.toPrevOuts(deposits, params.PkScript)
405406
totalValue := withdrawalValue(prevOuts)
406-
withdrawalTx, err := m.createWithdrawalTx(
407-
outpoints, totalValue, withdrawalAddress,
408-
withdrawalSweepFeeRate,
407+
withdrawalTx, withdrawAmount, changeAmount, err := m.createWithdrawalTx(
408+
outpoints, totalValue, btcutil.Amount(selectedWithdrawalAmount),
409+
withdrawalAddress, changeAddress, withdrawalSweepFeeRate,
410+
)
411+
if err != nil {
412+
return nil, err
413+
}
414+
415+
// Request the server to sign the withdrawal transaction.
416+
//
417+
// The withdrawal and change amount are sent to the server with the
418+
// expectation that the server just signs the transaction, without
419+
// performing fee calculations and dust considerations. The client is
420+
// responsible for that.
421+
resp, err := m.cfg.StaticAddressServerClient.ServerWithdrawDeposits(
422+
ctx, &staticaddressrpc.ServerWithdrawRequest{
423+
Outpoints: toPrevoutInfo(outpoints),
424+
ClientNonces: clientNonces,
425+
ClientWithdrawalAddr: withdrawalAddress.String(),
426+
WithdrawAmount: int64(withdrawAmount),
427+
ChangeAmount: int64(changeAmount),
428+
},
409429
)
410430
if err != nil {
411431
return nil, err
@@ -613,9 +633,10 @@ func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
613633
}
614634

615635
func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
616-
withdrawlAmount btcutil.Amount, clientSweepAddress btcutil.Address,
617-
feeRate chainfee.SatPerKWeight) (*wire.MsgTx,
618-
error) {
636+
totalWithdrawalAmount btcutil.Amount,
637+
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
638+
changeAddress *btcutil.AddressTaproot, feeRate chainfee.SatPerKWeight) (
639+
*wire.MsgTx, btcutil.Amount, btcutil.Amount, error) {
619640

620641
// First Create the tx.
621642
msgTx := wire.NewMsgTx(2)
@@ -628,33 +649,101 @@ func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
628649
})
629650
}
630651

631-
// Estimate the fee.
632-
weight, err := withdrawalFee(len(outpoints), clientSweepAddress)
652+
var (
653+
hasChange bool
654+
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
655+
withdrawalAmount btcutil.Amount
656+
changeAmount btcutil.Amount
657+
)
658+
659+
// Estimate the transaction weight without change.
660+
weight, err := withdrawalTxWeight(len(outpoints), withdrawAddr, false)
633661
if err != nil {
634-
return nil, err
662+
return nil, 0, 0, err
635663
}
664+
feeWithoutChange := feeRate.FeeForWeight(weight)
636665

637-
pkscript, err := txscript.PayToAddrScript(clientSweepAddress)
638-
if err != nil {
639-
return nil, err
666+
// If the user selected a fraction of the sum of the selected deposits
667+
// to withdraw, check if a change output is needed.
668+
if selectedWithdrawalAmount > 0 {
669+
// Estimate the transaction weight with change.
670+
weight, err = withdrawalTxWeight(
671+
len(outpoints), withdrawAddr, true,
672+
)
673+
if err != nil {
674+
return nil, 0, 0, err
675+
}
676+
feeWithChange := feeRate.FeeForWeight(weight)
677+
678+
// The available change that can cover fees is the total
679+
// selected deposit amount minus the selected withdrawal amount.
680+
change := totalWithdrawalAmount - selectedWithdrawalAmount
681+
682+
switch {
683+
case change-feeWithChange >= dustLimit:
684+
// If the change can cover the fees without turning into
685+
// dust, add a non-dust change output.
686+
hasChange = true
687+
changeAmount = change - feeWithChange
688+
withdrawalAmount = selectedWithdrawalAmount
689+
690+
case change-feeWithChange >= 0:
691+
// If the change is dust, we give it to the miners.
692+
hasChange = false
693+
withdrawalAmount = selectedWithdrawalAmount
694+
695+
default:
696+
// If the fees eat into our withdrawal amount, we fail
697+
// the withdrawal.
698+
return nil, 0, 0, fmt.Errorf("the change doesn't " +
699+
"cover for fees. Consider lowering the fee " +
700+
"rate or increase the withdrawal amount")
701+
}
702+
} else {
703+
// If the user wants to withdraw the full amount, we don't need
704+
// a change output.
705+
hasChange = false
706+
withdrawalAmount = totalWithdrawalAmount - feeWithoutChange
707+
}
708+
709+
if withdrawalAmount < dustLimit {
710+
return nil, 0, 0, fmt.Errorf("withdrawal amount is below " +
711+
"dust limit")
640712
}
641713

642-
fee := feeRate.FeeForWeight(weight)
714+
if changeAmount < 0 {
715+
return nil, 0, 0, fmt.Errorf("change amount is negative")
716+
}
643717

644-
// Create the sweep output
645-
sweepOutput := &wire.TxOut{
646-
Value: int64(withdrawlAmount) - int64(fee),
647-
PkScript: pkscript,
718+
withdrawScript, err := txscript.PayToAddrScript(withdrawAddr)
719+
if err != nil {
720+
return nil, 0, 0, err
648721
}
649722

650-
msgTx.AddTxOut(sweepOutput)
723+
// Create the withdrawal output.
724+
msgTx.AddTxOut(&wire.TxOut{
725+
Value: int64(withdrawalAmount),
726+
PkScript: withdrawScript,
727+
})
728+
729+
if hasChange {
730+
changeScript, err := txscript.PayToAddrScript(changeAddress)
731+
if err != nil {
732+
return nil, 0, 0, err
733+
}
734+
735+
msgTx.AddTxOut(&wire.TxOut{
736+
Value: int64(changeAmount),
737+
PkScript: changeScript,
738+
})
739+
}
651740

652-
return msgTx, nil
741+
return msgTx, withdrawalAmount, changeAmount, nil
653742
}
654743

655744
// withdrawalFee returns the weight for the withdrawal transaction.
656-
func withdrawalFee(numInputs int,
657-
sweepAddress btcutil.Address) (lntypes.WeightUnit, error) {
745+
func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
746+
hasChange bool) (lntypes.WeightUnit, error) {
658747

659748
var weightEstimator input.TxWeightEstimator
660749
for i := 0; i < numInputs; i++ {
@@ -676,6 +765,11 @@ func withdrawalFee(numInputs int,
676765
sweepAddress)
677766
}
678767

768+
// If there's a change output add the weight of the static address.
769+
if hasChange {
770+
weightEstimator.AddP2TROutput()
771+
}
772+
679773
return weightEstimator.Weight(), nil
680774
}
681775

@@ -814,13 +908,14 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
814908
// DeliverWithdrawalRequest forwards a withdrawal request to the manager main
815909
// loop.
816910
func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
817-
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64) (string,
818-
string, error) {
911+
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
912+
amount int64) (string, string, error) {
819913

820914
request := newWithdrawalRequest{
821915
outpoints: outpoints,
822916
destAddr: destAddr,
823917
satPerVbyte: satPerVbyte,
918+
amount: amount,
824919
respChan: make(chan *newWithdrawalResponse),
825920
}
826921

0 commit comments

Comments
 (0)