Skip to content

Commit b646ed1

Browse files
committed
staticaddr: arbitrary withdrawal amount
1 parent 90057d8 commit b646ed1

File tree

1 file changed

+148
-46
lines changed

1 file changed

+148
-46
lines changed

staticaddr/withdraw/manager.go

Lines changed: 148 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88
"strings"
99

10+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1011
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
1112
"github.com/btcsuite/btcd/btcutil"
1213
"github.com/btcsuite/btcd/chaincfg"
@@ -75,6 +76,7 @@ type newWithdrawalRequest struct {
7576
respChan chan *newWithdrawalResponse
7677
destAddr string
7778
satPerVbyte int64
79+
amount int64
7880
}
7981

8082
// newWithdrawalResponse is used to return withdrawal info and error to the
@@ -156,10 +158,10 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
156158
err)
157159
}
158160

159-
case request := <-m.newWithdrawalRequestChan:
161+
case req := <-m.newWithdrawalRequestChan:
160162
txHash, pkScript, err = m.WithdrawDeposits(
161-
ctx, request.outpoints, request.destAddr,
162-
request.satPerVbyte,
163+
ctx, req.outpoints, req.destAddr,
164+
req.satPerVbyte, req.amount,
163165
)
164166
if err != nil {
165167
log.Errorf("Error withdrawing deposits: %v",
@@ -174,7 +176,7 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
174176
err: err,
175177
}
176178
select {
177-
case request.respChan <- resp:
179+
case req.respChan <- resp:
178180

179181
case <-ctx.Done():
180182
// Notify subroutines that the main loop has
@@ -261,8 +263,8 @@ func (m *Manager) WaitInitComplete() {
261263

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

267269
if len(outpoints) == 0 {
268270
return "", "", fmt.Errorf("no outpoints selected to " +
@@ -272,7 +274,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
272274
// Ensure that the deposits are in a state in which they can be
273275
// withdrawn.
274276
deposits, allActive := m.cfg.DepositManager.AllOutpointsActiveDeposits(
275-
outpoints, deposit.Deposited)
277+
outpoints, deposit.Deposited,
278+
)
276279

277280
if !allActive {
278281
return "", "", ErrWithdrawingInactiveDeposits
@@ -303,7 +306,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
303306
}
304307

305308
finalizedTx, err := m.createFinalizedWithdrawalTx(
306-
ctx, deposits, withdrawalAddress, satPerVbyte,
309+
ctx, deposits, withdrawalAddress, satPerVbyte, amount,
307310
)
308311
if err != nil {
309312
return "", "", err
@@ -355,7 +358,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
355358

356359
func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
357360
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address,
358-
satPerVbyte int64) (*wire.MsgTx, error) {
361+
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx,
362+
error) {
359363

360364
// Create a musig2 session for each deposit.
361365
withdrawalSessions, clientNonces, err := m.createMusig2Sessions(
@@ -380,32 +384,55 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
380384
).FeePerKWeight()
381385
}
382386

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-
},
387+
params, err := m.cfg.AddressManager.GetStaticAddressParameters(
388+
ctx,
391389
)
392390
if err != nil {
393-
return nil, err
391+
return nil, fmt.Errorf("couldn't get confirmation height for "+
392+
"deposit, %w", err)
394393
}
395394

396-
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
397-
ctx,
395+
// Send change back to the static address.
396+
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
397+
if err != nil {
398+
log.Warnf("error retrieving taproot address %w", err)
399+
400+
return nil, fmt.Errorf("withdrawal failed")
401+
}
402+
403+
changeAddress, err := btcutil.NewAddressTaproot(
404+
schnorr.SerializePubKey(staticAddress.TaprootKey),
405+
m.cfg.ChainParams,
398406
)
399407
if err != nil {
400-
return nil, fmt.Errorf("couldn't get confirmation height for "+
401-
"deposit, %w", err)
408+
return nil, err
402409
}
403410

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

615642
func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
616-
withdrawlAmount btcutil.Amount, clientSweepAddress btcutil.Address,
617-
feeRate chainfee.SatPerKWeight) (*wire.MsgTx,
618-
error) {
643+
totalWithdrawalAmount btcutil.Amount,
644+
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
645+
changeAddress *btcutil.AddressTaproot, feeRate chainfee.SatPerKWeight) (
646+
*wire.MsgTx, btcutil.Amount, btcutil.Amount, error) {
619647

620648
// First Create the tx.
621649
msgTx := wire.NewMsgTx(2)
@@ -628,33 +656,101 @@ func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
628656
})
629657
}
630658

631-
// Estimate the fee.
632-
weight, err := withdrawalFee(len(outpoints), clientSweepAddress)
659+
var (
660+
hasChange bool
661+
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
662+
withdrawalAmount btcutil.Amount
663+
changeAmount btcutil.Amount
664+
)
665+
666+
// Estimate the transaction weight without change.
667+
weight, err := withdrawalTxWeight(len(outpoints), withdrawAddr, false)
633668
if err != nil {
634-
return nil, err
669+
return nil, 0, 0, err
635670
}
671+
feeWithoutChange := feeRate.FeeForWeight(weight)
636672

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

642-
fee := feeRate.FeeForWeight(weight)
716+
if withdrawalAmount < dustLimit {
717+
return nil, 0, 0, fmt.Errorf("withdrawal amount is below " +
718+
"dust limit")
719+
}
720+
721+
if changeAmount < 0 {
722+
return nil, 0, 0, fmt.Errorf("change amount is negative")
723+
}
643724

644-
// Create the sweep output
645-
sweepOutput := &wire.TxOut{
646-
Value: int64(withdrawlAmount) - int64(fee),
647-
PkScript: pkscript,
725+
withdrawScript, err := txscript.PayToAddrScript(withdrawAddr)
726+
if err != nil {
727+
return nil, 0, 0, err
648728
}
649729

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

652-
return msgTx, nil
742+
msgTx.AddTxOut(&wire.TxOut{
743+
Value: int64(changeAmount),
744+
PkScript: changeScript,
745+
})
746+
}
747+
748+
return msgTx, withdrawalAmount, changeAmount, nil
653749
}
654750

655751
// withdrawalFee returns the weight for the withdrawal transaction.
656-
func withdrawalFee(numInputs int,
657-
sweepAddress btcutil.Address) (lntypes.WeightUnit, error) {
752+
func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
753+
hasChange bool) (lntypes.WeightUnit, error) {
658754

659755
var weightEstimator input.TxWeightEstimator
660756
for i := 0; i < numInputs; i++ {
@@ -676,6 +772,11 @@ func withdrawalFee(numInputs int,
676772
sweepAddress)
677773
}
678774

775+
// If there's a change output add the weight of the static address.
776+
if hasChange {
777+
weightEstimator.AddP2TROutput()
778+
}
779+
679780
return weightEstimator.Weight(), nil
680781
}
681782

@@ -814,13 +915,14 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
814915
// DeliverWithdrawalRequest forwards a withdrawal request to the manager main
815916
// loop.
816917
func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
817-
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64) (string,
818-
string, error) {
918+
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
919+
amount int64) (string, string, error) {
819920

820921
request := newWithdrawalRequest{
821922
outpoints: outpoints,
822923
destAddr: destAddr,
823924
satPerVbyte: satPerVbyte,
925+
amount: amount,
824926
respChan: make(chan *newWithdrawalResponse),
825927
}
826928

0 commit comments

Comments
 (0)