mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Final deposit states should not stall while deposit locks are held, because a blocked manager receive loop can otherwise hold up the deposit FSM; if shutdown happens before notification delivery, startup recovery can still resume from the final state. Send finalization notifications from a goroutine so final states are recorded without waiting on the manager receive loop, and add tests for blocked manager delivery and shutdown races.
185 lines
4.7 KiB
Go
185 lines
4.7 KiB
Go
package deposit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/staticaddr/script"
|
|
"github.com/lightninglabs/loop/utils"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
)
|
|
|
|
const (
|
|
DefaultConfTarget = 3
|
|
)
|
|
|
|
// PublishDepositExpirySweepAction creates and publishes the timeout transaction
|
|
// that spends the deposit from the static address timeout leaf to the
|
|
// predefined timeout sweep pkscript.
|
|
func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
|
|
_ fsm.EventContext) fsm.EventType {
|
|
|
|
msgTx := wire.NewMsgTx(2)
|
|
|
|
params, err := f.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
|
if err != nil {
|
|
return fsm.OnError
|
|
}
|
|
|
|
// Add the deposit outpoint as input to the transaction.
|
|
msgTx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: f.deposit.OutPoint,
|
|
Sequence: params.Expiry,
|
|
SignatureScript: nil,
|
|
})
|
|
|
|
// Estimate the fee rate of an expiry spend transaction.
|
|
feeRateEstimator, err := f.cfg.WalletKit.EstimateFeeRate(
|
|
ctx, DefaultConfTarget,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(fmt.Errorf("timeout sweep fee "+
|
|
"estimation failed: %w", err))
|
|
}
|
|
|
|
minRelayFeeRate, err := f.cfg.WalletKit.MinRelayFee(ctx)
|
|
if err != nil {
|
|
return f.HandleError(fmt.Errorf("timeout sweep min relay "+
|
|
"query failed: %w", err))
|
|
}
|
|
|
|
weight := script.ExpirySpendWeight()
|
|
|
|
fee := feeRateEstimator.FeeForWeight(lntypes.WeightUnit(weight))
|
|
|
|
// We cap the fee at 20% of the deposit value.
|
|
_, clamped, err := utils.ClampSweepFee(
|
|
fee, f.deposit.Value, utils.MaxFeeToAmountRatio,
|
|
minRelayFeeRate, lntypes.WeightUnit(weight),
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
if clamped {
|
|
return f.HandleError(errors.New("fee is greater than 20% of " +
|
|
"the deposit value"))
|
|
}
|
|
|
|
output := &wire.TxOut{
|
|
Value: int64(f.deposit.Value - fee),
|
|
PkScript: f.deposit.TimeOutSweepPkScript,
|
|
}
|
|
msgTx.AddTxOut(output)
|
|
|
|
txOut := &wire.TxOut{
|
|
Value: int64(f.deposit.Value),
|
|
PkScript: params.PkScript,
|
|
}
|
|
|
|
prevOut := []*wire.TxOut{txOut}
|
|
|
|
signDesc, err := f.SignDescriptor(ctx)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
rawSigs, err := f.cfg.Signer.SignOutputRaw(
|
|
ctx, msgTx, []*lndclient.SignDescriptor{signDesc}, prevOut,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
address, err := f.cfg.AddressManager.GetStaticAddress(ctx)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
sig := rawSigs[0]
|
|
msgTx.TxIn[0].Witness, err = address.GenTimeoutWitness(sig)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
txLabel := fmt.Sprintf("timeout sweep for deposit %v",
|
|
f.deposit.OutPoint)
|
|
|
|
err = f.cfg.WalletKit.PublishTransaction(ctx, msgTx, txLabel)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "output already spent") {
|
|
log.Errorf("%v: %v", txLabel, err)
|
|
f.LastActionError = err
|
|
return fsm.OnError
|
|
}
|
|
} else {
|
|
txHash := msgTx.TxHash()
|
|
f.deposit.ExpirySweepTxid = txHash
|
|
f.Debugf("published timeout sweep with txid: %v", txHash)
|
|
}
|
|
|
|
return OnExpiryPublished
|
|
}
|
|
|
|
// WaitForExpirySweepAction waits for enough confirmations before a timeout
|
|
// sweep is considered successful.
|
|
func (f *FSM) WaitForExpirySweepAction(ctx context.Context,
|
|
_ fsm.EventContext) fsm.EventType {
|
|
|
|
var txID *chainhash.Hash
|
|
// Only pass the txid if we know it from our own publication.
|
|
if f.deposit.ExpirySweepTxid != (chainhash.Hash{}) {
|
|
txID = &f.deposit.ExpirySweepTxid
|
|
}
|
|
|
|
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll
|
|
ctx, txID, f.deposit.TimeOutSweepPkScript, DefaultConfTarget,
|
|
int32(f.deposit.ConfirmationHeight),
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
select {
|
|
case err = <-errSpendChan:
|
|
log.Debugf("error while sweeping expired deposit: %v", err)
|
|
return fsm.OnError
|
|
|
|
case confirmedTx := <-spendChan:
|
|
f.deposit.ExpirySweepTxid = confirmedTx.Tx.TxHash()
|
|
return OnExpirySwept
|
|
|
|
case <-ctx.Done():
|
|
return fsm.OnError
|
|
}
|
|
}
|
|
|
|
// FinalizeDepositAction is the final action after a withdrawal. It signals to
|
|
// the manager that the deposit has been swept and the FSM can be removed.
|
|
func (f *FSM) FinalizeDepositAction(_ context.Context,
|
|
_ fsm.EventContext) fsm.EventType {
|
|
|
|
outpoint := f.deposit.OutPoint
|
|
|
|
// The finalization notification only tells the manager to remove the
|
|
// deposit from its active set. Send it asynchronously so a busy manager
|
|
// loop can't stall withdrawal confirmation while deposit locks are held.
|
|
go func() {
|
|
select {
|
|
case <-f.quitChan:
|
|
// The deposit is already in a final state. If shutdown wins
|
|
// this race, startup recovery will skip it instead of
|
|
// re-adding it to the active set.
|
|
return
|
|
|
|
case f.finalizedDepositChan <- outpoint:
|
|
}
|
|
}()
|
|
|
|
return fsm.NoOp
|
|
}
|