gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/seed.go (about)

     1  package wallet
     2  
     3  import (
     4  	"runtime"
     5  	"sync"
     6  
     7  	bolt "github.com/coreos/bbolt"
     8  	"gitlab.com/NebulousLabs/errors"
     9  	"gitlab.com/NebulousLabs/fastrand"
    10  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    11  	"gitlab.com/SiaPrime/SiaPrime/encoding"
    12  	"gitlab.com/SiaPrime/SiaPrime/modules"
    13  	"gitlab.com/SiaPrime/SiaPrime/types"
    14  )
    15  
    16  var (
    17  	errKnownSeed = errors.New("seed is already known")
    18  )
    19  
    20  type (
    21  	// uniqueID is a unique id randomly generated and put at the front of every
    22  	// persistence object. It is used to make sure that a different encryption
    23  	// key can be used for every persistence object.
    24  	uniqueID [crypto.EntropySize]byte
    25  
    26  	// seedFile stores an encrypted wallet seed on disk.
    27  	seedFile struct {
    28  		UID                    uniqueID
    29  		EncryptionVerification crypto.Ciphertext
    30  		Seed                   crypto.Ciphertext
    31  	}
    32  )
    33  
    34  // generateSpendableKey creates the keys and unlock conditions for seed at a
    35  // given index.
    36  func generateSpendableKey(seed modules.Seed, index uint64) spendableKey {
    37  	sk, pk := crypto.GenerateKeyPairDeterministic(crypto.HashAll(seed, index))
    38  	return spendableKey{
    39  		UnlockConditions: types.UnlockConditions{
    40  			PublicKeys:         []types.SiaPublicKey{types.Ed25519PublicKey(pk)},
    41  			SignaturesRequired: 1,
    42  		},
    43  		SecretKeys: []crypto.SecretKey{sk},
    44  	}
    45  }
    46  
    47  // generateKeys generates n keys from seed, starting from index start.
    48  func generateKeys(seed modules.Seed, start, n uint64) []spendableKey {
    49  	// generate in parallel, one goroutine per core.
    50  	keys := make([]spendableKey, n)
    51  	var wg sync.WaitGroup
    52  	wg.Add(runtime.NumCPU())
    53  	for cpu := 0; cpu < runtime.NumCPU(); cpu++ {
    54  		go func(offset uint64) {
    55  			defer wg.Done()
    56  			for i := offset; i < n; i += uint64(runtime.NumCPU()) {
    57  				// NOTE: don't bother trying to optimize generateSpendableKey;
    58  				// profiling shows that ed25519 key generation consumes far
    59  				// more CPU time than encoding or hashing.
    60  				keys[i] = generateSpendableKey(seed, start+i)
    61  			}
    62  		}(uint64(cpu))
    63  	}
    64  	wg.Wait()
    65  	return keys
    66  }
    67  
    68  // createSeedFile creates and encrypts a seedFile.
    69  func createSeedFile(masterKey crypto.CipherKey, seed modules.Seed) seedFile {
    70  	var sf seedFile
    71  	fastrand.Read(sf.UID[:])
    72  	sek := uidEncryptionKey(masterKey, sf.UID)
    73  	sf.EncryptionVerification = sek.EncryptBytes(verificationPlaintext)
    74  	sf.Seed = sek.EncryptBytes(seed[:])
    75  	return sf
    76  }
    77  
    78  // decryptSeedFile decrypts a seed file using the encryption key.
    79  func decryptSeedFile(masterKey crypto.CipherKey, sf seedFile) (seed modules.Seed, err error) {
    80  	// Verify that the provided master key is the correct key.
    81  	decryptionKey := uidEncryptionKey(masterKey, sf.UID)
    82  	err = verifyEncryption(decryptionKey, sf.EncryptionVerification)
    83  	if err != nil {
    84  		return modules.Seed{}, err
    85  	}
    86  
    87  	// Decrypt and return the seed.
    88  	plainSeed, err := decryptionKey.DecryptBytes(sf.Seed)
    89  	if err != nil {
    90  		return modules.Seed{}, err
    91  	}
    92  	copy(seed[:], plainSeed)
    93  	return seed, nil
    94  }
    95  
    96  // regenerateLookahead creates future keys up to a maximum of maxKeys keys
    97  func (w *Wallet) regenerateLookahead(start uint64) {
    98  	// Check how many keys need to be generated
    99  	maxKeys := maxLookahead(start)
   100  	existingKeys := uint64(len(w.lookahead))
   101  
   102  	for i, k := range generateKeys(w.primarySeed, start+existingKeys, maxKeys-existingKeys) {
   103  		w.lookahead[k.UnlockConditions.UnlockHash()] = start + existingKeys + uint64(i)
   104  	}
   105  }
   106  
   107  // integrateSeed generates n spendableKeys from the seed and loads them into
   108  // the wallet.
   109  func (w *Wallet) integrateSeed(seed modules.Seed, n uint64) {
   110  	for _, sk := range generateKeys(seed, 0, n) {
   111  		w.keys[sk.UnlockConditions.UnlockHash()] = sk
   112  	}
   113  }
   114  
   115  // nextPrimarySeedAddress fetches the next n addresses from the primary seed.
   116  func (w *Wallet) nextPrimarySeedAddresses(tx *bolt.Tx, n uint64) ([]types.UnlockConditions, error) {
   117  	// Check that the wallet has been unlocked.
   118  	if !w.unlocked {
   119  		return []types.UnlockConditions{}, modules.ErrLockedWallet
   120  	}
   121  
   122  	// Fetch and increment the seed progress.
   123  	progress, err := dbGetPrimarySeedProgress(tx)
   124  	if err != nil {
   125  		return []types.UnlockConditions{}, err
   126  	}
   127  	if err = dbPutPrimarySeedProgress(tx, progress+n); err != nil {
   128  		return []types.UnlockConditions{}, err
   129  	}
   130  	// Integrate the next keys into the wallet, and return the unlock
   131  	// conditions. Also remove new keys from the future keys and update them
   132  	// according to new progress
   133  	spendableKeys := generateKeys(w.primarySeed, progress, n)
   134  	ucs := make([]types.UnlockConditions, 0, len(spendableKeys))
   135  	for _, spendableKey := range spendableKeys {
   136  		w.keys[spendableKey.UnlockConditions.UnlockHash()] = spendableKey
   137  		delete(w.lookahead, spendableKey.UnlockConditions.UnlockHash())
   138  		ucs = append(ucs, spendableKey.UnlockConditions)
   139  	}
   140  	w.regenerateLookahead(progress + n)
   141  
   142  	return ucs, nil
   143  }
   144  
   145  // nextPrimarySeedAddress fetches the next address from the primary seed.
   146  func (w *Wallet) nextPrimarySeedAddress(tx *bolt.Tx) (types.UnlockConditions, error) {
   147  	ucs, err := w.nextPrimarySeedAddresses(tx, 1)
   148  	if err != nil {
   149  		return types.UnlockConditions{}, err
   150  	}
   151  	return ucs[0], nil
   152  }
   153  
   154  // AllSeeds returns a list of all seeds known to and used by the wallet.
   155  func (w *Wallet) AllSeeds() ([]modules.Seed, error) {
   156  	w.mu.Lock()
   157  	defer w.mu.Unlock()
   158  	if !w.unlocked {
   159  		return nil, modules.ErrLockedWallet
   160  	}
   161  	return append([]modules.Seed{w.primarySeed}, w.seeds...), nil
   162  }
   163  
   164  // PrimarySeed returns the decrypted primary seed of the wallet, as well as
   165  // the number of addresses that the seed can be safely used to generate.
   166  func (w *Wallet) PrimarySeed() (modules.Seed, uint64, error) {
   167  	w.mu.Lock()
   168  	defer w.mu.Unlock()
   169  	if !w.unlocked {
   170  		return modules.Seed{}, 0, modules.ErrLockedWallet
   171  	}
   172  	progress, err := dbGetPrimarySeedProgress(w.dbTx)
   173  	if err != nil {
   174  		return modules.Seed{}, 0, err
   175  	}
   176  
   177  	// addresses remaining is maxScanKeys-progress; generating more keys than
   178  	// that risks not being able to recover them when using SweepSeed or
   179  	// InitFromSeed.
   180  	remaining := maxScanKeys - progress
   181  	if progress > maxScanKeys {
   182  		remaining = 0
   183  	}
   184  	return w.primarySeed, remaining, nil
   185  }
   186  
   187  // NextAddresses returns n unlock hashes that are ready to receive siacoins or
   188  // siafunds. The addresses are generated using the primary address seed.
   189  //
   190  // Warning: If this function is used to generate large numbers of addresses,
   191  // those addresses should be used. Otherwise the lookahead might not be able to
   192  // keep up and multiple wallets with the same seed might desync.
   193  func (w *Wallet) NextAddresses(n uint64) ([]types.UnlockConditions, error) {
   194  	if err := w.tg.Add(); err != nil {
   195  		return []types.UnlockConditions{}, err
   196  	}
   197  	defer w.tg.Done()
   198  
   199  	// TODO: going to the db is slow; consider creating 100 addresses at a
   200  	// time.
   201  	w.mu.Lock()
   202  	ucs, err := w.nextPrimarySeedAddresses(w.dbTx, n)
   203  	err = errors.Compose(err, w.syncDB())
   204  	w.mu.Unlock()
   205  	if err != nil {
   206  		return []types.UnlockConditions{}, err
   207  	}
   208  
   209  	return ucs, err
   210  }
   211  
   212  // NextAddress returns an unlock hash that is ready to receive siacoins or
   213  // siafunds. The address is generated using the primary address seed.
   214  func (w *Wallet) NextAddress() (types.UnlockConditions, error) {
   215  	ucs, err := w.NextAddresses(1)
   216  	if err != nil {
   217  		return types.UnlockConditions{}, err
   218  	}
   219  	return ucs[0], nil
   220  }
   221  
   222  // LoadSeed will track all of the addresses generated by the input seed,
   223  // reclaiming any funds that were lost due to a deleted file or lost encryption
   224  // key. An error will be returned if the seed has already been integrated with
   225  // the wallet.
   226  func (w *Wallet) LoadSeed(masterKey crypto.CipherKey, seed modules.Seed) error {
   227  	if err := w.tg.Add(); err != nil {
   228  		return err
   229  	}
   230  	defer w.tg.Done()
   231  
   232  	if !w.cs.Synced() {
   233  		return errors.New("cannot load seed until blockchain is synced")
   234  	}
   235  
   236  	if !w.scanLock.TryLock() {
   237  		return errScanInProgress
   238  	}
   239  	defer w.scanLock.Unlock()
   240  
   241  	// Because the recovery seed does not have a UID, duplication must be
   242  	// prevented by comparing with the list of decrypted seeds. This can only
   243  	// occur while the wallet is unlocked.
   244  	w.mu.RLock()
   245  	if !w.unlocked {
   246  		w.mu.RUnlock()
   247  		return modules.ErrLockedWallet
   248  	}
   249  	for _, wSeed := range append([]modules.Seed{w.primarySeed}, w.seeds...) {
   250  		if seed == wSeed {
   251  			w.mu.RUnlock()
   252  			return errKnownSeed
   253  		}
   254  	}
   255  	w.mu.RUnlock()
   256  
   257  	// scan blockchain to determine how many keys to generate for the seed
   258  	s := newSeedScanner(seed, w.log)
   259  	if err := s.scan(w.cs, w.tg.StopChan()); err != nil {
   260  		return err
   261  	}
   262  	// Add 4% as a buffer because the seed may have addresses in the wild
   263  	// that have not appeared in the blockchain yet.
   264  	seedProgress := s.largestIndexSeen + 500
   265  	seedProgress += seedProgress / 25
   266  	w.log.Printf("INFO: found key index %v in blockchain. Setting auxiliary seed progress to %v", s.largestIndexSeen, seedProgress)
   267  
   268  	err := func() error {
   269  		w.mu.Lock()
   270  		defer w.mu.Unlock()
   271  
   272  		err := checkMasterKey(w.dbTx, masterKey)
   273  		if err != nil {
   274  			return err
   275  		}
   276  
   277  		// create a seedFile for the seed
   278  		sf := createSeedFile(masterKey, seed)
   279  
   280  		// add the seedFile
   281  		var current []seedFile
   282  		err = encoding.Unmarshal(w.dbTx.Bucket(bucketWallet).Get(keyAuxiliarySeedFiles), &current)
   283  		if err != nil {
   284  			return err
   285  		}
   286  		err = w.dbTx.Bucket(bucketWallet).Put(keyAuxiliarySeedFiles, encoding.Marshal(append(current, sf)))
   287  		if err != nil {
   288  			return err
   289  		}
   290  
   291  		// load the seed's keys
   292  		w.integrateSeed(seed, seedProgress)
   293  		w.seeds = append(w.seeds, seed)
   294  
   295  		// delete the set of processed transactions; they will be recreated
   296  		// when we rescan
   297  		if err = w.dbTx.DeleteBucket(bucketProcessedTransactions); err != nil {
   298  			return err
   299  		}
   300  		if _, err = w.dbTx.CreateBucket(bucketProcessedTransactions); err != nil {
   301  			return err
   302  		}
   303  		w.unconfirmedProcessedTransactions = nil
   304  
   305  		// reset the consensus change ID and height in preparation for rescan
   306  		err = dbPutConsensusChangeID(w.dbTx, modules.ConsensusChangeBeginning)
   307  		if err != nil {
   308  			return err
   309  		}
   310  		return dbPutConsensusHeight(w.dbTx, 0)
   311  	}()
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	// rescan the blockchain
   317  	w.cs.Unsubscribe(w)
   318  	w.tpool.Unsubscribe(w)
   319  
   320  	done := make(chan struct{})
   321  	go w.rescanMessage(done)
   322  	defer close(done)
   323  
   324  	err = w.cs.ConsensusSetSubscribe(w, modules.ConsensusChangeBeginning, w.tg.StopChan())
   325  	if err != nil {
   326  		return err
   327  	}
   328  	w.tpool.TransactionPoolSubscribe(w)
   329  	return nil
   330  }
   331  
   332  // SweepSeed scans the blockchain for outputs generated from seed and creates
   333  // a transaction that transfers them to the wallet. Note that this incurs a
   334  // transaction fee. It returns the total value of the outputs, minus the fee.
   335  // If only siafunds were found, the fee is deducted from the wallet.
   336  func (w *Wallet) SweepSeed(seed modules.Seed) (coins, funds types.Currency, err error) {
   337  	if err = w.tg.Add(); err != nil {
   338  		return
   339  	}
   340  	defer w.tg.Done()
   341  
   342  	if !w.scanLock.TryLock() {
   343  		return types.Currency{}, types.Currency{}, errScanInProgress
   344  	}
   345  	defer w.scanLock.Unlock()
   346  
   347  	w.mu.RLock()
   348  	match := seed == w.primarySeed
   349  	w.mu.RUnlock()
   350  	if match {
   351  		return types.Currency{}, types.Currency{}, errors.New("cannot sweep primary seed")
   352  	}
   353  
   354  	if !w.cs.Synced() {
   355  		return types.Currency{}, types.Currency{}, errors.New("cannot sweep until blockchain is synced")
   356  	}
   357  
   358  	// get an address to spend into
   359  	w.mu.Lock()
   360  	uc, err := w.nextPrimarySeedAddress(w.dbTx)
   361  	height, err2 := dbGetConsensusHeight(w.dbTx)
   362  	w.mu.Unlock()
   363  	if err != nil {
   364  		return types.Currency{}, types.Currency{}, err
   365  	}
   366  	if err2 != nil {
   367  		return types.Currency{}, types.Currency{}, err2
   368  	}
   369  
   370  	// scan blockchain for outputs, filtering out 'dust' (outputs that cost
   371  	// more in fees than they are worth)
   372  	s := newSeedScanner(seed, w.log)
   373  	_, maxFee := w.tpool.FeeEstimation()
   374  	const outputSize = 350 // approx. size in bytes of an output and accompanying signature
   375  	const maxOutputs = 50  // approx. number of outputs that a transaction can handle
   376  	s.dustThreshold = maxFee.Mul64(outputSize)
   377  	if err = s.scan(w.cs, w.tg.StopChan()); err != nil {
   378  		return
   379  	}
   380  
   381  	if len(s.siacoinOutputs) == 0 && len(s.siafundOutputs) == 0 {
   382  		// if we aren't sweeping any coins or funds, then just return an
   383  		// error; no reason to proceed
   384  		return types.Currency{}, types.Currency{}, errors.New("nothing to sweep")
   385  	}
   386  
   387  	// Flatten map to slice
   388  	var siacoinOutputs, siafundOutputs []scannedOutput
   389  	for _, sco := range s.siacoinOutputs {
   390  		siacoinOutputs = append(siacoinOutputs, sco)
   391  	}
   392  	for _, sfo := range s.siafundOutputs {
   393  		siafundOutputs = append(siafundOutputs, sfo)
   394  	}
   395  
   396  	for len(siacoinOutputs) > 0 || len(siafundOutputs) > 0 {
   397  		// process up to maxOutputs siacoinOutputs
   398  		txnSiacoinOutputs := make([]scannedOutput, maxOutputs)
   399  		n := copy(txnSiacoinOutputs, siacoinOutputs)
   400  		txnSiacoinOutputs = txnSiacoinOutputs[:n]
   401  		siacoinOutputs = siacoinOutputs[n:]
   402  
   403  		// process up to (maxOutputs-n) siafundOutputs
   404  		txnSiafundOutputs := make([]scannedOutput, maxOutputs-n)
   405  		n = copy(txnSiafundOutputs, siafundOutputs)
   406  		txnSiafundOutputs = txnSiafundOutputs[:n]
   407  		siafundOutputs = siafundOutputs[n:]
   408  
   409  		var txnCoins, txnFunds types.Currency
   410  
   411  		// construct a transaction that spends the outputs
   412  		tb, err := w.StartTransaction()
   413  		if err != nil {
   414  			return types.ZeroCurrency, types.ZeroCurrency, err
   415  		}
   416  		defer func() {
   417  			if err != nil {
   418  				tb.Drop()
   419  			}
   420  		}()
   421  		var sweptCoins, sweptFunds types.Currency // total values of swept outputs
   422  		for _, output := range txnSiacoinOutputs {
   423  			// construct a siacoin input that spends the output
   424  			sk := generateSpendableKey(seed, output.seedIndex)
   425  			tb.AddSiacoinInput(types.SiacoinInput{
   426  				ParentID:         types.SiacoinOutputID(output.id),
   427  				UnlockConditions: sk.UnlockConditions,
   428  			})
   429  			// add a signature for the input
   430  			sweptCoins = sweptCoins.Add(output.value)
   431  		}
   432  		for _, output := range txnSiafundOutputs {
   433  			// construct a siafund input that spends the output
   434  			sk := generateSpendableKey(seed, output.seedIndex)
   435  			tb.AddSiafundInput(types.SiafundInput{
   436  				ParentID:         types.SiafundOutputID(output.id),
   437  				UnlockConditions: sk.UnlockConditions,
   438  			})
   439  			// add a signature for the input
   440  			sweptFunds = sweptFunds.Add(output.value)
   441  		}
   442  
   443  		// estimate the transaction size and fee. NOTE: this equation doesn't
   444  		// account for other fields in the transaction, but since we are
   445  		// multiplying by maxFee, lowballing is ok
   446  		estTxnSize := (len(txnSiacoinOutputs) + len(txnSiafundOutputs)) * outputSize
   447  		estFee := maxFee.Mul64(uint64(estTxnSize))
   448  		tb.AddMinerFee(estFee)
   449  
   450  		// calculate total siacoin payout
   451  		if sweptCoins.Cmp(estFee) > 0 {
   452  			txnCoins = sweptCoins.Sub(estFee)
   453  		}
   454  		txnFunds = sweptFunds
   455  
   456  		switch {
   457  		case txnCoins.IsZero() && txnFunds.IsZero():
   458  			// if we aren't sweeping any coins or funds, then just return an
   459  			// error; no reason to proceed
   460  			return types.Currency{}, types.Currency{}, errors.New("transaction fee exceeds value of swept outputs")
   461  
   462  		case !txnCoins.IsZero() && txnFunds.IsZero():
   463  			// if we're sweeping coins but not funds, add a siacoin output for
   464  			// them
   465  			tb.AddSiacoinOutput(types.SiacoinOutput{
   466  				Value:      txnCoins,
   467  				UnlockHash: uc.UnlockHash(),
   468  			})
   469  
   470  		case txnCoins.IsZero() && !txnFunds.IsZero():
   471  			// if we're sweeping funds but not coins, add a siafund output for
   472  			// them. This is tricky because we still need to pay for the
   473  			// transaction fee, but we can't simply subtract the fee from the
   474  			// output value like we can with swept coins. Instead, we need to fund
   475  			// the fee using the existing wallet balance.
   476  			tb.AddSiafundOutput(types.SiafundOutput{
   477  				Value:      txnFunds,
   478  				UnlockHash: uc.UnlockHash(),
   479  			})
   480  			err = tb.FundSiacoins(estFee)
   481  			if err != nil {
   482  				return types.Currency{}, types.Currency{}, errors.New("couldn't pay transaction fee on swept funds: " + err.Error())
   483  			}
   484  
   485  		case !txnCoins.IsZero() && !txnFunds.IsZero():
   486  			// if we're sweeping both coins and funds, add a siacoin output and a
   487  			// siafund output
   488  			tb.AddSiacoinOutput(types.SiacoinOutput{
   489  				Value:      txnCoins,
   490  				UnlockHash: uc.UnlockHash(),
   491  			})
   492  			tb.AddSiafundOutput(types.SiafundOutput{
   493  				Value:      txnFunds,
   494  				UnlockHash: uc.UnlockHash(),
   495  			})
   496  		}
   497  
   498  		// add signatures for all coins and funds (manually, since tb doesn't have
   499  		// access to the signing keys)
   500  		txn, parents := tb.View()
   501  		for _, output := range txnSiacoinOutputs {
   502  			sk := generateSpendableKey(seed, output.seedIndex)
   503  			addSignatures(&txn, types.FullCoveredFields, sk.UnlockConditions, crypto.Hash(output.id), sk, height)
   504  		}
   505  		for _, sfo := range txnSiafundOutputs {
   506  			sk := generateSpendableKey(seed, sfo.seedIndex)
   507  			addSignatures(&txn, types.FullCoveredFields, sk.UnlockConditions, crypto.Hash(sfo.id), sk, height)
   508  		}
   509  		// Usually, all the inputs will come from swept outputs. However, there is
   510  		// an edge case in which inputs will be added from the wallet. To cover
   511  		// this case, we iterate through the SiacoinInputs and add a signature for
   512  		// any input that belongs to the wallet.
   513  		w.mu.RLock()
   514  		for _, input := range txn.SiacoinInputs {
   515  			if key, ok := w.keys[input.UnlockConditions.UnlockHash()]; ok {
   516  				addSignatures(&txn, types.FullCoveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key, height)
   517  			}
   518  		}
   519  		w.mu.RUnlock()
   520  
   521  		// Append transaction to txnSet
   522  		txnSet := append(parents, txn)
   523  
   524  		// submit the transactions
   525  		err = w.tpool.AcceptTransactionSet(txnSet)
   526  		if err != nil {
   527  			return types.ZeroCurrency, types.ZeroCurrency, err
   528  		}
   529  
   530  		w.log.Println("Creating a transaction set to sweep a seed, IDs:")
   531  		for _, txn := range txnSet {
   532  			w.log.Println("\t", txn.ID())
   533  		}
   534  
   535  		coins = coins.Add(txnCoins)
   536  		funds = funds.Add(txnFunds)
   537  	}
   538  	return
   539  }