-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
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 | ||
) | ||
|
||
// PublishExpiredDepositAction 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) PublishExpiredDepositAction(_ fsm.EventContext) fsm.EventType { | ||
msgTx := wire.NewMsgTx(2) | ||
|
||
// Add the deposit outpoint as input to the transaction. | ||
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.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 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: f.addressParameters.PkScript, | ||
} | ||
|
||
prevOut := []*wire.TxOut{txOut} | ||
|
||
signDesc, err := f.SignDescriptor() | ||
if err != nil { | ||
return f.HandleError(err) | ||
} | ||
|
||
rawSigs, err := f.cfg.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.WalletKit.PublishTransaction( | ||
f.ctx, msgTx, f.deposit.OutPoint.Hash.String()+"-close-sweep", | ||
) | ||
if err != nil { | ||
return f.HandleError(err) | ||
} | ||
|
||
f.Debugf("published timeout sweep with txid: %v", msgTx.TxHash()) | ||
|
||
return OnExpiryPublished | ||
} | ||
|
||
// WaitForExpirySweepAction waits for a sufficient number of confirmations | ||
// before a timeout sweep is considered successful. | ||
func (f *FSM) WaitForExpirySweepAction(_ fsm.EventContext) fsm.EventType { | ||
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll | ||
f.ctx, nil, f.deposit.TimeOutSweepPkScript, defaultConfTarget, | ||
int32(f.deposit.ConfirmationHeight), | ||
) | ||
if err != nil { | ||
return f.HandleError(err) | ||
} | ||
|
||
select { | ||
case err := <-errSpendChan: | ||
log.Debugf("spend expired deposit error: %v", err) | ||
return fsm.OnError | ||
|
||
case <-spendChan: | ||
return OnExpirySwept | ||
|
||
case <-f.ctx.Done(): | ||
return fsm.OnError | ||
} | ||
} | ||
|
||
// SweptExpiredDepositAction is the final action of the FSM. It signals to the | ||
// manager that the deposit has been swept and the FSM can be removed. It also | ||
// ends the state machine main loop by cancelling its context. | ||
func (f *FSM) SweptExpiredDepositAction(_ fsm.EventContext) fsm.EventType { | ||
select { | ||
case <-f.ctx.Done(): | ||
return fsm.OnError | ||
|
||
default: | ||
f.finalizedDepositChan <- f.deposit.OutPoint | ||
f.ctx.Done() | ||
} | ||
|
||
return fsm.NoOp | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
package staticaddr | ||
|
||
import ( | ||
"context" | ||
|
||
"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" | ||
) | ||
|
||
const ( | ||
DefaultObserverSize = 20 | ||
) | ||
|
||
// States. | ||
var ( | ||
Deposited = fsm.StateType("Deposited") | ||
|
||
PublishExpiredDeposit = fsm.StateType("PublishExpiredDeposit") | ||
WaitForExpirySweep = fsm.StateType("WaitForExpirySweep") | ||
|
||
SweptExpiredDeposit = fsm.StateType("SweptExpiredDeposit") | ||
|
||
Failed = fsm.StateType("DepositFailed") | ||
) | ||
|
||
// Events. | ||
var ( | ||
OnStart = fsm.EventType("OnStart") | ||
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 | ||
|
||
addressParameters *AddressParameters | ||
|
||
staticAddress *script.StaticAddress | ||
|
||
deposit *Deposit | ||
|
||
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, addressParameters *AddressParameters, | ||
staticAddress *script.StaticAddress, deposit *Deposit, | ||
cfg *ManagerConfig, finalizedDepositChan chan wire.OutPoint, | ||
recoverStateMachine bool) (*FSM, error) { | ||
|
||
depoFsm := &FSM{ | ||
cfg: cfg, | ||
addressParameters: addressParameters, | ||
staticAddress: staticAddress, | ||
deposit: deposit, | ||
ctx: ctx, | ||
blockNtfnChan: make(chan uint32), | ||
finalizedDepositChan: finalizedDepositChan, | ||
} | ||
|
||
if recoverStateMachine { | ||
depoFsm.StateMachine = fsm.NewStateMachineWithState( | ||
depoFsm.DepositStates(), deposit.State, | ||
DefaultObserverSize, | ||
) | ||
} else { | ||
depoFsm.StateMachine = fsm.NewStateMachine( | ||
depoFsm.DepositStates(), DefaultObserverSize, | ||
) | ||
} | ||
|
||
depoFsm.ActionEntryFunc = depoFsm.updateDeposit | ||
|
||
go func() { | ||
for { | ||
select { | ||
case currentHeight := <-depoFsm.blockNtfnChan: | ||
depoFsm.handleBlockNotification(currentHeight) | ||
|
||
case <-ctx.Done(): | ||
return | ||
} | ||
} | ||
}() | ||
|
||
return depoFsm, nil | ||
} | ||
|
||
func (f *FSM) handleBlockNotification(currentHeight uint32) { | ||
isExpired := func() bool { | ||
return currentHeight >= uint32(f.deposit.ConfirmationHeight)+ | ||
f.addressParameters.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) | ||
} | ||
}() | ||
} | ||
} | ||
|
||
// 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: PublishExpiredDeposit, | ||
OnRecover: 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.PublishExpiredDepositAction, | ||
}, | ||
WaitForExpirySweep: fsm.State{ | ||
Transitions: fsm.Transitions{ | ||
OnExpirySwept: SweptExpiredDeposit, | ||
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, | ||
}, | ||
SweptExpiredDeposit: fsm.State{ | ||
Transitions: fsm.Transitions{ | ||
OnExpiry: SweptExpiredDeposit, | ||
}, | ||
Action: f.SweptExpiredDepositAction, | ||
}, | ||
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 | ||
if state == fsm.EmptyState || state == Deposited || | ||
(notification.PreviousState == Deposited && state == Failed) { | ||
|
||
return | ||
} | ||
|
||
err := f.cfg.Store.UpdateDeposit(f.ctx, f.deposit) | ||
if err != nil { | ||
f.Errorf("unable to update deposit: %v", err) | ||
} | ||
} | ||
|
||
// 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 | ||
} |