Skip to content

Commit 46150c1

Browse files
committed
staticaddr: deposit state machine
1 parent dcf2151 commit 46150c1

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed

staticaddr/actions.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package staticaddr
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/wire"
8+
"github.com/lightninglabs/lndclient"
9+
"github.com/lightninglabs/loop/fsm"
10+
"github.com/lightninglabs/loop/staticaddr/script"
11+
)
12+
13+
const (
14+
defaultConfTarget = 3
15+
)
16+
17+
// PublishExpiredDepositAction creates and publishes the timeout transaction
18+
// that spends the deposit from the static address timeout leaf to the
19+
// predefined timeout sweep pkscript.
20+
func (f *FSM) PublishExpiredDepositAction(_ fsm.EventContext) fsm.EventType {
21+
msgTx := wire.NewMsgTx(2)
22+
23+
// Add the deposit outpoint as input to the transaction.
24+
msgTx.AddTxIn(&wire.TxIn{
25+
PreviousOutPoint: f.deposit.OutPoint,
26+
Sequence: f.addressParameters.Expiry,
27+
SignatureScript: nil,
28+
})
29+
30+
// Estimate the fee rate of an expiry spend transaction.
31+
feeRateEstimator, err := f.cfg.WalletKit.EstimateFeeRate(
32+
f.ctx, defaultConfTarget,
33+
)
34+
if err != nil {
35+
return f.HandleError(fmt.Errorf("timeout sweep fee "+
36+
"estimation failed: %v", err))
37+
}
38+
39+
weight := script.ExpirySpendWeight()
40+
41+
fee := feeRateEstimator.FeeForWeight(weight)
42+
43+
// We cap the fee at 20% of the deposit value.
44+
if fee > f.deposit.Value/5 {
45+
return f.HandleError(errors.New("fee is greater than 20% of " +
46+
"the deposit value"))
47+
}
48+
49+
output := &wire.TxOut{
50+
Value: int64(f.deposit.Value - fee),
51+
PkScript: f.deposit.TimeOutSweepPkScript,
52+
}
53+
msgTx.AddTxOut(output)
54+
55+
txOut := &wire.TxOut{
56+
Value: int64(f.deposit.Value),
57+
PkScript: f.addressParameters.PkScript,
58+
}
59+
60+
prevOut := []*wire.TxOut{txOut}
61+
62+
signDesc, err := f.SignDescriptor()
63+
if err != nil {
64+
return f.HandleError(err)
65+
}
66+
67+
rawSigs, err := f.cfg.Signer.SignOutputRaw(
68+
f.ctx, msgTx, []*lndclient.SignDescriptor{&signDesc}, prevOut,
69+
)
70+
if err != nil {
71+
return f.HandleError(err)
72+
}
73+
74+
sig := rawSigs[0]
75+
msgTx.TxIn[0].Witness, err = f.staticAddress.GenTimeoutWitness(sig)
76+
if err != nil {
77+
return f.HandleError(err)
78+
}
79+
80+
err = f.cfg.WalletKit.PublishTransaction(
81+
f.ctx, msgTx, f.deposit.OutPoint.Hash.String()+"-close-sweep",
82+
)
83+
if err != nil {
84+
return f.HandleError(err)
85+
}
86+
87+
f.Debugf("published timeout sweep with txid: %v", msgTx.TxHash())
88+
89+
return OnExpiryPublished
90+
}
91+
92+
// WaitForExpirySweepAction waits for a sufficient number of confirmations
93+
// before a timeout sweep is considered successful.
94+
func (f *FSM) WaitForExpirySweepAction(_ fsm.EventContext) fsm.EventType {
95+
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll
96+
f.ctx, nil, f.deposit.TimeOutSweepPkScript, defaultConfTarget,
97+
int32(f.deposit.ConfirmationHeight),
98+
)
99+
if err != nil {
100+
return f.HandleError(err)
101+
}
102+
103+
select {
104+
case err := <-errSpendChan:
105+
log.Debugf("spend expired deposit error: %v", err)
106+
return fsm.OnError
107+
108+
case <-spendChan:
109+
return OnExpirySwept
110+
111+
case <-f.ctx.Done():
112+
return fsm.OnError
113+
}
114+
}
115+
116+
// SweptExpiredDepositAction is the final action of the FSM. It signals to the
117+
// manager that the deposit has been swept and the FSM can be removed. It also
118+
// ends the state machine main loop by cancelling its context.
119+
func (f *FSM) SweptExpiredDepositAction(_ fsm.EventContext) fsm.EventType {
120+
select {
121+
case <-f.ctx.Done():
122+
return fsm.OnError
123+
124+
default:
125+
f.finalizedDepositChan <- f.deposit.OutPoint
126+
f.ctx.Done()
127+
}
128+
129+
return fsm.NoOp
130+
}

staticaddr/fsm.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package staticaddr
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/txscript"
7+
"github.com/btcsuite/btcd/wire"
8+
"github.com/lightninglabs/lndclient"
9+
"github.com/lightninglabs/loop/fsm"
10+
"github.com/lightninglabs/loop/staticaddr/script"
11+
"github.com/lightningnetwork/lnd/input"
12+
"github.com/lightningnetwork/lnd/keychain"
13+
)
14+
15+
const (
16+
DefaultObserverSize = 20
17+
)
18+
19+
// States.
20+
var (
21+
Deposited = fsm.StateType("Deposited")
22+
23+
PublishExpiredDeposit = fsm.StateType("PublishExpiredDeposit")
24+
WaitForExpirySweep = fsm.StateType("WaitForExpirySweep")
25+
26+
SweptExpiredDeposit = fsm.StateType("SweptExpiredDeposit")
27+
28+
Failed = fsm.StateType("DepositFailed")
29+
)
30+
31+
// Events.
32+
var (
33+
OnStart = fsm.EventType("OnStart")
34+
OnExpiry = fsm.EventType("OnExpiry")
35+
OnExpiryPublished = fsm.EventType("OnExpiryPublished")
36+
OnExpirySwept = fsm.EventType("OnExpirySwept")
37+
OnRecover = fsm.EventType("OnRecover")
38+
)
39+
40+
// FSM is the state machine that handles the instant out.
41+
type FSM struct {
42+
*fsm.StateMachine
43+
44+
cfg *ManagerConfig
45+
46+
addressParameters *AddressParameters
47+
48+
staticAddress *script.StaticAddress
49+
50+
deposit *Deposit
51+
52+
ctx context.Context
53+
54+
blockNtfnChan chan uint32
55+
56+
finalizedDepositChan chan wire.OutPoint
57+
}
58+
59+
// NewFSM creates a new state machine that can action on all static address
60+
// feature requests.
61+
func NewFSM(ctx context.Context, addressParameters *AddressParameters,
62+
staticAddress *script.StaticAddress, deposit *Deposit,
63+
cfg *ManagerConfig, finalizedDepositChan chan wire.OutPoint,
64+
recoverStateMachine bool) (*FSM, error) {
65+
66+
depoFsm := &FSM{
67+
cfg: cfg,
68+
addressParameters: addressParameters,
69+
staticAddress: staticAddress,
70+
deposit: deposit,
71+
ctx: ctx,
72+
blockNtfnChan: make(chan uint32),
73+
finalizedDepositChan: finalizedDepositChan,
74+
}
75+
76+
if recoverStateMachine {
77+
depoFsm.StateMachine = fsm.NewStateMachineWithState(
78+
depoFsm.DepositStates(), deposit.State,
79+
DefaultObserverSize,
80+
)
81+
} else {
82+
depoFsm.StateMachine = fsm.NewStateMachine(
83+
depoFsm.DepositStates(), DefaultObserverSize,
84+
)
85+
}
86+
87+
depoFsm.ActionEntryFunc = depoFsm.updateDeposit
88+
89+
go func() {
90+
for {
91+
select {
92+
case currentHeight := <-depoFsm.blockNtfnChan:
93+
depoFsm.handleBlockNotification(currentHeight)
94+
95+
case <-ctx.Done():
96+
return
97+
}
98+
}
99+
}()
100+
101+
return depoFsm, nil
102+
}
103+
104+
func (f *FSM) handleBlockNotification(currentHeight uint32) {
105+
isExpired := func() bool {
106+
return currentHeight >= uint32(f.deposit.ConfirmationHeight)+
107+
f.addressParameters.Expiry
108+
}
109+
110+
if isExpired() && f.deposit.State != WaitForExpirySweep &&
111+
!f.deposit.IsFinal() {
112+
113+
go func() {
114+
err := f.SendEvent(OnExpiry, nil)
115+
if err != nil {
116+
log.Debugf("error sending OnExpiry event: %v",
117+
err)
118+
}
119+
}()
120+
}
121+
}
122+
123+
// DepositStates returns the states a deposit can be in.
124+
func (f *FSM) DepositStates() fsm.States {
125+
return fsm.States{
126+
fsm.EmptyState: fsm.State{
127+
Transitions: fsm.Transitions{
128+
OnStart: Deposited,
129+
},
130+
Action: fsm.NoOpAction,
131+
},
132+
Deposited: fsm.State{
133+
Transitions: fsm.Transitions{
134+
OnExpiry: PublishExpiredDeposit,
135+
OnRecover: Deposited,
136+
},
137+
Action: fsm.NoOpAction,
138+
},
139+
PublishExpiredDeposit: fsm.State{
140+
Transitions: fsm.Transitions{
141+
OnRecover: PublishExpiredDeposit,
142+
OnExpiryPublished: WaitForExpirySweep,
143+
// If the timeout sweep failed we go back to
144+
// Deposited, hoping that another timeout sweep
145+
// attempt will be successful. Alternatively,
146+
// the client can try to coop-spend the deposit.
147+
fsm.OnError: Deposited,
148+
},
149+
Action: f.PublishExpiredDepositAction,
150+
},
151+
WaitForExpirySweep: fsm.State{
152+
Transitions: fsm.Transitions{
153+
OnExpirySwept: SweptExpiredDeposit,
154+
OnRecover: PublishExpiredDeposit,
155+
// If the timeout sweep failed we go back to
156+
// Deposited, hoping that another timeout sweep
157+
// attempt will be successful. Alternatively,
158+
// the client can try to coop-spend the deposit.
159+
fsm.OnError: Deposited,
160+
},
161+
Action: f.WaitForExpirySweepAction,
162+
},
163+
SweptExpiredDeposit: fsm.State{
164+
Transitions: fsm.Transitions{
165+
OnExpiry: SweptExpiredDeposit,
166+
},
167+
Action: f.SweptExpiredDepositAction,
168+
},
169+
Failed: fsm.State{
170+
Action: fsm.NoOpAction,
171+
},
172+
}
173+
}
174+
175+
// DepositEntryFunction is called after every action and updates the deposit in
176+
// the db.
177+
func (f *FSM) updateDeposit(notification fsm.Notification) {
178+
if f.deposit == nil {
179+
return
180+
}
181+
182+
f.Debugf("NextState: %v, PreviousState: %v, Event: %v",
183+
notification.NextState, notification.PreviousState,
184+
notification.Event,
185+
)
186+
187+
f.deposit.State = notification.NextState
188+
189+
// Don't update the deposit if we are in an initial state or if we
190+
// are transitioning from an initial state to a failed state.
191+
state := f.deposit.State
192+
if state == fsm.EmptyState || state == Deposited ||
193+
(notification.PreviousState == Deposited && state == Failed) {
194+
195+
return
196+
}
197+
198+
err := f.cfg.Store.UpdateDeposit(f.ctx, f.deposit)
199+
if err != nil {
200+
f.Errorf("unable to update deposit: %v", err)
201+
}
202+
}
203+
204+
// Infof logs an info message with the deposit outpoint.
205+
func (f *FSM) Infof(format string, args ...interface{}) {
206+
log.Infof(
207+
"Deposit %v: "+format,
208+
append(
209+
[]interface{}{f.deposit.OutPoint},
210+
args...,
211+
)...,
212+
)
213+
}
214+
215+
// Debugf logs a debug message with the deposit outpoint.
216+
func (f *FSM) Debugf(format string, args ...interface{}) {
217+
log.Debugf(
218+
"Deposit %v: "+format,
219+
append(
220+
[]interface{}{f.deposit.OutPoint},
221+
args...,
222+
)...,
223+
)
224+
}
225+
226+
// Errorf logs an error message with the deposit outpoint.
227+
func (f *FSM) Errorf(format string, args ...interface{}) {
228+
log.Errorf(
229+
"Deposit %v: "+format,
230+
append(
231+
[]interface{}{f.deposit.OutPoint},
232+
args...,
233+
)...,
234+
)
235+
}
236+
237+
// SignDescriptor returns the sign descriptor for the static address output.
238+
func (f *FSM) SignDescriptor() (lndclient.SignDescriptor, error) {
239+
return lndclient.SignDescriptor{
240+
WitnessScript: f.staticAddress.TimeoutLeaf.Script,
241+
KeyDesc: keychain.KeyDescriptor{
242+
PubKey: f.addressParameters.ClientPubkey,
243+
},
244+
Output: wire.NewTxOut(
245+
int64(f.deposit.Value), f.addressParameters.PkScript,
246+
),
247+
HashType: txscript.SigHashDefault,
248+
InputIndex: 0,
249+
SignMethod: input.TaprootScriptSpendSignMethod,
250+
}, nil
251+
}

0 commit comments

Comments
 (0)