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

     1  package wallet
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"gitlab.com/NebulousLabs/errors"
    10  	"gitlab.com/NebulousLabs/fastrand"
    11  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    12  	"gitlab.com/SiaPrime/SiaPrime/encoding"
    13  	"gitlab.com/SiaPrime/SiaPrime/modules"
    14  	"gitlab.com/SiaPrime/SiaPrime/persist"
    15  	"gitlab.com/SiaPrime/SiaPrime/types"
    16  
    17  	bolt "github.com/coreos/bbolt"
    18  )
    19  
    20  const (
    21  	compatFile = modules.WalletDir + ".json"
    22  	dbFile     = modules.WalletDir + ".db"
    23  	logFile    = modules.WalletDir + ".log"
    24  )
    25  
    26  var (
    27  	dbMetadata = persist.Metadata{
    28  		Header:  "Wallet Database",
    29  		Version: "1.1.0",
    30  	}
    31  )
    32  
    33  // spendableKeyFile stores an encrypted spendable key on disk.
    34  type spendableKeyFile struct {
    35  	UID                    uniqueID
    36  	EncryptionVerification crypto.Ciphertext
    37  	SpendableKey           crypto.Ciphertext
    38  }
    39  
    40  // openDB loads the set database and populates it with the necessary buckets.
    41  func (w *Wallet) openDB(filename string) (err error) {
    42  	w.db, err = persist.OpenDatabase(dbMetadata, filename)
    43  	if err != nil {
    44  		return err
    45  	}
    46  	// initialize the database
    47  	err = w.db.Update(func(tx *bolt.Tx) error {
    48  		// check whether we need to init bucketAddrTransactions
    49  		buildAddrTxns := tx.Bucket(bucketAddrTransactions) == nil
    50  		// ensure that all buckets exist
    51  		for _, b := range dbBuckets {
    52  			_, err := tx.CreateBucketIfNotExists(b)
    53  			if err != nil {
    54  				return fmt.Errorf("could not create bucket %v: %v", string(b), err)
    55  			}
    56  		}
    57  		// if the wallet does not have a UID, create one
    58  		if tx.Bucket(bucketWallet).Get(keyUID) == nil {
    59  			uid := make([]byte, len(uniqueID{}))
    60  			fastrand.Read(uid[:])
    61  			tx.Bucket(bucketWallet).Put(keyUID, uid)
    62  		}
    63  		// if fields in bucketWallet are nil, set them to zero to prevent unmarshal errors
    64  		wb := tx.Bucket(bucketWallet)
    65  		if wb.Get(keyConsensusHeight) == nil {
    66  			wb.Put(keyConsensusHeight, encoding.Marshal(uint64(0)))
    67  		}
    68  		if wb.Get(keyAuxiliarySeedFiles) == nil {
    69  			wb.Put(keyAuxiliarySeedFiles, encoding.Marshal([]seedFile{}))
    70  		}
    71  		if wb.Get(keySpendableKeyFiles) == nil {
    72  			wb.Put(keySpendableKeyFiles, encoding.Marshal([]spendableKeyFile{}))
    73  		}
    74  		if wb.Get(keySiafundPool) == nil {
    75  			wb.Put(keySiafundPool, encoding.Marshal(types.ZeroCurrency))
    76  		}
    77  		if wb.Get(keyWatchedAddrs) == nil {
    78  			wb.Put(keyWatchedAddrs, encoding.Marshal([]types.UnlockHash{}))
    79  		}
    80  
    81  		// build the bucketAddrTransactions bucket if necessary
    82  		if buildAddrTxns {
    83  			it := dbProcessedTransactionsIterator(tx)
    84  			for it.next() {
    85  				index, pt := it.key(), it.value()
    86  				if err := dbAddProcessedTransactionAddrs(tx, pt, index); err != nil {
    87  					return err
    88  				}
    89  			}
    90  		}
    91  
    92  		// check whether wallet is encrypted
    93  		w.encrypted = tx.Bucket(bucketWallet).Get(keyEncryptionVerification) != nil
    94  		return nil
    95  	})
    96  	return err
    97  }
    98  
    99  // initPersist loads all of the wallet's persistence files into memory,
   100  // creating them if they do not exist.
   101  func (w *Wallet) initPersist() error {
   102  	// Create a directory for the wallet without overwriting an existing
   103  	// directory.
   104  	err := os.MkdirAll(w.persistDir, 0700)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	// Start logging.
   110  	w.log, err = persist.NewFileLogger(filepath.Join(w.persistDir, logFile))
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	// Open the database.
   116  	dbFilename := filepath.Join(w.persistDir, dbFile)
   117  	compatFilename := filepath.Join(w.persistDir, compatFile)
   118  	_, dbErr := os.Stat(dbFilename)
   119  	_, compatErr := os.Stat(compatFilename)
   120  	if dbErr != nil && compatErr == nil {
   121  		// database does not exist, but old persist does; convert it
   122  		err = w.convertPersistFrom112To120(dbFilename, compatFilename)
   123  	} else {
   124  		// either database exists or neither exists; open/create the database
   125  		err = w.openDB(filepath.Join(w.persistDir, dbFile))
   126  	}
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	// begin the initial transaction
   132  	w.dbTx, err = w.db.Begin(true)
   133  	if err != nil {
   134  		w.log.Critical("ERROR: failed to start database update:", err)
   135  	}
   136  
   137  	// COMPATv131 we need to create the bucketProcessedTxnIndex if it doesn't exist
   138  	if w.dbTx.Bucket(bucketProcessedTransactions).Stats().KeyN > 0 &&
   139  		w.dbTx.Bucket(bucketProcessedTxnIndex).Stats().KeyN == 0 {
   140  		err = initProcessedTxnIndex(w.dbTx)
   141  		if err != nil {
   142  			return err
   143  		}
   144  		// Save changes to disk
   145  		if err = w.syncDB(); err != nil {
   146  			return err
   147  		}
   148  	}
   149  
   150  	// ensure that the final db transaction is committed when the wallet closes
   151  	err = w.tg.AfterStop(func() error {
   152  		w.mu.Lock()
   153  		defer w.mu.Unlock()
   154  		var err error
   155  		if w.dbRollback {
   156  			// rollback txn if necessry.
   157  			err = errors.New("database unable to sync - rollback requested")
   158  			err = errors.Compose(err, w.dbTx.Rollback())
   159  		} else {
   160  			// else commit the transaction.
   161  			err = w.dbTx.Commit()
   162  		}
   163  		if err != nil {
   164  			w.log.Severe("ERROR: failed to apply database update:", err)
   165  			return errors.AddContext(err, "unable to commit dbTx in syncDB")
   166  		}
   167  		return w.db.Close()
   168  	})
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	// spawn a goroutine to commit the db transaction at regular intervals
   174  	go w.threadedDBUpdate()
   175  	return nil
   176  }
   177  
   178  // createBackup copies the wallet database to dst.
   179  func (w *Wallet) createBackup(dst io.Writer) error {
   180  	_, err := w.dbTx.WriteTo(dst)
   181  	return err
   182  }
   183  
   184  // CreateBackup creates a backup file at the desired filepath.
   185  func (w *Wallet) CreateBackup(backupFilepath string) error {
   186  	if err := w.tg.Add(); err != nil {
   187  		return err
   188  	}
   189  	defer w.tg.Done()
   190  	w.mu.Lock()
   191  	defer w.mu.Unlock()
   192  	f, err := os.Create(backupFilepath)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	defer f.Close()
   197  	return w.createBackup(f)
   198  }
   199  
   200  // compat112Persist is the structure of the wallet.json file used in v1.1.2
   201  type compat112Persist struct {
   202  	UID                    uniqueID
   203  	EncryptionVerification crypto.Ciphertext
   204  	PrimarySeedFile        seedFile
   205  	PrimarySeedProgress    uint64
   206  	AuxiliarySeedFiles     []seedFile
   207  	UnseededKeys           []spendableKeyFile
   208  }
   209  
   210  // compat112Meta is the metadata of the wallet.json file used in v1.1.2
   211  var compat112Meta = persist.Metadata{
   212  	Header:  "Wallet Settings",
   213  	Version: "0.4.0",
   214  }
   215  
   216  // convertPersistFrom112To120 converts an old (pre-v1.2.0) wallet.json file to
   217  // a wallet.db database.
   218  func (w *Wallet) convertPersistFrom112To120(dbFilename, compatFilename string) error {
   219  	var data compat112Persist
   220  	err := persist.LoadJSON(compat112Meta, &data, compatFilename)
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	w.db, err = persist.OpenDatabase(dbMetadata, dbFilename)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	// initialize the database
   230  	err = w.db.Update(func(tx *bolt.Tx) error {
   231  		for _, b := range dbBuckets {
   232  			_, err := tx.CreateBucket(b)
   233  			if err != nil {
   234  				return fmt.Errorf("could not create bucket %v: %v", string(b), err)
   235  			}
   236  		}
   237  		// set UID, verification, seeds, and seed progress
   238  		tx.Bucket(bucketWallet).Put(keyUID, data.UID[:])
   239  		tx.Bucket(bucketWallet).Put(keyEncryptionVerification, data.EncryptionVerification)
   240  		tx.Bucket(bucketWallet).Put(keyPrimarySeedFile, encoding.Marshal(data.PrimarySeedFile))
   241  		tx.Bucket(bucketWallet).Put(keyAuxiliarySeedFiles, encoding.Marshal(data.AuxiliarySeedFiles))
   242  		tx.Bucket(bucketWallet).Put(keySpendableKeyFiles, encoding.Marshal(data.UnseededKeys))
   243  		// old wallets had a "preload depth" of 25
   244  		dbPutPrimarySeedProgress(tx, data.PrimarySeedProgress+25)
   245  
   246  		// set consensus height and CCID to zero so that a full rescan is
   247  		// triggered
   248  		dbPutConsensusHeight(tx, 0)
   249  		dbPutConsensusChangeID(tx, modules.ConsensusChangeBeginning)
   250  		return nil
   251  	})
   252  	w.encrypted = true
   253  	return err
   254  }
   255  
   256  /*
   257  // LoadBackup loads a backup file from the provided filepath. The backup file
   258  // primary seed is loaded as an auxiliary seed.
   259  func (w *Wallet) LoadBackup(masterKey, backupMasterKey crypto.TwofishKey, backupFilepath string) error {
   260  	if err := w.tg.Add(); err != nil {
   261  		return err
   262  	}
   263  	defer w.tg.Done()
   264  
   265  	lockID := w.mu.Lock()
   266  	defer w.mu.Unlock(lockID)
   267  
   268  	// Load all of the seed files, check for duplicates, re-encrypt them (but
   269  	// keep the UID), and add them to the walletPersist object)
   270  	var backupPersist walletPersist
   271  	err := persist.LoadFile(settingsMetadata, &backupPersist, backupFilepath)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	backupSeeds := append(backupPersist.AuxiliarySeedFiles, backupPersist.PrimarySeedFile)
   276  	TODO: more
   277  }
   278  */