Skip to content

Commit c04b8b7

Browse files
committed
staticaddr: deposit manager and fsm
1 parent c56019e commit c04b8b7

File tree

5 files changed

+1007
-27
lines changed

5 files changed

+1007
-27
lines changed

staticaddr/manager.go renamed to staticaddr/address/manager.go

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package staticaddr
1+
package address
22

33
import (
44
"bytes"
@@ -12,6 +12,7 @@ import (
1212
"github.com/btcsuite/btcd/chaincfg"
1313
"github.com/lightninglabs/lndclient"
1414
"github.com/lightninglabs/loop"
15+
"github.com/lightninglabs/loop/staticaddr"
1516
"github.com/lightninglabs/loop/staticaddr/script"
1617
"github.com/lightninglabs/loop/swap"
1718
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
@@ -31,7 +32,7 @@ type ManagerConfig struct {
3132

3233
// Store is the database store that is used to store static address
3334
// related records.
34-
Store AddressStore
35+
Store Store
3536

3637
// WalletKit is the wallet client that is used to derive new keys from
3738
// lnd's wallet.
@@ -46,31 +47,20 @@ type ManagerConfig struct {
4647
type Manager struct {
4748
cfg *ManagerConfig
4849

49-
initChan chan struct{}
50-
5150
sync.Mutex
5251
}
5352

54-
// NewAddressManager creates a new address manager.
55-
func NewAddressManager(cfg *ManagerConfig) *Manager {
53+
// NewManager creates a new address manager.
54+
func NewManager(cfg *ManagerConfig) *Manager {
5655
return &Manager{
57-
cfg: cfg,
58-
initChan: make(chan struct{}),
56+
cfg: cfg,
5957
}
6058
}
6159

6260
// Run runs the address manager.
6361
func (m *Manager) Run(ctx context.Context) error {
64-
log.Debugf("Starting address manager.")
65-
defer log.Debugf("Address manager stopped.")
66-
67-
// Communicate to the caller that the address manager has completed its
68-
// initialization.
69-
close(m.initChan)
70-
7162
<-ctx.Done()
72-
73-
return nil
63+
return ctx.Err()
7464
}
7565

7666
// NewAddress starts a new address creation flow.
@@ -113,7 +103,7 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
113103

114104
// Send our clientPubKey to the server and wait for the server to
115105
// respond with he serverPubKey and the static address CSV expiry.
116-
protocolVersion := CurrentRPCProtocolVersion()
106+
protocolVersion := staticaddr.CurrentRPCProtocolVersion()
117107
resp, err := m.cfg.AddressClient.ServerNewAddress(
118108
ctx, &staticaddressrpc.ServerNewAddressRequest{
119109
ProtocolVersion: protocolVersion,
@@ -146,7 +136,7 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
146136

147137
// Create the static address from the parameters the server provided and
148138
// store all parameters in the database.
149-
addrParams := &AddressParameters{
139+
addrParams := &Parameters{
150140
ClientPubkey: clientPubKey.PubKey,
151141
ServerPubkey: serverPubKey,
152142
PkScript: pkScript,
@@ -155,7 +145,9 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
155145
Family: clientPubKey.Family,
156146
Index: clientPubKey.Index,
157147
},
158-
ProtocolVersion: AddressProtocolVersion(protocolVersion),
148+
ProtocolVersion: staticaddr.AddressProtocolVersion(
149+
protocolVersion,
150+
),
159151
}
160152
err = m.cfg.Store.CreateStaticAddress(ctx, addrParams)
161153
if err != nil {
@@ -197,12 +189,6 @@ func (m *Manager) getTaprootAddress(clientPubkey,
197189
)
198190
}
199191

200-
// WaitInitComplete waits until the address manager has completed its setup.
201-
func (m *Manager) WaitInitComplete() {
202-
defer log.Debugf("Address manager initiation complete.")
203-
<-m.initChan
204-
}
205-
206192
// ListUnspentRaw returns a list of utxos at the static address.
207193
func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
208194
maxConfs int32) (*btcutil.AddressTaproot, []*lnwallet.Utxo, error) {
@@ -213,7 +199,7 @@ func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
213199
return nil, nil, err
214200

215201
case len(addresses) == 0:
216-
return nil, nil, fmt.Errorf("no address found")
202+
return nil, nil, nil
217203

218204
case len(addresses) > 1:
219205
return nil, nil, fmt.Errorf("more than one address found")
@@ -249,3 +235,52 @@ func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
249235

250236
return taprootAddress, filteredUtxos, nil
251237
}
238+
239+
// GetStaticAddressParameters returns the parameters of the static address.
240+
func (m *Manager) GetStaticAddressParameters(ctx context.Context) (*Parameters,
241+
error) {
242+
243+
params, err := m.cfg.Store.GetAllStaticAddresses(ctx)
244+
if err != nil {
245+
return nil, err
246+
}
247+
248+
if len(params) == 0 {
249+
return nil, fmt.Errorf("no static address parameters found")
250+
}
251+
252+
return params[0], nil
253+
}
254+
255+
// GetStaticAddress returns a taproot address for the given client and server
256+
// public keys and expiry.
257+
func (m *Manager) GetStaticAddress(ctx context.Context) (*script.StaticAddress,
258+
error) {
259+
260+
params, err := m.GetStaticAddressParameters(ctx)
261+
if err != nil {
262+
return nil, err
263+
}
264+
265+
address, err := script.NewStaticAddress(
266+
input.MuSig2Version100RC2, int64(params.Expiry),
267+
params.ClientPubkey, params.ServerPubkey,
268+
)
269+
if err != nil {
270+
return nil, err
271+
}
272+
273+
return address, nil
274+
}
275+
276+
// ListUnspent returns a list of utxos at the static address.
277+
func (m *Manager) ListUnspent(ctx context.Context, minConfs,
278+
maxConfs int32) ([]*lnwallet.Utxo, error) {
279+
280+
_, utxos, err := m.ListUnspentRaw(ctx, minConfs, maxConfs)
281+
if err != nil {
282+
return nil, err
283+
}
284+
285+
return utxos, nil
286+
}

staticaddr/deposit/actions.go

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

staticaddr/deposit/deposit.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package deposit
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightninglabs/loop/fsm"
11+
)
12+
13+
// ID is a unique identifier for a deposit.
14+
type ID [IdLength]byte
15+
16+
// FromByteSlice creates a deposit id from a byte slice.
17+
func (r *ID) FromByteSlice(b []byte) error {
18+
if len(b) != IdLength {
19+
return fmt.Errorf("deposit id must be 32 bytes, got %d, %x",
20+
len(b), b)
21+
}
22+
23+
copy(r[:], b)
24+
25+
return nil
26+
}
27+
28+
// Deposit bundles an utxo at a static address together with manager-relevant
29+
// data.
30+
type Deposit struct {
31+
// ID is the unique identifier of the deposit.
32+
ID ID
33+
34+
// State is the current state of the deposit.
35+
State fsm.StateType
36+
37+
// The outpoint of the deposit.
38+
wire.OutPoint
39+
40+
// Value is the amount of the deposit.
41+
Value btcutil.Amount
42+
43+
// ConfirmationHeight is the absolute height at which the deposit was
44+
// first confirmed.
45+
ConfirmationHeight int64
46+
47+
// TimeOutSweepPkScript is the pk script that is used to sweep the
48+
// deposit to after it is expired.
49+
TimeOutSweepPkScript []byte
50+
51+
// ExpirySweepTxid is the transaction id of the expiry sweep.
52+
ExpirySweepTxid chainhash.Hash
53+
}
54+
55+
// IsInPendingState returns true if the deposit is pending.
56+
func (d *Deposit) IsInPendingState() bool {
57+
return !d.IsInFinalState()
58+
}
59+
60+
// IsInFinalState returns true if the deposit is final.
61+
func (d *Deposit) IsInFinalState() bool {
62+
return d.State == Expired || d.State == Failed
63+
}
64+
65+
func (d *Deposit) isExpired(currentHeight, expiry uint32) bool {
66+
return currentHeight >= uint32(d.ConfirmationHeight)+expiry
67+
}
68+
69+
// GetRandomDepositID generates a random deposit ID.
70+
func GetRandomDepositID() (ID, error) {
71+
var id ID
72+
_, err := rand.Read(id[:])
73+
return id, err
74+
}

0 commit comments

Comments
 (0)