10 "github.com/vapor/account"
11 "github.com/vapor/blockchain/signers"
12 "github.com/vapor/crypto/ed25519/chainkd"
13 "github.com/vapor/crypto/sha3pool"
14 "github.com/vapor/errors"
15 "github.com/vapor/protocol/bc"
16 "github.com/vapor/protocol/bc/types"
20 // acctRecoveryWindow defines the account derivation lookahead used when
21 // attempting to recover the set of used accounts.
22 acctRecoveryWindow = uint64(6)
24 // addrRecoveryWindow defines the address derivation lookahead used when
25 // attempting to recover the set of used addresses.
26 addrRecoveryWindow = uint64(128)
30 // ErrRecoveryBusy another recovery in progress, can not get recovery manager lock
31 ErrRecoveryBusy = errors.New("another recovery in progress")
33 // ErrInvalidAcctID can not find account by account id
34 ErrInvalidAcctID = errors.New("invalid account id")
35 ErrGetRecoveryStatus = errors.New("failed to get recovery status.")
36 ErrRecoveryStatus = errors.New("recovery status is nil.")
39 // branchRecoveryState maintains the required state in-order to properly
40 // recover addresses derived from a particular account's internal or external
43 // A branch recovery state supports operations for:
44 // - Expanding the look-ahead horizon based on which indexes have been found.
45 // - Registering derived addresses with indexes within the horizon.
46 // - Reporting an invalid child index that falls into the horizon.
47 // - Reporting that an address has been found.
48 // - Retrieving all currently derived addresses for the branch.
49 // - Looking up a particular address by its child index.
50 type branchRecoveryState struct {
51 // recoveryWindow defines the key-derivation lookahead used when
52 // attempting to recover the set of addresses on this branch.
55 // horizion records the highest child index watched by this branch.
58 // nextUnfound maintains the child index of the successor to the highest
59 // index that has been found during recovery of this branch.
63 // newBranchRecoveryState creates a new branchRecoveryState that can be used to
64 // track either the external or internal branch of an account's derivation path.
65 func newBranchRecoveryState(recoveryWindow uint64) *branchRecoveryState {
66 return &branchRecoveryState{
67 RecoveryWindow: recoveryWindow,
73 // extendHorizon returns the current horizon and the number of addresses that
74 // must be derived in order to maintain the desired recovery window.
75 func (brs *branchRecoveryState) extendHorizon() (uint64, uint64) {
76 // Compute the new horizon, which should surpass our last found address
77 // by the recovery window.
78 curHorizon := brs.Horizon
80 minValidHorizon := brs.NextUnfound + brs.RecoveryWindow
82 // If the current horizon is sufficient, we will not have to derive any
84 if curHorizon >= minValidHorizon {
88 // Otherwise, the number of addresses we should derive corresponds to
89 // the delta of the two horizons, and we update our new horizon.
90 delta := minValidHorizon - curHorizon
91 brs.Horizon = minValidHorizon
93 return curHorizon, delta
96 // reportFound updates the last found index if the reported index exceeds the
98 func (brs *branchRecoveryState) reportFound(index uint64) {
99 if index >= brs.NextUnfound {
100 brs.NextUnfound = index + 1
104 // addressRecoveryState is used to manage the recovery of addresses generated
105 // under a particular BIP32/BIP44 account. Each account tracks both an external and
106 // internal branch recovery state, both of which use the same recovery window.
107 type addressRecoveryState struct {
108 // ExternalBranch is the recovery state of addresses generated for
109 // external use, i.e. receiving addresses.
110 ExternalBranch *branchRecoveryState
112 // InternalBranch is the recovery state of addresses generated for
113 // internal use, i.e. change addresses.
114 InternalBranch *branchRecoveryState
116 Account *account.Account
119 func newAddressRecoveryState(recoveryWindow uint64, account *account.Account) *addressRecoveryState {
120 return &addressRecoveryState{
121 ExternalBranch: newBranchRecoveryState(recoveryWindow),
122 InternalBranch: newBranchRecoveryState(recoveryWindow),
127 // RecoveryState used to record the status of a recovery process.
128 type RecoveryState struct {
129 // XPubs recovery account xPubs
132 // The time to start the recovery task, used to detemine whether
133 // recovery task is completed.
136 // XPubsStatus maintains a map of each requested XPub to its active
137 // account recovery state.
138 XPubsStatus *branchRecoveryState
140 // AcctStatus maintains a map of each requested key scope to its active
142 AccountsStatus map[string]*addressRecoveryState
145 func newRecoveryState() *RecoveryState {
146 return &RecoveryState{
147 AccountsStatus: make(map[string]*addressRecoveryState),
148 StartTime: time.Now(),
152 // stateForScope returns a ScopeRecoveryState for the provided key scope. If one
153 // does not already exist, a new one will be generated with the RecoveryState's
155 func (rs *RecoveryState) stateForScope(account *account.Account) {
156 // If the account recovery state already exists, return it.
157 if _, ok := rs.AccountsStatus[account.ID]; ok {
161 // Otherwise, initialize the recovery state for this scope with the
162 // chosen recovery window.
163 rs.AccountsStatus[account.ID] = newAddressRecoveryState(addrRecoveryWindow, account)
166 // recoveryManager manage recovery wallet from key.
167 type recoveryManager struct {
171 accountMgr *account.Manager
177 // state encapsulates and allocates the necessary recovery state for all
178 // key scopes and subsidiary derivation paths.
181 //addresses all addresses derivation lookahead used when
182 // attempting to recover the set of used addresses.
183 addresses map[bc.Hash]*account.CtrlProgram
186 // NewRecoveryManager create recovery manger.
187 func NewRecoveryManager(store WalletStore, accountMgr *account.Manager) *recoveryManager {
188 return &recoveryManager{
190 accountMgr: accountMgr,
191 addresses: make(map[bc.Hash]*account.CtrlProgram),
192 state: newRecoveryState(),
196 func (m *recoveryManager) AddrResurrect(accts []*account.Account) error {
200 for _, acct := range accts {
201 m.state.stateForScope(acct)
202 if err := m.extendScanAddresses(acct.ID, false); err != nil {
206 //Bip32 path no change field, no need to create addresses repeatedly.
207 if acct.DeriveRule == signers.BIP0032 {
210 if err := m.extendScanAddresses(acct.ID, true); err != nil {
215 m.state.StartTime = time.Now()
216 if err := m.commitStatusInfo(); err != nil {
224 func (m *recoveryManager) AcctResurrect(xPubs []chainkd.XPub) error {
228 if !m.tryStartXPubsRec() {
229 return ErrRecoveryBusy
232 m.state.XPubs = xPubs
233 m.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
235 if err := m.extendScanAccounts(); err != nil {
239 m.state.StartTime = time.Now()
240 if err := m.commitStatusInfo(); err != nil {
248 func (m *recoveryManager) commitStatusInfo() error {
249 return m.store.SetRecoveryStatus(m.state)
252 func genAcctAlias(xPubs []chainkd.XPub, index uint64) string {
254 for _, xPub := range xPubs {
255 tmp = append(tmp, xPub[:6]...)
257 return fmt.Sprintf("%x:%x", tmp, index)
260 func (m *recoveryManager) extendScanAccounts() error {
261 if m.state.XPubsStatus == nil {
265 curHorizon, delta := m.state.XPubsStatus.extendHorizon()
266 for index := curHorizon; index < curHorizon+delta; index++ {
267 alias := genAcctAlias(m.state.XPubs, index)
268 account, err := account.CreateAccount(m.state.XPubs, len(m.state.XPubs), alias, index, signers.BIP0044)
273 m.state.stateForScope(account)
274 //generate resurrect address for new account.
275 if err := m.extendScanAddresses(account.ID, true); err != nil {
279 if err := m.extendScanAddresses(account.ID, false); err != nil {
287 func getCPHash(cp []byte) bc.Hash {
289 sha3pool.Sum256(h[:], cp)
293 func (m *recoveryManager) extendAddress(acct *account.Account, index uint64, change bool) error {
294 cp, err := account.CreateCtrlProgram(acct, index, change)
299 m.addresses[getCPHash(cp.ControlProgram)] = cp
303 func (m *recoveryManager) extendScanAddresses(accountID string, change bool) error {
304 state, ok := m.state.AccountsStatus[accountID]
306 return ErrInvalidAcctID
309 var curHorizon, delta uint64
311 curHorizon, delta = state.InternalBranch.extendHorizon()
313 curHorizon, delta = state.ExternalBranch.extendHorizon()
315 for index := curHorizon; index < curHorizon+delta; index++ {
316 if err := m.extendAddress(state.Account, index, change); err != nil {
323 func (m *recoveryManager) processBlock(b *types.Block) error {
324 for _, tx := range b.Transactions {
325 for _, output := range tx.Outputs {
326 if cp, ok := m.addresses[getCPHash(output.ControlProgram())]; ok {
327 status, ok := m.state.AccountsStatus[cp.AccountID]
329 return ErrInvalidAcctID
332 if err := m.reportFound(status.Account, cp); err != nil {
342 // FilterRecoveryTxs Filter transactions that meet the recovery address
343 func (m *recoveryManager) FilterRecoveryTxs(b *types.Block) error {
350 if b.Time().After(m.state.StartTime) {
354 return m.processBlock(b)
357 func (m *recoveryManager) finished() {
358 m.store.DeleteRecoveryStatus()
360 m.addresses = make(map[bc.Hash]*account.CtrlProgram)
361 m.state = newRecoveryState()
365 func (m *recoveryManager) LoadStatusInfo() error {
370 return ErrRecoveryStatus
372 status, err := m.store.GetRecoveryStatus()
373 if err == ErrGetRecoveryStatus {
381 if m.state.XPubs != nil && !m.tryStartXPubsRec() {
382 return ErrRecoveryBusy
385 if err := m.restoreAddresses(); err != nil {
394 // restoreAddresses resume addresses for unfinished tasks
395 func (m *recoveryManager) restoreAddresses() error {
396 for _, state := range m.state.AccountsStatus {
397 for index := uint64(1); index <= state.InternalBranch.Horizon; index++ {
398 if err := m.extendAddress(state.Account, index, true); err != nil {
403 for index := uint64(1); index <= state.ExternalBranch.Horizon; index++ {
404 if err := m.extendAddress(state.Account, index, false); err != nil {
412 // reportFound found your own address operation.
413 func (m *recoveryManager) reportFound(account *account.Account, cp *account.CtrlProgram) error {
414 if m.state.XPubsStatus != nil && reflect.DeepEqual(m.state.XPubs, account.XPubs) {
415 //recovery from XPubs need save account to db.
416 if err := m.saveAccount(account); err != nil {
420 m.state.XPubsStatus.reportFound(account.KeyIndex)
421 if err := m.extendScanAccounts(); err != nil {
427 m.state.AccountsStatus[account.ID].InternalBranch.reportFound(cp.KeyIndex)
429 m.state.AccountsStatus[account.ID].ExternalBranch.reportFound(cp.KeyIndex)
432 if err := m.extendScanAddresses(account.ID, cp.Change); err != nil {
436 if err := m.accountMgr.CreateBatchAddresses(account.ID, cp.Change, cp.KeyIndex); err != nil {
440 return m.commitStatusInfo()
443 func (m *recoveryManager) saveAccount(acct *account.Account) error {
444 tmp, err := m.accountMgr.FindByID(acct.ID)
445 if err != nil && errors.Root(err) != account.ErrFindAccount {
452 return m.accountMgr.SaveAccount(acct)
455 //tryStartXPubsRec guarantee that only one xPubs recovery is in progress.
456 func (m *recoveryManager) tryStartXPubsRec() bool {
457 return atomic.CompareAndSwapInt32(&m.locked, 0, 1)
460 //stopXPubsRec release xPubs recovery lock.
461 func (m *recoveryManager) stopXPubsRec() {
463 atomic.StoreInt32(&m.locked, 0)