From 341f003524371b05719a0a1b0ad29bc3e3282abd Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 20 Dec 2023 19:21:33 +0100 Subject: [PATCH] staticaddr: deposit timeout fsm and timeout actions --- staticaddr/actions.go | 109 ++++++++++++++++++++++++++ staticaddr/fsm.go | 173 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 staticaddr/actions.go create mode 100644 staticaddr/fsm.go diff --git a/staticaddr/actions.go b/staticaddr/actions.go new file mode 100644 index 000000000..c96220fd9 --- /dev/null +++ b/staticaddr/actions.go @@ -0,0 +1,109 @@ +package staticaddr + +import ( + "errors" + "fmt" + + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/staticaddr/script" +) + +const ( + defaultConfTarget = 3 +) + +// SweepExpiredDepositAction ... +func (f *FSM) SweepExpiredDepositAction( + eventCtx fsm.EventContext) fsm.EventType { + + depoCtx, ok := eventCtx.(*DepositContext) + if !ok { + return f.HandleError(fsm.ErrInvalidContextType) + } + f.Debugf("SweepExpiredDepositAction: %v", depoCtx) + + msgTx := wire.NewMsgTx(2) + + msgTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: f.deposit.OutPoint, + Sequence: f.addressParameters.Expiry, + SignatureScript: nil, + }) + + // Estimate the fee rate of an expiry spend transaction. + feeRateEstimator, err := f.cfg.Lnd.WalletKit.EstimateFeeRate( + f.ctx, defaultConfTarget, + ) + if err != nil { + return f.HandleError(fmt.Errorf("timeout sweep fee "+ + "estimation failed: %v", err)) + } + + weight := script.ExpirySpendWeight() + + fee := feeRateEstimator.FeeForWeight(weight) + + // We cap the fee at 20% of the deposit value. + if fee > f.deposit.Value/5 { + return f.HandleError(errors.New("fee is higher than 20% of " + + "deposit value")) + } + + output := &wire.TxOut{ + Value: int64(f.deposit.Value - fee), + PkScript: f.deposit.TimeOutSweepPkScript, + } + msgTx.AddTxOut(output) + + signDesc, err := f.SignDescriptor() + if err != nil { + return f.HandleError(err) + } + + txOut := &wire.TxOut{ + Value: int64(f.deposit.Value), + PkScript: f.addressParameters.PkScript, + } + + prevOut := []*wire.TxOut{txOut} + + rawSigs, err := f.cfg.Lnd.Signer.SignOutputRaw( + f.ctx, msgTx, []*lndclient.SignDescriptor{&signDesc}, prevOut, + ) + if err != nil { + return f.HandleError(err) + } + + sig := rawSigs[0] + msgTx.TxIn[0].Witness, err = f.staticAddress.GenTimeoutWitness(sig) + if err != nil { + return f.HandleError(err) + } + + err = f.cfg.Lnd.WalletKit.PublishTransaction( + f.ctx, msgTx, f.deposit.OutPoint.Hash.String()+"-close-sweep", + ) + if err != nil { + return f.HandleError(err) + } + + f.Debugf("publishing timeout sweep with txid: %v", msgTx.TxHash()) + + return OnSwept +} + +// FinalizeDeposit is the first action that is executed when the instant +// out FSM is started. It will send the instant out request to the server. +func (f *FSM) FinalizeDeposit(eventCtx fsm.EventContext) fsm.EventType { + + depoCtx, ok := eventCtx.(*DepositContext) + if !ok { + return f.HandleError(fsm.ErrInvalidContextType) + } + + f.Debugf("FinalizeDeposit: %v", depoCtx) + + return fsm.NoOp +} diff --git a/staticaddr/fsm.go b/staticaddr/fsm.go new file mode 100644 index 000000000..7a5010f53 --- /dev/null +++ b/staticaddr/fsm.go @@ -0,0 +1,173 @@ +package staticaddr + +import ( + "context" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +// DepositContext contains the context parameters for a deposit state machine. +type DepositContext struct { + Deposit *Deposit +} + +// States. +var ( + Deposited = fsm.StateType("Deposited") + SweepExpiredDeposit = fsm.StateType("SweepExpiredDeposit") + + DepositFinalized = fsm.StateType("DepositFinalized") + + Failed = fsm.StateType("InstantFailedOutFailed") +) + +// Events. +var ( + OnStart = fsm.EventType("OnStart") + OnExpiry = fsm.EventType("OnExpiry") + OnSwept = fsm.EventType("OnSwept") +) + +// Config contains the services required for the instant out FSM. +type Config struct { + // Store is used to store the instant out. + Store SqlStore + + Lnd lndclient.LndServices + + Manager *ManagerConfig + + // Network is the network that is used for the swap. + Network *chaincfg.Params +} + +// FSM is the state machine that handles the instant out. +type FSM struct { + *fsm.StateMachine + + addressParameters *AddressParameters + + staticAddress *script.StaticAddress + + deposit *Deposit + + ctx context.Context + + // cfg contains all the services that the reservation manager needs to + // operate. + cfg *ManagerConfig +} + +// NewFSM creates a new state machine that can action on all static address +// feature requests. +func NewFSM(ctx context.Context, addressParameters *AddressParameters, + staticAddress *script.StaticAddress, deposit *Deposit, + cfg *ManagerConfig) (*FSM, error) { + + depoFsm := &FSM{ + ctx: ctx, + addressParameters: addressParameters, + staticAddress: staticAddress, + deposit: deposit, + cfg: cfg, + } + + depoFsm.StateMachine = fsm.NewStateMachine(depoFsm.DepositStates(), 20) + depoFsm.ActionEntryFunc = depoFsm.DepositEntryFunction + + return depoFsm, nil +} + +// DepositStates returns the states a deposit can be in. +func (f *FSM) DepositStates() fsm.States { + return fsm.States{ + fsm.EmptyState: fsm.State{ + Transitions: fsm.Transitions{ + OnStart: Deposited, + }, + Action: fsm.NoOpAction, + }, + Deposited: fsm.State{ + Transitions: fsm.Transitions{ + OnExpiry: SweepExpiredDeposit, + fsm.OnError: Failed, + }, + Action: fsm.NoOpAction, + }, + SweepExpiredDeposit: fsm.State{ + Transitions: fsm.Transitions{ + OnSwept: DepositFinalized, + fsm.OnError: Failed, + }, + Action: f.SweepExpiredDepositAction, + }, + Failed: fsm.State{ + Action: fsm.NoOpAction, + }, + DepositFinalized: fsm.State{ + Action: f.FinalizeDeposit, + }, + } +} + +// DepositEntryFunction is called after every action and updates the reservation +// in the db. +func (f *FSM) DepositEntryFunction(notification fsm.Notification) { + log.Debugf("DepositEntryFunction: %v", notification) +} + +// Infof logs an info message with the deposit outpoint. +func (f *FSM) Infof(format string, args ...interface{}) { + log.Infof( + "Deposit %v: "+format, + append( + []interface{}{f.deposit.OutPoint}, + args..., + )..., + ) +} + +// Debugf logs a debug message with the deposit outpoint. +func (f *FSM) Debugf(format string, args ...interface{}) { + log.Debugf( + "Deposit %v: "+format, + append( + []interface{}{f.deposit.OutPoint}, + args..., + )..., + ) +} + +// Errorf logs an error message with the deposit outpoint. +func (f *FSM) Errorf(format string, args ...interface{}) { + log.Errorf( + "Deposit %v: "+format, + append( + []interface{}{f.deposit.OutPoint}, + args..., + )..., + ) +} + +// SignDescriptor returns the sign descriptor for the static address output. +func (f *FSM) SignDescriptor() (lndclient.SignDescriptor, error) { + return lndclient.SignDescriptor{ + WitnessScript: f.staticAddress.TimeoutLeaf.Script, + KeyDesc: keychain.KeyDescriptor{ + PubKey: f.addressParameters.ClientPubkey, + }, + Output: wire.NewTxOut( + int64(f.deposit.Value), f.addressParameters.PkScript, + ), + HashType: txscript.SigHashDefault, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + }, nil +}