mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
367 lines
10 KiB
Go
367 lines
10 KiB
Go
package loopin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/staticaddr/deposit"
|
|
"github.com/lightninglabs/loop/staticaddr/version"
|
|
)
|
|
|
|
// FSM embeds an FSM and extends it with a static address loop-in and a config.
|
|
type FSM struct {
|
|
*fsm.StateMachine
|
|
|
|
cfg *Config
|
|
|
|
// loopIn stores the loop-in details that are relevant during the
|
|
// lifetime of the swap.
|
|
loopIn *StaticAddressLoopIn
|
|
|
|
// MuSig2 data must not be re-used across restarts, hence it is not
|
|
// persisted.
|
|
//
|
|
// htlcServerNonces contains all the nonces that the server generated
|
|
// for the htlc musig2 sessions.
|
|
htlcServerNonces [][musig2.PubNonceSize]byte
|
|
|
|
// htlcServerNoncesHighFee contains all the high fee nonces that the
|
|
// server generated for the htlc musig2 sessions.
|
|
htlcServerNoncesHighFee [][musig2.PubNonceSize]byte
|
|
|
|
// htlcServerNoncesExtremelyHighFee contains all the extremely high fee
|
|
// nonces that the server generated for the htlc musig2 sessions.
|
|
htlcServerNoncesExtremelyHighFee [][musig2.PubNonceSize]byte
|
|
}
|
|
|
|
// NewFSM creates a new loop-in state machine.
|
|
func NewFSM(ctx context.Context, loopIn *StaticAddressLoopIn, cfg *Config,
|
|
recoverStateMachine bool) (*FSM, error) {
|
|
|
|
loopInFsm := &FSM{
|
|
cfg: cfg,
|
|
loopIn: loopIn,
|
|
}
|
|
|
|
params, err := cfg.AddressManager.GetStaticAddressParameters(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get static address "+
|
|
"parameters: %w", err)
|
|
}
|
|
|
|
loopInStates := loopInFsm.LoopInStatesV0()
|
|
switch params.ProtocolVersion {
|
|
case version.ProtocolVersion_V0:
|
|
|
|
default:
|
|
return nil, deposit.ErrProtocolVersionNotSupported
|
|
}
|
|
|
|
if recoverStateMachine {
|
|
loopInFsm.StateMachine = fsm.NewStateMachineWithState(
|
|
loopInStates, loopIn.GetState(),
|
|
deposit.DefaultObserverSize,
|
|
)
|
|
} else {
|
|
loopInFsm.StateMachine = fsm.NewStateMachine(
|
|
loopInStates, deposit.DefaultObserverSize,
|
|
)
|
|
}
|
|
|
|
loopInFsm.ActionEntryFunc = loopInFsm.updateLoopIn
|
|
|
|
return loopInFsm, nil
|
|
}
|
|
|
|
// States that the loop-in fsm can transition to.
|
|
var (
|
|
// InitHtlcTx initiates the htlc tx creation with the server.
|
|
InitHtlcTx = fsm.StateType("InitHtlcTx")
|
|
|
|
// SignHtlcTx partially signs the htlc transaction with the received
|
|
// server nonces. The client doesn't hold a final signature hence can't
|
|
// publish the htlc.
|
|
SignHtlcTx = fsm.StateType("SignHtlcTx")
|
|
|
|
// MonitorInvoiceAndHtlcTx monitors the swap invoice payment and the
|
|
// htlc transaction confirmation.
|
|
// Since the client provided its partial signature to spend to the htlc
|
|
// pkScript, the server could publish the htlc transaction prematurely.
|
|
// We need to monitor the htlc transaction to sweep our timeout path in
|
|
// this case.
|
|
// If the server pays the swap invoice as expected we can stop to
|
|
// monitor the htlc timeout path.
|
|
MonitorInvoiceAndHtlcTx = fsm.StateType("MonitorInvoiceAndHtlcTx")
|
|
|
|
// PaymentReceived is the state where the swap invoice was paid by the
|
|
// server. The client can now sign the sweepless sweep transaction.
|
|
PaymentReceived = fsm.StateType("PaymentReceived")
|
|
|
|
// SweepHtlcTimeout is the state where the htlc timeout path is
|
|
// published because the server did not pay the invoice on time.
|
|
SweepHtlcTimeout = fsm.StateType("SweepHtlcTimeout")
|
|
|
|
// MonitorHtlcTimeoutSweep monitors the htlc timeout sweep transaction
|
|
// confirmation.
|
|
MonitorHtlcTimeoutSweep = fsm.StateType("MonitorHtlcTimeoutSweep")
|
|
|
|
// HtlcTimeoutSwept is the state where the htlc timeout sweep
|
|
// transaction was sufficiently confirmed.
|
|
HtlcTimeoutSwept = fsm.StateType("HtlcTimeoutSwept")
|
|
|
|
// Succeeded is the state the swap is in if it was successful.
|
|
Succeeded = fsm.StateType("Succeeded")
|
|
|
|
// SucceededTransitioningFailed is the state the swap is in if the swap
|
|
// payment was received but the client was not able to transition
|
|
// the deposits to the looped-in state.
|
|
SucceededTransitioningFailed = fsm.StateType("SucceededTransitioningFailed") //nolint:lll
|
|
|
|
// UnlockDeposits is the state where the deposits are reset. This
|
|
// happens when the state machine encountered an error and the swap
|
|
// process needs to start from the beginning.
|
|
UnlockDeposits = fsm.StateType("UnlockDeposits")
|
|
|
|
// Failed is the state the swap is in if it failed.
|
|
Failed = fsm.StateType("Failed")
|
|
)
|
|
|
|
var PendingStates = []fsm.StateType{
|
|
InitHtlcTx, SignHtlcTx, MonitorInvoiceAndHtlcTx, PaymentReceived,
|
|
SweepHtlcTimeout, MonitorHtlcTimeoutSweep, UnlockDeposits,
|
|
}
|
|
|
|
var FinalStates = []fsm.StateType{
|
|
HtlcTimeoutSwept, Succeeded, SucceededTransitioningFailed, Failed,
|
|
}
|
|
|
|
var AllStates = append(
|
|
append([]fsm.StateType{}, PendingStates...), FinalStates...,
|
|
)
|
|
|
|
// Events.
|
|
var (
|
|
OnInitHtlc = fsm.EventType("OnInitHtlc")
|
|
OnHtlcInitiated = fsm.EventType("OnHtlcInitiated")
|
|
OnHtlcTxSigned = fsm.EventType("OnHtlcTxSigned")
|
|
OnSweepHtlcTimeout = fsm.EventType("OnSweepHtlcTimeout")
|
|
OnHtlcTimeoutSweepPublished = fsm.EventType("OnHtlcTimeoutSweepPublished")
|
|
OnHtlcTimeoutSwept = fsm.EventType("OnHtlcTimeoutSwept")
|
|
OnPaymentReceived = fsm.EventType("OnPaymentReceived")
|
|
OnSwapTimedOut = fsm.EventType("OnSwapTimedOut")
|
|
OnSucceeded = fsm.EventType("OnSucceeded")
|
|
OnRecover = fsm.EventType("OnRecover")
|
|
)
|
|
|
|
// LoopInStatesV0 returns the state and transition map for the loop-in state
|
|
// machine.
|
|
func (f *FSM) LoopInStatesV0() fsm.States {
|
|
return fsm.States{
|
|
fsm.EmptyState: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnInitHtlc: InitHtlcTx,
|
|
},
|
|
Action: fsm.NoOpAction,
|
|
},
|
|
InitHtlcTx: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnHtlcInitiated: SignHtlcTx,
|
|
OnRecover: UnlockDeposits,
|
|
fsm.OnError: UnlockDeposits,
|
|
},
|
|
Action: f.InitHtlcAction,
|
|
},
|
|
SignHtlcTx: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnHtlcTxSigned: MonitorInvoiceAndHtlcTx,
|
|
OnRecover: UnlockDeposits,
|
|
fsm.OnError: UnlockDeposits,
|
|
},
|
|
Action: f.SignHtlcTxAction,
|
|
},
|
|
MonitorInvoiceAndHtlcTx: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnPaymentReceived: PaymentReceived,
|
|
OnSweepHtlcTimeout: SweepHtlcTimeout,
|
|
OnSwapTimedOut: Failed,
|
|
OnRecover: MonitorInvoiceAndHtlcTx,
|
|
fsm.OnError: UnlockDeposits,
|
|
},
|
|
Action: f.MonitorInvoiceAndHtlcTxAction,
|
|
},
|
|
SweepHtlcTimeout: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnHtlcTimeoutSweepPublished: MonitorHtlcTimeoutSweep,
|
|
OnRecover: SweepHtlcTimeout,
|
|
fsm.OnError: Failed,
|
|
},
|
|
Action: f.SweepHtlcTimeoutAction,
|
|
},
|
|
MonitorHtlcTimeoutSweep: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnHtlcTimeoutSwept: HtlcTimeoutSwept,
|
|
OnRecover: MonitorHtlcTimeoutSweep,
|
|
fsm.OnError: Failed,
|
|
},
|
|
Action: f.MonitorHtlcTimeoutSweepAction,
|
|
},
|
|
PaymentReceived: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnSucceeded: Succeeded,
|
|
OnRecover: Succeeded,
|
|
fsm.OnError: SucceededTransitioningFailed,
|
|
},
|
|
Action: f.PaymentReceivedAction,
|
|
},
|
|
HtlcTimeoutSwept: fsm.State{
|
|
Action: fsm.NoOpAction,
|
|
},
|
|
Succeeded: fsm.State{
|
|
Action: fsm.NoOpAction,
|
|
},
|
|
SucceededTransitioningFailed: fsm.State{
|
|
Action: fsm.NoOpAction,
|
|
},
|
|
UnlockDeposits: fsm.State{
|
|
Transitions: fsm.Transitions{
|
|
OnRecover: UnlockDeposits,
|
|
fsm.OnError: Failed,
|
|
},
|
|
Action: f.UnlockDepositsAction,
|
|
},
|
|
Failed: fsm.State{
|
|
Action: fsm.NoOpAction,
|
|
},
|
|
}
|
|
}
|
|
|
|
// updateLoopIn is called after every action and updates the loop-in in the db.
|
|
func (f *FSM) updateLoopIn(ctx context.Context, notification fsm.Notification) {
|
|
f.Infof("Current: %v", notification.NextState)
|
|
|
|
// Skip the update if the loop-in is not yet initialized. This happens
|
|
// on the entry action of the fsm.
|
|
if f.loopIn == nil {
|
|
return
|
|
}
|
|
|
|
f.loopIn.SetState(notification.NextState)
|
|
|
|
// Check if we can skip updating the loop-in in the database.
|
|
if isUpdateSkipped(notification, f.loopIn) {
|
|
return
|
|
}
|
|
|
|
stored, err := f.cfg.Store.IsStored(ctx, f.loopIn.SwapHash)
|
|
if err != nil {
|
|
f.Errorf("Error checking if loop-in is stored: %v", err)
|
|
|
|
return
|
|
}
|
|
|
|
if !stored {
|
|
f.Warnf("Loop-in not stored in db, can't update")
|
|
|
|
return
|
|
}
|
|
|
|
err = f.cfg.Store.UpdateLoopIn(ctx, f.loopIn)
|
|
if err != nil {
|
|
f.Errorf("Error updating loop-in: %v", err)
|
|
|
|
return
|
|
}
|
|
|
|
err = f.sendUpdate(ctx)
|
|
if err != nil {
|
|
f.Errorf("Error sending loop-in update: %v", err)
|
|
}
|
|
}
|
|
|
|
// sendUpdate publishes the latest loop-in state after it has been persisted.
|
|
// The callback must remain lightweight because it runs synchronously with FSM
|
|
// state transitions.
|
|
func (f *FSM) sendUpdate(ctx context.Context) error {
|
|
if f.cfg.SendUpdate == nil {
|
|
return nil
|
|
}
|
|
|
|
return f.cfg.SendUpdate(ctx, f.loopIn)
|
|
}
|
|
|
|
// isUpdateSkipped returns true if the loop-in should not be updated for the
|
|
// given notification.
|
|
func isUpdateSkipped(notification fsm.Notification,
|
|
l *StaticAddressLoopIn) bool {
|
|
|
|
prevState := notification.PreviousState
|
|
|
|
// Skip if we are in the empty state because no loop-in has been
|
|
// persisted yet.
|
|
if l.IsInState(fsm.EmptyState) {
|
|
return true
|
|
}
|
|
|
|
// We don't update in self-loops, e.g. in the case of recovery.
|
|
if l.IsInState(prevState) {
|
|
return true
|
|
}
|
|
|
|
// If we transitioned from the empty state to InitHtlcTx there's still
|
|
// no loop-in persisted, so we don't need to update it.
|
|
if prevState == fsm.EmptyState && l.IsInState(InitHtlcTx) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Infof logs an info message with the loop-in swap hash.
|
|
func (f *FSM) Infof(format string, args ...any) {
|
|
if f.loopIn == nil {
|
|
log.Infof(format, args...)
|
|
return
|
|
}
|
|
log.Infof(
|
|
"StaticAddr loop-in %s: %s", f.loopIn.SwapHash.String(),
|
|
fmt.Sprintf(format, args...),
|
|
)
|
|
}
|
|
|
|
// Debugf logs a debug message with the loop-in swap hash.
|
|
func (f *FSM) Debugf(format string, args ...any) {
|
|
if f.loopIn == nil {
|
|
log.Debugf(format, args...)
|
|
return
|
|
}
|
|
log.Debugf(
|
|
"StaticAddr loop-in %s: %s", f.loopIn.SwapHash.String(),
|
|
fmt.Sprintf(format, args...),
|
|
)
|
|
}
|
|
|
|
// Warnf logs a warning message with the loop-in swap hash.
|
|
func (f *FSM) Warnf(format string, args ...any) {
|
|
if f.loopIn == nil {
|
|
log.Warnf(format, args...)
|
|
return
|
|
}
|
|
log.Warnf(
|
|
"StaticAddr loop-in %s: %s", f.loopIn.SwapHash.String(),
|
|
fmt.Sprintf(format, args...),
|
|
)
|
|
}
|
|
|
|
// Errorf logs an error message with the loop-in swap hash.
|
|
func (f *FSM) Errorf(format string, args ...any) {
|
|
if f.loopIn == nil {
|
|
log.Errorf(format, args...)
|
|
return
|
|
}
|
|
log.Errorf(
|
|
"StaticAddr loop-in %s: %s", f.loopIn.SwapHash.String(),
|
|
fmt.Sprintf(format, args...),
|
|
)
|
|
}
|