mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Document deposit lock ownership for mutable confirmation state and route production reads through deposit accessors. Keep store persistence on no-lock helpers while callers hold the deposit lock, preserving the existing transition behavior without leaving direct field reads in user-facing paths.
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.GetConfirmationHeight()),
|
|
)
|
|
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
|
|
}
|