7
7
"reflect"
8
8
"strings"
9
9
10
+ "github.com/btcsuite/btcd/btcec/v2/schnorr"
10
11
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
11
12
"github.com/btcsuite/btcd/btcutil"
12
13
"github.com/btcsuite/btcd/chaincfg"
@@ -75,6 +76,7 @@ type newWithdrawalRequest struct {
75
76
respChan chan * newWithdrawalResponse
76
77
destAddr string
77
78
satPerVbyte int64
79
+ amount int64
78
80
}
79
81
80
82
// 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 {
156
158
err )
157
159
}
158
160
159
- case request := <- m .newWithdrawalRequestChan :
161
+ case req := <- m .newWithdrawalRequestChan :
160
162
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 ,
163
165
)
164
166
if err != nil {
165
167
log .Errorf ("Error withdrawing deposits: %v" ,
@@ -174,7 +176,7 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
174
176
err : err ,
175
177
}
176
178
select {
177
- case request .respChan <- resp :
179
+ case req .respChan <- resp :
178
180
179
181
case <- ctx .Done ():
180
182
// Notify subroutines that the main loop has
@@ -259,10 +261,11 @@ func (m *Manager) WaitInitComplete() {
259
261
<- m .initChan
260
262
}
261
263
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.
263
266
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 ) {
266
269
267
270
if len (outpoints ) == 0 {
268
271
return "" , "" , fmt .Errorf ("no outpoints selected to " +
@@ -272,7 +275,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
272
275
// Ensure that the deposits are in a state in which they can be
273
276
// withdrawn.
274
277
deposits , allActive := m .cfg .DepositManager .AllOutpointsActiveDeposits (
275
- outpoints , deposit .Deposited )
278
+ outpoints , deposit .Deposited ,
279
+ )
276
280
277
281
if ! allActive {
278
282
return "" , "" , ErrWithdrawingInactiveDeposits
@@ -303,7 +307,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
303
307
}
304
308
305
309
finalizedTx , err := m .createFinalizedWithdrawalTx (
306
- ctx , deposits , withdrawalAddress , satPerVbyte ,
310
+ ctx , deposits , withdrawalAddress , satPerVbyte , amount ,
307
311
)
308
312
if err != nil {
309
313
return "" , "" , err
@@ -355,7 +359,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
355
359
356
360
func (m * Manager ) createFinalizedWithdrawalTx (ctx context.Context ,
357
361
deposits []* deposit.Deposit , withdrawalAddress btcutil.Address ,
358
- satPerVbyte int64 ) (* wire.MsgTx , error ) {
362
+ satPerVbyte int64 , selectedWithdrawalAmount int64 ) (* wire.MsgTx ,
363
+ error ) {
359
364
360
365
// Create a musig2 session for each deposit.
361
366
withdrawalSessions , clientNonces , err := m .createMusig2Sessions (
@@ -380,59 +385,42 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
380
385
).FeePerKWeight ()
381
386
}
382
387
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 )
389
389
if err != nil {
390
390
return nil , fmt .Errorf ("couldn't get confirmation height for " +
391
391
"deposit, %w" , err )
392
392
}
393
393
394
- // Calculate the fee value in satoshis.
395
394
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
+ )
397
401
if err != nil {
398
402
return nil , err
399
403
}
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
- }
415
404
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.
416
411
resp , err := m .cfg .StaticAddressServerClient .ServerWithdrawDeposits (
417
412
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 ),
422
418
},
423
419
)
424
420
if err != nil {
425
421
return nil , err
426
422
}
427
423
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
-
436
424
coopServerNonces , err := toNonces (resp .ServerNonces )
437
425
if err != nil {
438
426
return nil , err
@@ -634,9 +622,11 @@ func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
634
622
return res , nil
635
623
}
636
624
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 ) {
640
630
641
631
// First Create the tx.
642
632
msgTx := wire .NewMsgTx (2 )
@@ -649,25 +639,131 @@ func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
649
639
})
650
640
}
651
641
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 )
653
651
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" )
655
703
}
656
704
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" )
661
707
}
662
708
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
+ }
664
754
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
666
762
}
667
763
668
764
// 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 ) {
671
767
672
768
var weightEstimator input.TxWeightEstimator
673
769
for i := 0 ; i < numInputs ; i ++ {
@@ -689,6 +785,11 @@ func withdrawalFee(numInputs int,
689
785
sweepAddress )
690
786
}
691
787
788
+ // If there's a change output add the weight of the static address.
789
+ if hasChange {
790
+ weightEstimator .AddP2TROutput ()
791
+ }
792
+
692
793
return weightEstimator .Weight (), nil
693
794
}
694
795
@@ -827,13 +928,14 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
827
928
// DeliverWithdrawalRequest forwards a withdrawal request to the manager main
828
929
// loop.
829
930
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 ) {
832
933
833
934
request := newWithdrawalRequest {
834
935
outpoints : outpoints ,
835
936
destAddr : destAddr ,
836
937
satPerVbyte : satPerVbyte ,
938
+ amount : amount ,
837
939
respChan : make (chan * newWithdrawalResponse ),
838
940
}
839
941
0 commit comments