@@ -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.
263264func (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
356358func (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
615635func (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.
816910func (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