11 "github.com/vapor/account"
12 "github.com/vapor/blockchain/signers"
13 "github.com/vapor/crypto/ed25519/chainkd"
14 "github.com/vapor/crypto/sha3pool"
15 dbm "github.com/vapor/database/leveldb"
16 "github.com/vapor/errors"
17 "github.com/vapor/protocol/bc"
18 "github.com/vapor/protocol/bc/types"
22 // acctRecoveryWindow defines the account derivation lookahead used when
23 // attempting to recover the set of used accounts.
24 acctRecoveryWindow = uint64(6)
26 // addrRecoveryWindow defines the address derivation lookahead used when
27 // attempting to recover the set of used addresses.
28 addrRecoveryWindow = uint64(128)
31 //recoveryKey key for db store recovery info.
33 recoveryKey = []byte("RecoveryInfo")
35 // ErrRecoveryBusy another recovery in progress, can not get recovery manager lock
36 ErrRecoveryBusy = errors.New("another recovery in progress")
38 // ErrInvalidAcctID can not find account by account id
39 ErrInvalidAcctID = errors.New("invalid account id")
42 // branchRecoveryState maintains the required state in-order to properly
43 // recover addresses derived from a particular account's internal or external
46 // A branch recovery state supports operations for:
47 // - Expanding the look-ahead horizon based on which indexes have been found.
48 // - Registering derived addresses with indexes within the horizon.
49 // - Reporting an invalid child index that falls into the horizon.
50 // - Reporting that an address has been found.
51 // - Retrieving all currently derived addresses for the branch.
52 // - Looking up a particular address by its child index.
53 type branchRecoveryState struct {
54 // recoveryWindow defines the key-derivation lookahead used when
55 // attempting to recover the set of addresses on this branch.
58 // horizion records the highest child index watched by this branch.
61 // nextUnfound maintains the child index of the successor to the highest
62 // index that has been found during recovery of this branch.
66 // newBranchRecoveryState creates a new branchRecoveryState that can be used to
67 // track either the external or internal branch of an account's derivation path.
68 func newBranchRecoveryState(recoveryWindow uint64) *branchRecoveryState {
69 return &branchRecoveryState{
70 RecoveryWindow: recoveryWindow,
76 // extendHorizon returns the current horizon and the number of addresses that
77 // must be derived in order to maintain the desired recovery window.
78 func (brs *branchRecoveryState) extendHorizon() (uint64, uint64) {
79 // Compute the new horizon, which should surpass our last found address
80 // by the recovery window.
81 curHorizon := brs.Horizon
83 minValidHorizon := brs.NextUnfound + brs.RecoveryWindow
85 // If the current horizon is sufficient, we will not have to derive any
87 if curHorizon >= minValidHorizon {
91 // Otherwise, the number of addresses we should derive corresponds to
92 // the delta of the two horizons, and we update our new horizon.
93 delta := minValidHorizon - curHorizon
94 brs.Horizon = minValidHorizon
96 return curHorizon, delta
99 // reportFound updates the last found index if the reported index exceeds the
101 func (brs *branchRecoveryState) reportFound(index uint64) {
102 if index >= brs.NextUnfound {
103 brs.NextUnfound = index + 1
107 // addressRecoveryState is used to manage the recovery of addresses generated
108 // under a particular BIP32/BIP44 account. Each account tracks both an external and
109 // internal branch recovery state, both of which use the same recovery window.
110 type addressRecoveryState struct {
111 // ExternalBranch is the recovery state of addresses generated for
112 // external use, i.e. receiving addresses.
113 ExternalBranch *branchRecoveryState
115 // InternalBranch is the recovery state of addresses generated for
116 // internal use, i.e. change addresses.
117 InternalBranch *branchRecoveryState
119 Account *account.Account
122 func newAddressRecoveryState(recoveryWindow uint64, account *account.Account) *addressRecoveryState {
123 return &addressRecoveryState{
124 ExternalBranch: newBranchRecoveryState(recoveryWindow),
125 InternalBranch: newBranchRecoveryState(recoveryWindow),
130 // recoveryState used to record the status of a recovery process.
131 type recoveryState struct {
132 // XPubs recovery account xPubs
135 // The time to start the recovery task, used to detemine whether
136 // recovery task is completed.
139 // XPubsStatus maintains a map of each requested XPub to its active
140 // account recovery state.
141 XPubsStatus *branchRecoveryState
143 // AcctStatus maintains a map of each requested key scope to its active
145 AccountsStatus map[string]*addressRecoveryState
148 func newRecoveryState() *recoveryState {
149 return &recoveryState{
150 AccountsStatus: make(map[string]*addressRecoveryState),
151 StartTime: time.Now(),
155 // stateForScope returns a ScopeRecoveryState for the provided key scope. If one
156 // does not already exist, a new one will be generated with the RecoveryState's
158 func (rs *recoveryState) stateForScope(account *account.Account) {
159 // If the account recovery state already exists, return it.
160 if _, ok := rs.AccountsStatus[account.ID]; ok {
164 // Otherwise, initialize the recovery state for this scope with the
165 // chosen recovery window.
166 rs.AccountsStatus[account.ID] = newAddressRecoveryState(addrRecoveryWindow, account)
169 // recoveryManager manage recovery wallet from key.
170 type recoveryManager struct {
174 accountMgr *account.Manager
180 // state encapsulates and allocates the necessary recovery state for all
181 // key scopes and subsidiary derivation paths.
184 //addresses all addresses derivation lookahead used when
185 // attempting to recover the set of used addresses.
186 addresses map[bc.Hash]*account.CtrlProgram
189 // newRecoveryManager create recovery manger.
190 func newRecoveryManager(db dbm.DB, accountMgr *account.Manager) *recoveryManager {
191 return &recoveryManager{
193 accountMgr: accountMgr,
194 addresses: make(map[bc.Hash]*account.CtrlProgram),
195 state: newRecoveryState(),
199 func (m *recoveryManager) AddrResurrect(accts []*account.Account) error {
203 for _, acct := range accts {
204 m.state.stateForScope(acct)
205 if err := m.extendScanAddresses(acct.ID, false); err != nil {
209 //Bip32 path no change field, no need to create addresses repeatedly.
210 if acct.DeriveRule == signers.BIP0032 {
213 if err := m.extendScanAddresses(acct.ID, true); err != nil {
218 m.state.StartTime = time.Now()
219 if err := m.commitStatusInfo(); err != nil {
227 func (m *recoveryManager) AcctResurrect(xPubs []chainkd.XPub) error {
231 if !m.tryStartXPubsRec() {
232 return ErrRecoveryBusy
235 m.state.XPubs = xPubs
236 m.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
238 if err := m.extendScanAccounts(); err != nil {
242 m.state.StartTime = time.Now()
243 if err := m.commitStatusInfo(); err != nil {
251 func (m *recoveryManager) commitStatusInfo() error {
252 rawStatus, err := json.Marshal(m.state)
257 m.db.Set(recoveryKey, rawStatus)
261 func genAcctAlias(xPubs []chainkd.XPub, index uint64) string {
263 for _, xPub := range xPubs {
264 tmp = append(tmp, xPub[:6]...)
266 return fmt.Sprintf("%x:%x", tmp, index)
269 func (m *recoveryManager) extendScanAccounts() error {
270 if m.state.XPubsStatus == nil {
274 curHorizon, delta := m.state.XPubsStatus.extendHorizon()
275 for index := curHorizon; index < curHorizon+delta; index++ {
276 alias := genAcctAlias(m.state.XPubs, index)
277 account, err := account.CreateAccount(m.state.XPubs, len(m.state.XPubs), alias, index, signers.BIP0044)
282 m.state.stateForScope(account)
283 //generate resurrect address for new account.
284 if err := m.extendScanAddresses(account.ID, true); err != nil {
288 if err := m.extendScanAddresses(account.ID, false); err != nil {
296 func getCPHash(cp []byte) bc.Hash {
298 sha3pool.Sum256(h[:], cp)
302 func (m *recoveryManager) extendAddress(acct *account.Account, index uint64, change bool) error {
303 cp, err := account.CreateCtrlProgram(acct, index, change)
308 m.addresses[getCPHash(cp.ControlProgram)] = cp
312 func (m *recoveryManager) extendScanAddresses(accountID string, change bool) error {
313 state, ok := m.state.AccountsStatus[accountID]
315 return ErrInvalidAcctID
318 var curHorizon, delta uint64
320 curHorizon, delta = state.InternalBranch.extendHorizon()
322 curHorizon, delta = state.ExternalBranch.extendHorizon()
324 for index := curHorizon; index < curHorizon+delta; index++ {
325 if err := m.extendAddress(state.Account, index, change); err != nil {
332 func (m *recoveryManager) processBlock(b *types.Block) error {
333 for _, tx := range b.Transactions {
334 for _, output := range tx.Outputs {
335 if cp, ok := m.addresses[getCPHash(output.ControlProgram)]; ok {
336 status, ok := m.state.AccountsStatus[cp.AccountID]
338 return ErrInvalidAcctID
341 if err := m.reportFound(status.Account, cp); err != nil {
351 // FilterRecoveryTxs Filter transactions that meet the recovery address
352 func (m *recoveryManager) FilterRecoveryTxs(b *types.Block) error {
359 if b.Time().After(m.state.StartTime) {
363 return m.processBlock(b)
366 func (m *recoveryManager) finished() {
367 m.db.Delete(recoveryKey)
369 m.addresses = make(map[bc.Hash]*account.CtrlProgram)
370 m.state = newRecoveryState()
374 func (m *recoveryManager) LoadStatusInfo() error {
378 rawStatus := m.db.Get(recoveryKey)
379 if rawStatus == nil {
383 if err := json.Unmarshal(rawStatus, m.state); err != nil {
387 if m.state.XPubs != nil && !m.tryStartXPubsRec() {
388 return ErrRecoveryBusy
391 if err := m.restoreAddresses(); err != nil {
400 // restoreAddresses resume addresses for unfinished tasks
401 func (m *recoveryManager) restoreAddresses() error {
402 for _, state := range m.state.AccountsStatus {
403 for index := uint64(1); index <= state.InternalBranch.Horizon; index++ {
404 if err := m.extendAddress(state.Account, index, true); err != nil {
409 for index := uint64(1); index <= state.ExternalBranch.Horizon; index++ {
410 if err := m.extendAddress(state.Account, index, false); err != nil {
418 // reportFound found your own address operation.
419 func (m *recoveryManager) reportFound(account *account.Account, cp *account.CtrlProgram) error {
420 if m.state.XPubsStatus != nil && reflect.DeepEqual(m.state.XPubs, account.XPubs) {
421 //recovery from XPubs need save account to db.
422 if err := m.saveAccount(account); err != nil {
426 m.state.XPubsStatus.reportFound(account.KeyIndex)
427 if err := m.extendScanAccounts(); err != nil {
433 m.state.AccountsStatus[account.ID].InternalBranch.reportFound(cp.KeyIndex)
435 m.state.AccountsStatus[account.ID].ExternalBranch.reportFound(cp.KeyIndex)
438 if err := m.extendScanAddresses(account.ID, cp.Change); err != nil {
442 if err := m.accountMgr.CreateBatchAddresses(account.ID, cp.Change, cp.KeyIndex); err != nil {
446 return m.commitStatusInfo()
449 func (m *recoveryManager) saveAccount(acct *account.Account) error {
450 tmp, err := m.accountMgr.FindByID(acct.ID)
451 if err != nil && errors.Root(err) != account.ErrFindAccount {
458 return m.accountMgr.SaveAccount(acct)
461 //tryStartXPubsRec guarantee that only one xPubs recovery is in progress.
462 func (m *recoveryManager) tryStartXPubsRec() bool {
463 return atomic.CompareAndSwapInt32(&m.locked, 0, 1)
466 //stopXPubsRec release xPubs recovery lock.
467 func (m *recoveryManager) stopXPubsRec() {
469 atomic.StoreInt32(&m.locked, 0)