decred.org/dcrwallet/v3@v3.1.0/wallet/udb/upgrades_test.go (about)

     1  // Copyright (c) 2017 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package udb
     6  
     7  import (
     8  	"bytes"
     9  	"compress/gzip"
    10  	"context"
    11  	"encoding/hex"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  	"testing"
    17  
    18  	_ "decred.org/dcrwallet/v3/wallet/drivers/bdb"
    19  	"decred.org/dcrwallet/v3/wallet/walletdb"
    20  	"github.com/decred/dcrd/chaincfg/chainhash"
    21  	"github.com/decred/dcrd/chaincfg/v3"
    22  	"github.com/decred/dcrd/dcrutil/v4"
    23  	"github.com/decred/dcrd/wire"
    24  )
    25  
    26  var dbUpgradeTests = [...]struct {
    27  	verify   func(context.Context, *testing.T, walletdb.DB)
    28  	filename string // in testdata directory
    29  }{
    30  	{verifyV2Upgrade, "v1.db.gz"},
    31  	{verifyV3Upgrade, "v2.db.gz"},
    32  	{verifyV4Upgrade, "v3.db.gz"},
    33  	{verifyV5Upgrade, "v4.db.gz"},
    34  	{verifyV6Upgrade, "v5.db.gz"},
    35  	// No upgrade test for V7, it is a backwards-compatible upgrade
    36  	{verifyV8Upgrade, "v7.db.gz"},
    37  	// No upgrade test for V9, it is a fix for V8 and the previous test still applies
    38  	// TODO: V10 upgrade test
    39  	{verifyV12Upgrade, "v11.db.gz"},
    40  	// TODO: V13-24 tests
    41  	{verifyV25Upgrade, "v24.db.gz"},
    42  }
    43  
    44  var pubPass = []byte("public")
    45  
    46  func TestUpgrades(t *testing.T) {
    47  	ctx := context.Background()
    48  	d, err := os.MkdirTemp("", "dcrwallet_udb_TestUpgrades")
    49  	if err != nil {
    50  		t.Fatal(err)
    51  	}
    52  
    53  	t.Run("group", func(t *testing.T) {
    54  		for i, test := range dbUpgradeTests {
    55  			test := test
    56  			name := fmt.Sprintf("test%d", i)
    57  			t.Run(name, func(t *testing.T) {
    58  				t.Parallel()
    59  				testFile, err := os.Open(filepath.Join("testdata", test.filename))
    60  				if err != nil {
    61  					t.Fatal(err)
    62  				}
    63  				defer testFile.Close()
    64  				r, err := gzip.NewReader(testFile)
    65  				if err != nil {
    66  					t.Fatal(err)
    67  				}
    68  				dbPath := filepath.Join(d, name+".db")
    69  				fi, err := os.Create(dbPath)
    70  				if err != nil {
    71  					t.Fatal(err)
    72  				}
    73  				_, err = io.Copy(fi, r)
    74  				fi.Close()
    75  				if err != nil {
    76  					t.Fatal(err)
    77  				}
    78  				db, err := walletdb.Open("bdb", dbPath)
    79  				if err != nil {
    80  					t.Fatal(err)
    81  				}
    82  				defer db.Close()
    83  				err = Upgrade(ctx, db, pubPass, chaincfg.TestNet3Params())
    84  				if err != nil {
    85  					t.Fatalf("Upgrade failed: %v", err)
    86  				}
    87  				test.verify(ctx, t, db)
    88  			})
    89  		}
    90  	})
    91  
    92  	os.RemoveAll(d)
    93  }
    94  
    95  func verifyV2Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
    96  	amgr, _, _, err := Open(ctx, db, chaincfg.TestNet3Params(), pubPass)
    97  	if err != nil {
    98  		t.Fatalf("Open after Upgrade failed: %v", err)
    99  	}
   100  
   101  	err = walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   102  		ns := tx.ReadBucket(waddrmgrBucketKey)
   103  		nsMetaBucket := ns.NestedReadBucket(metaBucketName)
   104  
   105  		accounts := []struct {
   106  			totalAddrs uint32
   107  			lastUsed   uint32
   108  		}{
   109  			{^uint32(0), ^uint32(0)},
   110  			{20, 18},
   111  			{20, 19},
   112  			{20, 19},
   113  			{30, 25},
   114  			{30, 29},
   115  			{30, 29},
   116  			{200, 185},
   117  			{200, 199},
   118  		}
   119  
   120  		switch lastAccount, err := fetchLastAccount(ns); {
   121  		case err != nil:
   122  			t.Errorf("fetchLastAccount: %v", err)
   123  		case lastAccount != uint32(len(accounts)-1):
   124  			t.Errorf("Number of BIP0044 accounts got %v want %v",
   125  				lastAccount+1, uint32(len(accounts)))
   126  		}
   127  
   128  		for i, a := range accounts {
   129  			account := uint32(i)
   130  
   131  			if nsMetaBucket.Get(accountNumberToAddrPoolKey(false, account)) != nil {
   132  				t.Errorf("Account %v external address pool bucket still exists", account)
   133  			}
   134  			if nsMetaBucket.Get(accountNumberToAddrPoolKey(true, account)) != nil {
   135  				t.Errorf("Account %v external address pool bucket still exists", account)
   136  			}
   137  
   138  			props, err := amgr.AccountProperties(ns, account)
   139  			if err != nil {
   140  				t.Errorf("AccountProperties: %v", err)
   141  				continue
   142  			}
   143  			if props.LastUsedExternalIndex != a.lastUsed {
   144  				t.Errorf("Account %v last used ext index got %v want %v",
   145  					account, props.LastUsedExternalIndex, a.lastUsed)
   146  			}
   147  			if props.LastUsedInternalIndex != a.lastUsed {
   148  				t.Errorf("Account %v last used int index got %v want %v",
   149  					account, props.LastUsedInternalIndex, a.lastUsed)
   150  			}
   151  		}
   152  
   153  		if ns.NestedReadBucket(usedAddrBucketName) != nil {
   154  			t.Error("Used address bucket still exists")
   155  		}
   156  
   157  		return nil
   158  	})
   159  	if err != nil {
   160  		t.Error(err)
   161  	}
   162  }
   163  
   164  func verifyV3Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
   165  	_, _, smgr, err := Open(ctx, db, chaincfg.TestNet3Params(), pubPass)
   166  	if err != nil {
   167  		t.Fatalf("Open after Upgrade failed: %v", err)
   168  	}
   169  
   170  	err = walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   171  		ns := tx.ReadBucket(wstakemgrBucketKey)
   172  
   173  		const (
   174  			ticketHashStr     = "4516ef1d548f3284c1a27b3e706c4677392031df7071ad2022050af376837033"
   175  			votingAddrStr     = "Tcu5oEdEp1W93fRT9FGSwMin7LonfRjNYe4"
   176  			ticketPurchaseHex = "01000000024bf0a303a7e6d174833d9eb761815b61f8ba8c6fa8852a6bf51c703daefc0ef60400000000ffffffff4bf0a303a7e6d174833d9eb761815b61f8ba8c6fa8852a6bf51c703daefc0ef60500000000ffffffff056f78d37a00000000000018baa914ec97b165a5f028b50fb12ae717c5f6c1b9057b5f8700000000000000000000206a1e7f686bc0e548bbb92f487db6da070e43a34117288ed59100000000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac00000000000000000000206a1e9d8e8bdc618035be32a14ab752af2e331f9abf3651074a7a000000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac00000000ad480000028ed59100000000009c480000010000006b483045022100c240bdd6a656c20e9035b839fc91faae6c766772f76149adb91a1fdcf20faf9c02203d68038b83263293f864b173c8f3f00e4371b67bf36fb9ec9f5132bdf68d2858012102adc226dec4de09a18c5a522f8f00917fb6d4eb2361a105218ac3f87d802ae3d451074a7a000000009c480000010000006a47304402205af53185f2662a30a22014b0d19760c1bfde8ec8f065b19cacab6a7abcec76a202204a2614cfcb4db3fc1c86eb0b1ca577f9039ec6db29e9c44ddcca2fe6e3c8bd5d012102adc226dec4de09a18c5a522f8f00917fb6d4eb2361a105218ac3f87d802ae3d4"
   177  
   178  			// Stored timestamp uses time.Now().  The generated database test
   179  			// artifact uses this time (2017-04-10 11:50:04 -0400 EDT).  If the
   180  			// db is ever regenerated, this expected value be updated as well.
   181  			timeStamp = 1491839404
   182  		)
   183  
   184  		// Verify ticket purchase is still present with correct info, and no
   185  		// vote bits.
   186  		ticketPurchaseHash, err := chainhash.NewHashFromStr(ticketHashStr)
   187  		if err != nil {
   188  			return err
   189  		}
   190  		rec, err := fetchSStxRecord(ns, ticketPurchaseHash, 3)
   191  		if err != nil {
   192  			return err
   193  		}
   194  		if rec.voteBitsSet || rec.voteBits != 0 || rec.voteBitsExt != nil {
   195  			t.Errorf("Ticket purchase record still has vote bits")
   196  		}
   197  		votingAddr, err := smgr.SStxAddress(ns, ticketPurchaseHash)
   198  		if err != nil {
   199  			return err
   200  		}
   201  		if votingAddr.String() != votingAddrStr {
   202  			t.Errorf("Unexpected voting address, got %v want %v",
   203  				votingAddr.String(), votingAddrStr)
   204  		}
   205  		if rec.ts.Unix() != timeStamp {
   206  			t.Errorf("Unexpected timestamp, got %v want %v", rec.ts.Unix(), timeStamp)
   207  		}
   208  		var buf bytes.Buffer
   209  		err = rec.tx.MsgTx().Serialize(&buf)
   210  		if err != nil {
   211  			return err
   212  		}
   213  		expectedBytes, err := hex.DecodeString(ticketPurchaseHex)
   214  		if err != nil {
   215  			return err
   216  		}
   217  		if !bytes.Equal(buf.Bytes(), expectedBytes) {
   218  			t.Errorf("Serialized transaction does not match expected")
   219  		}
   220  
   221  		// Verify that the agenda preferences bucket was created.
   222  		if tx.ReadBucket(agendaPreferences.defaultBucketKey()) == nil {
   223  			t.Errorf("Agenda preferences bucket was not created")
   224  		}
   225  
   226  		return nil
   227  	})
   228  	if err != nil {
   229  		t.Error(err)
   230  	}
   231  }
   232  
   233  func verifyV4Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
   234  	err := walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   235  		ns := tx.ReadBucket(waddrmgrBucketKey)
   236  		mainBucket := ns.NestedReadBucket(mainBucketName)
   237  		if mainBucket.Get(seedName) != nil {
   238  			t.Errorf("Seed was not deleted")
   239  		}
   240  		return nil
   241  	})
   242  	if err != nil {
   243  		t.Error(err)
   244  	}
   245  }
   246  
   247  func verifyV5Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
   248  	err := walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   249  		ns := tx.ReadBucket(waddrmgrBucketKey)
   250  
   251  		data := []struct {
   252  			acct             uint32
   253  			lastUsedExtChild uint32
   254  			lastUsedIntChild uint32
   255  		}{
   256  			{0, ^uint32(0), ^uint32(0)},
   257  			{1, 0, 0},
   258  			{2, 9, 9},
   259  			{3, 5, 15},
   260  			{4, 19, 20},
   261  			{5, 20, 19},
   262  			{6, 29, 30},
   263  			{7, 30, 29},
   264  			{8, 1<<31 - 1, 1<<31 - 1},
   265  			{ImportedAddrAccount, 0, 0},
   266  		}
   267  
   268  		const dbVersion = 5
   269  
   270  		for _, d := range data {
   271  			acct, err := fetchDBAccount(ns, d.acct, dbVersion)
   272  			if err != nil {
   273  				return err
   274  			}
   275  			a, ok := acct.(*dbBIP0044Account)
   276  			if !ok {
   277  				return fmt.Errorf("unknown account type %T", acct)
   278  			}
   279  			if a.lastUsedExternalIndex != d.lastUsedExtChild {
   280  				t.Errorf("Account %d last used ext child mismatch %d != %d",
   281  					d.acct, a.lastUsedExternalIndex, d.lastUsedExtChild)
   282  			}
   283  			if a.lastReturnedExternalIndex != d.lastUsedExtChild {
   284  				t.Errorf("Account %d last returned ext child mismatch %d != %d",
   285  					d.acct, a.lastReturnedExternalIndex, d.lastUsedExtChild)
   286  			}
   287  			if a.lastUsedInternalIndex != d.lastUsedIntChild {
   288  				t.Errorf("Account %d last used int child mismatch %d != %d",
   289  					d.acct, a.lastUsedInternalIndex, d.lastUsedIntChild)
   290  			}
   291  			if a.lastReturnedInternalIndex != d.lastUsedIntChild {
   292  				t.Errorf("Account %d last returned int child mismatch %d != %d",
   293  					d.acct, a.lastReturnedInternalIndex, d.lastUsedIntChild)
   294  			}
   295  		}
   296  
   297  		return nil
   298  	})
   299  	if err != nil {
   300  		t.Error(err)
   301  	}
   302  }
   303  
   304  func verifyV6Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
   305  	err := walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   306  		ns := tx.ReadBucket(wtxmgrBucketKey)
   307  
   308  		data := []*chainhash.Hash{
   309  			decodeHash("7bc19eb0bf3a57be73d6879b6c411404b14b0156353dd47c5e0456768704bfd1"),
   310  			decodeHash("a6abeb0127c347b5f38ebc2401134b324612d5b1ad9a9b8bdf6a91521842b7b1"),
   311  			decodeHash("1107757fa4f238803192c617c7b60bf35bdc57bc0fc94b408c71239ff9eaeb98"),
   312  			decodeHash("3fd00cda28c4d148e0cd38e1d646ba1365116b3ddd9a49aca4483bef80513ff9"),
   313  			decodeHash("f4bdebefaa174470182960046fa53f554108b8ea09a86de5306a14c3a0124566"),
   314  			decodeHash("bca8c2649860585f10b27d774b354ea7b80007e9ad79c090ea05596d63995cf5"),
   315  		}
   316  
   317  		c := ns.NestedReadBucket(bucketTickets).ReadCursor()
   318  		found := 0
   319  		for k, v := c.First(); k != nil; k, v = c.Next() {
   320  			var hash chainhash.Hash
   321  			copy(hash[:], k)
   322  			var foundHash *chainhash.Hash
   323  			for _, foundHash = range data {
   324  				if hash == *foundHash {
   325  					goto Found
   326  				}
   327  			}
   328  			t.Errorf("tickets bucket records %v as a ticket", &hash)
   329  			continue
   330  		Found:
   331  			found++
   332  			if extractRawTicketPickedHeight(v) != -1 {
   333  				t.Errorf("ticket purchase %v was not set with picked height -1", foundHash)
   334  			}
   335  		}
   336  		if found != len(data) {
   337  			t.Errorf("missing ticket purchase transactions from tickets bucket")
   338  		}
   339  
   340  		// Ensure that the stakebase input recorded for an unmined vote was
   341  		// removed.
   342  		stakebaseKey := canonicalOutPoint(&chainhash.Hash{}, ^uint32(0))
   343  		if ns.NestedReadBucket(bucketUnminedInputs).Get(stakebaseKey) != nil {
   344  			t.Errorf("stakebase input for unmined vote was not removed")
   345  		}
   346  
   347  		return nil
   348  	})
   349  	if err != nil {
   350  		t.Error(err)
   351  	}
   352  }
   353  
   354  func verifyV8Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
   355  	err := walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   356  		ns := tx.ReadBucket(wtxmgrBucketKey)
   357  		creditBucket := ns.NestedReadBucket(bucketCredits)
   358  		err := creditBucket.ForEach(func(k []byte, v []byte) error {
   359  			hasExpiry := fetchRawCreditHasExpiry(v, DBVersion)
   360  			if !hasExpiry {
   361  				t.Errorf("expected expiry to be set")
   362  			}
   363  			return nil
   364  		})
   365  		if err != nil {
   366  			t.Error(err)
   367  		}
   368  
   369  		unminedCreditBucket := ns.NestedReadBucket(bucketUnminedCredits)
   370  		err = unminedCreditBucket.ForEach(func(k []byte, v []byte) error {
   371  			hasExpiry := fetchRawCreditHasExpiry(v, DBVersion)
   372  
   373  			if !hasExpiry {
   374  				t.Errorf("expected expiry to be set")
   375  			}
   376  			return nil
   377  		})
   378  		if err != nil {
   379  			t.Error(err)
   380  		}
   381  
   382  		txBucket := ns.NestedReadBucket(bucketTxRecords)
   383  		minedTxWithExpiryCount := 0
   384  		minedTxWithoutExpiryCount := 0
   385  		err = txBucket.ForEach(func(k []byte, v []byte) error {
   386  			var txHash chainhash.Hash
   387  			var rec TxRecord
   388  			err := readRawTxRecordHash(k, &txHash)
   389  			if err != nil {
   390  				t.Error(err)
   391  			}
   392  			err = readRawTxRecord(&txHash, v, &rec)
   393  			if err != nil {
   394  				t.Error(err)
   395  			}
   396  
   397  			if rec.MsgTx.Expiry != wire.NoExpiryValue {
   398  				minedTxWithExpiryCount++
   399  			} else {
   400  				minedTxWithoutExpiryCount++
   401  			}
   402  			return nil
   403  		})
   404  		if err != nil {
   405  			t.Error(err)
   406  		}
   407  
   408  		if minedTxWithExpiryCount != 3 {
   409  			t.Errorf("expected 3 txs with expiries set, got %d", minedTxWithExpiryCount)
   410  		}
   411  		if minedTxWithoutExpiryCount != 3 {
   412  			t.Errorf("expected 3 txs without expiries set, got %d", minedTxWithoutExpiryCount)
   413  		}
   414  		return err
   415  	})
   416  	if err != nil {
   417  		t.Error(err)
   418  	}
   419  }
   420  
   421  // verifyV12Upgrade tests whether the upgrade to the v12 database was
   422  // successful, using the v11 test database.
   423  //
   424  // See the v11.db.go file for an explanation of the database layout and test
   425  // plan.
   426  func verifyV12Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
   427  	_, txmgr, _, err := Open(ctx, db, chaincfg.TestNet3Params(), pubPass)
   428  	if err != nil {
   429  		t.Fatalf("Open after Upgrade failed: %v", err)
   430  	}
   431  
   432  	err = walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   433  		txmgrns := tx.ReadBucket(wtxmgrBucketKey)
   434  
   435  		if b := txmgrns.NestedReadBucket(bucketTicketCommitments); b == nil {
   436  			t.Fatalf("upgrade should have created bucketTicketCommitments")
   437  		}
   438  
   439  		if b := txmgrns.NestedReadBucket(bucketTicketCommitmentsUsp); b == nil {
   440  			t.Fatalf("upgrade should have created bucketTicketCommitmentsUsp")
   441  		}
   442  
   443  		balances, err := txmgr.AccountBalances(tx, 0)
   444  		if err != nil {
   445  			t.Fatal(err)
   446  		}
   447  
   448  		expectedBalances := []struct {
   449  			acct             uint32
   450  			spendable        dcrutil.Amount
   451  			votingAuth       dcrutil.Amount
   452  			total            dcrutil.Amount
   453  			unconfirmed      dcrutil.Amount
   454  			locked           dcrutil.Amount
   455  			immatureStakeGen dcrutil.Amount
   456  			empty            bool
   457  		}{
   458  			// unmined ticket
   459  			{acct: 1, votingAuth: 1100},
   460  			{acct: 2, locked: 1000, total: 1000},
   461  
   462  			// mined ticket
   463  			{acct: 3, votingAuth: 1100},
   464  			{acct: 4, locked: 1000, total: 1000},
   465  
   466  			// mined ticket + unmined vote
   467  			{acct: 5, empty: true},
   468  			{acct: 6, total: 1300, immatureStakeGen: 1300},
   469  
   470  			// mined ticket + mined vote
   471  			{acct: 7, empty: true},
   472  			{acct: 8, total: 1300, immatureStakeGen: 1300},
   473  
   474  			// mined ticket + unmined revocation
   475  			{acct: 9, empty: true},
   476  			{acct: 10, total: 700, immatureStakeGen: 700},
   477  
   478  			// mined ticket + mined revocation
   479  			{acct: 11, empty: true},
   480  			{acct: 12, total: 700, immatureStakeGen: 700},
   481  		}
   482  
   483  		testFunc := func(testIdx int) func(t *testing.T) {
   484  			return func(t *testing.T) {
   485  				expected := expectedBalances[testIdx]
   486  				actual, has := balances[expected.acct]
   487  
   488  				if expected.empty {
   489  					if !has {
   490  						// this account was actually supposed to be empty
   491  						return
   492  					}
   493  					t.Fatalf("Balance should have been empty")
   494  				}
   495  				if !has {
   496  					t.Fatalf("Database does not have balance for expected account")
   497  				}
   498  
   499  				if actual.Spendable != expected.spendable {
   500  					t.Errorf("Actual spendable (%d) different than expected (%d)",
   501  						actual.Spendable, expected.spendable)
   502  				}
   503  				if actual.Unconfirmed != expected.unconfirmed {
   504  					t.Errorf("Actual unconfirmed (%d) different than expected (%d)",
   505  						actual.Unconfirmed, expected.unconfirmed)
   506  				}
   507  				if actual.LockedByTickets != expected.locked {
   508  					t.Errorf("Actual locked by tickets (%d) different than expected (%d)",
   509  						actual.LockedByTickets, expected.locked)
   510  				}
   511  				if actual.ImmatureStakeGeneration != expected.immatureStakeGen {
   512  					t.Errorf("Actual immature stake gen (%d) different than expected (%d)",
   513  						actual.ImmatureStakeGeneration, expected.immatureStakeGen)
   514  				}
   515  				if actual.VotingAuthority != expected.votingAuth {
   516  					t.Errorf("Actual voting authority (%d) different than expected (%d)",
   517  						actual.VotingAuthority, expected.votingAuth)
   518  				}
   519  				if actual.Total != expected.total {
   520  					t.Errorf("Actual total (%d) different than expected (%d)",
   521  						actual.Total, expected.total)
   522  				}
   523  			}
   524  		}
   525  
   526  		for i, e := range expectedBalances {
   527  			t.Run(fmt.Sprintf("acct=%d", e.acct), testFunc(i))
   528  		}
   529  
   530  		return nil
   531  	})
   532  	if err != nil {
   533  		t.Error(err)
   534  	}
   535  }
   536  
   537  func verifyV25Upgrade(ctx context.Context, t *testing.T, db walletdb.DB) {
   538  	const wantVer = 25
   539  	_, _, _, err := Open(ctx, db, chaincfg.TestNet3Params(), pubPass)
   540  	if err != nil {
   541  		t.Fatalf("Open after Upgrade failed: %v", err)
   542  	}
   543  	if err = walletdb.View(ctx, db, func(tx walletdb.ReadTx) error {
   544  		metadataBucket := tx.ReadBucket(unifiedDBMetadata{}.rootBucketKey())
   545  
   546  		dbVer, err := unifiedDBMetadata{}.getVersion(metadataBucket)
   547  		if err != nil {
   548  			return err
   549  		}
   550  		if dbVer != wantVer {
   551  			return fmt.Errorf("wanted version %d but got %d", wantVer, dbVer)
   552  		}
   553  		return nil
   554  	}); err != nil {
   555  		t.Fatal(err)
   556  	}
   557  }