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  }