decred.org/dcrdex@v1.0.5/client/db/bolt/upgrades.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package bolt
     5  
     6  import (
     7  	"fmt"
     8  	"path/filepath"
     9  
    10  	dexdb "decred.org/dcrdex/client/db"
    11  	"decred.org/dcrdex/dex"
    12  	"decred.org/dcrdex/dex/encode"
    13  	"decred.org/dcrdex/dex/order"
    14  	"go.etcd.io/bbolt"
    15  )
    16  
    17  type upgradefunc func(tx *bbolt.Tx) error
    18  
    19  // TODO: consider changing upgradefunc to accept a bbolt.DB so it may create its
    20  // own transactions. The individual upgrades have to happen in separate
    21  // transactions because they get too big for a single transaction. Plus we
    22  // should consider switching certain upgrades from ForEach to a more cumbersome
    23  // Cursor()-based iteration that will facilitate partitioning updates into
    24  // smaller batches of buckets.
    25  
    26  // Each database upgrade function should be keyed by the database
    27  // version it upgrades.
    28  var upgrades = [...]upgradefunc{
    29  	// v0 => v1 adds a version key. Upgrades the MatchProof struct to
    30  	// differentiate between server revokes and self revokes.
    31  	v1Upgrade,
    32  	// v1 => v2 adds a MaxFeeRate field to the OrderMetaData, used for match
    33  	// validation.
    34  	v2Upgrade,
    35  	// v2 => v3 adds a tx data field to the match proof.
    36  	v3Upgrade,
    37  	// v3 => v4 splits orders into active and archived.
    38  	v4Upgrade,
    39  	// v4 => v5 adds PrimaryCredentials with determinstic client seed, but the
    40  	// only thing we need to do during the DB upgrade is to update the
    41  	// db.AccountInfo to differentiate legacy vs. new-style key.
    42  	v5Upgrade,
    43  	// v5 => v6 splits matches into separate active and archived buckets.
    44  	v6Upgrade,
    45  }
    46  
    47  // DBVersion is the latest version of the database that is understood. Databases
    48  // with recorded versions higher than this will fail to open (meaning any
    49  // upgrades prevent reverting to older software).
    50  const DBVersion = uint32(len(upgrades))
    51  
    52  func setDBVersion(tx *bbolt.Tx, newVersion uint32) error {
    53  	bucket := tx.Bucket(appBucket)
    54  	if bucket == nil {
    55  		return fmt.Errorf("app bucket not found")
    56  	}
    57  
    58  	return bucket.Put(versionKey, encode.Uint32Bytes(newVersion))
    59  }
    60  
    61  var upgradeLog = dex.Disabled
    62  
    63  // upgradeDB checks whether any upgrades are necessary before the database is
    64  // ready for application usage.  If any are, they are performed.
    65  func (db *BoltDB) upgradeDB() error {
    66  	var version uint32
    67  	version, err := db.getVersion()
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	if version > DBVersion {
    73  		return fmt.Errorf("unknown database version %d, "+
    74  			"client recognizes up to %d", version, DBVersion)
    75  	}
    76  
    77  	if version == DBVersion {
    78  		// No upgrades necessary.
    79  		return nil
    80  	}
    81  
    82  	db.log.Infof("Upgrading database from version %d to %d", version, DBVersion)
    83  	upgradeLog = db.log
    84  
    85  	// Backup the current version's DB file before processing the upgrades to
    86  	// DBVersion. Note that any intermediate versions are not stored.
    87  	currentFile := filepath.Base(db.Path())
    88  	backupPath := fmt.Sprintf("%s.v%d.bak", currentFile, version) // e.g. bisonw.db.v1.bak
    89  	if err = db.backup(backupPath, true); err != nil {
    90  		return fmt.Errorf("failed to backup DB prior to upgrade: %w", err)
    91  	}
    92  
    93  	// Each upgrade its own tx, otherwise bolt eats too much RAM.
    94  	for i, upgrade := range upgrades[version:] {
    95  		newVersion := version + uint32(i) + 1
    96  		db.log.Debugf("Upgrading to version %d...", newVersion)
    97  		err = db.Update(func(tx *bbolt.Tx) error {
    98  			return doUpgrade(tx, upgrade, newVersion)
    99  		})
   100  		if err != nil {
   101  			return err
   102  		}
   103  	}
   104  	return nil
   105  }
   106  
   107  // Get the currently stored DB version.
   108  func (db *BoltDB) getVersion() (version uint32, err error) {
   109  	return version, db.View(func(tx *bbolt.Tx) error {
   110  		version, err = getVersionTx(tx)
   111  		return err
   112  	})
   113  }
   114  
   115  // Get the uint32 stored in the appBucket's versionKey entry.
   116  func getVersionTx(tx *bbolt.Tx) (uint32, error) {
   117  	bucket := tx.Bucket(appBucket)
   118  	if bucket == nil {
   119  		return 0, fmt.Errorf("appBucket not found")
   120  	}
   121  	versionB := bucket.Get(versionKey)
   122  	if versionB == nil {
   123  		return 0, fmt.Errorf("database version not found")
   124  	}
   125  	return intCoder.Uint32(versionB), nil
   126  }
   127  
   128  func v1Upgrade(dbtx *bbolt.Tx) error {
   129  	bkt := dbtx.Bucket(appBucket)
   130  	if bkt == nil {
   131  		return fmt.Errorf("appBucket not found")
   132  	}
   133  	skipCancels := true // cancel matches don't get revoked, only cancel orders
   134  	matchesBucket := []byte("matches")
   135  	return reloadMatchProofs(dbtx, skipCancels, matchesBucket)
   136  }
   137  
   138  // v2Upgrade adds a MaxFeeRate field to the OrderMetaData. The upgrade sets the
   139  // MaxFeeRate field for all historical trade orders to the max uint64. This
   140  // avoids any chance of rejecting a pre-existing active match.
   141  func v2Upgrade(dbtx *bbolt.Tx) error {
   142  	const oldVersion = 1
   143  	ordersBucket := []byte("orders")
   144  
   145  	dbVersion, err := getVersionTx(dbtx)
   146  	if err != nil {
   147  		return fmt.Errorf("error fetching database version: %w", err)
   148  	}
   149  
   150  	if dbVersion != oldVersion {
   151  		return fmt.Errorf("v2Upgrade inappropriately called")
   152  	}
   153  
   154  	// For each order, set a maxfeerate of max uint64.
   155  	maxFeeB := uint64Bytes(^uint64(0))
   156  
   157  	master := dbtx.Bucket(ordersBucket)
   158  	if master == nil {
   159  		return fmt.Errorf("failed to open orders bucket")
   160  	}
   161  
   162  	return master.ForEach(func(oid, _ []byte) error {
   163  		oBkt := master.Bucket(oid)
   164  		if oBkt == nil {
   165  			return fmt.Errorf("order %x bucket is not a bucket", oid)
   166  		}
   167  		// Cancel orders should be stored with a zero maxFeeRate, as done in
   168  		// (*Core).tryCancelTrade. Besides, the maxFeeRate should not be applied
   169  		// to cancel matches, as done in (*dexConnection).parseMatches.
   170  		oTypeB := oBkt.Get(typeKey)
   171  		if len(oTypeB) != 1 {
   172  			return fmt.Errorf("order %x type invalid: %x", oid, oTypeB)
   173  		}
   174  		if order.OrderType(oTypeB[0]) == order.CancelOrderType {
   175  			// Don't bother setting maxFeeRate for cancel orders.
   176  			// decodeOrderBucket will default to zero for cancels.
   177  			return nil
   178  		}
   179  		return oBkt.Put(maxFeeRateKey, maxFeeB)
   180  	})
   181  }
   182  
   183  func v3Upgrade(dbtx *bbolt.Tx) error {
   184  	// Upgrade the match proof. We just have to retrieve and re-store the
   185  	// buckets. The decoder will recognize the the old version and add the new
   186  	// field.
   187  	skipCancels := true // cancel matches have no tx data
   188  	matchesBucket := []byte("matches")
   189  	return reloadMatchProofs(dbtx, skipCancels, matchesBucket)
   190  }
   191  
   192  // v4Upgrade moves active orders from what will become the archivedOrdersBucket
   193  // to a new ordersBucket. This is done in order to make searching active orders
   194  // faster, as they do not need to be pulled out of all orders any longer. This
   195  // upgrade moves active orders as opposed to inactive orders under the
   196  // assumption that there are less active orders to move, and so a smaller
   197  // database transaction occurs.
   198  func v4Upgrade(dbtx *bbolt.Tx) error {
   199  	const oldVersion = 3
   200  
   201  	if err := ensureVersion(dbtx, oldVersion); err != nil {
   202  		return err
   203  	}
   204  
   205  	// Move any inactive orders to the new archivedOrdersBucket.
   206  	return moveActiveOrders(dbtx)
   207  }
   208  
   209  // v5Upgrade changes the database structure to accommodate PrimaryCredentials.
   210  // The OuterKeyParams bucket is populated with the existing application
   211  // serialized Crypter, but other fields are not populated since the password
   212  // would be required. The caller should generate new PrimaryCredentials and
   213  // call Recrypt during the next login.
   214  func v5Upgrade(dbtx *bbolt.Tx) error {
   215  	const oldVersion = 4
   216  
   217  	if err := ensureVersion(dbtx, oldVersion); err != nil {
   218  		return err
   219  	}
   220  
   221  	acctsBkt := dbtx.Bucket(accountsBucket)
   222  	if acctsBkt == nil {
   223  		return fmt.Errorf("failed to open accounts bucket")
   224  	}
   225  
   226  	if err := acctsBkt.ForEach(func(hostB, _ []byte) error {
   227  		acctBkt := acctsBkt.Bucket(hostB)
   228  		if acctBkt == nil {
   229  			return fmt.Errorf("account %s bucket is not a bucket", string(hostB))
   230  		}
   231  		acctB := getCopy(acctBkt, accountKey)
   232  		if acctB == nil {
   233  			return fmt.Errorf("empty account found for %s", (hostB))
   234  		}
   235  		acctInfo, err := dexdb.DecodeAccountInfo(acctB)
   236  		if err != nil {
   237  			return err
   238  		}
   239  		return acctBkt.Put(accountKey, acctInfo.Encode())
   240  	}); err != nil {
   241  		return fmt.Errorf("error updating account buckets: %w", err)
   242  	}
   243  
   244  	appBkt := dbtx.Bucket(appBucket)
   245  	if appBkt == nil {
   246  		return fmt.Errorf("no app bucket")
   247  	}
   248  
   249  	legacyKeyParams := appBkt.Get(legacyKeyParamsKey)
   250  	if len(legacyKeyParams) == 0 {
   251  		// Database is uninitialized. Nothing to do.
   252  		return nil
   253  	}
   254  
   255  	// Really, we should just be able to dbtx.Bucket here, since actual upgrades
   256  	// are performed after calling NewDB, which runs makeTopLevelBuckets
   257  	// internally before the upgrade. But the TestUpgrades runs the test on the
   258  	// bbolt.DB directly, so the bucket won't have been created during that
   259  	// test. That makes me think that we should be running those upgrade tests
   260  	// on DB, not bbolt.DB. TODO?
   261  	credsBkt, err := dbtx.CreateBucketIfNotExists(credentialsBucket)
   262  	if err != nil {
   263  		return fmt.Errorf("error creating credentials bucket: %w", err)
   264  	}
   265  
   266  	return credsBkt.Put(outerKeyParamsKey, legacyKeyParams)
   267  }
   268  
   269  // Probably not worth doing. Just let decodeWallet_v0 append the nil and pass it
   270  // up the chain.
   271  // func v6Upgrade(dbtx *bbolt.Tx) error {
   272  // 	wallets := dbtx.Bucket(walletsBucket)
   273  // 	if wallets == nil {
   274  // 		return fmt.Errorf("failed to open orders bucket")
   275  // 	}
   276  
   277  // 	return wallets.ForEach(func(wid, _ []byte) error {
   278  // 		wBkt := wallets.Bucket(wid)
   279  // 		w, err := dexdb.DecodeWallet(getCopy(wBkt, walletKey))
   280  // 		if err != nil {
   281  // 			return fmt.Errorf("DecodeWallet error: %v", err)
   282  // 		}
   283  // 		return wBkt.Put(walletKey, w.Encode())
   284  // 	})
   285  // }
   286  
   287  // v6Upgrade moves active matches from what will become the
   288  // archivedMatchesBucket to a new matchesBucket. This is done in order to make
   289  // searching active matches faster, as they do not need to be pulled out of all
   290  // matches any longer. This upgrade moves active matches as opposed to inactive
   291  // matches under the assumption that there are less active matches to move, and
   292  // so a smaller database transaction occurs.
   293  //
   294  // NOTE: Earlier taker cancel order matches may be MatchComplete AND have an
   295  // Address set because the msgjson.Match included the maker's/trade's address.
   296  // However, this upgrade does not patch the address field because MatchIsActive
   297  // instead keys off of InitSig to detect cancel matches, and this is a
   298  // potentially huge set of matches and bolt eats too much memory with ForEach.
   299  func v6Upgrade(dbtx *bbolt.Tx) error {
   300  	const oldVersion = 5
   301  
   302  	if err := ensureVersion(dbtx, oldVersion); err != nil {
   303  		return err
   304  	}
   305  
   306  	oldMatchesBucket := []byte("matches")
   307  	newActiveMatchesBucket := []byte("activeMatches")
   308  	// NOTE: newActiveMatchesBucket created in NewDB, but TestUpgrades skips that.
   309  	_, err := dbtx.CreateBucketIfNotExists(newActiveMatchesBucket)
   310  	if err != nil {
   311  		return err
   312  	}
   313  
   314  	var nActive, nArchived int
   315  
   316  	defer func() {
   317  		upgradeLog.Infof("%d active matches moved, %d archived matches unmoved", nActive, nArchived)
   318  	}()
   319  
   320  	archivedMatchesBkt := dbtx.Bucket(oldMatchesBucket)
   321  	activeMatchesBkt := dbtx.Bucket(newActiveMatchesBucket)
   322  
   323  	return archivedMatchesBkt.ForEach(func(k, _ []byte) error {
   324  		archivedMBkt := archivedMatchesBkt.Bucket(k)
   325  		if archivedMBkt == nil {
   326  			return fmt.Errorf("match %x bucket is not a bucket", k)
   327  		}
   328  		matchB := getCopy(archivedMBkt, matchKey)
   329  		if matchB == nil {
   330  			return fmt.Errorf("nil match bytes for %x", k)
   331  		}
   332  		match, _, err := order.DecodeMatch(matchB)
   333  		if err != nil {
   334  			return fmt.Errorf("error decoding match %x: %w", k, err)
   335  		}
   336  		proofB := getCopy(archivedMBkt, proofKey)
   337  		if len(proofB) == 0 {
   338  			return fmt.Errorf("empty proof")
   339  		}
   340  		proof, _, err := dexdb.DecodeMatchProof(proofB)
   341  		if err != nil {
   342  			return fmt.Errorf("error decoding proof: %w", err)
   343  		}
   344  		// If match is active, move to activeMatchesBucket.
   345  		if !dexdb.MatchIsActiveV6Upgrade(match, proof) {
   346  			nArchived++
   347  			return nil
   348  		}
   349  
   350  		upgradeLog.Infof("Moving match %v (%v, revoked = %v, refunded = %v, sigs (init/redeem): %v, %v) to active bucket.",
   351  			match, match.Status, proof.IsRevoked(), len(proof.RefundCoin) > 0,
   352  			len(proof.Auth.InitSig) > 0, len(proof.Auth.RedeemSig) > 0)
   353  		nActive++
   354  
   355  		activeMBkt, err := activeMatchesBkt.CreateBucket(k)
   356  		if err != nil {
   357  			return err
   358  		}
   359  		// Assume the match bucket contains only values, no sub-buckets
   360  		if err := archivedMBkt.ForEach(func(k, v []byte) error {
   361  			return activeMBkt.Put(k, v)
   362  		}); err != nil {
   363  			return err
   364  		}
   365  		return archivedMatchesBkt.DeleteBucket(k)
   366  	})
   367  }
   368  
   369  func ensureVersion(tx *bbolt.Tx, ver uint32) error {
   370  	dbVersion, err := getVersionTx(tx)
   371  	if err != nil {
   372  		return fmt.Errorf("error fetching database version: %w", err)
   373  	}
   374  	if dbVersion != ver {
   375  		return fmt.Errorf("wrong version for upgrade. expected %d, got %d", ver, dbVersion)
   376  	}
   377  	return nil
   378  }
   379  
   380  // Note that reloadMatchProofs will rewrite the MatchProof with the current
   381  // match proof encoding version. Thus, multiple upgrades in a row calling
   382  // reloadMatchProofs may be no-ops. Matches with cancel orders may be skipped.
   383  func reloadMatchProofs(tx *bbolt.Tx, skipCancels bool, matchesBucket []byte) error {
   384  	matches := tx.Bucket(matchesBucket)
   385  	return matches.ForEach(func(k, _ []byte) error {
   386  		mBkt := matches.Bucket(k)
   387  		if mBkt == nil {
   388  			return fmt.Errorf("match %x bucket is not a bucket", k)
   389  		}
   390  		proofB := mBkt.Get(proofKey)
   391  		if len(proofB) == 0 {
   392  			return fmt.Errorf("empty match proof")
   393  		}
   394  		proof, ver, err := dexdb.DecodeMatchProof(proofB)
   395  		if err != nil {
   396  			return fmt.Errorf("error decoding proof: %w", err)
   397  		}
   398  		// No need to rewrite this if it was loaded from the current version.
   399  		if ver == dexdb.MatchProofVer {
   400  			return nil
   401  		}
   402  		// No Script, and MatchComplete status means this is a cancel match.
   403  		if skipCancels && len(proof.ContractData) == 0 {
   404  			statusB := mBkt.Get(statusKey)
   405  			if len(statusB) != 1 {
   406  				return fmt.Errorf("no match status")
   407  			}
   408  			if order.MatchStatus(statusB[0]) == order.MatchComplete {
   409  				return nil
   410  			}
   411  		}
   412  
   413  		err = mBkt.Put(proofKey, proof.Encode())
   414  		if err != nil {
   415  			return fmt.Errorf("error re-storing match proof: %w", err)
   416  		}
   417  		return nil
   418  	})
   419  }
   420  
   421  // moveActiveOrders searches the v1 ordersBucket for orders that are inactive,
   422  // adds those to the v2 ordersBucket, and deletes them from the v1 ordersBucket,
   423  // which becomes the archived orders bucket.
   424  func moveActiveOrders(tx *bbolt.Tx) error {
   425  	oldOrdersBucket := []byte("orders")
   426  	newActiveOrdersBucket := []byte("activeOrders")
   427  	// NOTE: newActiveOrdersBucket created in NewDB, but TestUpgrades skips that.
   428  	_, err := tx.CreateBucketIfNotExists(newActiveOrdersBucket)
   429  	if err != nil {
   430  		return err
   431  	}
   432  
   433  	archivedOrdersBkt := tx.Bucket(oldOrdersBucket)
   434  	activeOrdersBkt := tx.Bucket(newActiveOrdersBucket)
   435  	return archivedOrdersBkt.ForEach(func(k, _ []byte) error {
   436  		archivedOBkt := archivedOrdersBkt.Bucket(k)
   437  		if archivedOBkt == nil {
   438  			return fmt.Errorf("order %x bucket is not a bucket", k)
   439  		}
   440  		status := order.OrderStatus(intCoder.Uint16(archivedOBkt.Get(statusKey)))
   441  		if status == order.OrderStatusUnknown {
   442  			fmt.Printf("Encountered order with unknown status: %x\n", k)
   443  			return nil
   444  		}
   445  		if !status.IsActive() {
   446  			return nil
   447  		}
   448  		activeOBkt, err := activeOrdersBkt.CreateBucket(k)
   449  		if err != nil {
   450  			return err
   451  		}
   452  		// Assume the order bucket contains only values, no sub-buckets
   453  		if err := archivedOBkt.ForEach(func(k, v []byte) error {
   454  			return activeOBkt.Put(k, v)
   455  		}); err != nil {
   456  			return err
   457  		}
   458  		return archivedOrdersBkt.DeleteBucket(k)
   459  	})
   460  }
   461  
   462  func doUpgrade(tx *bbolt.Tx, upgrade upgradefunc, newVersion uint32) error {
   463  	if err := ensureVersion(tx, newVersion-1); err != nil {
   464  		return err
   465  	}
   466  	err := upgrade(tx)
   467  	if err != nil {
   468  		return fmt.Errorf("error upgrading DB: %v", err)
   469  	}
   470  	// Persist the database version.
   471  	err = setDBVersion(tx, newVersion)
   472  	if err != nil {
   473  		return fmt.Errorf("error setting DB version: %v", err)
   474  	}
   475  	return nil
   476  }