wtxmgr/migrations: add drop transaction history migration

In this commit, we follow up our previous migration to reset our synced
block to our birthday block with another migration to drop our
transaction history. We'll need to do this to ensure that the
transaction store matches the exact state of our outputs on-chain to
prevent inadvertently crafting any invalid transactions.
This commit is contained in:
Wilmer Paulino 2018-11-08 01:45:40 -08:00
parent a0a5e46177
commit 9d329e111e
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F
2 changed files with 214 additions and 0 deletions

View file

@ -14,6 +14,10 @@ var versions = []migration.Version{
Number: 1, Number: 1,
Migration: nil, Migration: nil,
}, },
{
Number: 2,
Migration: dropTransactionHistory,
},
} }
// getLatestVersion returns the version number of the latest database version. // getLatestVersion returns the version number of the latest database version.
@ -81,3 +85,26 @@ func (m *MigrationManager) SetVersion(ns walletdb.ReadWriteBucket,
func (m *MigrationManager) Versions() []migration.Version { func (m *MigrationManager) Versions() []migration.Version {
return versions return versions
} }
// dropTransactionHistory is a migration that attempts to recreate the
// transaction store with a clean state.
func dropTransactionHistory(ns walletdb.ReadWriteBucket) error {
log.Info("Dropping wallet transaction history")
// To drop the store's transaction history, we'll need to remove all of
// the relevant descendant buckets and key/value pairs.
if err := deleteBuckets(ns); err != nil {
return err
}
if err := ns.Delete(rootMinedBalance); err != nil {
return err
}
// With everything removed, we'll now recreate our buckets.
if err := createBuckets(ns); err != nil {
return err
}
// Finally, we'll insert a 0 value for our mined balance.
return putMinedBalance(ns, 0)
}

187
wtxmgr/migrations_test.go Normal file
View file

@ -0,0 +1,187 @@
package wtxmgr
import (
"errors"
"fmt"
"testing"
"github.com/btcsuite/btcwallet/walletdb"
)
// applyMigration is a helper function that allows us to assert the state of the
// top-level bucket before and after a migration. This can be used to ensure
// the correctness of migrations.
func applyMigration(t *testing.T,
beforeMigration, afterMigration func(walletdb.ReadWriteBucket, *Store) error,
migration func(walletdb.ReadWriteBucket) error, shouldFail bool) {
t.Helper()
// We'll start by setting up our transaction store backed by a database.
store, db, teardown, err := testStore()
if err != nil {
t.Fatalf("unable to create test store: %v", err)
}
defer teardown()
// First, we'll run the beforeMigration closure, which contains the
// database modifications/assertions needed before proceeding with the
// migration.
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(namespaceKey)
if ns == nil {
return errors.New("top-level namespace does not exist")
}
return beforeMigration(ns, store)
})
if err != nil {
t.Fatalf("unable to run beforeMigration func: %v", err)
}
// Then, we'll run the migration itself and fail if it does not match
// its expected result.
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(namespaceKey)
if ns == nil {
return errors.New("top-level namespace does not exist")
}
return migration(ns)
})
if err != nil && !shouldFail {
t.Fatalf("unable to perform migration: %v", err)
} else if err == nil && shouldFail {
t.Fatal("expected migration to fail, but did not")
}
// Finally, we'll run the afterMigration closure, which contains the
// assertions needed in order to guarantee than the migration was
// successful.
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(namespaceKey)
if ns == nil {
return errors.New("top-level namespace does not exist")
}
return afterMigration(ns, store)
})
if err != nil {
t.Fatalf("unable to run afterMigration func: %v", err)
}
}
// TestMigrationDropTransactionHistory ensures that a transaction store is reset
// to a clean state after dropping its transaction history.
func TestMigrationDropTransactionHistory(t *testing.T) {
t.Parallel()
// checkTransactions is a helper function that will assert the correct
// state of the transaction store based on whether the migration has
// completed or not.
checkTransactions := func(ns walletdb.ReadWriteBucket, s *Store,
afterMigration bool) error {
// We should see one confirmed unspent output before the
// migration, and none after.
utxos, err := s.UnspentOutputs(ns)
if err != nil {
return err
}
if len(utxos) == 0 && !afterMigration {
return errors.New("expected to find 1 utxo, found none")
}
if len(utxos) > 0 && afterMigration {
return fmt.Errorf("expected to find 0 utxos, found %d",
len(utxos))
}
// We should see one unconfirmed transaction before the
// migration, and none after.
unconfirmedTxs, err := s.UnminedTxs(ns)
if err != nil {
return err
}
if len(unconfirmedTxs) == 0 && !afterMigration {
return errors.New("expected to find 1 unconfirmed " +
"transaction, found none")
}
if len(unconfirmedTxs) > 0 && afterMigration {
return fmt.Errorf("expected to find 0 unconfirmed "+
"transactions, found %d", len(unconfirmedTxs))
}
// We should have a non-zero balance before the migration, and
// zero after.
minedBalance, err := fetchMinedBalance(ns)
if err != nil {
return err
}
if minedBalance == 0 && !afterMigration {
return errors.New("expected non-zero balance before " +
"migration")
}
if minedBalance > 0 && afterMigration {
return fmt.Errorf("expected zero balance after "+
"migration, got %d", minedBalance)
}
return nil
}
beforeMigration := func(ns walletdb.ReadWriteBucket, s *Store) error {
// We'll start by adding two transactions to the store: a
// confirmed transaction and an unconfirmed transaction one.
// The confirmed transaction will spend from a coinbase output,
// while the unconfirmed will spend an output from the confirmed
// transaction.
cb := newCoinBase(1e8)
cbRec, err := NewTxRecordFromMsgTx(cb, timeNow())
if err != nil {
return err
}
b := &BlockMeta{Block: Block{Height: 100}}
confirmedSpend := spendOutput(&cbRec.Hash, 0, 5e7, 4e7)
confirmedSpendRec, err := NewTxRecordFromMsgTx(
confirmedSpend, timeNow(),
)
if err := s.InsertTx(ns, confirmedSpendRec, b); err != nil {
return err
}
err = s.AddCredit(ns, confirmedSpendRec, b, 1, true)
if err != nil {
return err
}
unconfimedSpend := spendOutput(
&confirmedSpendRec.Hash, 0, 5e6, 5e6,
)
unconfirmedSpendRec, err := NewTxRecordFromMsgTx(
unconfimedSpend, timeNow(),
)
if err != nil {
return err
}
if err := s.InsertTx(ns, unconfirmedSpendRec, nil); err != nil {
return err
}
err = s.AddCredit(ns, unconfirmedSpendRec, nil, 1, true)
if err != nil {
return err
}
// Ensure these transactions exist within the store.
return checkTransactions(ns, s, false)
}
afterMigration := func(ns walletdb.ReadWriteBucket, s *Store) error {
// Assuming the migration was successful, we should see that the
// store no longer has the transaction history prior to the
// migration.
return checkTransactions(ns, s, true)
}
// We can now apply the migration and expect it not to fail.
applyMigration(
t, beforeMigration, afterMigration, dropTransactionHistory,
false,
)
}