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.
263266func (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
356360func (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.
829930func (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