Skip to content

Commit 2622882

Browse files
committed
staticaddr: arbitrary withdrawal amount
1 parent e8d1a43 commit 2622882

File tree

1 file changed

+162
-60
lines changed

1 file changed

+162
-60
lines changed

staticaddr/withdraw/manager.go

Lines changed: 162 additions & 60 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
@@ -259,10 +261,11 @@ func (m *Manager) WaitInitComplete() {
259261
<-m.initChan
260262
}
261263

262-
// WithdrawDeposits starts a deposits withdrawal flow.
264+
// WithdrawDeposits starts a deposits withdrawal flow. If the amount is set to 0
265+
// the full amount of the selected deposits will be withdrawn.
263266
func (m *Manager) WithdrawDeposits(ctx context.Context,
264-
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64) (string,
265-
string, error) {
267+
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
268+
amount int64) (string, string, error) {
266269

267270
if len(outpoints) == 0 {
268271
return "", "", fmt.Errorf("no outpoints selected to " +
@@ -272,7 +275,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
272275
// Ensure that the deposits are in a state in which they can be
273276
// withdrawn.
274277
deposits, allActive := m.cfg.DepositManager.AllOutpointsActiveDeposits(
275-
outpoints, deposit.Deposited)
278+
outpoints, deposit.Deposited,
279+
)
276280

277281
if !allActive {
278282
return "", "", ErrWithdrawingInactiveDeposits
@@ -303,7 +307,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
303307
}
304308

305309
finalizedTx, err := m.createFinalizedWithdrawalTx(
306-
ctx, deposits, withdrawalAddress, satPerVbyte,
310+
ctx, deposits, withdrawalAddress, satPerVbyte, amount,
307311
)
308312
if err != nil {
309313
return "", "", err
@@ -355,7 +359,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
355359

356360
func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
357361
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address,
358-
satPerVbyte int64) (*wire.MsgTx, error) {
362+
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx,
363+
error) {
359364

360365
// Create a musig2 session for each deposit.
361366
withdrawalSessions, clientNonces, err := m.createMusig2Sessions(
@@ -380,59 +385,42 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
380385
).FeePerKWeight()
381386
}
382387

383-
// We'll now check the selected fee rate leaves a withdrawal output that
384-
// is above the dust limit. If not we cancel the withdrawal instead of
385-
// requesting a signature from the server.
386-
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
387-
ctx,
388-
)
388+
params, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
389389
if err != nil {
390390
return nil, fmt.Errorf("couldn't get confirmation height for "+
391391
"deposit, %w", err)
392392
}
393393

394-
// Calculate the fee value in satoshis.
395394
outpoints := toOutpoints(deposits)
396-
weight, err := withdrawalFee(len(outpoints), withdrawalAddress)
395+
prevOuts := m.toPrevOuts(deposits, params.PkScript)
396+
withdrawalTx, withdrawAmount, changeAmount, err := m.createWithdrawalTx(
397+
ctx, outpoints, prevOuts,
398+
btcutil.Amount(selectedWithdrawalAmount), withdrawalAddress,
399+
withdrawalSweepFeeRate,
400+
)
397401
if err != nil {
398402
return nil, err
399403
}
400-
feeValue := withdrawalSweepFeeRate.FeeForWeight(weight)
401-
402-
var (
403-
prevOuts = m.toPrevOuts(deposits, addressParams.PkScript)
404-
totalValue = withdrawalValue(prevOuts)
405-
outputValue = int64(totalValue) - int64(feeValue)
406-
// P2TRSize calculates a dust limit based on a 40 byte maximum
407-
// size witness output.
408-
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
409-
)
410-
411-
if outputValue < int64(dustLimit) {
412-
return nil, fmt.Errorf("withdrawal output value %d sats "+
413-
"below dust limit %d sats", outputValue, dustLimit)
414-
}
415404

405+
// Request the server to sign the withdrawal transaction.
406+
//
407+
// The withdrawal and change amount are sent to the server with the
408+
// expectation that the server just signs the transaction, without
409+
// performing fee calculations and dust considerations. The client is
410+
// responsible for that.
416411
resp, err := m.cfg.StaticAddressServerClient.ServerWithdrawDeposits(
417412
ctx, &staticaddressrpc.ServerWithdrawRequest{
418-
Outpoints: toPrevoutInfo(outpoints),
419-
ClientNonces: clientNonces,
420-
ClientSweepAddr: withdrawalAddress.String(),
421-
TxFeeRate: uint64(withdrawalSweepFeeRate),
413+
Outpoints: toPrevoutInfo(outpoints),
414+
ClientNonces: clientNonces,
415+
ClientWithdrawalAddr: withdrawalAddress.String(),
416+
WithdrawAmount: int64(withdrawAmount),
417+
ChangeAmount: int64(changeAmount),
422418
},
423419
)
424420
if err != nil {
425421
return nil, err
426422
}
427423

428-
withdrawalOutputValue := int64(totalValue - feeValue)
429-
withdrawalTx, err := m.createWithdrawalTx(
430-
outpoints, withdrawalOutputValue, withdrawalAddress,
431-
)
432-
if err != nil {
433-
return nil, err
434-
}
435-
436424
coopServerNonces, err := toNonces(resp.ServerNonces)
437425
if err != nil {
438426
return nil, err
@@ -634,9 +622,11 @@ func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
634622
return res, nil
635623
}
636624

637-
func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
638-
withdrawlOutputValue int64, clientSweepAddress btcutil.Address) (
639-
*wire.MsgTx, error) {
625+
func (m *Manager) createWithdrawalTx(ctx context.Context,
626+
outpoints []wire.OutPoint, prevOuts map[wire.OutPoint]*wire.TxOut,
627+
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
628+
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, btcutil.Amount,
629+
btcutil.Amount, error) {
640630

641631
// First Create the tx.
642632
msgTx := wire.NewMsgTx(2)
@@ -649,25 +639,131 @@ func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
649639
})
650640
}
651641

652-
pkscript, err := txscript.PayToAddrScript(clientSweepAddress)
642+
var (
643+
hasChange bool
644+
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
645+
withdrawalAmount btcutil.Amount
646+
changeAmount btcutil.Amount
647+
)
648+
649+
// Estimate the transaction weight without change.
650+
weight, err := withdrawalTxWeight(len(outpoints), withdrawAddr, false)
653651
if err != nil {
654-
return nil, err
652+
return nil, 0, 0, err
653+
}
654+
feeWithoutChange := feeRate.FeeForWeight(weight)
655+
656+
// If the user selected a fraction of the sum of the selected deposits
657+
// to withdraw, check if a change output is needed.
658+
totalWithdrawalAmount := withdrawalValue(prevOuts)
659+
if selectedWithdrawalAmount > 0 {
660+
// Estimate the transaction weight with change.
661+
weight, err = withdrawalTxWeight(
662+
len(outpoints), withdrawAddr, true,
663+
)
664+
if err != nil {
665+
return nil, 0, 0, err
666+
}
667+
feeWithChange := feeRate.FeeForWeight(weight)
668+
669+
// The available change that can cover fees is the total
670+
// selected deposit amount minus the selected withdrawal amount.
671+
change := totalWithdrawalAmount - selectedWithdrawalAmount
672+
673+
switch {
674+
case change-feeWithChange >= dustLimit:
675+
// If the change can cover the fees without turning into
676+
// dust, add a non-dust change output.
677+
hasChange = true
678+
changeAmount = change - feeWithChange
679+
withdrawalAmount = selectedWithdrawalAmount
680+
681+
case change-feeWithoutChange >= 0:
682+
// If the change is dust, we give it to the miners.
683+
hasChange = false
684+
withdrawalAmount = selectedWithdrawalAmount
685+
686+
default:
687+
// If the fees eat into our withdrawal amount, we fail
688+
// the withdrawal.
689+
return nil, 0, 0, fmt.Errorf("the change doesn't " +
690+
"cover for fees. Consider lowering the fee " +
691+
"rate or decrease the withdrawal amount")
692+
}
693+
} else {
694+
// If the user wants to withdraw the full amount, we don't need
695+
// a change output.
696+
hasChange = false
697+
withdrawalAmount = totalWithdrawalAmount - feeWithoutChange
698+
}
699+
700+
if withdrawalAmount < dustLimit {
701+
return nil, 0, 0, fmt.Errorf("withdrawal amount is below " +
702+
"dust limit")
655703
}
656704

657-
// Create the sweep output
658-
sweepOutput := &wire.TxOut{
659-
Value: withdrawlOutputValue,
660-
PkScript: pkscript,
705+
if changeAmount < 0 {
706+
return nil, 0, 0, fmt.Errorf("change amount is negative")
661707
}
662708

663-
msgTx.AddTxOut(sweepOutput)
709+
// For the users convenience we check that the change amount is lower
710+
// than each input's value. If the change amount is higher than an
711+
// input's value, we wouldn't have to include that input into the
712+
// transaction, saving fees.
713+
for outpoint, txOut := range prevOuts {
714+
if changeAmount >= btcutil.Amount(txOut.Value) {
715+
return nil, 0, 0, fmt.Errorf("change amount %v is "+
716+
"higher than an input value %v of input %v",
717+
changeAmount, btcutil.Amount(txOut.Value),
718+
outpoint)
719+
}
720+
}
721+
722+
withdrawScript, err := txscript.PayToAddrScript(withdrawAddr)
723+
if err != nil {
724+
return nil, 0, 0, err
725+
}
726+
727+
// Create the withdrawal output.
728+
msgTx.AddTxOut(&wire.TxOut{
729+
Value: int64(withdrawalAmount),
730+
PkScript: withdrawScript,
731+
})
732+
733+
if hasChange {
734+
// Send change back to the same static address.
735+
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
736+
if err != nil {
737+
log.Errorf("error retrieving taproot address %w", err)
738+
739+
return nil, 0, 0, fmt.Errorf("withdrawal failed")
740+
}
741+
742+
changeAddress, err := btcutil.NewAddressTaproot(
743+
schnorr.SerializePubKey(staticAddress.TaprootKey),
744+
m.cfg.ChainParams,
745+
)
746+
if err != nil {
747+
return nil, 0, 0, err
748+
}
749+
750+
changeScript, err := txscript.PayToAddrScript(changeAddress)
751+
if err != nil {
752+
return nil, 0, 0, err
753+
}
664754

665-
return msgTx, nil
755+
msgTx.AddTxOut(&wire.TxOut{
756+
Value: int64(changeAmount),
757+
PkScript: changeScript,
758+
})
759+
}
760+
761+
return msgTx, withdrawalAmount, changeAmount, nil
666762
}
667763

668764
// withdrawalFee returns the weight for the withdrawal transaction.
669-
func withdrawalFee(numInputs int,
670-
sweepAddress btcutil.Address) (lntypes.WeightUnit, error) {
765+
func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
766+
hasChange bool) (lntypes.WeightUnit, error) {
671767

672768
var weightEstimator input.TxWeightEstimator
673769
for i := 0; i < numInputs; i++ {
@@ -689,6 +785,11 @@ func withdrawalFee(numInputs int,
689785
sweepAddress)
690786
}
691787

788+
// If there's a change output add the weight of the static address.
789+
if hasChange {
790+
weightEstimator.AddP2TROutput()
791+
}
792+
692793
return weightEstimator.Weight(), nil
693794
}
694795

@@ -827,13 +928,14 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
827928
// DeliverWithdrawalRequest forwards a withdrawal request to the manager main
828929
// loop.
829930
func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
830-
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64) (string,
831-
string, error) {
931+
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
932+
amount int64) (string, string, error) {
832933

833934
request := newWithdrawalRequest{
834935
outpoints: outpoints,
835936
destAddr: destAddr,
836937
satPerVbyte: satPerVbyte,
938+
amount: amount,
837939
respChan: make(chan *newWithdrawalResponse),
838940
}
839941

0 commit comments

Comments
 (0)