You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/staticaddr/deposit/fsm.go

344 lines
8.2 KiB
Go

package deposit
import (
"context"
"errors"
"fmt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
const (
DefaultObserverSize = 20
)
var (
ErrProtocolVersionNotSupported = errors.New("protocol version not " +
"supported")
)
// States.
var (
Deposited = fsm.StateType("Deposited")
Withdrawing = fsm.StateType("Withdrawing")
Withdrawn = fsm.StateType("Withdrawn")
PublishExpiredDeposit = fsm.StateType("PublishExpiredDeposit")
WaitForExpirySweep = fsm.StateType("WaitForExpirySweep")
Expired = fsm.StateType("Expired")
Failed = fsm.StateType("DepositFailed")
)
// Events.
var (
OnStart = fsm.EventType("OnStart")
OnWithdraw = fsm.EventType("OnWithdraw")
OnWithdrawn = fsm.EventType("OnWithdrawn")
OnExpiry = fsm.EventType("OnExpiry")
OnExpiryPublished = fsm.EventType("OnExpiryPublished")
OnExpirySwept = fsm.EventType("OnExpirySwept")
OnRecover = fsm.EventType("OnRecover")
)
// FSM is the state machine that handles the instant out.
type FSM struct {
*fsm.StateMachine
cfg *ManagerConfig
deposit *Deposit
params *address.Parameters
address *script.StaticAddress
ctx context.Context
blockNtfnChan chan uint32
finalizedDepositChan chan wire.OutPoint
}
// NewFSM creates a new state machine that can action on all static address
// feature requests.
func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig,
finalizedDepositChan chan wire.OutPoint,
recoverStateMachine bool) (*FSM, error) {
params, err := cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get static address "+
"parameters: %v", err)
}
address, err := cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get static address: %v", err)
}
depoFsm := &FSM{
cfg: cfg,
deposit: deposit,
params: params,
address: address,
ctx: ctx,
blockNtfnChan: make(chan uint32),
finalizedDepositChan: finalizedDepositChan,
}
depositStates := depoFsm.DepositStatesV0()
switch params.ProtocolVersion {
case staticaddr.ProtocolVersion_V0:
default:
return nil, ErrProtocolVersionNotSupported
}
if recoverStateMachine {
depoFsm.StateMachine = fsm.NewStateMachineWithState(
depositStates, deposit.State,
DefaultObserverSize,
)
} else {
depoFsm.StateMachine = fsm.NewStateMachine(
depositStates, DefaultObserverSize,
)
}
depoFsm.ActionEntryFunc = depoFsm.updateDeposit
go func() {
for {
select {
case currentHeight := <-depoFsm.blockNtfnChan:
err := depoFsm.handleBlockNotification(
currentHeight,
)
if err != nil {
log.Errorf("error handling block "+
"notification: %v", err)
}
case <-ctx.Done():
return
}
}
}()
return depoFsm, nil
}
func (f *FSM) handleBlockNotification(currentHeight uint32) error {
params, err := f.cfg.AddressManager.GetStaticAddressParameters(f.ctx)
if err != nil {
return err
}
isExpired := func() bool {
return currentHeight >= uint32(f.deposit.ConfirmationHeight)+
params.Expiry
}
if isExpired() && f.deposit.State != WaitForExpirySweep &&
!f.deposit.IsFinal() {
go func() {
err := f.SendEvent(OnExpiry, nil)
if err != nil {
log.Debugf("error sending OnExpiry event: %v",
err)
}
}()
}
return nil
}
// DepositStatesV0 returns the states a deposit can be in.
func (f *FSM) DepositStatesV0() fsm.States {
return fsm.States{
fsm.EmptyState: fsm.State{
Transitions: fsm.Transitions{
OnStart: Deposited,
},
Action: fsm.NoOpAction,
},
Deposited: fsm.State{
Transitions: fsm.Transitions{
OnWithdraw: Withdrawing,
OnExpiry: PublishExpiredDeposit,
OnRecover: Deposited,
},
Action: fsm.NoOpAction,
},
Withdrawing: fsm.State{
Transitions: fsm.Transitions{
OnWithdrawn: Withdrawn,
// Upon recovery, we go back to the Deposited
// state. The deposit by then has a withdrawal
// address stamped to it which will cause it to
// transition into the Withdrawing state again.
OnRecover: Deposited,
// A precondition for the Withdrawing state is
// that the withdrawal transaction has been
// broadcast. If the deposit expires while the
// withdrawal isn't confirmed, we can ignore the
// expiry.
OnExpiry: Withdrawing,
// If the withdrawal failed we go back to
// Deposited, hoping that another withdrawal
// attempt will be successful. Alternatively,
// the client can wait for the timeout sweep.
fsm.OnError: Deposited,
},
Action: fsm.NoOpAction,
},
PublishExpiredDeposit: fsm.State{
Transitions: fsm.Transitions{
OnRecover: PublishExpiredDeposit,
OnExpiryPublished: WaitForExpirySweep,
// If the timeout sweep failed we go back to
// Deposited, hoping that another timeout sweep
// attempt will be successful. Alternatively,
// the client can try to coop-spend the deposit.
fsm.OnError: Deposited,
},
Action: f.PublishDepositExpirySweepAction,
},
WaitForExpirySweep: fsm.State{
Transitions: fsm.Transitions{
OnExpirySwept: Expired,
OnRecover: PublishExpiredDeposit,
// If the timeout sweep failed we go back to
// Deposited, hoping that another timeout sweep
// attempt will be successful. Alternatively,
// the client can try to coop-spend the deposit.
fsm.OnError: Deposited,
},
Action: f.WaitForExpirySweepAction,
},
Expired: fsm.State{
Transitions: fsm.Transitions{
OnExpiry: Expired,
},
Action: f.SweptExpiredDepositAction,
},
Withdrawn: fsm.State{
Action: f.WithdrawnDepositAction,
},
Failed: fsm.State{
Action: fsm.NoOpAction,
},
}
}
// DepositEntryFunction is called after every action and updates the deposit in
// the db.
func (f *FSM) updateDeposit(notification fsm.Notification) {
if f.deposit == nil {
return
}
f.Debugf("NextState: %v, PreviousState: %v, Event: %v",
notification.NextState, notification.PreviousState,
notification.Event,
)
f.deposit.State = notification.NextState
// Don't update the deposit if we are in an initial state or if we
// are transitioning from an initial state to a failed state.
state := f.deposit.State
prevState := notification.PreviousState
if state == fsm.EmptyState ||
state == Deposited && !isRecoverable(prevState) ||
(prevState == Deposited && state == Failed) {
return
}
err := f.cfg.Store.UpdateDeposit(f.ctx, f.deposit)
if err != nil {
f.Errorf("unable to update deposit: %v", err)
}
}
func isRecoverable(state fsm.StateType) bool {
if state == Withdrawing || state == PublishExpiredDeposit ||
state == WaitForExpirySweep {
return true
}
return false
}
// 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) {
address, err := f.cfg.AddressManager.GetStaticAddress(f.ctx)
if err != nil {
return nil, err
}
return &lndclient.SignDescriptor{
WitnessScript: address.TimeoutLeaf.Script,
KeyDesc: keychain.KeyDescriptor{
PubKey: f.params.ClientPubkey,
},
Output: wire.NewTxOut(
int64(f.deposit.Value), f.params.PkScript,
),
HashType: txscript.SigHashDefault,
InputIndex: 0,
SignMethod: input.TaprootScriptSpendSignMethod,
}, nil
}