forked from btcsuite/btcwallet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrecovery.go
410 lines (347 loc) · 14.3 KB
/
recovery.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
package wallet
import (
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
// RecoveryManager maintains the state required to recover previously used
// addresses, and coordinates batched processing of the blocks to search.
type RecoveryManager struct {
// recoveryWindow defines the key-derivation lookahead used when
// attempting to recover the set of used addresses.
recoveryWindow uint32
// started is true after the first block has been added to the batch.
started bool
// blockBatch contains a list of blocks that have not yet been searched
// for recovered addresses.
blockBatch []wtxmgr.BlockMeta
// state encapsulates and allocates the necessary recovery state for all
// key scopes and subsidiary derivation paths.
state *RecoveryState
// chainParams are the parameters that describe the chain we're trying
// to recover funds on.
chainParams *chaincfg.Params
}
// NewRecoveryManager initializes a new RecoveryManager with a derivation
// look-ahead of `recoveryWindow` child indexes, and pre-allocates a backing
// array for `batchSize` blocks to scan at once.
func NewRecoveryManager(recoveryWindow, batchSize uint32,
chainParams *chaincfg.Params) *RecoveryManager {
return &RecoveryManager{
recoveryWindow: recoveryWindow,
blockBatch: make([]wtxmgr.BlockMeta, 0, batchSize),
chainParams: chainParams,
state: NewRecoveryState(recoveryWindow),
}
}
// Resurrect restores all known addresses for the provided scopes that can be
// found in the walletdb namespace, in addition to restoring all outpoints that
// have been previously found. This method ensures that the recovery state's
// horizons properly start from the last found address of a prior recovery
// attempt.
func (rm *RecoveryManager) Resurrect(ns walletdb.ReadBucket,
scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager,
credits []wtxmgr.Credit) error {
// First, for each scope that we are recovering, rederive all of the
// addresses up to the last found address known to each branch.
for keyScope, scopedMgr := range scopedMgrs {
// Load the current account properties for this scope, using the
// the default account number.
// TODO(conner): rescan for all created accounts if we allow
// users to use non-default address
scopeState := rm.state.StateForScope(keyScope)
acctProperties, err := scopedMgr.AccountProperties(
ns, waddrmgr.DefaultAccountNum,
)
if err != nil {
return err
}
// Fetch the external key count, which bounds the indexes we
// will need to rederive.
externalCount := acctProperties.ExternalKeyCount
// Walk through all indexes through the last external key,
// deriving each address and adding it to the external branch
// recovery state's set of addresses to look for.
for i := uint32(0); i < externalCount; i++ {
keyPath := externalKeyPath(i)
addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath)
if err != nil && err != hdkeychain.ErrInvalidChild {
return err
} else if err == hdkeychain.ErrInvalidChild {
scopeState.ExternalBranch.MarkInvalidChild(i)
continue
}
scopeState.ExternalBranch.AddAddr(i, addr.Address())
}
// Fetch the internal key count, which bounds the indexes we
// will need to rederive.
internalCount := acctProperties.InternalKeyCount
// Walk through all indexes through the last internal key,
// deriving each address and adding it to the internal branch
// recovery state's set of addresses to look for.
for i := uint32(0); i < internalCount; i++ {
keyPath := internalKeyPath(i)
addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath)
if err != nil && err != hdkeychain.ErrInvalidChild {
return err
} else if err == hdkeychain.ErrInvalidChild {
scopeState.InternalBranch.MarkInvalidChild(i)
continue
}
scopeState.InternalBranch.AddAddr(i, addr.Address())
}
// The key counts will point to the next key that can be
// derived, so we subtract one to point to last known key. If
// the key count is zero, then no addresses have been found.
if externalCount > 0 {
scopeState.ExternalBranch.ReportFound(externalCount - 1)
}
if internalCount > 0 {
scopeState.InternalBranch.ReportFound(internalCount - 1)
}
}
// In addition, we will re-add any outpoints that are known the wallet
// to our global set of watched outpoints, so that we can watch them for
// spends.
for _, credit := range credits {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
credit.PkScript, rm.chainParams,
)
if err != nil {
return err
}
rm.state.AddWatchedOutPoint(&credit.OutPoint, addrs[0])
}
return nil
}
// AddToBlockBatch appends the block information, consisting of hash and height,
// to the batch of blocks to be searched.
func (rm *RecoveryManager) AddToBlockBatch(hash *chainhash.Hash, height int32,
timestamp time.Time) {
if !rm.started {
log.Infof("Seed birthday surpassed, starting recovery "+
"of wallet from height=%d hash=%v with "+
"recovery-window=%d", height, *hash, rm.recoveryWindow)
rm.started = true
}
block := wtxmgr.BlockMeta{
Block: wtxmgr.Block{
Hash: *hash,
Height: height,
},
Time: timestamp,
}
rm.blockBatch = append(rm.blockBatch, block)
}
// BlockBatch returns a buffer of blocks that have not yet been searched.
func (rm *RecoveryManager) BlockBatch() []wtxmgr.BlockMeta {
return rm.blockBatch
}
// ResetBlockBatch resets the internal block buffer to conserve memory.
func (rm *RecoveryManager) ResetBlockBatch() {
rm.blockBatch = rm.blockBatch[:0]
}
// State returns the current RecoveryState.
func (rm *RecoveryManager) State() *RecoveryState {
return rm.state
}
// RecoveryState manages the initialization and lookup of ScopeRecoveryStates
// for any actively used key scopes.
//
// In order to ensure that all addresses are properly recovered, the window
// should be sized as the sum of maximum possible inter-block and intra-block
// gap between used addresses of a particular branch.
//
// These are defined as:
// - Inter-Block Gap: The maximum difference between the derived child indexes
// of the last addresses used in any block and the next address consumed
// by a later block.
// - Intra-Block Gap: The maximum difference between the derived child indexes
// of the first address used in any block and the last address used in the
// same block.
type RecoveryState struct {
// recoveryWindow defines the key-derivation lookahead used when
// attempting to recover the set of used addresses. This value will be
// used to instantiate a new RecoveryState for each requested scope.
recoveryWindow uint32
// scopes maintains a map of each requested key scope to its active
// RecoveryState.
scopes map[waddrmgr.KeyScope]*ScopeRecoveryState
// watchedOutPoints contains the set of all outpoints known to the
// wallet. This is updated iteratively as new outpoints are found during
// a rescan.
watchedOutPoints map[wire.OutPoint]btcutil.Address
}
// NewRecoveryState creates a new RecoveryState using the provided
// recoveryWindow. Each RecoveryState that is subsequently initialized for a
// particular key scope will receive the same recoveryWindow.
func NewRecoveryState(recoveryWindow uint32) *RecoveryState {
scopes := make(map[waddrmgr.KeyScope]*ScopeRecoveryState)
return &RecoveryState{
recoveryWindow: recoveryWindow,
scopes: scopes,
watchedOutPoints: make(map[wire.OutPoint]btcutil.Address),
}
}
// StateForScope returns a ScopeRecoveryState for the provided key scope. If one
// does not already exist, a new one will be generated with the RecoveryState's
// recoveryWindow.
func (rs *RecoveryState) StateForScope(
keyScope waddrmgr.KeyScope) *ScopeRecoveryState {
// If the account recovery state already exists, return it.
if scopeState, ok := rs.scopes[keyScope]; ok {
return scopeState
}
// Otherwise, initialize the recovery state for this scope with the
// chosen recovery window.
rs.scopes[keyScope] = NewScopeRecoveryState(rs.recoveryWindow)
return rs.scopes[keyScope]
}
// WatchedOutPoints returns the global set of outpoints that are known to belong
// to the wallet during recovery.
func (rs *RecoveryState) WatchedOutPoints() map[wire.OutPoint]btcutil.Address {
return rs.watchedOutPoints
}
// AddWatchedOutPoint updates the recovery state's set of known outpoints that
// we will monitor for spends during recovery.
func (rs *RecoveryState) AddWatchedOutPoint(outPoint *wire.OutPoint,
addr btcutil.Address) {
rs.watchedOutPoints[*outPoint] = addr
}
// ScopeRecoveryState is used to manage the recovery of addresses generated
// under a particular BIP32 account. Each account tracks both an external and
// internal branch recovery state, both of which use the same recovery window.
type ScopeRecoveryState struct {
// ExternalBranch is the recovery state of addresses generated for
// external use, i.e. receiving addresses.
ExternalBranch *BranchRecoveryState
// InternalBranch is the recovery state of addresses generated for
// internal use, i.e. change addresses.
InternalBranch *BranchRecoveryState
}
// NewScopeRecoveryState initializes an ScopeRecoveryState with the chosen
// recovery window.
func NewScopeRecoveryState(recoveryWindow uint32) *ScopeRecoveryState {
return &ScopeRecoveryState{
ExternalBranch: NewBranchRecoveryState(recoveryWindow),
InternalBranch: NewBranchRecoveryState(recoveryWindow),
}
}
// BranchRecoveryState maintains the required state in-order to properly
// recover addresses derived from a particular account's internal or external
// derivation branch.
//
// A branch recovery state supports operations for:
// - Expanding the look-ahead horizon based on which indexes have been found.
// - Registering derived addresses with indexes within the horizon.
// - Reporting an invalid child index that falls into the horizon.
// - Reporting that an address has been found.
// - Retrieving all currently derived addresses for the branch.
// - Looking up a particular address by its child index.
type BranchRecoveryState struct {
// recoveryWindow defines the key-derivation lookahead used when
// attempting to recover the set of addresses on this branch.
recoveryWindow uint32
// horizion records the highest child index watched by this branch.
horizon uint32
// nextUnfound maintains the child index of the successor to the highest
// index that has been found during recovery of this branch.
nextUnfound uint32
// addresses is a map of child index to address for all actively watched
// addresses belonging to this branch.
addresses map[uint32]btcutil.Address
// invalidChildren records the set of child indexes that derive to
// invalid keys.
invalidChildren map[uint32]struct{}
}
// NewBranchRecoveryState creates a new BranchRecoveryState that can be used to
// track either the external or internal branch of an account's derivation path.
func NewBranchRecoveryState(recoveryWindow uint32) *BranchRecoveryState {
return &BranchRecoveryState{
recoveryWindow: recoveryWindow,
addresses: make(map[uint32]btcutil.Address),
invalidChildren: make(map[uint32]struct{}),
}
}
// ExtendHorizon returns the current horizon and the number of addresses that
// must be derived in order to maintain the desired recovery window.
func (brs *BranchRecoveryState) ExtendHorizon() (uint32, uint32) {
// Compute the new horizon, which should surpass our last found address
// by the recovery window.
curHorizon := brs.horizon
nInvalid := brs.NumInvalidInHorizon()
minValidHorizon := brs.nextUnfound + brs.recoveryWindow + nInvalid
// If the current horizon is sufficient, we will not have to derive any
// new keys.
if curHorizon >= minValidHorizon {
return curHorizon, 0
}
// Otherwise, the number of addresses we should derive corresponds to
// the delta of the two horizons, and we update our new horizon.
delta := minValidHorizon - curHorizon
brs.horizon = minValidHorizon
return curHorizon, delta
}
// AddAddr adds a freshly derived address from our lookahead into the map of
// known addresses for this branch.
func (brs *BranchRecoveryState) AddAddr(index uint32, addr btcutil.Address) {
brs.addresses[index] = addr
}
// GetAddr returns the address derived from a given child index.
func (brs *BranchRecoveryState) GetAddr(index uint32) btcutil.Address {
return brs.addresses[index]
}
// ReportFound updates the last found index if the reported index exceeds the
// current value.
func (brs *BranchRecoveryState) ReportFound(index uint32) {
if index >= brs.nextUnfound {
brs.nextUnfound = index + 1
// Prune all invalid child indexes that fall below our last
// found index. We don't need to keep these entries any longer,
// since they will not affect our required look-ahead.
for childIndex := range brs.invalidChildren {
if childIndex < index {
delete(brs.invalidChildren, childIndex)
}
}
}
}
// MarkInvalidChild records that a particular child index results in deriving an
// invalid address. In addition, the branch's horizon is increment, as we expect
// the caller to perform an additional derivation to replace the invalid child.
// This is used to ensure that we are always have the proper lookahead when an
// invalid child is encountered.
func (brs *BranchRecoveryState) MarkInvalidChild(index uint32) {
brs.invalidChildren[index] = struct{}{}
brs.horizon++
}
// NextUnfound returns the child index of the successor to the highest found
// child index.
func (brs *BranchRecoveryState) NextUnfound() uint32 {
return brs.nextUnfound
}
// Addrs returns a map of all currently derived child indexes to the their
// corresponding addresses.
func (brs *BranchRecoveryState) Addrs() map[uint32]btcutil.Address {
return brs.addresses
}
// NumInvalidInHorizon computes the number of invalid child indexes that lie
// between the last found and current horizon. This informs how many additional
// indexes to derive in order to maintain the proper number of valid addresses
// within our horizon.
func (brs *BranchRecoveryState) NumInvalidInHorizon() uint32 {
var nInvalid uint32
for childIndex := range brs.invalidChildren {
if brs.nextUnfound <= childIndex && childIndex < brs.horizon {
nInvalid++
}
}
return nInvalid
}