decred.org/dcrdex@v1.0.5/client/db/bolt/upgrades_test.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  	"bytes"
     8  	"compress/gzip"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  
    19  	dexdb "decred.org/dcrdex/client/db"
    20  	"decred.org/dcrdex/dex/order"
    21  	"go.etcd.io/bbolt"
    22  )
    23  
    24  var dbUpgradeTests = [...]struct {
    25  	name       string
    26  	upgrade    upgradefunc
    27  	verify     func(*testing.T, *bbolt.DB)
    28  	filename   string // in testdata directory
    29  	newVersion uint32
    30  }{
    31  	// {"testnetbot", v4Upgrade, verifyV4Upgrade, "dexbot-testnet.db.gz", 6}, // only for TestUpgradeDB, using just filename
    32  	{"upgradeFromV0", v1Upgrade, verifyV1Upgrade, "v0.db.gz", 1},
    33  	{"upgradeFromV1", v2Upgrade, verifyV2Upgrade, "v1.db.gz", 2},
    34  	{"upgradeFromV2", v3Upgrade, verifyV3Upgrade, "v2.db.gz", 3},
    35  	{"upgradeFromV3", v4Upgrade, verifyV4Upgrade, "v3.db.gz", 4},
    36  	{"upgradeFromV4", v5Upgrade, verifyV5Upgrade, "v4.db.gz", 5},
    37  	{"upgradeFromV5", v6Upgrade, verifyV6Upgrade, "v5.db.gz", 6},
    38  }
    39  
    40  func TestUpgrades(t *testing.T) {
    41  	upgradeLog = tLogger
    42  	t.Run("group", func(t *testing.T) {
    43  		for _, tc := range dbUpgradeTests {
    44  			tc := tc // capture range variable
    45  			t.Run(tc.name, func(t *testing.T) {
    46  				t.Parallel()
    47  				dbPath := unpack(t, tc.filename)
    48  				db, err := bbolt.Open(dbPath, 0600,
    49  					&bbolt.Options{Timeout: 1 * time.Second})
    50  				if err != nil {
    51  					t.Fatal(err)
    52  				}
    53  				defer db.Close()
    54  				err = db.Update(func(dbtx *bbolt.Tx) error {
    55  					return doUpgrade(dbtx, tc.upgrade, tc.newVersion)
    56  				})
    57  				if err != nil {
    58  					t.Fatalf("Upgrade %d -> %d failed: %v", tc.newVersion-1, tc.newVersion, err)
    59  				}
    60  				tc.verify(t, db)
    61  			})
    62  		}
    63  	})
    64  }
    65  
    66  func TestUpgradeDB(t *testing.T) {
    67  	runUpgrade := func(archiveName string) error {
    68  		dbPath := unpack(t, archiveName)
    69  		// NewDB runs upgradeDB.
    70  		dbi, err := NewDB(dbPath, tLogger)
    71  		if err != nil {
    72  			return fmt.Errorf("database initialization or upgrade error: %w", err)
    73  		}
    74  		db := dbi.(*BoltDB)
    75  		// Run upgradeDB again and it should be happy.
    76  		err = db.upgradeDB()
    77  		if err != nil {
    78  			return fmt.Errorf("upgradeDB error: %v", err)
    79  		}
    80  		newVersion, err := db.getVersion()
    81  		if err != nil {
    82  			return fmt.Errorf("getVersion error: %v", err)
    83  		}
    84  		if newVersion != DBVersion {
    85  			return fmt.Errorf("DB version not set. Expected %d, got %d", DBVersion, newVersion)
    86  		}
    87  		return nil
    88  	}
    89  
    90  	for _, tt := range dbUpgradeTests {
    91  		err := runUpgrade(tt.filename)
    92  		if err != nil {
    93  			t.Fatalf("upgrade error for version %d database: %v", tt.newVersion-1, err)
    94  		}
    95  	}
    96  
    97  }
    98  
    99  func verifyV1Upgrade(t *testing.T, db *bbolt.DB) {
   100  	t.Helper()
   101  	err := db.View(func(dbtx *bbolt.Tx) error {
   102  		return checkVersion(dbtx, 1)
   103  	})
   104  	if err != nil {
   105  		t.Error(err)
   106  	}
   107  }
   108  
   109  func verifyV2Upgrade(t *testing.T, db *bbolt.DB) {
   110  	t.Helper()
   111  	maxFeeB := uint64Bytes(^uint64(0))
   112  	ordersBucket := []byte("orders")
   113  
   114  	err := db.View(func(dbtx *bbolt.Tx) error {
   115  		err := checkVersion(dbtx, 2)
   116  		if err != nil {
   117  			return err
   118  		}
   119  
   120  		master := dbtx.Bucket(ordersBucket)
   121  		if master == nil {
   122  			return fmt.Errorf("orders bucket not found")
   123  		}
   124  		return master.ForEach(func(oid, _ []byte) error {
   125  			oBkt := master.Bucket(oid)
   126  			if oBkt == nil {
   127  				return fmt.Errorf("order %x bucket is not a bucket", oid)
   128  			}
   129  			if !bytes.Equal(oBkt.Get(maxFeeRateKey), maxFeeB) {
   130  				return fmt.Errorf("max fee not upgraded")
   131  			}
   132  			return nil
   133  		})
   134  	})
   135  	if err != nil {
   136  		t.Error(err)
   137  	}
   138  }
   139  
   140  // Nothing to really check here. Any errors would have come out during the
   141  // upgrade process itself, since we just added a default nil field.
   142  func verifyV3Upgrade(t *testing.T, db *bbolt.DB) {
   143  	t.Helper()
   144  	err := db.View(func(dbtx *bbolt.Tx) error {
   145  		return checkVersion(dbtx, 3)
   146  	})
   147  	if err != nil {
   148  		t.Error(err)
   149  	}
   150  }
   151  
   152  func verifyV4Upgrade(t *testing.T, db *bbolt.DB) {
   153  	oldOrdersBucket := []byte("orders")
   154  	newActiveOrdersBucket := []byte("activeOrders")
   155  	err := db.View(func(dbtx *bbolt.Tx) error {
   156  		err := checkVersion(dbtx, 4)
   157  		if err != nil {
   158  			return err
   159  		}
   160  		// Ensure we have both old and new buckets.
   161  		archivedOrdersBkt := dbtx.Bucket(oldOrdersBucket)
   162  		if archivedOrdersBkt == nil {
   163  			return fmt.Errorf("archived orders bucket not found")
   164  		}
   165  		activeOrdersBkt := dbtx.Bucket(newActiveOrdersBucket)
   166  		if activeOrdersBkt == nil {
   167  			return fmt.Errorf("active orders bucket not found")
   168  		}
   169  
   170  		// Ensure the old bucket now only contains finished orders.
   171  		err = archivedOrdersBkt.ForEach(func(k, _ []byte) error {
   172  			archivedOBkt := archivedOrdersBkt.Bucket(k)
   173  			if archivedOBkt == nil {
   174  				return fmt.Errorf("order %x bucket is not a bucket", k)
   175  			}
   176  			status := order.OrderStatus(intCoder.Uint16(archivedOBkt.Get(statusKey)))
   177  			if status == order.OrderStatusUnknown {
   178  				fmt.Printf("Encountered order with unknown status: %x\n", k)
   179  				return nil
   180  			}
   181  			if status.IsActive() {
   182  				return fmt.Errorf("archived bucket has active order: %x", k)
   183  			}
   184  			return nil
   185  		})
   186  		if err != nil {
   187  			return err
   188  		}
   189  
   190  		// Ensure the new bucket only contains active orders.
   191  		err = activeOrdersBkt.ForEach(func(k, _ []byte) error {
   192  			activeOBkt := activeOrdersBkt.Bucket(k)
   193  			if activeOBkt == nil {
   194  				return fmt.Errorf("order %x bucket is not a bucket", k)
   195  			}
   196  			status := order.OrderStatus(intCoder.Uint16(activeOBkt.Get(statusKey)))
   197  			if status == order.OrderStatusUnknown {
   198  				return fmt.Errorf("encountered order with unknown status: %x", k)
   199  			}
   200  			if !status.IsActive() {
   201  				return fmt.Errorf("active orders bucket has archived order: %x", k)
   202  			}
   203  			return nil
   204  		})
   205  		if err != nil {
   206  			return err
   207  		}
   208  		return nil
   209  	})
   210  	if err != nil {
   211  		t.Error(err)
   212  	}
   213  }
   214  
   215  // Ensure that the LegacyEncKey field is populated for the accounts in the DB.
   216  func verifyV5Upgrade(t *testing.T, db *bbolt.DB) {
   217  	if err := db.View(func(tx *bbolt.Tx) error {
   218  		return checkVersion(tx, 5)
   219  	}); err != nil {
   220  		t.Error(err)
   221  	}
   222  
   223  	if err := db.View(func(tx *bbolt.Tx) error {
   224  		accts := tx.Bucket(accountsBucket)
   225  		c := accts.Cursor()
   226  		for acctKey, _ := c.First(); acctKey != nil; acctKey, _ = c.Next() {
   227  			acct := accts.Bucket(acctKey)
   228  			if acct == nil {
   229  				return fmt.Errorf("account bucket %s value not a nested bucket", string(acctKey))
   230  			}
   231  			acctB := getCopy(acct, accountKey)
   232  			if acctB == nil {
   233  				return fmt.Errorf("empty account found for %s", string(acctKey))
   234  			}
   235  			acctInfo, err := dexdb.DecodeAccountInfo(acctB)
   236  			if err != nil {
   237  				return err
   238  			}
   239  			if len(acctInfo.LegacyEncKey) == 0 {
   240  				return errors.New("LegacyEncKey not sets")
   241  			}
   242  		}
   243  		return nil
   244  	}); err != nil {
   245  		t.Error(err)
   246  	}
   247  }
   248  
   249  func verifyV6Upgrade(t *testing.T, db *bbolt.DB) {
   250  	verifyMatches := map[string]bool{ // matchid: active
   251  		"52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649": true,  // status < MatchComplete, not revoked, not refunded
   252  		"81855a1e00167939cb6694d2c422acd208a0072939487f6999eb9d18a4478404": false, // status < MatchComplete, revoked at NewlyMatched
   253  		"5d87f3c67cf2367951baa2ff6cd471c483f15fb90badb37c5821b6d95526a41a": true,  // status == MakerSwapCast, side Maker, revoked, requires refund
   254  		"9504680b4e7c8b763a1b1d49d4955c8486216325253fec738dd7a9e28bf92111": false, // status == TakerSwapCast, side Maker, revoked, refunded
   255  		"9c160f0702448615bbda08313f6a8eb668d20bf5059875921e668a5bdf2c7fc4": true,  // status == MatchComplete, no RedeemSig !!! TODO: missing InitSig
   256  		"844592d2572bcd0668d2d6c52f5054e2d0836bf84c7174cb7476364cc3dbd968": false, // status == MatchComplete, RedeemSig set
   257  		"b0f7172ed85794bb358b0c3b525da1786f9fff094279db1944ebd7a19d0f7bba": false, // cancel order match
   258  	}
   259  
   260  	bdb := &BoltDB{DB: db}
   261  	err := bdb.matchesView(func(mb, amb *bbolt.Bucket) error {
   262  		// active matches
   263  		err := mb.ForEach(func(k, _ []byte) error {
   264  			matchID := hex.EncodeToString(k)
   265  			mBkt := mb.Bucket(k)
   266  			if mBkt == nil {
   267  				return fmt.Errorf("match %s bucket is not a bucket", matchID)
   268  			}
   269  			midB := getCopy(mBkt, matchIDKey)
   270  			if midB == nil {
   271  				return fmt.Errorf("nil match ID bytes")
   272  			}
   273  			mid := hex.EncodeToString(midB)
   274  			if active, found := verifyMatches[mid]; !found {
   275  				return fmt.Errorf("match %v not found in test DB", mid)
   276  			} else if !active {
   277  				return fmt.Errorf("inactive match found in active matches bucket: %v", mid)
   278  			}
   279  			return nil
   280  		})
   281  		if err != nil {
   282  			return err
   283  		}
   284  
   285  		// archived
   286  		return amb.ForEach(func(k, _ []byte) error {
   287  			matchID := hex.EncodeToString(k)
   288  			mBkt := amb.Bucket(k)
   289  			if mBkt == nil {
   290  				return fmt.Errorf("match %s bucket is not a bucket", matchID)
   291  			}
   292  			midB := getCopy(mBkt, matchIDKey)
   293  			if midB == nil {
   294  				return fmt.Errorf("nil match ID bytes")
   295  			}
   296  			mid := hex.EncodeToString(midB)
   297  			if active, found := verifyMatches[mid]; !found {
   298  				return fmt.Errorf("match %v not found in test DB", mid)
   299  			} else if active {
   300  				return fmt.Errorf("active match found in archived matches bucket: %v", mid)
   301  			}
   302  			return nil
   303  		})
   304  	})
   305  	if err != nil {
   306  		t.Error(err)
   307  	}
   308  }
   309  
   310  func checkVersion(dbtx *bbolt.Tx, expectedVersion uint32) error {
   311  	bkt := dbtx.Bucket(appBucket)
   312  	if bkt == nil {
   313  		return fmt.Errorf("appBucket not found")
   314  	}
   315  	versionB := bkt.Get(versionKey)
   316  	if versionB == nil {
   317  		return fmt.Errorf("expected a non-nil version value")
   318  	}
   319  	version := intCoder.Uint32(versionB)
   320  	if version != expectedVersion {
   321  		return fmt.Errorf("expected db version %d, got %d",
   322  			expectedVersion, version)
   323  	}
   324  	return nil
   325  }
   326  
   327  func unpack(t *testing.T, db string) string {
   328  	t.Helper()
   329  	d := t.TempDir()
   330  
   331  	t.Helper()
   332  	archive, err := os.Open(filepath.Join("testdata", db))
   333  	if err != nil {
   334  		t.Fatal(err)
   335  	}
   336  
   337  	r, err := gzip.NewReader(archive)
   338  	if err != nil {
   339  		t.Fatal(err)
   340  	}
   341  	dbPath := filepath.Join(d, strings.TrimSuffix(db, ".gz"))
   342  	dbFile, err := os.Create(dbPath)
   343  	if err != nil {
   344  		t.Fatal(err)
   345  	}
   346  	_, err = io.Copy(dbFile, r)
   347  	archive.Close()
   348  	dbFile.Close()
   349  	if err != nil {
   350  		t.Fatal(err)
   351  	}
   352  	return dbPath
   353  }