github.com/bytom/bytom@v1.1.2-0.20221014091027-bbcba3df6075/wallet/recovery.go (about) 1 package wallet 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "reflect" 7 "sync" 8 "sync/atomic" 9 "time" 10 11 "github.com/bytom/bytom/account" 12 "github.com/bytom/bytom/blockchain/signers" 13 "github.com/bytom/bytom/crypto/ed25519/chainkd" 14 "github.com/bytom/bytom/crypto/sha3pool" 15 "github.com/bytom/bytom/errors" 16 "github.com/bytom/bytom/protocol/bc" 17 "github.com/bytom/bytom/protocol/bc/types" 18 dbm "github.com/bytom/bytom/database/leveldb" 19 ) 20 21 const ( 22 // acctRecoveryWindow defines the account derivation lookahead used when 23 // attempting to recover the set of used accounts. 24 acctRecoveryWindow = uint64(6) 25 26 // addrRecoveryWindow defines the address derivation lookahead used when 27 // attempting to recover the set of used addresses. 28 addrRecoveryWindow = uint64(128) 29 ) 30 31 //recoveryKey key for db store recovery info. 32 var ( 33 recoveryKey = []byte("RecoveryInfo") 34 35 // ErrRecoveryBusy another recovery in progress, can not get recovery manager lock 36 ErrRecoveryBusy = errors.New("another recovery in progress") 37 38 // ErrInvalidAcctID can not find account by account id 39 ErrInvalidAcctID = errors.New("invalid account id") 40 ) 41 42 // branchRecoveryState maintains the required state in-order to properly 43 // recover addresses derived from a particular account's internal or external 44 // derivation branch. 45 // 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. 56 RecoveryWindow uint64 57 58 // horizion records the highest child index watched by this branch. 59 Horizon uint64 60 61 // nextUnfound maintains the child index of the successor to the highest 62 // index that has been found during recovery of this branch. 63 NextUnfound uint64 64 } 65 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, 71 Horizon: 1, 72 NextUnfound: 1, 73 } 74 } 75 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 82 83 minValidHorizon := brs.NextUnfound + brs.RecoveryWindow 84 85 // If the current horizon is sufficient, we will not have to derive any 86 // new keys. 87 if curHorizon >= minValidHorizon { 88 return curHorizon, 0 89 } 90 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 95 96 return curHorizon, delta 97 } 98 99 // reportFound updates the last found index if the reported index exceeds the 100 // current value. 101 func (brs *branchRecoveryState) reportFound(index uint64) { 102 if index >= brs.NextUnfound { 103 brs.NextUnfound = index + 1 104 } 105 } 106 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 114 115 // InternalBranch is the recovery state of addresses generated for 116 // internal use, i.e. change addresses. 117 InternalBranch *branchRecoveryState 118 119 Account *account.Account 120 } 121 122 func newAddressRecoveryState(recoveryWindow uint64, account *account.Account) *addressRecoveryState { 123 return &addressRecoveryState{ 124 ExternalBranch: newBranchRecoveryState(recoveryWindow), 125 InternalBranch: newBranchRecoveryState(recoveryWindow), 126 Account: account, 127 } 128 } 129 130 // recoveryState used to record the status of a recovery process. 131 type recoveryState struct { 132 // XPubs recovery account xPubs 133 XPubs []chainkd.XPub 134 135 // The time to start the recovery task, used to detemine whether 136 // recovery task is completed. 137 StartTime time.Time 138 139 // XPubsStatus maintains a map of each requested XPub to its active 140 // account recovery state. 141 XPubsStatus *branchRecoveryState 142 143 // AcctStatus maintains a map of each requested key scope to its active 144 // recovery state. 145 AccountsStatus map[string]*addressRecoveryState 146 } 147 148 func newRecoveryState() *recoveryState { 149 return &recoveryState{ 150 AccountsStatus: make(map[string]*addressRecoveryState), 151 StartTime: time.Now(), 152 } 153 } 154 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 157 // recoveryWindow. 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 { 161 return 162 } 163 164 // Otherwise, initialize the recovery state for this scope with the 165 // chosen recovery window. 166 rs.AccountsStatus[account.ID] = newAddressRecoveryState(addrRecoveryWindow, account) 167 } 168 169 // recoveryManager manage recovery wallet from key. 170 type recoveryManager struct { 171 mu sync.Mutex 172 173 db dbm.DB 174 accountMgr *account.Manager 175 176 locked int32 177 178 started bool 179 180 // state encapsulates and allocates the necessary recovery state for all 181 // key scopes and subsidiary derivation paths. 182 state *recoveryState 183 184 //addresses all addresses derivation lookahead used when 185 // attempting to recover the set of used addresses. 186 addresses map[bc.Hash]*account.CtrlProgram 187 } 188 189 // newRecoveryManager create recovery manger. 190 func newRecoveryManager(db dbm.DB, accountMgr *account.Manager) *recoveryManager { 191 return &recoveryManager{ 192 db: db, 193 accountMgr: accountMgr, 194 addresses: make(map[bc.Hash]*account.CtrlProgram), 195 state: newRecoveryState(), 196 } 197 } 198 199 func (m *recoveryManager) AddrResurrect(accts []*account.Account) error { 200 m.mu.Lock() 201 defer m.mu.Unlock() 202 203 for _, acct := range accts { 204 m.state.stateForScope(acct) 205 if err := m.extendScanAddresses(acct.ID, false); err != nil { 206 return err 207 } 208 209 //Bip32 path no change field, no need to create addresses repeatedly. 210 if acct.DeriveRule == signers.BIP0032 { 211 continue 212 } 213 if err := m.extendScanAddresses(acct.ID, true); err != nil { 214 return err 215 } 216 } 217 218 m.state.StartTime = time.Now() 219 if err := m.commitStatusInfo(); err != nil { 220 return err 221 } 222 223 m.started = true 224 return nil 225 } 226 227 func (m *recoveryManager) AcctResurrect(xPubs []chainkd.XPub) error { 228 m.mu.Lock() 229 defer m.mu.Unlock() 230 231 if !m.tryStartXPubsRec() { 232 return ErrRecoveryBusy 233 } 234 235 m.state.XPubs = xPubs 236 m.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow) 237 238 if err := m.extendScanAccounts(); err != nil { 239 m.stopXPubsRec() 240 return err 241 } 242 m.state.StartTime = time.Now() 243 if err := m.commitStatusInfo(); err != nil { 244 return err 245 } 246 247 m.started = true 248 return nil 249 } 250 251 func (m *recoveryManager) commitStatusInfo() error { 252 rawStatus, err := json.Marshal(m.state) 253 if err != nil { 254 return err 255 } 256 257 m.db.Set(recoveryKey, rawStatus) 258 return nil 259 } 260 261 func genAcctAlias(xPubs []chainkd.XPub, index uint64) string { 262 var tmp []byte 263 for _, xPub := range xPubs { 264 tmp = append(tmp, xPub[:6]...) 265 } 266 return fmt.Sprintf("%x:%x", tmp, index) 267 } 268 269 func (m *recoveryManager) extendScanAccounts() error { 270 if m.state.XPubsStatus == nil { 271 return nil 272 } 273 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) 278 if err != nil { 279 return err 280 } 281 282 m.state.stateForScope(account) 283 //generate resurrect address for new account. 284 if err := m.extendScanAddresses(account.ID, true); err != nil { 285 return err 286 } 287 288 if err := m.extendScanAddresses(account.ID, false); err != nil { 289 return err 290 } 291 } 292 293 return nil 294 } 295 296 func getCPHash(cp []byte) bc.Hash { 297 var h [32]byte 298 sha3pool.Sum256(h[:], cp) 299 return bc.NewHash(h) 300 } 301 302 func (m *recoveryManager) extendAddress(acct *account.Account, index uint64, change bool) error { 303 cp, err := account.CreateCtrlProgram(acct, index, change) 304 if err != nil { 305 return err 306 } 307 308 m.addresses[getCPHash(cp.ControlProgram)] = cp 309 return nil 310 } 311 312 func (m *recoveryManager) extendScanAddresses(accountID string, change bool) error { 313 state, ok := m.state.AccountsStatus[accountID] 314 if !ok { 315 return ErrInvalidAcctID 316 } 317 318 var curHorizon, delta uint64 319 if change { 320 curHorizon, delta = state.InternalBranch.extendHorizon() 321 } else { 322 curHorizon, delta = state.ExternalBranch.extendHorizon() 323 } 324 for index := curHorizon; index < curHorizon+delta; index++ { 325 if err := m.extendAddress(state.Account, index, change); err != nil { 326 return err 327 } 328 } 329 return nil 330 } 331 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] 337 if !ok { 338 return ErrInvalidAcctID 339 } 340 341 if err := m.reportFound(status.Account, cp); err != nil { 342 return err 343 } 344 } 345 } 346 } 347 348 return nil 349 } 350 351 // FilterRecoveryTxs Filter transactions that meet the recovery address 352 func (m *recoveryManager) FilterRecoveryTxs(b *types.Block) error { 353 m.mu.Lock() 354 defer m.mu.Unlock() 355 356 if !m.started { 357 return nil 358 } 359 if b.Time().After(m.state.StartTime) { 360 m.finished() 361 return nil 362 } 363 return m.processBlock(b) 364 } 365 366 func (m *recoveryManager) finished() { 367 m.db.Delete(recoveryKey) 368 m.started = false 369 m.addresses = make(map[bc.Hash]*account.CtrlProgram) 370 m.state = newRecoveryState() 371 m.stopXPubsRec() 372 } 373 374 func (m *recoveryManager) LoadStatusInfo() error { 375 m.mu.Lock() 376 defer m.mu.Unlock() 377 378 rawStatus := m.db.Get(recoveryKey) 379 if rawStatus == nil { 380 return nil 381 } 382 383 if err := json.Unmarshal(rawStatus, m.state); err != nil { 384 return err 385 } 386 387 if m.state.XPubs != nil && !m.tryStartXPubsRec() { 388 return ErrRecoveryBusy 389 } 390 391 if err := m.restoreAddresses(); err != nil { 392 m.stopXPubsRec() 393 return err 394 } 395 396 m.started = true 397 return nil 398 } 399 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 { 405 return err 406 } 407 } 408 409 for index := uint64(1); index <= state.ExternalBranch.Horizon; index++ { 410 if err := m.extendAddress(state.Account, index, false); err != nil { 411 return err 412 } 413 } 414 } 415 return nil 416 } 417 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 { 423 return err 424 } 425 426 m.state.XPubsStatus.reportFound(account.KeyIndex) 427 if err := m.extendScanAccounts(); err != nil { 428 return err 429 } 430 } 431 432 if cp.Change { 433 m.state.AccountsStatus[account.ID].InternalBranch.reportFound(cp.KeyIndex) 434 } else { 435 m.state.AccountsStatus[account.ID].ExternalBranch.reportFound(cp.KeyIndex) 436 } 437 438 if err := m.extendScanAddresses(account.ID, cp.Change); err != nil { 439 return err 440 } 441 442 if err := m.accountMgr.CreateBatchAddresses(account.ID, cp.Change, cp.KeyIndex); err != nil { 443 return err 444 } 445 446 return m.commitStatusInfo() 447 } 448 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 { 452 return err 453 } 454 455 if tmp != nil { 456 return nil 457 } 458 return m.accountMgr.SaveAccount(acct) 459 } 460 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) 464 } 465 466 //stopXPubsRec release xPubs recovery lock. 467 func (m *recoveryManager) stopXPubsRec() { 468 m.state.XPubs = nil 469 atomic.StoreInt32(&m.locked, 0) 470 }