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:
parent
a0a5e46177
commit
9d329e111e
2 changed files with 214 additions and 0 deletions
|
@ -14,6 +14,10 @@ var versions = []migration.Version{
|
|||
Number: 1,
|
||||
Migration: nil,
|
||||
},
|
||||
{
|
||||
Number: 2,
|
||||
Migration: dropTransactionHistory,
|
||||
},
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
187
wtxmgr/migrations_test.go
Normal 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,
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue