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 }