Merge pull request #573 from wpaulino/notify-received
wallet: notify wallet upon relevant transaction confirmation
This commit is contained in:
commit
4c01c0878c
12 changed files with 470 additions and 44 deletions
|
@ -1,6 +1,7 @@
|
||||||
package waddrmgr
|
package waddrmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -26,6 +27,10 @@ var versions = []migration.Version{
|
||||||
Number: 6,
|
Number: 6,
|
||||||
Migration: populateBirthdayBlock,
|
Migration: populateBirthdayBlock,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Number: 7,
|
||||||
|
Migration: resetSyncedBlockToBirthday,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLatestVersion returns the version number of the latest database version.
|
// getLatestVersion returns the version number of the latest database version.
|
||||||
|
@ -350,3 +355,20 @@ func populateBirthdayBlock(ns walletdb.ReadWriteBucket) error {
|
||||||
Hash: *birthdayHash,
|
Hash: *birthdayHash,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetSyncedBlockToBirthday is a migration that resets the wallet's currently
|
||||||
|
// synced block to its birthday block. This essentially serves as a migration to
|
||||||
|
// force a rescan of the wallet.
|
||||||
|
func resetSyncedBlockToBirthday(ns walletdb.ReadWriteBucket) error {
|
||||||
|
syncBucket := ns.NestedReadWriteBucket(syncBucketName)
|
||||||
|
if syncBucket == nil {
|
||||||
|
return errors.New("sync bucket does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
birthdayBlock, err := fetchBirthdayBlock(ns)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return putSyncedTo(ns, &birthdayBlock)
|
||||||
|
}
|
||||||
|
|
|
@ -215,3 +215,84 @@ func TestMigrationPopulateBirthdayBlockEstimateTooFar(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMigrationResetSyncedBlockToBirthday ensures that the wallet properly sees
|
||||||
|
// its synced to block as the birthday block after resetting it.
|
||||||
|
func TestMigrationResetSyncedBlockToBirthday(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var birthdayBlock BlockStamp
|
||||||
|
beforeMigration := func(ns walletdb.ReadWriteBucket) error {
|
||||||
|
// To test this migration, we'll assume we're synced to a chain
|
||||||
|
// of 100 blocks, with our birthday being the 50th block.
|
||||||
|
block := &BlockStamp{}
|
||||||
|
for i := int32(1); i < 100; i++ {
|
||||||
|
block.Height = i
|
||||||
|
blockHash := bytes.Repeat([]byte(string(i)), 32)
|
||||||
|
copy(block.Hash[:], blockHash)
|
||||||
|
if err := putSyncedTo(ns, block); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const birthdayHeight = 50
|
||||||
|
birthdayHash, err := fetchBlockHash(ns, birthdayHeight)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
birthdayBlock = BlockStamp{
|
||||||
|
Hash: *birthdayHash, Height: birthdayHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
return putBirthdayBlock(ns, birthdayBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterMigration := func(ns walletdb.ReadWriteBucket) error {
|
||||||
|
// After the migration has succeeded, we should see that the
|
||||||
|
// database's synced block now reflects the birthday block.
|
||||||
|
syncedBlock, err := fetchSyncedTo(ns)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if syncedBlock.Height != birthdayBlock.Height {
|
||||||
|
return fmt.Errorf("expected synced block height %d, "+
|
||||||
|
"got %d", birthdayBlock.Height,
|
||||||
|
syncedBlock.Height)
|
||||||
|
}
|
||||||
|
if !syncedBlock.Hash.IsEqual(&birthdayBlock.Hash) {
|
||||||
|
return fmt.Errorf("expected synced block height %v, "+
|
||||||
|
"got %v", birthdayBlock.Hash, syncedBlock.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can now apply the migration and expect it not to fail.
|
||||||
|
applyMigration(
|
||||||
|
t, beforeMigration, afterMigration, resetSyncedBlockToBirthday,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMigrationResetSyncedBlockToBirthdayWithNoBirthdayBlock ensures that we
|
||||||
|
// cannot reset our synced to block to our birthday block if one isn't
|
||||||
|
// available.
|
||||||
|
func TestMigrationResetSyncedBlockToBirthdayWithNoBirthdayBlock(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// To replicate the scenario where the database is not aware of a
|
||||||
|
// birthday block, we won't set one. This should cause the migration to
|
||||||
|
// fail.
|
||||||
|
beforeMigration := func(walletdb.ReadWriteBucket) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
afterMigration := func(walletdb.ReadWriteBucket) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
applyMigration(
|
||||||
|
t, beforeMigration, afterMigration, resetSyncedBlockToBirthday,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -170,6 +170,21 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
|
||||||
" %v from imported account into default account.", changeAmount)
|
" %v from imported account into default account.", changeAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, we'll request the backend to notify us of the transaction
|
||||||
|
// that pays to the change address, if there is one, when it confirms.
|
||||||
|
if tx.ChangeIndex >= 0 {
|
||||||
|
changePkScript := tx.Tx.TxOut[tx.ChangeIndex].PkScript
|
||||||
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
||||||
|
changePkScript, w.chainParams,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := chainClient.NotifyReceived(addrs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tx, nil
|
return tx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/btcsuite/btclog"
|
"github.com/btcsuite/btclog"
|
||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
"github.com/btcsuite/btcwallet/walletdb/migration"
|
"github.com/btcsuite/btcwallet/walletdb/migration"
|
||||||
|
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// log is a logger that is initialized with no output filters. This
|
// log is a logger that is initialized with no output filters. This
|
||||||
|
@ -34,6 +35,7 @@ func UseLogger(logger btclog.Logger) {
|
||||||
|
|
||||||
migration.UseLogger(logger)
|
migration.UseLogger(logger)
|
||||||
waddrmgr.UseLogger(logger)
|
waddrmgr.UseLogger(logger)
|
||||||
|
wtxmgr.UseLogger(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogClosure is a closure that can be printed with %v to be used to
|
// LogClosure is a closure that can be printed with %v to be used to
|
||||||
|
|
|
@ -2786,6 +2786,15 @@ func (w *Wallet) resendUnminedTxs() {
|
||||||
for _, tx := range txs {
|
for _, tx := range txs {
|
||||||
resp, err := chainClient.SendRawTransaction(tx, false)
|
resp, err := chainClient.SendRawTransaction(tx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// If the transaction has already been accepted into the
|
||||||
|
// mempool, we can continue without logging the error.
|
||||||
|
switch {
|
||||||
|
case strings.Contains(err.Error(), "already have transaction"):
|
||||||
|
fallthrough
|
||||||
|
case strings.Contains(err.Error(), "txn-already-known"):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("Could not resend transaction %v: %v",
|
log.Debugf("Could not resend transaction %v: %v",
|
||||||
tx.TxHash(), err)
|
tx.TxHash(), err)
|
||||||
|
|
||||||
|
@ -2817,9 +2826,12 @@ func (w *Wallet) resendUnminedTxs() {
|
||||||
|
|
||||||
// As the transaction was rejected, we'll attempt to
|
// As the transaction was rejected, we'll attempt to
|
||||||
// remove the unmined transaction all together.
|
// remove the unmined transaction all together.
|
||||||
// Otherwise, we'll keep attempting to rebroadcast
|
// Otherwise, we'll keep attempting to rebroadcast this,
|
||||||
// this, and we may be computing our balance
|
// and we may be computing our balance incorrectly if
|
||||||
// incorrectly if this tx credits or debits to us.
|
// this transaction credits or debits to us.
|
||||||
|
//
|
||||||
|
// TODO(wilmer): if already confirmed, move to mined
|
||||||
|
// bucket - need to determine the confirmation block.
|
||||||
err := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error {
|
err := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error {
|
||||||
txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey)
|
txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey)
|
||||||
|
|
||||||
|
@ -2952,6 +2964,11 @@ func (w *Wallet) NewChangeAddress(account uint32,
|
||||||
return addr, nil
|
return addr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newChangeAddress returns a new change address for the wallet.
|
||||||
|
//
|
||||||
|
// NOTE: This method requires the caller to use the backend's NotifyReceived
|
||||||
|
// method in order to detect when an on-chain transaction pays to the address
|
||||||
|
// being created.
|
||||||
func (w *Wallet) newChangeAddress(addrmgrNs walletdb.ReadWriteBucket,
|
func (w *Wallet) newChangeAddress(addrmgrNs walletdb.ReadWriteBucket,
|
||||||
account uint32) (btcutil.Address, error) {
|
account uint32) (btcutil.Address, error) {
|
||||||
|
|
||||||
|
@ -3305,7 +3322,7 @@ func (w *Wallet) PublishTransaction(tx *wire.MsgTx) error {
|
||||||
// from the database (along with cleaning up all inputs used, and outputs
|
// from the database (along with cleaning up all inputs used, and outputs
|
||||||
// created) if the transaction is rejected by the back end.
|
// created) if the transaction is rejected by the back end.
|
||||||
func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
|
func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
|
||||||
server, err := w.requireChainClient()
|
chainClient, err := w.requireChainClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -3325,7 +3342,33 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
txid, err := server.SendRawTransaction(tx, false)
|
// We'll also ask to be notified of the transaction once it confirms
|
||||||
|
// on-chain. This is done outside of the database transaction to prevent
|
||||||
|
// backend interaction within it.
|
||||||
|
//
|
||||||
|
// NOTE: In some cases, it's possible that the transaction to be
|
||||||
|
// broadcast is not directly relevant to the user's wallet, e.g.,
|
||||||
|
// multisig. In either case, we'll still ask to be notified of when it
|
||||||
|
// confirms to maintain consistency.
|
||||||
|
//
|
||||||
|
// TODO(wilmer): import script as external if the address does not
|
||||||
|
// belong to the wallet to handle confs during restarts?
|
||||||
|
for _, txOut := range tx.TxOut {
|
||||||
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
||||||
|
txOut.PkScript, w.chainParams,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// Non-standard outputs can safely be skipped because
|
||||||
|
// they're not supported by the wallet.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chainClient.NotifyReceived(addrs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txid, err := chainClient.SendRawTransaction(tx, false)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return txid, nil
|
return txid, nil
|
||||||
|
|
77
wtxmgr/db.go
77
wtxmgr/db.go
|
@ -1311,50 +1311,42 @@ func createStore(ns walletdb.ReadWriteBucket) error {
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = ns.CreateBucket(bucketBlocks)
|
// Finally, create all of our required descendant buckets.
|
||||||
if err != nil {
|
return createBuckets(ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBuckets creates all of the descendants buckets required for the
|
||||||
|
// transaction store to properly carry its duties.
|
||||||
|
func createBuckets(ns walletdb.ReadWriteBucket) error {
|
||||||
|
if _, err := ns.CreateBucket(bucketBlocks); err != nil {
|
||||||
str := "failed to create blocks bucket"
|
str := "failed to create blocks bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
if _, err := ns.CreateBucket(bucketTxRecords); err != nil {
|
||||||
_, err = ns.CreateBucket(bucketTxRecords)
|
|
||||||
if err != nil {
|
|
||||||
str := "failed to create tx records bucket"
|
str := "failed to create tx records bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
if _, err := ns.CreateBucket(bucketCredits); err != nil {
|
||||||
_, err = ns.CreateBucket(bucketCredits)
|
|
||||||
if err != nil {
|
|
||||||
str := "failed to create credits bucket"
|
str := "failed to create credits bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
if _, err := ns.CreateBucket(bucketDebits); err != nil {
|
||||||
_, err = ns.CreateBucket(bucketDebits)
|
|
||||||
if err != nil {
|
|
||||||
str := "failed to create debits bucket"
|
str := "failed to create debits bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
if _, err := ns.CreateBucket(bucketUnspent); err != nil {
|
||||||
_, err = ns.CreateBucket(bucketUnspent)
|
|
||||||
if err != nil {
|
|
||||||
str := "failed to create unspent bucket"
|
str := "failed to create unspent bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
if _, err := ns.CreateBucket(bucketUnmined); err != nil {
|
||||||
_, err = ns.CreateBucket(bucketUnmined)
|
|
||||||
if err != nil {
|
|
||||||
str := "failed to create unmined bucket"
|
str := "failed to create unmined bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
if _, err := ns.CreateBucket(bucketUnminedCredits); err != nil {
|
||||||
_, err = ns.CreateBucket(bucketUnminedCredits)
|
|
||||||
if err != nil {
|
|
||||||
str := "failed to create unmined credits bucket"
|
str := "failed to create unmined credits bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
if _, err := ns.CreateBucket(bucketUnminedInputs); err != nil {
|
||||||
_, err = ns.CreateBucket(bucketUnminedInputs)
|
|
||||||
if err != nil {
|
|
||||||
str := "failed to create unmined inputs bucket"
|
str := "failed to create unmined inputs bucket"
|
||||||
return storeError(ErrDatabase, str, err)
|
return storeError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
@ -1362,6 +1354,45 @@ func createStore(ns walletdb.ReadWriteBucket) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteBuckets deletes all of the descendants buckets required for the
|
||||||
|
// transaction store to properly carry its duties.
|
||||||
|
func deleteBuckets(ns walletdb.ReadWriteBucket) error {
|
||||||
|
if err := ns.DeleteNestedBucket(bucketBlocks); err != nil {
|
||||||
|
str := "failed to delete blocks bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
if err := ns.DeleteNestedBucket(bucketTxRecords); err != nil {
|
||||||
|
str := "failed to delete tx records bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
if err := ns.DeleteNestedBucket(bucketCredits); err != nil {
|
||||||
|
str := "failed to delete credits bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
if err := ns.DeleteNestedBucket(bucketDebits); err != nil {
|
||||||
|
str := "failed to delete debits bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
if err := ns.DeleteNestedBucket(bucketUnspent); err != nil {
|
||||||
|
str := "failed to delete unspent bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
if err := ns.DeleteNestedBucket(bucketUnmined); err != nil {
|
||||||
|
str := "failed to delete unmined bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
if err := ns.DeleteNestedBucket(bucketUnminedCredits); err != nil {
|
||||||
|
str := "failed to delete unmined credits bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
if err := ns.DeleteNestedBucket(bucketUnminedInputs); err != nil {
|
||||||
|
str := "failed to delete unmined inputs bucket"
|
||||||
|
return storeError(ErrDatabase, str, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// putVersion modifies the version of the store to reflect the given version
|
// putVersion modifies the version of the store to reflect the given version
|
||||||
// number.
|
// number.
|
||||||
func putVersion(ns walletdb.ReadWriteBucket, version uint32) error {
|
func putVersion(ns walletdb.ReadWriteBucket, version uint32) error {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by an ISC
|
// Use of this source code is governed by an ISC
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package wtxmgr_test
|
package wtxmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -11,29 +11,28 @@ import (
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Spends: bogus
|
// Spends: bogus
|
||||||
// Outputs: 10 BTC
|
// Outputs: 10 BTC
|
||||||
exampleTxRecordA *wtxmgr.TxRecord
|
exampleTxRecordA *TxRecord
|
||||||
|
|
||||||
// Spends: A:0
|
// Spends: A:0
|
||||||
// Outputs: 5 BTC, 5 BTC
|
// Outputs: 5 BTC, 5 BTC
|
||||||
exampleTxRecordB *wtxmgr.TxRecord
|
exampleTxRecordB *TxRecord
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
tx := spendOutput(&chainhash.Hash{}, 0, 10e8)
|
tx := spendOutput(&chainhash.Hash{}, 0, 10e8)
|
||||||
rec, err := wtxmgr.NewTxRecordFromMsgTx(tx, timeNow())
|
rec, err := NewTxRecordFromMsgTx(tx, timeNow())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
exampleTxRecordA = rec
|
exampleTxRecordA = rec
|
||||||
|
|
||||||
tx = spendOutput(&exampleTxRecordA.Hash, 0, 5e8, 5e8)
|
tx = spendOutput(&exampleTxRecordA.Hash, 0, 5e8, 5e8)
|
||||||
rec, err = wtxmgr.NewTxRecordFromMsgTx(tx, timeNow())
|
rec, err = NewTxRecordFromMsgTx(tx, timeNow())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -183,12 +182,12 @@ func Example_basicUsage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and open the transaction store in the provided namespace.
|
// Create and open the transaction store in the provided namespace.
|
||||||
err = wtxmgr.Create(b)
|
err = Create(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s, err := wtxmgr.Open(b, &chaincfg.TestNet3Params)
|
s, err := Open(b, &chaincfg.TestNet3Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -9,12 +9,17 @@ import "github.com/btcsuite/btclog"
|
||||||
// log is a logger that is initialized with no output filters. This
|
// log is a logger that is initialized with no output filters. This
|
||||||
// means the package will not perform any logging by default until the caller
|
// means the package will not perform any logging by default until the caller
|
||||||
// requests it.
|
// requests it.
|
||||||
var log = btclog.Disabled
|
var log btclog.Logger
|
||||||
|
|
||||||
|
// The default amount of logging is none.
|
||||||
|
func init() {
|
||||||
|
DisableLog()
|
||||||
|
}
|
||||||
|
|
||||||
// DisableLog disables all library log output. Logging output is disabled
|
// DisableLog disables all library log output. Logging output is disabled
|
||||||
// by default until either UseLogger or SetLogWriter are called.
|
// by default until either UseLogger or SetLogWriter are called.
|
||||||
func DisableLog() {
|
func DisableLog() {
|
||||||
log = btclog.Disabled
|
UseLogger(btclog.Disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseLogger uses a specified Logger to output package logging info.
|
// UseLogger uses a specified Logger to output package logging info.
|
||||||
|
@ -23,3 +28,20 @@ func DisableLog() {
|
||||||
func UseLogger(logger btclog.Logger) {
|
func UseLogger(logger btclog.Logger) {
|
||||||
log = logger
|
log = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogClosure is a closure that can be printed with %v to be used to
|
||||||
|
// generate expensive-to-create data for a detailed log level and avoid doing
|
||||||
|
// the work if the data isn't printed.
|
||||||
|
type logClosure func() string
|
||||||
|
|
||||||
|
// String invokes the log closure and returns the results string.
|
||||||
|
func (c logClosure) String() string {
|
||||||
|
return c()
|
||||||
|
}
|
||||||
|
|
||||||
|
// newLogClosure returns a new closure over the passed function which allows
|
||||||
|
// it to be used as a parameter in a logging function that is only invoked when
|
||||||
|
// the logging level is such that the message will actually be logged.
|
||||||
|
func newLogClosure(c func() string) logClosure {
|
||||||
|
return logClosure(c)
|
||||||
|
}
|
||||||
|
|
|
@ -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
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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by an ISC
|
// Use of this source code is governed by an ISC
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package wtxmgr_test
|
package wtxmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -16,7 +16,6 @@ import (
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
. "github.com/btcsuite/btcwallet/wtxmgr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type queryState struct {
|
type queryState struct {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by an ISC
|
// Use of this source code is governed by an ISC
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package wtxmgr_test
|
package wtxmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -19,8 +19,6 @@ import (
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
|
||||||
. "github.com/btcsuite/btcwallet/wtxmgr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Received transaction output for mainnet outpoint
|
// Received transaction output for mainnet outpoint
|
||||||
|
@ -1572,7 +1570,7 @@ func testInsertMempoolDoubleSpendTx(t *testing.T, first bool) {
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var confirmedSpendRec *wtxmgr.TxRecord
|
var confirmedSpendRec *TxRecord
|
||||||
if first {
|
if first {
|
||||||
confirmedSpendRec = firstSpendRec
|
confirmedSpendRec = firstSpendRec
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue