From b688a6d891ddce9270d2f191a2b06defbe515e95 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 10 Dec 2018 11:57:23 +0100 Subject: [PATCH 1/4] walletdb/bdb: update interface implementation godoc Recently the Bucket and Tx interfaces were split into Read and ReadWrite versions. This commit updates the godoc for the bdb interface implementation to be consistent with this change. --- walletdb/bdb/db.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/walletdb/bdb/db.go b/walletdb/bdb/db.go index 86ba173..956a190 100644 --- a/walletdb/bdb/db.go +++ b/walletdb/bdb/db.go @@ -86,7 +86,7 @@ func (tx *transaction) DeleteTopLevelBucket(key []byte) error { // Commit commits all changes that have been made through the root bucket and // all of its sub-buckets to persistent storage. // -// This function is part of the walletdb.Tx interface implementation. +// This function is part of the walletdb.ReadWriteTx interface implementation. func (tx *transaction) Commit() error { return convertErr(tx.boltTx.Commit()) } @@ -94,7 +94,7 @@ func (tx *transaction) Commit() error { // Rollback undoes all changes that have been made to the root bucket and all of // its sub-buckets. // -// This function is part of the walletdb.Tx interface implementation. +// This function is part of the walletdb.ReadTx interface implementation. func (tx *transaction) Rollback() error { return convertErr(tx.boltTx.Rollback()) } @@ -128,7 +128,7 @@ func (b *bucket) NestedReadBucket(key []byte) walletdb.ReadBucket { // if the key is empty, or ErrIncompatibleValue if the key value is otherwise // invalid. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) CreateBucket(key []byte) (walletdb.ReadWriteBucket, error) { boltBucket, err := (*bbolt.Bucket)(b).CreateBucket(key) if err != nil { @@ -141,7 +141,7 @@ func (b *bucket) CreateBucket(key []byte) (walletdb.ReadWriteBucket, error) { // given key if it does not already exist. Returns ErrBucketNameRequired if the // key is empty or ErrIncompatibleValue if the key value is otherwise invalid. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.ReadWriteBucket, error) { boltBucket, err := (*bbolt.Bucket)(b).CreateBucketIfNotExists(key) if err != nil { @@ -154,7 +154,7 @@ func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.ReadWriteBucket, // ErrTxNotWritable if attempted against a read-only transaction and // ErrBucketNotFound if the specified bucket does not exist. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) DeleteNestedBucket(key []byte) error { return convertErr((*bbolt.Bucket)(b).DeleteBucket(key)) } @@ -167,7 +167,7 @@ func (b *bucket) DeleteNestedBucket(key []byte) error { // transaction. Attempting to access them after a transaction has ended will // likely result in an access violation. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadBucket interface implementation. func (b *bucket) ForEach(fn func(k, v []byte) error) error { return convertErr((*bbolt.Bucket)(b).ForEach(fn)) } @@ -176,7 +176,7 @@ func (b *bucket) ForEach(fn func(k, v []byte) error) error { // already exist are added and keys that already exist are overwritten. Returns // ErrTxNotWritable if attempted against a read-only transaction. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) Put(key, value []byte) error { return convertErr((*bbolt.Bucket)(b).Put(key, value)) } @@ -188,7 +188,7 @@ func (b *bucket) Put(key, value []byte) error { // transaction. Attempting to access it after a transaction has ended // will likely result in an access violation. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadBucket interface implementation. func (b *bucket) Get(key []byte) []byte { return (*bbolt.Bucket)(b).Get(key) } @@ -197,7 +197,7 @@ func (b *bucket) Get(key []byte) []byte { // not exist does not return an error. Returns ErrTxNotWritable if attempted // against a read-only transaction. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) Delete(key []byte) error { return convertErr((*bbolt.Bucket)(b).Delete(key)) } @@ -209,7 +209,7 @@ func (b *bucket) ReadCursor() walletdb.ReadCursor { // ReadWriteCursor returns a new cursor, allowing for iteration over the bucket's // key/value pairs and nested buckets in forward or backward order. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) ReadWriteCursor() walletdb.ReadWriteCursor { return (*cursor)((*bbolt.Bucket)(b).Cursor()) } @@ -228,35 +228,35 @@ type cursor bbolt.Cursor // transaction, or ErrIncompatibleValue if attempted when the cursor points to a // nested bucket. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadWriteCursor interface implementation. func (c *cursor) Delete() error { return convertErr((*bbolt.Cursor)(c).Delete()) } // First positions the cursor at the first key/value pair and returns the pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) First() (key, value []byte) { return (*bbolt.Cursor)(c).First() } // Last positions the cursor at the last key/value pair and returns the pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Last() (key, value []byte) { return (*bbolt.Cursor)(c).Last() } // Next moves the cursor one key/value pair forward and returns the new pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Next() (key, value []byte) { return (*bbolt.Cursor)(c).Next() } // Prev moves the cursor one key/value pair backward and returns the new pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Prev() (key, value []byte) { return (*bbolt.Cursor)(c).Prev() } @@ -264,7 +264,7 @@ func (c *cursor) Prev() (key, value []byte) { // Seek positions the cursor at the passed seek key. If the key does not exist, // the cursor is moved to the next key after seek. Returns the new pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Seek(seek []byte) (key, value []byte) { return (*bbolt.Cursor)(c).Seek(seek) } From 3378be750bd49bcad4649c3610069051855b0e8b Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 10 Dec 2018 11:57:23 +0100 Subject: [PATCH 2/4] walletdb/interface: add OnCommit and Tx methods This commit adds the method OnCommit to the ReadWriteTx interface, making it possible to add closures to be executed once the transaction is commitited. The method Tx is added to the ReadWriteBucket interface, for getting the bucket's underlying tx. The bdb implementation is updated to satisfy the interface change. --- walletdb/bdb/db.go | 17 +++++++++++++++++ walletdb/interface.go | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/walletdb/bdb/db.go b/walletdb/bdb/db.go index 956a190..bf34e7a 100644 --- a/walletdb/bdb/db.go +++ b/walletdb/bdb/db.go @@ -99,6 +99,14 @@ func (tx *transaction) Rollback() error { return convertErr(tx.boltTx.Rollback()) } +// OnCommit takes a function closure that will be executed when the transaction +// successfully gets committed. +// +// This function is part of the walletdb.ReadWriteTx interface implementation. +func (tx *transaction) OnCommit(f func()) { + tx.boltTx.OnCommit(f) +} + // bucket is an internal type used to represent a collection of key/value pairs // and implements the walletdb Bucket interfaces. type bucket bbolt.Bucket @@ -214,6 +222,15 @@ func (b *bucket) ReadWriteCursor() walletdb.ReadWriteCursor { return (*cursor)((*bbolt.Bucket)(b).Cursor()) } +// Tx returns the bucket's transaction. +// +// This function is part of the walletdb.ReadWriteBucket interface implementation. +func (b *bucket) Tx() walletdb.ReadWriteTx { + return &transaction{ + (*bbolt.Bucket)(b).Tx(), + } +} + // cursor represents a cursor over key/value pairs and nested buckets of a // bucket. // diff --git a/walletdb/interface.go b/walletdb/interface.go index 754e08b..f89fa91 100644 --- a/walletdb/interface.go +++ b/walletdb/interface.go @@ -42,6 +42,10 @@ type ReadWriteTx interface { // Commit commits all changes that have been on the transaction's root // buckets and all of their sub-buckets to persistent storage. Commit() error + + // OnCommit takes a function closure that will be executed when the + // transaction successfully gets committed. + OnCommit(func()) } // ReadBucket represents a bucket (a hierarchical structure within the database) @@ -119,6 +123,9 @@ type ReadWriteBucket interface { // Cursor returns a new cursor, allowing for iteration over the bucket's // key/value pairs and nested buckets in forward or backward order. ReadWriteCursor() ReadWriteCursor + + // Tx returns the bucket's transaction. + Tx() ReadWriteTx } // ReadCursor represents a bucket cursor that can be positioned at the start or From 918d9c2f882cd28123d0c052e452814c1b042e83 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 10 Dec 2018 11:57:24 +0100 Subject: [PATCH 3/4] waddrmgr/scoped_manager: add nextAddresses cache update to db tx's OnCommit() This commit makes nextAddresses add a function to the transactions OnCommit handler used to update the cache on successful database transaction commit. Before this we would risk the cache and database of get out of sync if the database transaction failed or was aborted after the cache was updated. --- waddrmgr/scoped_manager.go | 53 ++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index dfa7a8c..0f6ee3a 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -818,33 +818,46 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, } } - // Finally update the next address tracking and add the addresses to - // the cache after the newly generated addresses have been successfully - // added to the db. managedAddresses := make([]ManagedAddress, 0, len(addressInfo)) for _, info := range addressInfo { ma := info.managedAddr - s.addrs[addrKey(ma.Address().ScriptAddress())] = ma - - // Add the new managed address to the list of addresses that - // need their private keys derived when the address manager is - // next unlocked. - if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() { - s.deriveOnUnlock = append(s.deriveOnUnlock, info) - } - managedAddresses = append(managedAddresses, ma) } - // Set the last address and next address for tracking. - ma := addressInfo[len(addressInfo)-1].managedAddr - if internal { - acctInfo.nextInternalIndex = nextIndex - acctInfo.lastInternalAddr = ma - } else { - acctInfo.nextExternalIndex = nextIndex - acctInfo.lastExternalAddr = ma + // Finally, create a closure that will update the next address tracking + // and add the addresses to the cache after the newly generated + // addresses have been successfully committed to the db. + onCommit := func() { + // Since this closure will be called when the DB transaction + // gets committed, we won't longer be holding the manager's + // mutex at that point. We must therefore re-acquire it before + // continuing. + s.mtx.Lock() + defer s.mtx.Unlock() + + for _, info := range addressInfo { + ma := info.managedAddr + s.addrs[addrKey(ma.Address().ScriptAddress())] = ma + + // Add the new managed address to the list of addresses + // that need their private keys derived when the + // address manager is next unlocked. + if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() { + s.deriveOnUnlock = append(s.deriveOnUnlock, info) + } + } + + // Set the last address and next address for tracking. + ma := addressInfo[len(addressInfo)-1].managedAddr + if internal { + acctInfo.nextInternalIndex = nextIndex + acctInfo.lastInternalAddr = ma + } else { + acctInfo.nextExternalIndex = nextIndex + acctInfo.lastExternalAddr = ma + } } + ns.Tx().OnCommit(onCommit) return managedAddresses, nil } From 650f859fdb6007c7942ff922028cfa3681464e49 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 10 Dec 2018 11:57:24 +0100 Subject: [PATCH 4/4] wallet: add dryrun arg to tx create, rolling back db if set --- wallet/createtx.go | 103 ++++++++++++-------- wallet/createtx_test.go | 204 ++++++++++++++++++++++++++++++++++++++++ wallet/mock.go | 81 ++++++++++++++++ wallet/wallet.go | 16 +++- 4 files changed, 359 insertions(+), 45 deletions(-) create mode 100644 wallet/createtx_test.go create mode 100644 wallet/mock.go diff --git a/wallet/createtx.go b/wallet/createtx.go index a9580cd..9fa0973 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -101,60 +101,77 @@ func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) { // UTXO set and minconf policy. An additional output may be added to return // change to the wallet. An appropriate fee is included based on the wallet's // current relay fee. The wallet must be unlocked to create the transaction. +// +// NOTE: The dryRun argument can be set true to create a tx that doesn't alter +// the database. A tx created with this set to true will intentionally have no +// input scripts added and SHOULD NOT be broadcasted. func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, - minconf int32, feeSatPerKb btcutil.Amount) (tx *txauthor.AuthoredTx, err error) { + minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) ( + tx *txauthor.AuthoredTx, err error) { chainClient, err := w.requireChainClient() if err != nil { return nil, err } - err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { - addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + dbtx, err := w.db.BeginReadWriteTx() + if err != nil { + return nil, err + } + defer dbtx.Rollback() - // Get current block's height and hash. - bs, err := chainClient.BlockStamp() + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + + // Get current block's height and hash. + bs, err := chainClient.BlockStamp() + if err != nil { + return nil, err + } + + eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) + if err != nil { + return nil, err + } + + inputSource := makeInputSource(eligible) + changeSource := func() ([]byte, error) { + // Derive the change output script. As a hack to allow + // spending from the imported account, change addresses are + // created from account 0. + var changeAddr btcutil.Address + var err error + if account == waddrmgr.ImportedAddrAccount { + changeAddr, err = w.newChangeAddress(addrmgrNs, 0) + } else { + changeAddr, err = w.newChangeAddress(addrmgrNs, account) + } if err != nil { - return err + return nil, err } + return txscript.PayToAddrScript(changeAddr) + } + tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb, + inputSource, changeSource) + if err != nil { + return nil, err + } - eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) - if err != nil { - return err - } + // Randomize change position, if change exists, before signing. This + // doesn't affect the serialize size, so the change amount will still + // be valid. + if tx.ChangeIndex >= 0 { + tx.RandomizeChangePosition() + } - inputSource := makeInputSource(eligible) - changeSource := func() ([]byte, error) { - // Derive the change output script. As a hack to allow - // spending from the imported account, change addresses - // are created from account 0. - var changeAddr btcutil.Address - var err error - if account == waddrmgr.ImportedAddrAccount { - changeAddr, err = w.newChangeAddress(addrmgrNs, 0) - } else { - changeAddr, err = w.newChangeAddress(addrmgrNs, account) - } - if err != nil { - return nil, err - } - return txscript.PayToAddrScript(changeAddr) - } - tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb, - inputSource, changeSource) - if err != nil { - return err - } + // If a dry run was requested, we return now before adding the input + // scripts, and don't commit the database transaction. The DB will be + // rolled back when this method returns to ensure the dry run didn't + // alter the DB in any way. + if dryRun { + return tx, nil + } - // Randomize change position, if change exists, before signing. - // This doesn't affect the serialize size, so the change amount - // will still be valid. - if tx.ChangeIndex >= 0 { - tx.RandomizeChangePosition() - } - - return tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) - }) + err = tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) if err != nil { return nil, err } @@ -164,6 +181,10 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, return nil, err } + if err := dbtx.Commit(); err != nil { + return nil, err + } + if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount { changeAmount := btcutil.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value) log.Warnf("Spend from imported account produced change: moving"+ diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go new file mode 100644 index 0000000..1ffccf3 --- /dev/null +++ b/wallet/createtx_test.go @@ -0,0 +1,204 @@ +// Copyright (c) 2018 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +// TestTxToOutput checks that no new address is added to he database if we +// request a dry run of the txToOutputs call. It also makes sure a subsequent +// non-dry run call produces a similar transaction to the dry-run. +func TestTxToOutputsDryRun(t *testing.T) { + // Set up a wallet. + dir, err := ioutil.TempDir("", "createtx_test") + if err != nil { + t.Fatalf("Failed to create db dir: %v", err) + } + defer os.RemoveAll(dir) + + seed, err := hdkeychain.GenerateSeed(hdkeychain.MinSeedBytes) + if err != nil { + t.Fatalf("unable to create seed: %v", err) + } + + pubPass := []byte("hello") + privPass := []byte("world") + + loader := NewLoader(&chaincfg.TestNet3Params, dir, 250) + w, err := loader.CreateNewWallet(pubPass, privPass, seed, time.Now()) + if err != nil { + t.Fatalf("unable to create wallet: %v", err) + } + chainClient := &mockChainClient{} + w.chainClient = chainClient + if err := w.Unlock(privPass, time.After(10*time.Minute)); err != nil { + t.Fatalf("unable to unlock wallet: %v", err) + } + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0044) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + p2shAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to p2sh: %v", err) + } + + // Add an output paying to the wallet's address to the database. + txOut := wire.NewTxOut(100000, p2shAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + {}, + }, + TxOut: []*wire.TxOut{ + txOut, + }, + } + + var b bytes.Buffer + if err := incomingTx.Serialize(&b); err != nil { + t.Fatalf("unable to serialize tx: %v", err) + } + txBytes := b.Bytes() + + rec, err := wtxmgr.NewTxRecord(txBytes, time.Now()) + if err != nil { + t.Fatalf("unable to create tx record: %v", err) + } + + // The block meta will be inserted to tell the wallet this is a + // confirmed transaction. + blockHash, _ := chainhash.NewHashFromStr( + "00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") + block := &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{Hash: *blockHash, Height: 276425}, + Time: time.Unix(1387737310, 0), + } + + if err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(wtxmgrNamespaceKey) + err = w.TxStore.InsertTx(ns, rec, block) + if err != nil { + return err + } + err = w.TxStore.AddCredit(ns, rec, block, 0, false) + if err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("failed inserting tx: %v", err) + } + + // Now tell the wallet to create a transaction paying to the specified + // outputs. + txOuts := []*wire.TxOut{ + { + PkScript: p2shAddr, + Value: 10000, + }, + { + PkScript: p2shAddr, + Value: 20000, + }, + } + + // First do a few dry-runs, making sure the number of addresses in the + // database us not inflated. + dryRunTx, err := w.txToOutputs(txOuts, 0, 1, 1000, true) + if err != nil { + t.Fatalf("unable to author tx: %v", err) + } + change := dryRunTx.Tx.TxOut[dryRunTx.ChangeIndex] + + addresses, err := w.AccountAddresses(0) + if err != nil { + t.Fatalf("unable to get addresses: %v", err) + } + + if len(addresses) != 1 { + t.Fatalf("expected 1 address, found %v", len(addresses)) + } + + dryRunTx2, err := w.txToOutputs(txOuts, 0, 1, 1000, true) + if err != nil { + t.Fatalf("unable to author tx: %v", err) + } + change2 := dryRunTx2.Tx.TxOut[dryRunTx2.ChangeIndex] + + addresses, err = w.AccountAddresses(0) + if err != nil { + t.Fatalf("unable to get addresses: %v", err) + } + + if len(addresses) != 1 { + t.Fatalf("expected 1 address, found %v", len(addresses)) + } + + // The two dry-run TXs should be invalid, since they don't have + // signatures. + err = validateMsgTx( + dryRunTx.Tx, dryRunTx.PrevScripts, dryRunTx.PrevInputValues, + ) + if err == nil { + t.Fatalf("Expected tx to be invalid") + } + + err = validateMsgTx( + dryRunTx2.Tx, dryRunTx2.PrevScripts, dryRunTx2.PrevInputValues, + ) + if err == nil { + t.Fatalf("Expected tx to be invalid") + } + + // Now we do a proper, non-dry run. This should add a change address + // to the database. + tx, err := w.txToOutputs(txOuts, 0, 1, 1000, false) + if err != nil { + t.Fatalf("unable to author tx: %v", err) + } + change3 := tx.Tx.TxOut[tx.ChangeIndex] + + addresses, err = w.AccountAddresses(0) + if err != nil { + t.Fatalf("unable to get addresses: %v", err) + } + + if len(addresses) != 2 { + t.Fatalf("expected 2 addresses, found %v", len(addresses)) + } + + err = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues) + if err != nil { + t.Fatalf("Expected tx to be valid: %v", err) + } + + // Finally, we check that all the transaction were using the same + // change address. + if !bytes.Equal(change.PkScript, change2.PkScript) { + t.Fatalf("first dry-run using different change address " + + "than second") + } + if !bytes.Equal(change2.PkScript, change3.PkScript) { + t.Fatalf("dry-run using different change address " + + "than wet run") + } +} diff --git a/wallet/mock.go b/wallet/mock.go new file mode 100644 index 0000000..a626515 --- /dev/null +++ b/wallet/mock.go @@ -0,0 +1,81 @@ +package wallet + +import ( + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +type mockChainClient struct { +} + +var _ chain.Interface = (*mockChainClient)(nil) + +func (m *mockChainClient) Start() error { + return nil +} + +func (m *mockChainClient) Stop() { +} + +func (m *mockChainClient) WaitForShutdown() {} + +func (m *mockChainClient) GetBestBlock() (*chainhash.Hash, int32, error) { + return nil, 0, nil +} + +func (m *mockChainClient) GetBlock(*chainhash.Hash) (*wire.MsgBlock, error) { + return nil, nil +} + +func (m *mockChainClient) GetBlockHash(int64) (*chainhash.Hash, error) { + return nil, nil +} + +func (m *mockChainClient) GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, + error) { + return nil, nil +} + +func (m *mockChainClient) FilterBlocks(*chain.FilterBlocksRequest) ( + *chain.FilterBlocksResponse, error) { + return nil, nil +} + +func (m *mockChainClient) BlockStamp() (*waddrmgr.BlockStamp, error) { + return &waddrmgr.BlockStamp{ + Height: 500000, + Hash: chainhash.Hash{}, + Timestamp: time.Unix(1234, 0), + }, nil +} + +func (m *mockChainClient) SendRawTransaction(*wire.MsgTx, bool) ( + *chainhash.Hash, error) { + return nil, nil +} + +func (m *mockChainClient) Rescan(*chainhash.Hash, []btcutil.Address, + map[wire.OutPoint]btcutil.Address) error { + return nil +} + +func (m *mockChainClient) NotifyReceived([]btcutil.Address) error { + return nil +} + +func (m *mockChainClient) NotifyBlocks() error { + return nil +} + +func (m *mockChainClient) Notifications() <-chan interface{} { + return nil +} + +func (m *mockChainClient) BackEnd() string { + return "mock" +} diff --git a/wallet/wallet.go b/wallet/wallet.go index b823d90..f6283d9 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1148,6 +1148,7 @@ type ( outputs []*wire.TxOut minconf int32 feeSatPerKB btcutil.Amount + dryRun bool resp chan createTxResponse } createTxResponse struct { @@ -1178,7 +1179,7 @@ out: continue } tx, err := w.txToOutputs(txr.outputs, txr.account, - txr.minconf, txr.feeSatPerKB) + txr.minconf, txr.feeSatPerKB, txr.dryRun) heldUnlock.release() txr.resp <- createTxResponse{tx, err} case <-quit: @@ -1189,19 +1190,24 @@ out: } // CreateSimpleTx creates a new signed transaction spending unspent P2PKH -// outputs with at laest minconf confirmations spending to any number of +// outputs with at least minconf confirmations spending to any number of // address/amount pairs. Change and an appropriate transaction fee are // automatically included, if necessary. All transaction creation through this // function is serialized to prevent the creation of many transactions which // spend the same outputs. +// +// NOTE: The dryRun argument can be set true to create a tx that doesn't alter +// the database. A tx created with this set to true SHOULD NOT be broadcasted. func (w *Wallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut, - minconf int32, satPerKb btcutil.Amount) (*txauthor.AuthoredTx, error) { + minconf int32, satPerKb btcutil.Amount, dryRun bool) ( + *txauthor.AuthoredTx, error) { req := createTxRequest{ account: account, outputs: outputs, minconf: minconf, feeSatPerKB: satPerKb, + dryRun: dryRun, resp: make(chan createTxResponse), } w.createTxRequests <- req @@ -3209,7 +3215,9 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32, // transaction will be added to the database in order to ensure that we // continue to re-broadcast the transaction upon restarts until it has // been confirmed. - createdTx, err := w.CreateSimpleTx(account, outputs, minconf, satPerKb) + createdTx, err := w.CreateSimpleTx( + account, outputs, minconf, satPerKb, false, + ) if err != nil { return nil, err }