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.
263265func (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
356359func (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
615642func (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.
816917func (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