Skip to content

Commit

Permalink
all withdrawal amount changes
Browse files Browse the repository at this point in the history
  • Loading branch information
hieblmi committed Dec 3, 2024
1 parent 5b68e42 commit 4f2ff5d
Show file tree
Hide file tree
Showing 8 changed files with 765 additions and 611 deletions.
13 changes: 10 additions & 3 deletions cmd/loop/staticaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ var withdrawalCommand = cli.Command{
Usage: "withdraws all static address deposits.",
},
cli.StringFlag{
Name: "addr",
Name: "dest_addr",
Usage: "the optional address that the withdrawn " +
"funds should be sent to, if let blank the " +
"funds will go to lnd's wallet",
Expand All @@ -154,6 +154,12 @@ var withdrawalCommand = cli.Command{
"sat/vbyte that should be used when crafting " +
"the transaction",
},
cli.IntFlag{
Name: "amount",
Usage: "the number of satoshis that should be " +
"withdrawn from the selected deposits. The " +
"change is sent back to the static address",
},
},
Action: withdraw,
}
Expand Down Expand Up @@ -193,8 +199,8 @@ func withdraw(ctx *cli.Context) error {
return fmt.Errorf("unknown withdrawal request")
}

if ctx.IsSet("addr") {
destAddr = ctx.String("addr")
if ctx.IsSet("dest_addr") {
destAddr = ctx.String("dest_addr")
}

resp, err := client.WithdrawDeposits(ctxb,
Expand All @@ -203,6 +209,7 @@ func withdraw(ctx *cli.Context) error {
All: isAllSelected,
DestAddr: destAddr,
SatPerVbyte: int64(ctx.Uint64("sat_per_vbyte")),
Amount: ctx.Int64("amount"),
})
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,7 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
}

txhash, pkScript, err := s.withdrawalManager.DeliverWithdrawalRequest(
ctx, outpoints, req.DestAddr, req.SatPerVbyte,
ctx, outpoints, req.DestAddr, req.SatPerVbyte, req.Amount,
)
if err != nil {
return nil, err
Expand Down
801 changes: 408 additions & 393 deletions looprpc/client.pb.go

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions looprpc/client.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,15 @@ message WithdrawDepositsRequest {
The fee rate in sat/vbyte to use for the withdrawal transaction.
*/
int64 sat_per_vbyte = 4;

/*
The amount in satoshis that should be withdrawn from the selected deposits.
If there is change, it will be sent back to the static address. The fees for
the transaction are taken from the change output. If the change is below
the dust limit, there won't be a change output and the dust goes towards
fees.
*/
int64 amount = 5;
}

message WithdrawDepositsResponse {
Expand Down
7 changes: 7 additions & 0 deletions staticaddr/withdraw/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package withdraw
import (
"context"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
Expand All @@ -24,6 +26,11 @@ type AddressManager interface {
// ListUnspent returns a list of utxos at the static address.
ListUnspent(ctx context.Context, minConfs,
maxConfs int32) ([]*lnwallet.Utxo, error)

// GetTaprootAddress returns a taproot address for the given client and
// server public keys and expiry.
GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey,
expiry int64) (*btcutil.AddressTaproot, error)
}

type DepositManager interface {
Expand Down
187 changes: 141 additions & 46 deletions staticaddr/withdraw/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type newWithdrawalRequest struct {
respChan chan *newWithdrawalResponse
destAddr string
satPerVbyte int64
amount int64
}

// newWithdrawalResponse is used to return withdrawal info and error to the
Expand Down Expand Up @@ -156,10 +157,10 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
err)
}

case request := <-m.newWithdrawalRequestChan:
case req := <-m.newWithdrawalRequestChan:
txHash, pkScript, err = m.WithdrawDeposits(
ctx, request.outpoints, request.destAddr,
request.satPerVbyte,
ctx, req.outpoints, req.destAddr,
req.satPerVbyte, req.amount,
)
if err != nil {
log.Errorf("Error withdrawing deposits: %v",
Expand All @@ -174,7 +175,7 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
err: err,
}
select {
case request.respChan <- resp:
case req.respChan <- resp:

case <-ctx.Done():
// Notify subroutines that the main loop has
Expand Down Expand Up @@ -261,8 +262,8 @@ func (m *Manager) WaitInitComplete() {

// WithdrawDeposits starts a deposits withdrawal flow.
func (m *Manager) WithdrawDeposits(ctx context.Context,
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64) (string,
string, error) {
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
amount int64) (string, string, error) {

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

if !allActive {
return "", "", ErrWithdrawingInactiveDeposits
Expand Down Expand Up @@ -303,7 +305,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
}

finalizedTx, err := m.createFinalizedWithdrawalTx(
ctx, deposits, withdrawalAddress, satPerVbyte,
ctx, deposits, withdrawalAddress, satPerVbyte, amount,
)
if err != nil {
return "", "", err
Expand Down Expand Up @@ -355,7 +357,8 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,

func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address,
satPerVbyte int64) (*wire.MsgTx, error) {
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx,
error) {

// Create a musig2 session for each deposit.
withdrawalSessions, clientNonces, err := m.createMusig2Sessions(
Expand All @@ -380,32 +383,49 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
).FeePerKWeight()
}

outpoints := toOutpoints(deposits)
resp, err := m.cfg.StaticAddressServerClient.ServerWithdrawDeposits(
ctx, &staticaddressrpc.ServerWithdrawRequest{
Outpoints: toPrevoutInfo(outpoints),
ClientNonces: clientNonces,
ClientSweepAddr: withdrawalAddress.String(),
TxFeeRate: uint64(withdrawalSweepFeeRate),
},
params, err := m.cfg.AddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %w", err)
}

addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
ctx,
// Send change back to the static address.
changeAddress, err := m.cfg.AddressManager.GetTaprootAddress(
params.ClientPubkey, params.ServerPubkey, int64(params.Expiry),
)
if err != nil {
return nil, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %w", err)
log.Warnf("error retrieving taproot address %w", err)

return nil, fmt.Errorf("withdrawal failed")
}

prevOuts := m.toPrevOuts(deposits, addressParams.PkScript)
outpoints := toOutpoints(deposits)
prevOuts := m.toPrevOuts(deposits, params.PkScript)
totalValue := withdrawalValue(prevOuts)
withdrawalTx, err := m.createWithdrawalTx(
outpoints, totalValue, withdrawalAddress,
withdrawalSweepFeeRate,
withdrawalTx, withdrawAmount, changeAmount, err := m.createWithdrawalTx(
outpoints, totalValue, btcutil.Amount(selectedWithdrawalAmount),
withdrawalAddress, changeAddress, withdrawalSweepFeeRate,
)
if err != nil {
return nil, err
}

// Request the server to sign the withdrawal transaction.
//
// The withdrawal and change amount are sent to the server with the
// expectation that the server just signs the transaction, without
// performing fee calculations and dust considerations. The client is
// responsible for that.
resp, err := m.cfg.StaticAddressServerClient.ServerWithdrawDeposits(
ctx, &staticaddressrpc.ServerWithdrawRequest{
Outpoints: toPrevoutInfo(outpoints),
ClientNonces: clientNonces,
ClientWithdrawalAddr: withdrawalAddress.String(),
WithdrawAmount: int64(withdrawAmount),
ChangeAmount: int64(changeAmount),
},
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -613,9 +633,10 @@ func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
}

func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
withdrawlAmount btcutil.Amount, clientSweepAddress btcutil.Address,
feeRate chainfee.SatPerKWeight) (*wire.MsgTx,
error) {
totalWithdrawalAmount btcutil.Amount,
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
changeAddress *btcutil.AddressTaproot, feeRate chainfee.SatPerKWeight) (
*wire.MsgTx, btcutil.Amount, btcutil.Amount, error) {

// First Create the tx.
msgTx := wire.NewMsgTx(2)
Expand All @@ -628,33 +649,101 @@ func (m *Manager) createWithdrawalTx(outpoints []wire.OutPoint,
})
}

// Estimate the fee.
weight, err := withdrawalFee(len(outpoints), clientSweepAddress)
var (
hasChange bool
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
withdrawalAmount btcutil.Amount
changeAmount btcutil.Amount
)

// Estimate the transaction weight without change.
weight, err := withdrawalTxWeight(len(outpoints), withdrawAddr, false)
if err != nil {
return nil, err
return nil, 0, 0, err
}
feeWithoutChange := feeRate.FeeForWeight(weight)

pkscript, err := txscript.PayToAddrScript(clientSweepAddress)
if err != nil {
return nil, err
// If the user selected a fraction of the sum of the selected deposits
// to withdraw, check if a change output is needed.
if selectedWithdrawalAmount > 0 {
// Estimate the transaction weight with change.
weight, err = withdrawalTxWeight(
len(outpoints), withdrawAddr, true,
)
if err != nil {
return nil, 0, 0, err
}
feeWithChange := feeRate.FeeForWeight(weight)

// The available change that can cover fees is the total
// selected deposit amount minus the selected withdrawal amount.
change := totalWithdrawalAmount - selectedWithdrawalAmount

switch {
case change-feeWithChange >= dustLimit:
// If the change can cover the fees without turning into
// dust, add a non-dust change output.
hasChange = true
changeAmount = change - feeWithChange
withdrawalAmount = selectedWithdrawalAmount

case change-feeWithChange >= 0:
// If the change is dust, we give it to the miners.
hasChange = false
withdrawalAmount = selectedWithdrawalAmount

default:
// If the fees eat into our withdrawal amount, we fail
// the withdrawal.
return nil, 0, 0, fmt.Errorf("the change doesn't " +
"cover for fees. Consider lowering the fee " +
"rate or increase the withdrawal amount")
}
} else {
// If the user wants to withdraw the full amount, we don't need
// a change output.
hasChange = false
withdrawalAmount = totalWithdrawalAmount - feeWithoutChange
}

if withdrawalAmount < dustLimit {
return nil, 0, 0, fmt.Errorf("withdrawal amount is below " +
"dust limit")
}

fee := feeRate.FeeForWeight(weight)
if changeAmount < 0 {
return nil, 0, 0, fmt.Errorf("change amount is negative")
}

// Create the sweep output
sweepOutput := &wire.TxOut{
Value: int64(withdrawlAmount) - int64(fee),
PkScript: pkscript,
withdrawScript, err := txscript.PayToAddrScript(withdrawAddr)
if err != nil {
return nil, 0, 0, err
}

msgTx.AddTxOut(sweepOutput)
// Create the withdrawal output.
msgTx.AddTxOut(&wire.TxOut{
Value: int64(withdrawalAmount),
PkScript: withdrawScript,
})

if hasChange {
changeScript, err := txscript.PayToAddrScript(changeAddress)
if err != nil {
return nil, 0, 0, err
}

msgTx.AddTxOut(&wire.TxOut{
Value: int64(changeAmount),
PkScript: changeScript,
})
}

return msgTx, nil
return msgTx, withdrawalAmount, changeAmount, nil
}

// withdrawalFee returns the weight for the withdrawal transaction.
func withdrawalFee(numInputs int,
sweepAddress btcutil.Address) (lntypes.WeightUnit, error) {
func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
hasChange bool) (lntypes.WeightUnit, error) {

var weightEstimator input.TxWeightEstimator
for i := 0; i < numInputs; i++ {
Expand All @@ -676,6 +765,11 @@ func withdrawalFee(numInputs int,
sweepAddress)
}

// If there's a change output add the weight of the static address.
if hasChange {
weightEstimator.AddP2TROutput()
}

return weightEstimator.Weight(), nil
}

Expand Down Expand Up @@ -814,13 +908,14 @@ func (m *Manager) republishWithdrawals(ctx context.Context) error {
// DeliverWithdrawalRequest forwards a withdrawal request to the manager main
// loop.
func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64) (string,
string, error) {
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
amount int64) (string, string, error) {

request := newWithdrawalRequest{
outpoints: outpoints,
destAddr: destAddr,
satPerVbyte: satPerVbyte,
amount: amount,
respChan: make(chan *newWithdrawalResponse),
}

Expand Down
Loading

0 comments on commit 4f2ff5d

Please sign in to comment.