From 4656a00705314f1dbf91db6cf3b58bb18a205cc2 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Mon, 16 Jan 2017 19:19:02 -0500 Subject: [PATCH] Improve wallet atomicity. This changes the database access APIs and each of the "manager" packages (waddrmgr/wstakemgr) so that transactions are opened (only) by the wallet package and the namespace buckets that each manager expects to operate on are passed in as parameters. This helps improve the atomicity situation as it means that many calls to these APIs can be grouped together into a single database transaction. This change does not attempt to completely fix the "half-processed" block problem. Mined transactions are still added to the wallet database under their own database transaction as this is how they are notified by the consensus JSON-RPC server (as loose transactions, without the rest of the block that contains them). It will make updating to a fixed notification model significantly easier, as the same "manager" APIs can still be used, but grouped into a single atomic transaction. --- LICENSE | 3 +- cmd/dropwtxmgr/main.go | 4 +- goclean.sh | 2 +- rpc/legacyrpc/methods.go | 240 ++--- rpc/rpcserver/server.go | 71 +- waddrmgr/address.go | 21 +- waddrmgr/common_test.go | 63 +- waddrmgr/db.go | 458 ++++----- waddrmgr/manager.go | 932 ++++++++--------- waddrmgr/manager_test.go | 485 ++++++--- waddrmgr/sync.go | 15 +- wallet/chainntfns.go | 110 +- wallet/common.go | 88 ++ wallet/createtx.go | 107 +- wallet/multisig.go | 105 ++ wallet/notifications.go | 51 +- wallet/rescan.go | 36 +- wallet/sync.go | 710 +++++++++++++ wallet/unstable.go | 44 + wallet/utxos.go | 90 ++ wallet/wallet.go | 1740 +++++++++++++++++++------------- walletdb/bdb/db.go | 271 ++--- walletdb/bdb/interface_test.go | 16 +- walletdb/interface.go | 267 ++--- walletsetup.go | 16 +- wtxmgr/db.go | 440 ++++---- wtxmgr/error.go | 8 +- wtxmgr/query.go | 229 ++--- wtxmgr/tx.go | 199 ++-- wtxmgr/unconfirmed.go | 36 +- 30 files changed, 4069 insertions(+), 2788 deletions(-) create mode 100644 wallet/common.go create mode 100644 wallet/multisig.go create mode 100644 wallet/sync.go create mode 100644 wallet/unstable.go create mode 100644 wallet/utxos.go diff --git a/LICENSE b/LICENSE index 49de919..53ba0c5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ ISC License -Copyright (c) 2013-2016 The btcsuite developers +Copyright (c) 2013-2017 The btcsuite developers +Copyright (c) 2015-2016 The Decred developers Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/cmd/dropwtxmgr/main.go b/cmd/dropwtxmgr/main.go index 1328c50..990b5c5 100644 --- a/cmd/dropwtxmgr/main.go +++ b/cmd/dropwtxmgr/main.go @@ -103,7 +103,9 @@ func mainInt() int { } defer db.Close() fmt.Println("Dropping wtxmgr namespace") - err = db.DeleteNamespace(wtxmgrNamespace) + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + return tx.DeleteTopLevelBucket(wtxmgrNamespace) + }) if err != nil && err != walletdb.ErrBucketNotFound { fmt.Println("Failed to drop namespace:", err) return 1 diff --git a/goclean.sh b/goclean.sh index cb1f605..bdb6c69 100755 --- a/goclean.sh +++ b/goclean.sh @@ -10,7 +10,7 @@ set -ex # Automatic checks test -z "$(go fmt $(glide novendor) | tee /dev/stderr)" # test -z "$(goimports -l -w . | tee /dev/stderr)" -test -z "$(for package in $(glide novendor); do golint $package; done | grep -v 'ALL_CAPS\|OP_\|NewFieldVal\|RpcCommand\|RpcRawCommand\|RpcSend\|Dns\|api.pb.go\|StartConsensusRpc\|factory_test.go\|legacy' | tee /dev/stderr)" +test -z "$(for package in $(glide novendor); do golint $package; done | grep -v 'ALL_CAPS\|OP_\|NewFieldVal\|RpcCommand\|RpcRawCommand\|RpcSend\|Dns\|api.pb.go\|StartConsensusRpc\|factory_test.go\|legacy\|UnstableAPI' | tee /dev/stderr)" test -z "$(go vet $(glide novendor) 2>&1 | grep -v '^exit status \|Example\|newestSha\| not a string in call to Errorf$' | tee /dev/stderr)" env GORACE="halt_on_error=1" go test -race $(glide novendor) diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index dc6ca2d..c2ed7d3 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -1,4 +1,5 @@ -// Copyright (c) 2013-2016 The btcsuite developers +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -29,13 +30,6 @@ import ( "github.com/btcsuite/btcwallet/wtxmgr" ) -const ( - // maxEmptyAccounts is the number of accounts to scan even if they have no - // transaction history. This is a deviation from BIP044 to make account - // creation easier by allowing a limited number of empty accounts. - maxEmptyAccounts = 100 -) - // confirmed checks whether a transaction at height txHeight has met minconf // confirmations for a blockchain at height curHeight. func confirmed(minconf, txHeight, curHeight int32) bool { @@ -131,9 +125,8 @@ var rpcHandlers = map[string]struct { "setaccount": {handler: unsupported, noHelp: true}, // Extensions to the reference client JSON-RPC API - "createnewaccount": {handler: createNewAccount}, - "exportwatchingwallet": {handler: exportWatchingWallet}, - "getbestblock": {handler: getBestBlock}, + "createnewaccount": {handler: createNewAccount}, + "getbestblock": {handler: getBestBlock}, // This was an extension but the reference implementation added it as // well, but with a different API (no account parameter). It's listed // here because it hasn't been update to use the reference @@ -292,25 +285,17 @@ func makeMultiSigScript(w *wallet.Wallet, keys []string, nRequired int) ([]byte, switch addr := a.(type) { case *btcutil.AddressPubKey: keysesPrecious[i] = addr - case *btcutil.AddressPubKeyHash: - ainfo, err := w.Manager.Address(addr) - if err != nil { - return nil, err - } - - apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) - - // This will be an addresspubkey - a, err := decodeAddress(apkinfo.ExportPubKey(), - w.ChainParams()) - if err != nil { - return nil, err - } - - apk := a.(*btcutil.AddressPubKey) - keysesPrecious[i] = apk default: - return nil, err + pubKey, err := w.PubKeyForAddress(addr) + if err != nil { + return nil, err + } + pubKeyAddr, err := btcutil.NewAddressPubKey( + pubKey.SerializeCompressed(), w.ChainParams()) + if err != nil { + return nil, err + } + keysesPrecious[i] = pubKeyAddr } } @@ -327,23 +312,26 @@ func addMultiSigAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) return nil, &ErrNotImportedAccount } - script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) - if err != nil { - return nil, ParseError{err} + secp256k1Addrs := make([]btcutil.Address, len(cmd.Keys)) + for i, k := range cmd.Keys { + addr, err := decodeAddress(k, w.ChainParams()) + if err != nil { + return nil, ParseError{err} + } + secp256k1Addrs[i] = addr } - // TODO(oga) blockstamp current block? - bs := &waddrmgr.BlockStamp{ - Hash: *w.ChainParams().GenesisHash, - Height: 0, - } - - addr, err := w.Manager.ImportScript(script, bs) + script, err := w.MakeMultiSigScript(secp256k1Addrs, cmd.NRequired) if err != nil { return nil, err } - return addr.Address().EncodeAddress(), nil + p2shAddr, err := w.ImportP2SHRedeemScript(script) + if err != nil { + return nil, err + } + + return p2shAddr.EncodeAddress(), nil } // createMultiSig handles an createmultisig request by returning a @@ -400,40 +388,27 @@ func dumpWallet(icmd interface{}, w *wallet.Wallet) (interface{}, error) { return keys, err } -// exportWatchingWallet handles an exportwatchingwallet request by exporting the -// current wallet as a watching wallet (with no private keys), and returning -// base64-encoding of serialized account files. -func exportWatchingWallet(icmd interface{}, w *wallet.Wallet) (interface{}, error) { - cmd := icmd.(*btcjson.ExportWatchingWalletCmd) - - if cmd.Account != nil && *cmd.Account != "*" { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCWallet, - Message: "Individual accounts can not be exported as watching-only", - } - } - - return w.ExportWatchingWallet() -} - // getAddressesByAccount handles a getaddressesbyaccount request by returning // all addresses for an account, or an error if the requested account does // not exist. func getAddressesByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*btcjson.GetAddressesByAccountCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } - var addrStrs []string - err = w.Manager.ForEachAccountAddress(account, - func(maddr waddrmgr.ManagedAddress) error { - addrStrs = append(addrStrs, maddr.Address().EncodeAddress()) - return nil - }) - return addrStrs, err + addrs, err := w.AccountAddresses(account) + if err != nil { + return nil, err + } + + addrStrs := make([]string, len(addrs)) + for i, a := range addrs { + addrStrs[i] = a.EncodeAddress() + } + return addrStrs, nil } // getBalance handles a getbalance request by returning the balance for an @@ -455,7 +430,7 @@ func getBalance(icmd interface{}, w *wallet.Wallet) (interface{}, error) { } } else { var account uint32 - account, err = w.Manager.LookupAccount(accountName) + account, err = w.AccountNumber(accountName) if err != nil { return nil, err } @@ -553,12 +528,12 @@ func getAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { } // Fetch the associated account - account, err := w.Manager.AddrAccount(addr) + account, err := w.AccountOfAddress(addr) if err != nil { return nil, &ErrAddressNotInWallet } - acctName, err := w.Manager.AccountName(account) + acctName, err := w.AccountName(account) if err != nil { return nil, &ErrAccountNameNotFound } @@ -574,7 +549,7 @@ func getAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func getAccountAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*btcjson.GetAccountAddressCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } @@ -595,7 +570,7 @@ func getUnconfirmedBalance(icmd interface{}, w *wallet.Wallet) (interface{}, err if cmd.Account != nil { acctName = *cmd.Account } - account, err := w.Manager.LookupAccount(acctName) + account, err := w.AccountNumber(acctName) if err != nil { return nil, err } @@ -664,23 +639,7 @@ func createNewAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { return nil, &ErrReservedAccountName } - // Check that we are within the maximum allowed non-empty accounts limit. - account, err := w.Manager.LastAccount() - if err != nil { - return nil, err - } - if account > maxEmptyAccounts { - used, err := w.AccountUsed(account) - if err != nil { - return nil, err - } - if !used { - return nil, errors.New("cannot create account: " + - "previous account has no transaction history") - } - } - - _, err = w.NextAccount(cmd.Account) + _, err := w.NextAccount(cmd.Account) if waddrmgr.IsError(err, waddrmgr.ErrLocked) { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCWalletUnlockNeeded, @@ -703,7 +662,7 @@ func renameAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { } // Check that given account exists - account, err := w.Manager.LookupAccount(cmd.OldAccount) + account, err := w.AccountNumber(cmd.OldAccount) if err != nil { return nil, err } @@ -722,7 +681,7 @@ func getNewAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { if cmd.Account != nil { acctName = *cmd.Account } - account, err := w.Manager.LookupAccount(acctName) + account, err := w.AccountNumber(acctName) if err != nil { return nil, err } @@ -747,7 +706,7 @@ func getRawChangeAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error if cmd.Account != nil { acctName = *cmd.Account } - account, err := w.Manager.LookupAccount(acctName) + account, err := w.AccountNumber(acctName) if err != nil { return nil, err } @@ -765,17 +724,23 @@ func getRawChangeAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error func getReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*btcjson.GetReceivedByAccountCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } - bal, _, err := w.TotalReceivedForAccount(account, int32(*cmd.MinConf)) + // TODO: This is more inefficient that it could be, but the entire + // algorithm is already dominated by reading every transaction in the + // wallet's history. + results, err := w.TotalReceivedForAccounts(int32(*cmd.MinConf)) if err != nil { return nil, err } - - return bal.ToBTC(), nil + acctIndex := int(account) + if account == waddrmgr.ImportedAddrAccount { + acctIndex = len(results) - 1 + } + return results[acctIndex].TotalReceived.ToBTC(), nil } // getReceivedByAddress handles a getreceivedbyaddress request by returning @@ -808,7 +773,7 @@ func getTransaction(icmd interface{}, w *wallet.Wallet) (interface{}, error) { } } - details, err := w.TxStore.TxDetails(txHash) + details, err := wallet.UnstableAPI(w).TxDetails(txHash) if err != nil { return nil, err } @@ -908,11 +873,11 @@ func getTransaction(icmd interface{}, w *wallet.Wallet) (interface{}, error) { if err == nil && len(addrs) == 1 { addr := addrs[0] address = addr.EncodeAddress() - account, err := w.Manager.AddrAccount(addr) + account, err := w.AccountOfAddress(addr) if err == nil { - accountName, err = w.Manager.AccountName(account) - if err != nil { - accountName = "" + name, err := w.AccountName(account) + if err == nil { + accountName = name } } } @@ -1059,25 +1024,12 @@ func listAccounts(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*btcjson.ListAccountsCmd) accountBalances := map[string]float64{} - var accounts []uint32 - err := w.Manager.ForEachAccount(func(account uint32) error { - accounts = append(accounts, account) - return nil - }) + results, err := w.AccountBalances(int32(*cmd.MinConf)) if err != nil { return nil, err } - minConf := int32(*cmd.MinConf) - for _, account := range accounts { - acctName, err := w.Manager.AccountName(account) - if err != nil { - return nil, &ErrAccountNameNotFound - } - bals, err := w.CalculateAccountBalances(account, minConf) - if err != nil { - return nil, err - } - accountBalances[acctName] = bals.Spendable.ToBTC() + for _, result := range results { + accountBalances[result.AccountName] = result.AccountBalance.ToBTC() } // Return the map. This will be marshaled into a JSON object. return accountBalances, nil @@ -1102,34 +1054,20 @@ func listLockUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func listReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*btcjson.ListReceivedByAccountCmd) - var accounts []uint32 - err := w.Manager.ForEachAccount(func(account uint32) error { - accounts = append(accounts, account) - return nil - }) + results, err := w.TotalReceivedForAccounts(int32(*cmd.MinConf)) if err != nil { return nil, err } - ret := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts)) - minConf := int32(*cmd.MinConf) - for _, account := range accounts { - acctName, err := w.Manager.AccountName(account) - if err != nil { - return nil, &ErrAccountNameNotFound - } - bal, confirmations, err := w.TotalReceivedForAccount(account, - minConf) - if err != nil { - return nil, err - } - ret = append(ret, btcjson.ListReceivedByAccountResult{ - Account: acctName, - Amount: bal.ToBTC(), - Confirmations: uint64(confirmations), + jsonResults := make([]btcjson.ListReceivedByAccountResult, 0, len(results)) + for _, result := range results { + jsonResults = append(jsonResults, btcjson.ListReceivedByAccountResult{ + Account: result.AccountName, + Amount: result.TotalReceived.ToBTC(), + Confirmations: uint64(result.LastConfirmation), }) } - return ret, nil + return jsonResults, nil } // listReceivedByAddress handles a listreceivedbyaddress request by returning @@ -1180,7 +1118,7 @@ func listReceivedByAddress(icmd interface{}, w *wallet.Wallet) (interface{}, err } else { endHeight = syncBlock.Height - int32(minConf) + 1 } - err = w.TxStore.RangeTransactions(0, endHeight, func(details []wtxmgr.TxDetails) (bool, error) { + err = wallet.UnstableAPI(w).RangeTransactions(0, endHeight, func(details []wtxmgr.TxDetails) (bool, error) { confirmations := confirms(details[0].Block.Height, syncBlock.Height) for _, tx := range details { for _, cred := range tx.Credits { @@ -1462,7 +1400,7 @@ func sendFrom(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) } } - account, err := w.Manager.LookupAccount(cmd.FromAccount) + account, err := w.AccountNumber(cmd.FromAccount) if err != nil { return nil, err } @@ -1504,7 +1442,7 @@ func sendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) { } } - account, err := w.Manager.LookupAccount(cmd.FromAccount) + account, err := w.AccountNumber(cmd.FromAccount) if err != nil { return nil, err } @@ -1515,7 +1453,7 @@ func sendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) { return nil, ErrNeedPositiveMinconf } - // Recreate address/amount pairs, using btcutil.Amount. + // Recreate address/amount pairs, using dcrutil.Amount. pairs := make(map[string]btcutil.Amount, len(cmd.Amounts)) for k, v := range cmd.Amounts { amt, err := btcutil.NewAmount(v) @@ -1593,19 +1531,7 @@ func signMessage(icmd interface{}, w *wallet.Wallet) (interface{}, error) { return nil, err } - ainfo, err := w.Manager.Address(addr) - if err != nil { - return nil, err - } - pka, ok := ainfo.(waddrmgr.ManagedPubKeyAddress) - if !ok { - msg := fmt.Sprintf("Address '%s' does not have an associated private key", addr) - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: msg, - } - } - privKey, err := pka.PrivKey() + privKey, err := w.PrivKeyForAddress(addr) if err != nil { return nil, err } @@ -1615,7 +1541,7 @@ func signMessage(icmd interface{}, w *wallet.Wallet) (interface{}, error) { wire.WriteVarString(&buf, 0, cmd.Message) messageHash := chainhash.DoubleHashB(buf.Bytes()) sigbytes, err := btcec.SignCompact(btcec.S256(), privKey, - messageHash, ainfo.Compressed()) + messageHash, true) if err != nil { return nil, err } @@ -1815,7 +1741,7 @@ func validateAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { result.Address = addr.EncodeAddress() result.IsValid = true - ainfo, err := w.Manager.Address(addr) + ainfo, err := w.AddressInfo(addr) if err != nil { if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { // No additional information available about the address. @@ -1827,7 +1753,7 @@ func validateAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { // The address lookup was successful which means there is further // information about it available and it is "mine". result.IsMine = true - acctName, err := w.Manager.AccountName(ainfo.Account()) + acctName, err := w.AccountName(ainfo.Account()) if err != nil { return nil, &ErrAccountNameNotFound } @@ -1962,7 +1888,7 @@ func walletPassphrase(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func walletPassphraseChange(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*btcjson.WalletPassphraseChangeCmd) - err := w.ChangePassphrase([]byte(cmd.OldPassphrase), + err := w.ChangePrivatePassphrase([]byte(cmd.OldPassphrase), []byte(cmd.NewPassphrase)) if waddrmgr.IsError(err, waddrmgr.ErrWrongPassphrase) { return nil, &btcjson.RPCError{ diff --git a/rpc/rpcserver/server.go b/rpc/rpcserver/server.go index 5efbe63..0836328 100644 --- a/rpc/rpcserver/server.go +++ b/rpc/rpcserver/server.go @@ -150,7 +150,7 @@ func (s *walletServer) Network(ctx context.Context, req *pb.NetworkRequest) ( func (s *walletServer) AccountNumber(ctx context.Context, req *pb.AccountNumberRequest) ( *pb.AccountNumberResponse, error) { - accountNum, err := s.wallet.Manager.LookupAccount(req.AccountName) + accountNum, err := s.wallet.AccountNumber(req.AccountName) if err != nil { return nil, translateError(err) } @@ -319,62 +319,31 @@ func confirms(txHeight, curHeight int32) int32 { func (s *walletServer) FundTransaction(ctx context.Context, req *pb.FundTransactionRequest) ( *pb.FundTransactionResponse, error) { - // TODO: A predicate function for selecting outputs should be created - // and passed to a database view of just a particular account's utxos to - // prevent reading every unspent transaction output from every account - // into memory at once. - - syncBlock := s.wallet.Manager.SyncedTo() - - outputs, err := s.wallet.TxStore.UnspentOutputs() + policy := wallet.OutputSelectionPolicy{ + Account: req.Account, + RequiredConfirmations: req.RequiredConfirmations, + } + unspentOutputs, err := s.wallet.UnspentOutputs(policy) if err != nil { return nil, translateError(err) } - selectedOutputs := make([]*pb.FundTransactionResponse_PreviousOutput, 0, len(outputs)) + selectedOutputs := make([]*pb.FundTransactionResponse_PreviousOutput, 0, len(unspentOutputs)) var totalAmount btcutil.Amount - for i := range outputs { - output := &outputs[i] - - if !confirmed(req.RequiredConfirmations, output.Height, syncBlock.Height) { - continue - } - target := int32(s.wallet.ChainParams().CoinbaseMaturity) - if !req.IncludeImmatureCoinbases && output.FromCoinBase && - !confirmed(target, output.Height, syncBlock.Height) { - continue - } - - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, s.wallet.ChainParams()) - if err != nil || len(addrs) == 0 { - // Cannot determine which account this belongs to - // without a valid address. Fix this by saving - // outputs per account (per-account wtxmgr). - continue - } - outputAcct, err := s.wallet.Manager.AddrAccount(addrs[0]) - if err != nil { - return nil, translateError(err) - } - if outputAcct != req.Account { - continue - } - + for _, output := range unspentOutputs { selectedOutputs = append(selectedOutputs, &pb.FundTransactionResponse_PreviousOutput{ TransactionHash: output.OutPoint.Hash[:], - OutputIndex: output.Index, - Amount: int64(output.Amount), - PkScript: output.PkScript, - ReceiveTime: output.Received.Unix(), - FromCoinbase: output.FromCoinBase, + OutputIndex: output.OutPoint.Index, + Amount: output.Output.Value, + PkScript: output.Output.PkScript, + ReceiveTime: output.ReceiveTime.Unix(), + FromCoinbase: output.OutputKind == wallet.OutputKindCoinbase, }) - totalAmount += output.Amount + totalAmount += btcutil.Amount(output.Output.Value) if req.TargetAmount != 0 && totalAmount > btcutil.Amount(req.TargetAmount) { break } - } var changeScript []byte @@ -470,12 +439,18 @@ func (s *walletServer) ChangePassphrase(ctx context.Context, req *pb.ChangePassp zero.Bytes(req.NewPassphrase) }() - err := s.wallet.Manager.ChangePassphrase(req.OldPassphrase, req.NewPassphrase, - req.Key != pb.ChangePassphraseRequest_PUBLIC, &waddrmgr.DefaultScryptOptions) + var err error + switch req.Key { + case pb.ChangePassphraseRequest_PRIVATE: + err = s.wallet.ChangePrivatePassphrase(req.OldPassphrase, req.NewPassphrase) + case pb.ChangePassphraseRequest_PUBLIC: + err = s.wallet.ChangePublicPassphrase(req.OldPassphrase, req.NewPassphrase) + default: + return nil, grpc.Errorf(codes.InvalidArgument, "Unknown key type (%d)", req.Key) + } if err != nil { return nil, translateError(err) } - return &pb.ChangePassphraseResponse{}, nil } diff --git a/waddrmgr/address.go b/waddrmgr/address.go index cc3fe17..fd879a6 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -9,11 +9,12 @@ import ( "fmt" "sync" - "github.com/roasbeef/btcd/btcec" - "github.com/roasbeef/btcd/txscript" - "github.com/roasbeef/btcutil" - "github.com/roasbeef/btcutil/hdkeychain" - "github.com/roasbeef/btcwallet/internal/zero" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/internal/zero" + "github.com/btcsuite/btcwallet/walletdb" ) // AddressType represents the various address types waddrmgr is currently able @@ -77,7 +78,7 @@ type ManagedAddress interface { Compressed() bool // Used returns true if the backing address has been used in a transaction. - Used() (bool, error) + Used(ns walletdb.ReadBucket) bool } // ManagedPubKeyAddress extends ManagedAddress and additionally provides the @@ -234,8 +235,8 @@ func (a *managedAddress) Compressed() bool { // Used returns true if the address has been used in a transaction. // // This is part of the ManagedAddress interface implementation. -func (a *managedAddress) Used() (bool, error) { - return a.manager.fetchUsed(a.AddrHash()) +func (a *managedAddress) Used(ns walletdb.ReadBucket) bool { + return a.manager.fetchUsed(ns, a.AddrHash()) } // PubKey returns the public key associated with the address. @@ -558,8 +559,8 @@ func (a *scriptAddress) Compressed() bool { // Used returns true if the address has been used in a transaction. // // This is part of the ManagedAddress interface implementation. -func (a *scriptAddress) Used() (bool, error) { - return a.manager.fetchUsed(a.AddrHash()) +func (a *scriptAddress) Used(ns walletdb.ReadBucket) bool { + return a.manager.fetchUsed(ns, a.AddrHash()) } // Script returns the script associated with the address. diff --git a/waddrmgr/common_test.go b/waddrmgr/common_test.go index cbcae56..22d5c71 100644 --- a/waddrmgr/common_test.go +++ b/waddrmgr/common_test.go @@ -184,43 +184,27 @@ func hexToBytes(origHex string) []byte { return buf } -// createDbNamespace creates a new wallet database at the provided path and -// returns it along with the address manager namespace. -func createDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) { - db, err := walletdb.Create("bdb", dbPath) +func emptyDB(t *testing.T) (tearDownFunc func(), db walletdb.DB) { + dirName, err := ioutil.TempDir("", "mgrtest") if err != nil { - return nil, nil, err + t.Fatalf("Failed to create db temp dir: %v", err) } - - namespace, err := db.Namespace(waddrmgrNamespaceKey) + dbPath := filepath.Join(dirName, "mgrtest.db") + db, err = walletdb.Create("bdb", dbPath) if err != nil { + _ = os.RemoveAll(dirName) + t.Fatalf("createDbNamespace: unexpected error: %v", err) + } + tearDownFunc = func() { db.Close() - return nil, nil, err + _ = os.RemoveAll(dirName) } - - return db, namespace, nil -} - -// openDbNamespace opens wallet database at the provided path and returns it -// along with the address manager namespace. -func openDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) { - db, err := walletdb.Open("bdb", dbPath) - if err != nil { - return nil, nil, err - } - - namespace, err := db.Namespace(waddrmgrNamespaceKey) - if err != nil { - db.Close() - return nil, nil, err - } - - return db, namespace, nil + return } // setupManager creates a new address manager and returns a teardown function // that should be invoked to ensure it is closed and removed upon completion. -func setupManager(t *testing.T) (tearDownFunc func(), mgr *waddrmgr.Manager) { +func setupManager(t *testing.T) (tearDownFunc func(), db walletdb.DB, mgr *waddrmgr.Manager) { t.Parallel() // Create a new manager in a temp directory. @@ -229,17 +213,24 @@ func setupManager(t *testing.T) (tearDownFunc func(), mgr *waddrmgr.Manager) { t.Fatalf("Failed to create db temp dir: %v", err) } dbPath := filepath.Join(dirName, "mgrtest.db") - db, namespace, err := createDbNamespace(dbPath) + db, err = walletdb.Create("bdb", dbPath) if err != nil { _ = os.RemoveAll(dirName) t.Fatalf("createDbNamespace: unexpected error: %v", err) } - err = waddrmgr.Create(namespace, seed, pubPassphrase, - privPassphrase, &chaincfg.MainNetParams, fastScrypt) - if err == nil { - mgr, err = waddrmgr.Open(namespace, pubPassphrase, - &chaincfg.MainNetParams, nil) - } + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + err = waddrmgr.Create(ns, seed, pubPassphrase, + privPassphrase, &chaincfg.MainNetParams, fastScrypt) + if err != nil { + return err + } + mgr, err = waddrmgr.Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) if err != nil { db.Close() _ = os.RemoveAll(dirName) @@ -250,5 +241,5 @@ func setupManager(t *testing.T) (tearDownFunc func(), mgr *waddrmgr.Manager) { db.Close() _ = os.RemoveAll(dirName) } - return tearDownFunc, mgr + return tearDownFunc, db, mgr } diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 6ccbc1a..1b6fa63 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -1,4 +1,5 @@ // Copyright (c) 2014-2017 The btcsuite developers +// Copyright (c) 2015 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -233,8 +234,8 @@ func stringToBytes(s string) []byte { } // fetchManagerVersion fetches the current manager version from the database. -func fetchManagerVersion(tx walletdb.Tx) (uint32, error) { - mainBucket := tx.RootBucket().Bucket(mainBucketName) +func fetchManagerVersion(ns walletdb.ReadBucket) (uint32, error) { + mainBucket := ns.NestedReadBucket(mainBucketName) verBytes := mainBucket.Get(mgrVersionName) if verBytes == nil { str := "required version number not stored in database" @@ -245,8 +246,8 @@ func fetchManagerVersion(tx walletdb.Tx) (uint32, error) { } // putManagerVersion stores the provided version to the database. -func putManagerVersion(tx walletdb.Tx, version uint32) error { - bucket := tx.RootBucket().Bucket(mainBucketName) +func putManagerVersion(ns walletdb.ReadWriteBucket, version uint32) error { + bucket := ns.NestedReadWriteBucket(mainBucketName) verBytes := uint32ToBytes(version) err := bucket.Put(mgrVersionName, verBytes) @@ -261,8 +262,8 @@ func putManagerVersion(tx walletdb.Tx, version uint32) error { // (when given the correct user-supplied passphrase) from the database. Either // returned value can be nil, but in practice only the private key params will // be nil for a watching-only database. -func fetchMasterKeyParams(tx walletdb.Tx) ([]byte, []byte, error) { - bucket := tx.RootBucket().Bucket(mainBucketName) +func fetchMasterKeyParams(ns walletdb.ReadBucket) ([]byte, []byte, error) { + bucket := ns.NestedReadBucket(mainBucketName) // Load the master public key parameters. Required. val := bucket.Get(masterPubKeyName) @@ -288,8 +289,8 @@ func fetchMasterKeyParams(tx walletdb.Tx) ([]byte, []byte, error) { // putMasterKeyParams stores the master key parameters needed to derive them // to the database. Either parameter can be nil in which case no value is // written for the parameter. -func putMasterKeyParams(tx walletdb.Tx, pubParams, privParams []byte) error { - bucket := tx.RootBucket().Bucket(mainBucketName) +func putMasterKeyParams(ns walletdb.ReadWriteBucket, pubParams, privParams []byte) error { + bucket := ns.NestedReadWriteBucket(mainBucketName) if privParams != nil { err := bucket.Put(masterPrivKeyName, privParams) @@ -312,8 +313,8 @@ func putMasterKeyParams(tx walletdb.Tx, pubParams, privParams []byte) error { // fetchCoinTypeKeys loads the encrypted cointype keys which are in turn used to // derive the extended keys for all accounts. -func fetchCoinTypeKeys(tx walletdb.Tx) ([]byte, []byte, error) { - bucket := tx.RootBucket().Bucket(mainBucketName) +func fetchCoinTypeKeys(ns walletdb.ReadBucket) ([]byte, []byte, error) { + bucket := ns.NestedReadBucket(mainBucketName) coinTypePubKeyEnc := bucket.Get(coinTypePubKeyName) if coinTypePubKeyEnc == nil { @@ -332,8 +333,8 @@ func fetchCoinTypeKeys(tx walletdb.Tx) ([]byte, []byte, error) { // putCoinTypeKeys stores the encrypted cointype keys which are in turn used to // derive the extended keys for all accounts. Either parameter can be nil in which // case no value is written for the parameter. -func putCoinTypeKeys(tx walletdb.Tx, coinTypePubKeyEnc []byte, coinTypePrivKeyEnc []byte) error { - bucket := tx.RootBucket().Bucket(mainBucketName) +func putCoinTypeKeys(ns walletdb.ReadWriteBucket, coinTypePubKeyEnc []byte, coinTypePrivKeyEnc []byte) error { + bucket := ns.NestedReadWriteBucket(mainBucketName) if coinTypePubKeyEnc != nil { err := bucket.Put(coinTypePubKeyName, coinTypePubKeyEnc) @@ -358,8 +359,8 @@ func putCoinTypeKeys(tx walletdb.Tx, coinTypePubKeyEnc []byte, coinTypePrivKeyEn // protect the extended keys, imported keys, and scripts. Any of the returned // values can be nil, but in practice only the crypto private and script keys // will be nil for a watching-only database. -func fetchCryptoKeys(tx walletdb.Tx) ([]byte, []byte, []byte, error) { - bucket := tx.RootBucket().Bucket(mainBucketName) +func fetchCryptoKeys(ns walletdb.ReadBucket) ([]byte, []byte, []byte, error) { + bucket := ns.NestedReadBucket(mainBucketName) // Load the crypto public key parameters. Required. val := bucket.Get(cryptoPubKeyName) @@ -392,8 +393,8 @@ func fetchCryptoKeys(tx walletdb.Tx) ([]byte, []byte, []byte, error) { // putCryptoKeys stores the encrypted crypto keys which are in turn used to // protect the extended and imported keys. Either parameter can be nil in which // case no value is written for the parameter. -func putCryptoKeys(tx walletdb.Tx, pubKeyEncrypted, privKeyEncrypted, scriptKeyEncrypted []byte) error { - bucket := tx.RootBucket().Bucket(mainBucketName) +func putCryptoKeys(ns walletdb.ReadWriteBucket, pubKeyEncrypted, privKeyEncrypted, scriptKeyEncrypted []byte) error { + bucket := ns.NestedReadWriteBucket(mainBucketName) if pubKeyEncrypted != nil { err := bucket.Put(cryptoPubKeyName, pubKeyEncrypted) @@ -423,8 +424,8 @@ func putCryptoKeys(tx walletdb.Tx, pubKeyEncrypted, privKeyEncrypted, scriptKeyE } // fetchWatchingOnly loads the watching-only flag from the database. -func fetchWatchingOnly(tx walletdb.Tx) (bool, error) { - bucket := tx.RootBucket().Bucket(mainBucketName) +func fetchWatchingOnly(ns walletdb.ReadBucket) (bool, error) { + bucket := ns.NestedReadBucket(mainBucketName) buf := bucket.Get(watchingOnlyName) if len(buf) != 1 { @@ -436,8 +437,8 @@ func fetchWatchingOnly(tx walletdb.Tx) (bool, error) { } // putWatchingOnly stores the watching-only flag to the database. -func putWatchingOnly(tx walletdb.Tx, watchingOnly bool) error { - bucket := tx.RootBucket().Bucket(mainBucketName) +func putWatchingOnly(ns walletdb.ReadWriteBucket, watchingOnly bool) error { + bucket := ns.NestedReadWriteBucket(mainBucketName) var encoded byte if watchingOnly { @@ -569,8 +570,8 @@ func serializeBIP0044AccountRow(encryptedPubKey, // forEachAccount calls the given function with each account stored in // the manager, breaking early on error. -func forEachAccount(tx walletdb.Tx, fn func(account uint32) error) error { - bucket := tx.RootBucket().Bucket(acctBucketName) +func forEachAccount(ns walletdb.ReadBucket, fn func(account uint32) error) error { + bucket := ns.NestedReadBucket(acctBucketName) return bucket.ForEach(func(k, v []byte) error { // Skip buckets. @@ -582,8 +583,8 @@ func forEachAccount(tx walletdb.Tx, fn func(account uint32) error) error { } // fetchLastAccount retreives the last account from the database. -func fetchLastAccount(tx walletdb.Tx) (uint32, error) { - bucket := tx.RootBucket().Bucket(metaBucketName) +func fetchLastAccount(ns walletdb.ReadBucket) (uint32, error) { + bucket := ns.NestedReadBucket(metaBucketName) val := bucket.Get(lastAccountName) if len(val) != 4 { @@ -597,8 +598,8 @@ func fetchLastAccount(tx walletdb.Tx) (uint32, error) { // fetchAccountName retreives the account name given an account number from // the database. -func fetchAccountName(tx walletdb.Tx, account uint32) (string, error) { - bucket := tx.RootBucket().Bucket(acctIDIdxBucketName) +func fetchAccountName(ns walletdb.ReadBucket, account uint32) (string, error) { + bucket := ns.NestedReadBucket(acctIDIdxBucketName) val := bucket.Get(uint32ToBytes(account)) if val == nil { @@ -614,8 +615,8 @@ func fetchAccountName(tx walletdb.Tx, account uint32) (string, error) { // fetchAccountByName retreives the account number given an account name // from the database. -func fetchAccountByName(tx walletdb.Tx, name string) (uint32, error) { - bucket := tx.RootBucket().Bucket(acctNameIdxBucketName) +func fetchAccountByName(ns walletdb.ReadBucket, name string) (uint32, error) { + bucket := ns.NestedReadBucket(acctNameIdxBucketName) val := bucket.Get(stringToBytes(name)) if val == nil { @@ -628,8 +629,8 @@ func fetchAccountByName(tx walletdb.Tx, name string) (uint32, error) { // fetchAccountInfo loads information about the passed account from the // database. -func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) { - bucket := tx.RootBucket().Bucket(acctBucketName) +func fetchAccountInfo(ns walletdb.ReadBucket, account uint32) (interface{}, error) { + bucket := ns.NestedReadBucket(acctBucketName) accountID := uint32ToBytes(account) serializedRow := bucket.Get(accountID) @@ -653,8 +654,8 @@ func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) { } // deleteAccountNameIndex deletes the given key from the account name index of the database. -func deleteAccountNameIndex(tx walletdb.Tx, name string) error { - bucket := tx.RootBucket().Bucket(acctNameIdxBucketName) +func deleteAccountNameIndex(ns walletdb.ReadWriteBucket, name string) error { + bucket := ns.NestedReadWriteBucket(acctNameIdxBucketName) // Delete the account name key err := bucket.Delete(stringToBytes(name)) @@ -666,8 +667,8 @@ func deleteAccountNameIndex(tx walletdb.Tx, name string) error { } // deleteAccounIdIndex deletes the given key from the account id index of the database. -func deleteAccountIDIndex(tx walletdb.Tx, account uint32) error { - bucket := tx.RootBucket().Bucket(acctIDIdxBucketName) +func deleteAccountIDIndex(ns walletdb.ReadWriteBucket, account uint32) error { + bucket := ns.NestedReadWriteBucket(acctIDIdxBucketName) // Delete the account id key err := bucket.Delete(uint32ToBytes(account)) @@ -679,8 +680,8 @@ func deleteAccountIDIndex(tx walletdb.Tx, account uint32) error { } // putAccountNameIndex stores the given key to the account name index of the database. -func putAccountNameIndex(tx walletdb.Tx, account uint32, name string) error { - bucket := tx.RootBucket().Bucket(acctNameIdxBucketName) +func putAccountNameIndex(ns walletdb.ReadWriteBucket, account uint32, name string) error { + bucket := ns.NestedReadWriteBucket(acctNameIdxBucketName) // Write the account number keyed by the account name. err := bucket.Put(stringToBytes(name), uint32ToBytes(account)) @@ -692,8 +693,8 @@ func putAccountNameIndex(tx walletdb.Tx, account uint32, name string) error { } // putAccountIDIndex stores the given key to the account id index of the database. -func putAccountIDIndex(tx walletdb.Tx, account uint32, name string) error { - bucket := tx.RootBucket().Bucket(acctIDIdxBucketName) +func putAccountIDIndex(ns walletdb.ReadWriteBucket, account uint32, name string) error { + bucket := ns.NestedReadWriteBucket(acctIDIdxBucketName) // Write the account number keyed by the account id. err := bucket.Put(uint32ToBytes(account), stringToBytes(name)) @@ -705,8 +706,8 @@ func putAccountIDIndex(tx walletdb.Tx, account uint32, name string) error { } // putAddrAccountIndex stores the given key to the address account index of the database. -func putAddrAccountIndex(tx walletdb.Tx, account uint32, addrHash []byte) error { - bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName) +func putAddrAccountIndex(ns walletdb.ReadWriteBucket, account uint32, addrHash []byte) error { + bucket := ns.NestedReadWriteBucket(addrAcctIdxBucketName) // Write account keyed by address hash err := bucket.Put(addrHash, uint32ToBytes(account)) @@ -729,8 +730,8 @@ func putAddrAccountIndex(tx walletdb.Tx, account uint32, addrHash []byte) error // putAccountRow stores the provided account information to the database. This // is used a common base for storing the various account types. -func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error { - bucket := tx.RootBucket().Bucket(acctBucketName) +func putAccountRow(ns walletdb.ReadWriteBucket, account uint32, row *dbAccountRow) error { + bucket := ns.NestedReadWriteBucket(acctBucketName) // Write the serialized value keyed by the account number. err := bucket.Put(uint32ToBytes(account), serializeAccountRow(row)) @@ -742,7 +743,7 @@ func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error { } // putAccountInfo stores the provided account information to the database. -func putAccountInfo(tx walletdb.Tx, account uint32, encryptedPubKey, +func putAccountInfo(ns walletdb.ReadWriteBucket, account uint32, encryptedPubKey, encryptedPrivKey []byte, nextExternalIndex, nextInternalIndex uint32, name string) error { @@ -753,15 +754,15 @@ func putAccountInfo(tx walletdb.Tx, account uint32, encryptedPubKey, acctType: actBIP0044, rawData: rawData, } - if err := putAccountRow(tx, account, &acctRow); err != nil { + if err := putAccountRow(ns, account, &acctRow); err != nil { return err } // Update account id index - if err := putAccountIDIndex(tx, account, name); err != nil { + if err := putAccountIDIndex(ns, account, name); err != nil { return err } // Update account name index - if err := putAccountNameIndex(tx, account, name); err != nil { + if err := putAccountNameIndex(ns, account, name); err != nil { return err } @@ -769,8 +770,8 @@ func putAccountInfo(tx walletdb.Tx, account uint32, encryptedPubKey, } // putLastAccount stores the provided metadata - last account - to the database. -func putLastAccount(tx walletdb.Tx, account uint32) error { - bucket := tx.RootBucket().Bucket(metaBucketName) +func putLastAccount(ns walletdb.ReadWriteBucket, account uint32) error { + bucket := ns.NestedReadWriteBucket(metaBucketName) err := bucket.Put(lastAccountName, uint32ToBytes(account)) if err != nil { @@ -977,8 +978,8 @@ func serializeScriptAddress(encryptedHash, encryptedScript []byte) []byte { // specific address type. The caller should use type assertions to ascertain // the type. The caller should prefix the error message with the address hash // which caused the failure. -func fetchAddressByHash(tx walletdb.Tx, addrHash []byte) (interface{}, error) { - bucket := tx.RootBucket().Bucket(addrBucketName) +func fetchAddressByHash(ns walletdb.ReadBucket, addrHash []byte) (interface{}, error) { + bucket := ns.NestedReadBucket(addrBucketName) serializedRow := bucket.Get(addrHash[:]) if serializedRow == nil { @@ -1009,16 +1010,16 @@ func fetchAddressByHash(tx walletdb.Tx, addrHash []byte) (interface{}, error) { } // fetchAddressUsed returns true if the provided address id was flagged as used. -func fetchAddressUsed(tx walletdb.Tx, addressID []byte) bool { - bucket := tx.RootBucket().Bucket(usedAddrBucketName) +func fetchAddressUsed(ns walletdb.ReadBucket, addressID []byte) bool { + bucket := ns.NestedReadBucket(usedAddrBucketName) addrHash := sha256.Sum256(addressID) return bucket.Get(addrHash[:]) != nil } // markAddressUsed flags the provided address id as used in the database. -func markAddressUsed(tx walletdb.Tx, addressID []byte) error { - bucket := tx.RootBucket().Bucket(usedAddrBucketName) +func markAddressUsed(ns walletdb.ReadWriteBucket, addressID []byte) error { + bucket := ns.NestedReadWriteBucket(usedAddrBucketName) addrHash := sha256.Sum256(addressID) val := bucket.Get(addrHash[:]) @@ -1038,15 +1039,15 @@ func markAddressUsed(tx walletdb.Tx, addressID []byte) error { // address type. The caller should use type assertions to ascertain the type. // The caller should prefix the error message with the address which caused the // failure. -func fetchAddress(tx walletdb.Tx, addressID []byte) (interface{}, error) { +func fetchAddress(ns walletdb.ReadBucket, addressID []byte) (interface{}, error) { addrHash := sha256.Sum256(addressID) - return fetchAddressByHash(tx, addrHash[:]) + return fetchAddressByHash(ns, addrHash[:]) } // putAddress stores the provided address information to the database. This // is used a common base for storing the various address types. -func putAddress(tx walletdb.Tx, addressID []byte, row *dbAddressRow) error { - bucket := tx.RootBucket().Bucket(addrBucketName) +func putAddress(ns walletdb.ReadWriteBucket, addressID []byte, row *dbAddressRow) error { + bucket := ns.NestedReadWriteBucket(addrBucketName) // Write the serialized value keyed by the hash of the address. The // additional hash is used to conceal the actual address while still @@ -1058,13 +1059,13 @@ func putAddress(tx walletdb.Tx, addressID []byte, row *dbAddressRow) error { return managerError(ErrDatabase, str, err) } // Update address account index - return putAddrAccountIndex(tx, row.account, addrHash[:]) + return putAddrAccountIndex(ns, row.account, addrHash[:]) } // putChainedAddress stores the provided chained address information to the // database. -func putChainedAddress(tx walletdb.Tx, addressID []byte, account uint32, - status syncStatus, branch, index uint32, addrType addressType) error { +func putChainedAddress(ns walletdb.ReadWriteBucket, addressID []byte, account uint32, + status syncStatus, branch, index uint32) error { addrRow := dbAddressRow{ addrType: addrType, @@ -1073,14 +1074,14 @@ func putChainedAddress(tx walletdb.Tx, addressID []byte, account uint32, syncStatus: status, rawData: serializeChainedAddress(branch, index), } - if err := putAddress(tx, addressID, &addrRow); err != nil { + if err := putAddress(ns, addressID, &addrRow); err != nil { return err } // Update the next index for the appropriate internal or external // branch. accountID := uint32ToBytes(account) - bucket := tx.RootBucket().Bucket(acctBucketName) + bucket := ns.NestedReadWriteBucket(acctBucketName) serializedAccount := bucket.Get(accountID) // Deserialize the account row. @@ -1118,7 +1119,7 @@ func putChainedAddress(tx walletdb.Tx, addressID []byte, account uint32, // putImportedAddress stores the provided imported address information to the // database. -func putImportedAddress(tx walletdb.Tx, addressID []byte, account uint32, +func putImportedAddress(ns walletdb.ReadWriteBucket, addressID []byte, account uint32, status syncStatus, encryptedPubKey, encryptedPrivKey []byte) error { rawData := serializeImportedAddress(encryptedPubKey, encryptedPrivKey) @@ -1129,12 +1130,12 @@ func putImportedAddress(tx walletdb.Tx, addressID []byte, account uint32, syncStatus: status, rawData: rawData, } - return putAddress(tx, addressID, &addrRow) + return putAddress(ns, addressID, &addrRow) } // putScriptAddress stores the provided script address information to the // database. -func putScriptAddress(tx walletdb.Tx, addressID []byte, account uint32, +func putScriptAddress(ns walletdb.ReadWriteBucket, addressID []byte, account uint32, status syncStatus, encryptedHash, encryptedScript []byte) error { rawData := serializeScriptAddress(encryptedHash, encryptedScript) @@ -1145,7 +1146,7 @@ func putScriptAddress(tx walletdb.Tx, addressID []byte, account uint32, syncStatus: status, rawData: rawData, } - if err := putAddress(tx, addressID, &addrRow); err != nil { + if err := putAddress(ns, addressID, &addrRow); err != nil { return err } @@ -1153,8 +1154,8 @@ func putScriptAddress(tx walletdb.Tx, addressID []byte, account uint32, } // existsAddress returns whether or not the address id exists in the database. -func existsAddress(tx walletdb.Tx, addressID []byte) bool { - bucket := tx.RootBucket().Bucket(addrBucketName) +func existsAddress(ns walletdb.ReadBucket, addressID []byte) bool { + bucket := ns.NestedReadBucket(addrBucketName) addrHash := sha256.Sum256(addressID) return bucket.Get(addrHash[:]) != nil @@ -1163,8 +1164,8 @@ func existsAddress(tx walletdb.Tx, addressID []byte) bool { // fetchAddrAccount returns the account to which the given address belongs to. // It looks up the account using the addracctidx index which maps the address // hash to its corresponding account id. -func fetchAddrAccount(tx walletdb.Tx, addressID []byte) (uint32, error) { - bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName) +func fetchAddrAccount(ns walletdb.ReadBucket, addressID []byte) (uint32, error) { + bucket := ns.NestedReadBucket(addrAcctIdxBucketName) addrHash := sha256.Sum256(addressID) val := bucket.Get(addrHash[:]) @@ -1177,9 +1178,9 @@ func fetchAddrAccount(tx walletdb.Tx, addressID []byte) (uint32, error) { // forEachAccountAddress calls the given function with each address of // the given account stored in the manager, breaking early on error. -func forEachAccountAddress(tx walletdb.Tx, account uint32, fn func(rowInterface interface{}) error) error { - bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName). - Bucket(uint32ToBytes(account)) +func forEachAccountAddress(ns walletdb.ReadBucket, account uint32, fn func(rowInterface interface{}) error) error { + bucket := ns.NestedReadBucket(addrAcctIdxBucketName). + NestedReadBucket(uint32ToBytes(account)) // if index bucket is missing the account, there hasn't been any address // entries yet if bucket == nil { @@ -1191,7 +1192,7 @@ func forEachAccountAddress(tx walletdb.Tx, account uint32, fn func(rowInterface if v == nil { return nil } - addrRow, err := fetchAddressByHash(tx, k) + addrRow, err := fetchAddressByHash(ns, k) if err != nil { if merr, ok := err.(*ManagerError); ok { desc := fmt.Sprintf("failed to fetch address hash '%s': %v", @@ -1212,8 +1213,8 @@ func forEachAccountAddress(tx walletdb.Tx, account uint32, fn func(rowInterface // forEachActiveAddress calls the given function with each active address // stored in the manager, breaking early on error. -func forEachActiveAddress(tx walletdb.Tx, fn func(rowInterface interface{}) error) error { - bucket := tx.RootBucket().Bucket(addrBucketName) +func forEachActiveAddress(ns walletdb.ReadBucket, fn func(rowInterface interface{}) error) error { + bucket := ns.NestedReadBucket(addrBucketName) err := bucket.ForEach(func(k, v []byte) error { // Skip buckets. @@ -1223,7 +1224,7 @@ func forEachActiveAddress(tx walletdb.Tx, fn func(rowInterface interface{}) erro // Deserialize the address row first to determine the field // values. - addrRow, err := fetchAddressByHash(tx, k) + addrRow, err := fetchAddressByHash(ns, k) if merr, ok := err.(*ManagerError); ok { desc := fmt.Sprintf("failed to fetch address hash '%s': %v", k, merr.Description) @@ -1249,8 +1250,8 @@ func forEachActiveAddress(tx walletdb.Tx, fn func(rowInterface interface{}) erro // keys from the main database without also marking it watching-only will result // in an unusable database. It will also make any imported scripts and private // keys unrecoverable unless there is a backup copy available. -func deletePrivateKeys(tx walletdb.Tx) error { - bucket := tx.RootBucket().Bucket(mainBucketName) +func deletePrivateKeys(ns walletdb.ReadWriteBucket) error { + bucket := ns.NestedReadWriteBucket(mainBucketName) // Delete the master private key params and the crypto private and // script keys. @@ -1272,7 +1273,7 @@ func deletePrivateKeys(tx walletdb.Tx) error { } // Delete the account extended private key for all accounts. - bucket = tx.RootBucket().Bucket(acctBucketName) + bucket = ns.NestedReadWriteBucket(acctBucketName) err := bucket.ForEach(func(k, v []byte) error { // Skip buckets. if v == nil { @@ -1312,7 +1313,7 @@ func deletePrivateKeys(tx walletdb.Tx) error { } // Delete the private key for all imported addresses. - bucket = tx.RootBucket().Bucket(addrBucketName) + bucket = ns.NestedReadWriteBucket(addrBucketName) err = bucket.ForEach(func(k, v []byte) error { // Skip buckets. if v == nil { @@ -1371,8 +1372,8 @@ func deletePrivateKeys(tx walletdb.Tx) error { // fetchSyncedTo loads the block stamp the manager is synced to from the // database. -func fetchSyncedTo(tx walletdb.Tx) (*BlockStamp, error) { - bucket := tx.RootBucket().Bucket(syncBucketName) +func fetchSyncedTo(ns walletdb.ReadBucket) (*BlockStamp, error) { + bucket := ns.NestedReadBucket(syncBucketName) // The serialized synced to format is: // @@ -1391,8 +1392,8 @@ func fetchSyncedTo(tx walletdb.Tx) (*BlockStamp, error) { } // putSyncedTo stores the provided synced to blockstamp to the database. -func putSyncedTo(tx walletdb.Tx, bs *BlockStamp) error { - bucket := tx.RootBucket().Bucket(syncBucketName) +func putSyncedTo(ns walletdb.ReadWriteBucket, bs *BlockStamp) error { + bucket := ns.NestedReadWriteBucket(syncBucketName) // The serialized synced to format is: // @@ -1412,8 +1413,8 @@ func putSyncedTo(tx walletdb.Tx, bs *BlockStamp) error { // fetchStartBlock loads the start block stamp for the manager from the // database. -func fetchStartBlock(tx walletdb.Tx) (*BlockStamp, error) { - bucket := tx.RootBucket().Bucket(syncBucketName) +func fetchStartBlock(ns walletdb.ReadBucket) (*BlockStamp, error) { + bucket := ns.NestedReadBucket(syncBucketName) // The serialized start block format is: // @@ -1432,8 +1433,8 @@ func fetchStartBlock(tx walletdb.Tx) (*BlockStamp, error) { } // putStartBlock stores the provided start block stamp to the database. -func putStartBlock(tx walletdb.Tx, bs *BlockStamp) error { - bucket := tx.RootBucket().Bucket(syncBucketName) +func putStartBlock(ns walletdb.ReadWriteBucket, bs *BlockStamp) error { + bucket := ns.NestedReadWriteBucket(syncBucketName) // The serialized start block format is: // @@ -1453,8 +1454,8 @@ func putStartBlock(tx walletdb.Tx, bs *BlockStamp) error { // fetchRecentBlocks returns the height of the most recent block height and // hashes of the most recent blocks. -func fetchRecentBlocks(tx walletdb.Tx) (int32, []chainhash.Hash, error) { - bucket := tx.RootBucket().Bucket(syncBucketName) +func fetchRecentBlocks(ns walletdb.ReadBucket) (int32, []chainhash.Hash, error) { + bucket := ns.NestedReadBucket(syncBucketName) // The serialized recent blocks format is: // @@ -1483,8 +1484,8 @@ func fetchRecentBlocks(tx walletdb.Tx) (int32, []chainhash.Hash, error) { } // putRecentBlocks stores the provided start block stamp to the database. -func putRecentBlocks(tx walletdb.Tx, recentHeight int32, recentHashes []chainhash.Hash) error { - bucket := tx.RootBucket().Bucket(syncBucketName) +func putRecentBlocks(ns walletdb.ReadWriteBucket, recentHeight int32, recentHashes []chainhash.Hash) error { + bucket := ns.NestedReadWriteBucket(syncBucketName) // The serialized recent blocks format is: // @@ -1511,102 +1512,87 @@ func putRecentBlocks(tx walletdb.Tx, recentHeight int32, recentHashes []chainhas // managerExists returns whether or not the manager has already been created // in the given database namespace. -func managerExists(namespace walletdb.Namespace) (bool, error) { - var exists bool - err := namespace.View(func(tx walletdb.Tx) error { - mainBucket := tx.RootBucket().Bucket(mainBucketName) - exists = mainBucket != nil - return nil - }) - if err != nil { - str := fmt.Sprintf("failed to obtain database view: %v", err) - return false, managerError(ErrDatabase, str, err) +func managerExists(ns walletdb.ReadBucket) bool { + if ns == nil { + return false } - return exists, nil + mainBucket := ns.NestedReadBucket(mainBucketName) + return mainBucket != nil } // createManagerNS creates the initial namespace structure needed for all of the // manager data. This includes things such as all of the buckets as well as the // version and creation date. -func createManagerNS(namespace walletdb.Namespace) error { - err := namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - mainBucket, err := rootBucket.CreateBucket(mainBucketName) - if err != nil { - str := "failed to create main bucket" - return managerError(ErrDatabase, str, err) - } - - _, err = rootBucket.CreateBucket(addrBucketName) - if err != nil { - str := "failed to create address bucket" - return managerError(ErrDatabase, str, err) - } - - _, err = rootBucket.CreateBucket(acctBucketName) - if err != nil { - str := "failed to create account bucket" - return managerError(ErrDatabase, str, err) - } - - _, err = rootBucket.CreateBucket(addrAcctIdxBucketName) - if err != nil { - str := "failed to create address index bucket" - return managerError(ErrDatabase, str, err) - } - - _, err = rootBucket.CreateBucket(syncBucketName) - if err != nil { - str := "failed to create sync bucket" - return managerError(ErrDatabase, str, err) - } - - // usedAddrBucketName bucket was added after manager version 1 release - _, err = rootBucket.CreateBucket(usedAddrBucketName) - if err != nil { - str := "failed to create used addresses bucket" - return managerError(ErrDatabase, str, err) - } - - _, err = rootBucket.CreateBucket(acctNameIdxBucketName) - if err != nil { - str := "failed to create an account name index bucket" - return managerError(ErrDatabase, str, err) - } - - _, err = rootBucket.CreateBucket(acctIDIdxBucketName) - if err != nil { - str := "failed to create an account id index bucket" - return managerError(ErrDatabase, str, err) - } - - _, err = rootBucket.CreateBucket(metaBucketName) - if err != nil { - str := "failed to create a meta bucket" - return managerError(ErrDatabase, str, err) - } - - if err := putLastAccount(tx, DefaultAccountNum); err != nil { - return err - } - - if err := putManagerVersion(tx, latestMgrVersion); err != nil { - return err - } - - createDate := uint64(time.Now().Unix()) - var dateBytes [8]byte - binary.LittleEndian.PutUint64(dateBytes[:], createDate) - err = mainBucket.Put(mgrCreateDateName, dateBytes[:]) - if err != nil { - str := "failed to store database creation time" - return managerError(ErrDatabase, str, err) - } - - return nil - }) +func createManagerNS(ns walletdb.ReadWriteBucket) error { + mainBucket, err := ns.CreateBucket(mainBucketName) if err != nil { - str := "failed to update database" + str := "failed to create main bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(addrBucketName) + if err != nil { + str := "failed to create address bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(acctBucketName) + if err != nil { + str := "failed to create account bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(addrAcctIdxBucketName) + if err != nil { + str := "failed to create address index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(syncBucketName) + if err != nil { + str := "failed to create sync bucket" + return managerError(ErrDatabase, str, err) + } + + // usedAddrBucketName bucket was added after manager version 1 release + _, err = ns.CreateBucket(usedAddrBucketName) + if err != nil { + str := "failed to create used addresses bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(acctNameIdxBucketName) + if err != nil { + str := "failed to create an account name index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(acctIDIdxBucketName) + if err != nil { + str := "failed to create an account id index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(metaBucketName) + if err != nil { + str := "failed to create a meta bucket" + return managerError(ErrDatabase, str, err) + } + + if err := putLastAccount(ns, DefaultAccountNum); err != nil { + return err + } + + if err := putManagerVersion(ns, latestMgrVersion); err != nil { + return err + } + + createDate := uint64(time.Now().Unix()) + var dateBytes [8]byte + binary.LittleEndian.PutUint64(dateBytes[:], createDate) + err = mainBucket.Put(mgrCreateDateName, dateBytes[:]) + if err != nil { + str := "failed to store database creation time" return managerError(ErrDatabase, str, err) } @@ -1616,36 +1602,28 @@ func createManagerNS(namespace walletdb.Namespace) error { // upgradeToVersion2 upgrades the database from version 1 to version 2 // 'usedAddrBucketName' a bucket for storing addrs flagged as marked is // initialized and it will be updated on the next rescan. -func upgradeToVersion2(namespace walletdb.Namespace) error { - err := namespace.Update(func(tx walletdb.Tx) error { - currentMgrVersion := uint32(2) - rootBucket := tx.RootBucket() +func upgradeToVersion2(ns walletdb.ReadWriteBucket) error { + currentMgrVersion := uint32(2) - _, err := rootBucket.CreateBucket(usedAddrBucketName) - if err != nil { - str := "failed to create used addresses bucket" - return managerError(ErrDatabase, str, err) - } - - if err := putManagerVersion(tx, currentMgrVersion); err != nil { - return err - } - - return nil - }) + _, err := ns.CreateBucketIfNotExists(usedAddrBucketName) if err != nil { - return maybeConvertDbError(err) + str := "failed to create used addresses bucket" + return managerError(ErrDatabase, str, err) } - return nil + + return putManagerVersion(ns, currentMgrVersion) } // upgradeManager upgrades the data in the provided manager namespace to newer // versions as neeeded. -func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, chainParams *chaincfg.Params, cbs *OpenCallbacks) error { +func upgradeManager(db walletdb.DB, namespaceKey []byte, pubPassPhrase []byte, + chainParams *chaincfg.Params, cbs *OpenCallbacks) error { + var version uint32 - err := namespace.View(func(tx walletdb.Tx) error { + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(namespaceKey) var err error - version, err = fetchManagerVersion(tx) + version, err = fetchManagerVersion(ns) return err }) if err != nil { @@ -1685,7 +1663,11 @@ func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, chainPar if version < 2 { // Upgrade from version 1 to 2. - if err := upgradeToVersion2(namespace); err != nil { + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(namespaceKey) + return upgradeToVersion2(ns) + }) + if err != nil { return err } @@ -1707,8 +1689,11 @@ func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, chainPar if err != nil { return err } - // Upgrade from version 2 to 3. - if err := upgradeToVersion3(namespace, seed, privPassPhrase, pubPassPhrase, chainParams); err != nil { + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(namespaceKey) + return upgradeToVersion3(ns, seed, privPassPhrase, pubPassPhrase, chainParams) + }) + if err != nil { return err } @@ -1717,7 +1702,11 @@ func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, chainPar } if version < 4 { - if err := upgradeToVersion4(namespace, pubPassPhrase); err != nil { + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(namespaceKey) + return upgradeToVersion4(ns, pubPassPhrase) + }) + if err != nil { return err } @@ -1743,18 +1732,17 @@ func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, chainPar // * acctNameIdxBucketName // * acctIDIdxBucketName // * metaBucketName -func upgradeToVersion3(namespace walletdb.Namespace, seed, privPassPhrase, pubPassPhrase []byte, chainParams *chaincfg.Params) error { - err := namespace.Update(func(tx walletdb.Tx) error { +func upgradeToVersion3(ns walletdb.ReadWriteBucket, seed, privPassPhrase, pubPassPhrase []byte, chainParams *chaincfg.Params) error { + err := func() error { currentMgrVersion := uint32(3) - rootBucket := tx.RootBucket() - woMgr, err := loadManager(namespace, pubPassPhrase, chainParams) + woMgr, err := loadManager(ns, pubPassPhrase, chainParams) if err != nil { return err } defer woMgr.Close() - err = woMgr.Unlock(privPassPhrase) + err = woMgr.Unlock(ns, privPassPhrase) if err != nil { return err } @@ -1793,57 +1781,57 @@ func upgradeToVersion3(namespace walletdb.Namespace, seed, privPassPhrase, pubPa } // Save the encrypted cointype keys to the database. - err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc) + err = putCoinTypeKeys(ns, coinTypePubEnc, coinTypePrivEnc) if err != nil { return err } - _, err = rootBucket.CreateBucket(acctNameIdxBucketName) + _, err = ns.CreateBucketIfNotExists(acctNameIdxBucketName) if err != nil { str := "failed to create an account name index bucket" return managerError(ErrDatabase, str, err) } - _, err = rootBucket.CreateBucket(acctIDIdxBucketName) + _, err = ns.CreateBucketIfNotExists(acctIDIdxBucketName) if err != nil { str := "failed to create an account id index bucket" return managerError(ErrDatabase, str, err) } - _, err = rootBucket.CreateBucket(metaBucketName) + _, err = ns.CreateBucketIfNotExists(metaBucketName) if err != nil { str := "failed to create a meta bucket" return managerError(ErrDatabase, str, err) } // Initialize metadata for all keys - if err := putLastAccount(tx, DefaultAccountNum); err != nil { + if err := putLastAccount(ns, DefaultAccountNum); err != nil { return err } // Update default account indexes - if err := putAccountIDIndex(tx, DefaultAccountNum, defaultAccountName); err != nil { + if err := putAccountIDIndex(ns, DefaultAccountNum, defaultAccountName); err != nil { return err } - if err := putAccountNameIndex(tx, DefaultAccountNum, defaultAccountName); err != nil { + if err := putAccountNameIndex(ns, DefaultAccountNum, defaultAccountName); err != nil { return err } // Update imported account indexes - if err := putAccountIDIndex(tx, ImportedAddrAccount, ImportedAddrAccountName); err != nil { + if err := putAccountIDIndex(ns, ImportedAddrAccount, ImportedAddrAccountName); err != nil { return err } - if err := putAccountNameIndex(tx, ImportedAddrAccount, ImportedAddrAccountName); err != nil { + if err := putAccountNameIndex(ns, ImportedAddrAccount, ImportedAddrAccountName); err != nil { return err } // Write current manager version - if err := putManagerVersion(tx, currentMgrVersion); err != nil { + if err := putManagerVersion(ns, currentMgrVersion); err != nil { return err } // Save "" alias for default account name for backward compat - return putAccountNameIndex(tx, DefaultAccountNum, "") - }) + return putAccountNameIndex(ns, DefaultAccountNum, "") + }() if err != nil { return maybeConvertDbError(err) } @@ -1853,17 +1841,17 @@ func upgradeToVersion3(namespace walletdb.Namespace, seed, privPassPhrase, pubPa // upgradeToVersion4 upgrades the database from version 3 to version 4. The // default account remains unchanged (even if it was modified by the user), but // the empty string alias to the default account is removed. -func upgradeToVersion4(namespace walletdb.Namespace, pubPassPhrase []byte) error { - err := namespace.Update(func(tx walletdb.Tx) error { +func upgradeToVersion4(ns walletdb.ReadWriteBucket, pubPassPhrase []byte) error { + err := func() error { // Write new manager version. - err := putManagerVersion(tx, 4) + err := putManagerVersion(ns, 4) if err != nil { return err } // Lookup the old account info to determine the real name of the // default account. All other names will be removed. - acctInfoIface, err := fetchAccountInfo(tx, DefaultAccountNum) + acctInfoIface, err := fetchAccountInfo(ns, DefaultAccountNum) if err != nil { return err } @@ -1876,7 +1864,7 @@ func upgradeToVersion4(namespace walletdb.Namespace, pubPassPhrase []byte) error var oldName string // Delete any other names for the default account. - c := tx.RootBucket().Bucket(acctNameIdxBucketName).Cursor() + c := ns.NestedReadWriteBucket(acctNameIdxBucketName).ReadWriteCursor() for k, v := c.First(); k != nil; k, v = c.Next() { // Skip nested buckets. if v == nil { @@ -1903,7 +1891,7 @@ func upgradeToVersion4(namespace walletdb.Namespace, pubPassPhrase []byte) error // The account number to name index may map to the wrong name, // so rewrite the entry with the true name from the account row // instead of leaving it set to an incorrect alias. - err = putAccountIDIndex(tx, DefaultAccountNum, acctInfo.name) + err = putAccountIDIndex(ns, DefaultAccountNum, acctInfo.name) if err != nil { const str = "account number to name index could not be " + "rewritten with actual account name" @@ -1912,7 +1900,7 @@ func upgradeToVersion4(namespace walletdb.Namespace, pubPassPhrase []byte) error // Ensure that the true name for the default account maps // forwards and backwards to the default account number. - name, err := fetchAccountName(tx, DefaultAccountNum) + name, err := fetchAccountName(ns, DefaultAccountNum) if err != nil { return err } @@ -1920,7 +1908,7 @@ func upgradeToVersion4(namespace walletdb.Namespace, pubPassPhrase []byte) error const str = "account name index does not map default account number to correct name" return managerError(ErrUpgrade, str, nil) } - acct, err := fetchAccountByName(tx, acctInfo.name) + acct, err := fetchAccountByName(ns, acctInfo.name) if err != nil { return err } @@ -1931,7 +1919,7 @@ func upgradeToVersion4(namespace walletdb.Namespace, pubPassPhrase []byte) error // Ensure that looking up the default account by the old name // cannot succeed. - _, err = fetchAccountByName(tx, oldName) + _, err = fetchAccountByName(ns, oldName) if err == nil { const str = "default account exists under old name" return managerError(ErrUpgrade, str, nil) @@ -1942,7 +1930,7 @@ func upgradeToVersion4(namespace walletdb.Namespace, pubPassPhrase []byte) error } return nil - }) + }() if err != nil { return maybeConvertDbError(err) } diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 0a9b386..a1563fa 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -247,7 +247,6 @@ var newCryptoKey = defaultNewCryptoKey type Manager struct { mtx sync.RWMutex - namespace walletdb.Namespace chainParams *chaincfg.Params addrs map[addrKey]ManagedAddress syncState syncState @@ -450,7 +449,7 @@ func (m *Manager) deriveKey(acctInfo *accountInfo, branch, index uint32, private // keys for it and track the state of the internal and external branches. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { +func (m *Manager) loadAccountInfo(ns walletdb.ReadBucket, account uint32) (*accountInfo, error) { // Return the account info from cache if it's available. if acctInfo, ok := m.acctInfo[account]; ok { return acctInfo, nil @@ -458,12 +457,7 @@ func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { // The account is either invalid or just wasn't cached, so attempt to // load the information from the database. - var rowInterface interface{} - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - rowInterface, err = fetchAccountInfo(tx, account) - return err - }) + rowInterface, err := fetchAccountInfo(ns, account) if err != nil { return nil, maybeConvertDbError(err) } @@ -563,7 +557,7 @@ func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { // then fetching the account properties with a new read tx, this can be made // more performant by simply returning the new account properties during the // change. -func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) { +func (m *Manager) AccountProperties(ns walletdb.ReadBucket, account uint32) (*AccountProperties, error) { defer m.mtx.RUnlock() m.mtx.RLock() @@ -581,7 +575,7 @@ func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) // imported account cannot contain non-imported keys, the external and // internal key counts for it are zero. if account != ImportedAddrAccount { - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -593,14 +587,11 @@ func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) // Could be more efficient if this was tracked by the db. var importedKeyCount uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - count := func(interface{}) error { - importedKeyCount++ - return nil - } - return forEachAccountAddress(tx, ImportedAddrAccount, - count) - }) + count := func(interface{}) error { + importedKeyCount++ + return nil + } + err := forEachAccountAddress(ns, ImportedAddrAccount, count) if err != nil { return nil, err } @@ -614,9 +605,9 @@ func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) // based on the private flag for the given an account, branch, and index. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) deriveKeyFromPath(account, branch, index uint32, private bool) (*hdkeychain.ExtendedKey, error) { +func (m *Manager) deriveKeyFromPath(ns walletdb.ReadBucket, account, branch, index uint32, private bool) (*hdkeychain.ExtendedKey, error) { // Look up the account key information. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -628,8 +619,8 @@ func (m *Manager) deriveKeyFromPath(account, branch, index uint32, private bool) // address data loaded from the database. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) chainAddressRowToManaged(row *dbChainAddressRow) (ManagedAddress, error) { - addressKey, err := m.deriveKeyFromPath(row.account, row.branch, +func (m *Manager) chainAddressRowToManaged(ns walletdb.ReadBucket, row *dbChainAddressRow) (ManagedAddress, error) { + addressKey, err := m.deriveKeyFromPath(ns, row.account, row.branch, row.index, !m.locked) if err != nil { return nil, err @@ -684,10 +675,10 @@ func (m *Manager) scriptAddressRowToManaged(row *dbScriptAddressRow) (ManagedAdd // appropriate type. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) rowInterfaceToManaged(rowInterface interface{}) (ManagedAddress, error) { +func (m *Manager) rowInterfaceToManaged(ns walletdb.ReadBucket, rowInterface interface{}) (ManagedAddress, error) { switch row := rowInterface.(type) { case *dbChainAddressRow: - return m.chainAddressRowToManaged(row) + return m.chainAddressRowToManaged(ns, row) case *dbImportedAddressRow: return m.importedAddressRowToManaged(row) @@ -704,14 +695,9 @@ func (m *Manager) rowInterfaceToManaged(rowInterface interface{}) (ManagedAddres // caches the associated managed address. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) loadAndCacheAddress(address btcutil.Address) (ManagedAddress, error) { +func (m *Manager) loadAndCacheAddress(ns walletdb.ReadBucket, address btcutil.Address) (ManagedAddress, error) { // Attempt to load the raw address information from the database. - var rowInterface interface{} - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - rowInterface, err = fetchAddress(tx, address.ScriptAddress()) - return err - }) + rowInterface, err := fetchAddress(ns, address.ScriptAddress()) if err != nil { if merr, ok := err.(*ManagerError); ok { desc := fmt.Sprintf("failed to fetch address '%s': %v", @@ -724,7 +710,7 @@ func (m *Manager) loadAndCacheAddress(address btcutil.Address) (ManagedAddress, // Create a new managed address for the specific type of address based // on type. - managedAddr, err := m.rowInterfaceToManaged(rowInterface) + managedAddr, err := m.rowInterfaceToManaged(ns, rowInterface) if err != nil { return nil, err } @@ -740,7 +726,7 @@ func (m *Manager) loadAndCacheAddress(address btcutil.Address) (ManagedAddress, // transactions such as the associated private key for pay-to-pubkey and // pay-to-pubkey-hash addresses and the script associated with // pay-to-script-hash addresses. -func (m *Manager) Address(address btcutil.Address) (ManagedAddress, error) { +func (m *Manager) Address(ns walletdb.ReadBucket, address btcutil.Address) (ManagedAddress, error) { // ScriptAddress will only return a script hash if we're // accessing an address that is either PKH or SH. In // the event we're passed a PK address, convert the @@ -767,17 +753,12 @@ func (m *Manager) Address(address btcutil.Address) (ManagedAddress, error) { defer m.mtx.Unlock() // Attempt to load the address from the database. - return m.loadAndCacheAddress(address) + return m.loadAndCacheAddress(ns, address) } // AddrAccount returns the account to which the given address belongs. -func (m *Manager) AddrAccount(address btcutil.Address) (uint32, error) { - var account uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - account, err = fetchAddrAccount(tx, address.ScriptAddress()) - return err - }) +func (m *Manager) AddrAccount(ns walletdb.ReadBucket, address btcutil.Address) (uint32, error) { + account, err := fetchAddrAccount(ns, address.ScriptAddress()) if err != nil { return 0, maybeConvertDbError(err) } @@ -790,7 +771,7 @@ func (m *Manager) AddrAccount(address btcutil.Address) (uint32, error) { // keys are derived using the scrypt parameters in the options, so changing the // passphrase may be used to bump the computational difficulty needed to brute // force the passphrase. -func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, private bool, config *ScryptOptions) error { +func (m *Manager) ChangePassphrase(ns walletdb.ReadWriteBucket, oldPassphrase, newPassphrase []byte, private bool, config *ScryptOptions) error { // No private passphrase to change for a watching-only address manager. if private && m.watchingOnly { return managerError(ErrWatchingOnly, errWatchingOnly, nil) @@ -894,14 +875,12 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, private // Save the new keys and params to the the db in a single // transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putCryptoKeys(tx, nil, encPriv, encScript) - if err != nil { - return err - } + err = putCryptoKeys(ns, nil, encPriv, encScript) + if err != nil { + return maybeConvertDbError(err) + } - return putMasterKeyParams(tx, nil, newKeyParams) - }) + err = putMasterKeyParams(ns, nil, newKeyParams) if err != nil { return maybeConvertDbError(err) } @@ -925,14 +904,12 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, private // Save the new keys and params to the the db in a single // transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putCryptoKeys(tx, encryptedPub, nil, nil) - if err != nil { - return err - } + err = putCryptoKeys(ns, encryptedPub, nil, nil) + if err != nil { + return maybeConvertDbError(err) + } - return putMasterKeyParams(tx, newKeyParams, nil) - }) + err = putMasterKeyParams(ns, newKeyParams, nil) if err != nil { return maybeConvertDbError(err) } @@ -956,7 +933,7 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, private // // Executing this function on a manager that is already watching-only will have // no effect. -func (m *Manager) ConvertToWatchingOnly() error { +func (m *Manager) ConvertToWatchingOnly(ns walletdb.ReadWriteBucket) error { m.mtx.Lock() defer m.mtx.Unlock() @@ -967,13 +944,12 @@ func (m *Manager) ConvertToWatchingOnly() error { // Remove all private key material and mark the new database as watching // only. - err := m.namespace.Update(func(tx walletdb.Tx) error { - if err := deletePrivateKeys(tx); err != nil { - return err - } + err := deletePrivateKeys(ns) + if err != nil { + return maybeConvertDbError(err) + } - return putWatchingOnly(tx, true) - }) + err = putWatchingOnly(ns, true) if err != nil { return maybeConvertDbError(err) } @@ -1031,23 +1007,14 @@ func (m *Manager) ConvertToWatchingOnly() error { // address manager. // // This function MUST be called with the manager lock held for reads. -func (m *Manager) existsAddress(addressID []byte) (bool, error) { +func (m *Manager) existsAddress(ns walletdb.ReadBucket, addressID []byte) bool { // Check the in-memory map first since it's faster than a db access. if _, ok := m.addrs[addrKey(addressID)]; ok { - return true, nil + return true } // Check the database if not already found above. - var exists bool - err := m.namespace.View(func(tx walletdb.Tx) error { - exists = existsAddress(tx, addressID) - return nil - }) - if err != nil { - return false, maybeConvertDbError(err) - } - - return exists, nil + return existsAddress(ns, addressID) } // ImportPrivateKey imports a WIF private key into the address manager. The @@ -1067,7 +1034,7 @@ func (m *Manager) existsAddress(addressID []byte) (bool, error) { // watching-only, or not for the same network as the key trying to be imported. // It will also return an error if the address already exists. Any other errors // returned are generally unexpected. -func (m *Manager) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (ManagedPubKeyAddress, error) { +func (m *Manager) ImportPrivateKey(ns walletdb.ReadWriteBucket, wif *btcutil.WIF, bs *BlockStamp) (ManagedPubKeyAddress, error) { // Ensure the address is intended for network the address manager is // associated with. if !wif.IsForNet(m.chainParams) { @@ -1088,10 +1055,7 @@ func (m *Manager) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (ManagedPub // Prevent duplicates. serializedPubKey := wif.SerializePubKey() pubKeyHash := btcutil.Hash160(serializedPubKey) - alreadyExists, err := m.existsAddress(pubKeyHash) - if err != nil { - return nil, err - } + alreadyExists := m.existsAddress(ns, pubKeyHash) if alreadyExists { str := fmt.Sprintf("address for public key %x already exists", serializedPubKey) @@ -1125,23 +1089,19 @@ func (m *Manager) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (ManagedPub // Save the new imported address to the db and update start block (if // needed) in a single transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putImportedAddress(tx, pubKeyHash, ImportedAddrAccount, - ssNone, encryptedPubKey, encryptedPrivKey) - if err != nil { - return err - } - - if updateStartBlock { - return putStartBlock(tx, bs) - } - - return nil - }) + err = putImportedAddress(ns, pubKeyHash, ImportedAddrAccount, ssNone, + encryptedPubKey, encryptedPrivKey) if err != nil { return nil, err } + if updateStartBlock { + err := putStartBlock(ns, bs) + if err != nil { + return nil, err + } + } + // Now that the database has been updated, update the start block in // memory too if needed. if updateStartBlock { @@ -1183,7 +1143,7 @@ func (m *Manager) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (ManagedPub // This function will return an error if the address manager is locked and not // watching-only, or the address already exists. Any other errors returned are // generally unexpected. -func (m *Manager) ImportScript(script []byte, bs *BlockStamp) (ManagedScriptAddress, error) { +func (m *Manager) ImportScript(ns walletdb.ReadWriteBucket, script []byte, bs *BlockStamp) (ManagedScriptAddress, error) { m.mtx.Lock() defer m.mtx.Unlock() @@ -1194,10 +1154,7 @@ func (m *Manager) ImportScript(script []byte, bs *BlockStamp) (ManagedScriptAddr // Prevent duplicates. scriptHash := btcutil.Hash160(script) - alreadyExists, err := m.existsAddress(scriptHash) - if err != nil { - return nil, err - } + alreadyExists := m.existsAddress(ns, scriptHash) if alreadyExists { str := fmt.Sprintf("address for script hash %x already exists", scriptHash) @@ -1234,23 +1191,19 @@ func (m *Manager) ImportScript(script []byte, bs *BlockStamp) (ManagedScriptAddr // Save the new imported address to the db and update start block (if // needed) in a single transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putScriptAddress(tx, scriptHash, ImportedAddrAccount, - ssNone, encryptedHash, encryptedScript) - if err != nil { - return err - } - - if updateStartBlock { - return putStartBlock(tx, bs) - } - - return nil - }) + err = putScriptAddress(ns, scriptHash, ImportedAddrAccount, + ssNone, encryptedHash, encryptedScript) if err != nil { return nil, maybeConvertDbError(err) } + if updateStartBlock { + err := putStartBlock(ns, bs) + if err != nil { + return nil, maybeConvertDbError(err) + } + } + // Now that the database has been updated, update the start block in // memory too if needed. if updateStartBlock { @@ -1314,23 +1267,17 @@ func (m *Manager) Lock() error { // account name // // This function MUST be called with the manager lock held for reads. -func (m *Manager) lookupAccount(name string) (uint32, error) { - var account uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - account, err = fetchAccountByName(tx, name) - return err - }) - return account, err +func (m *Manager) lookupAccount(ns walletdb.ReadBucket, name string) (uint32, error) { + return fetchAccountByName(ns, name) } // LookupAccount loads account number stored in the manager for the given // account name -func (m *Manager) LookupAccount(name string) (uint32, error) { +func (m *Manager) LookupAccount(ns walletdb.ReadBucket, name string) (uint32, error) { m.mtx.RLock() defer m.mtx.RUnlock() - return m.lookupAccount(name) + return m.lookupAccount(ns, name) } // Unlock derives the master private key from the specified passphrase. An @@ -1341,7 +1288,7 @@ func (m *Manager) LookupAccount(name string) (uint32, error) { // // This function will return an error if invoked on a watching-only address // manager. -func (m *Manager) Unlock(passphrase []byte) error { +func (m *Manager) Unlock(ns walletdb.ReadBucket, passphrase []byte) error { // A watching-only address manager can't be unlocked. if m.watchingOnly { return managerError(ErrWatchingOnly, errWatchingOnly, nil) @@ -1412,8 +1359,7 @@ func (m *Manager) Unlock(passphrase []byte) error { // Derive any private keys that are pending due to them being created // while the address manager was locked. for _, info := range m.deriveOnUnlock { - // TODO(roasbeef): addr type here? - addressKey, err := m.deriveKeyFromPath(info.managedAddr.Account(), + addressKey, err := m.deriveKeyFromPath(ns, info.managedAddr.account, info.branch, info.index, true) if err != nil { m.lock() @@ -1457,21 +1403,14 @@ func (m *Manager) Unlock(passphrase []byte) error { } // fetchUsed returns true if the provided address id was flagged used. -func (m *Manager) fetchUsed(addressID []byte) (bool, error) { - var used bool - err := m.namespace.View(func(tx walletdb.Tx) error { - used = fetchAddressUsed(tx, addressID) - return nil - }) - return used, err +func (m *Manager) fetchUsed(ns walletdb.ReadBucket, addressID []byte) bool { + return fetchAddressUsed(ns, addressID) } // MarkUsed updates the used flag for the provided address. -func (m *Manager) MarkUsed(address btcutil.Address) error { +func (m *Manager) MarkUsed(ns walletdb.ReadWriteBucket, address btcutil.Address) error { addressID := address.ScriptAddress() - err := m.namespace.Update(func(tx walletdb.Tx) error { - return markAddressUsed(tx, addressID) - }) + err := markAddressUsed(ns, addressID) if err != nil { return maybeConvertDbError(err) } @@ -1494,12 +1433,10 @@ func (m *Manager) ChainParams() *chaincfg.Params { // branch indicated by the internal flag. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) nextAddresses(account uint32, numAddresses uint32, internal bool, - addrType addressType) ([]ManagedAddress, error) { - +func (m *Manager) nextAddresses(ns walletdb.ReadWriteBucket, account uint32, numAddresses uint32, internal bool) ([]ManagedAddress, error) { // The next address can only be generated for accounts that have already // been created. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -1618,12 +1555,7 @@ func (m *Manager) nextAddresses(account uint32, numAddresses uint32, internal bo } } } - - return nil }) - if err != nil { - return nil, maybeConvertDbError(err) - } // Finally update the next address tracking and add the addresses to the // cache after the newly generated addresses have been successfully @@ -1658,9 +1590,7 @@ func (m *Manager) nextAddresses(account uint32, numAddresses uint32, internal bo // NextExternalAddresses returns the specified number of next chained addresses // that are intended for external use from the address manager. -func (m *Manager) NextExternalAddresses(account uint32, numAddresses uint32, - addrType AddressType) ([]ManagedAddress, error) { - +func (m *Manager) NextExternalAddresses(ns walletdb.ReadWriteBucket, account uint32, numAddresses uint32) ([]ManagedAddress, error) { // Enforce maximum account number. if account > MaxAccountNum { err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) @@ -1670,15 +1600,12 @@ func (m *Manager) NextExternalAddresses(account uint32, numAddresses uint32, m.mtx.Lock() defer m.mtx.Unlock() - t := addressTypeToInternal(addrType) - return m.nextAddresses(account, numAddresses, false, t) + return m.nextAddresses(ns, account, numAddresses, false) } // NextInternalAddresses returns the specified number of next chained addresses // that are intended for internal use such as change from the address manager. -func (m *Manager) NextInternalAddresses(account uint32, numAddresses uint32, - addrType AddressType) ([]ManagedAddress, error) { - +func (m *Manager) NextInternalAddresses(ns walletdb.ReadWriteBucket, account uint32, numAddresses uint32) ([]ManagedAddress, error) { // Enforce maximum account number. if account > MaxAccountNum { err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) @@ -1688,8 +1615,7 @@ func (m *Manager) NextInternalAddresses(account uint32, numAddresses uint32, m.mtx.Lock() defer m.mtx.Unlock() - t := addressTypeToInternal(addrType) - return m.nextAddresses(account, numAddresses, true, t) + return m.nextAddresses(ns, account, numAddresses, true) } // LastExternalAddress returns the most recently requested chained external @@ -1700,7 +1626,7 @@ func (m *Manager) NextInternalAddresses(account uint32, numAddresses uint32, // This function will return an error if the provided account number is greater // than the MaxAccountNum constant or there is no account information for the // passed account. Any other errors returned are generally unexpected. -func (m *Manager) LastExternalAddress(account uint32) (ManagedAddress, error) { +func (m *Manager) LastExternalAddress(ns walletdb.ReadBucket, account uint32) (ManagedAddress, error) { // Enforce maximum account number. if account > MaxAccountNum { err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) @@ -1712,7 +1638,7 @@ func (m *Manager) LastExternalAddress(account uint32) (ManagedAddress, error) { // Load account information for the passed account. It is typically // cached, but if not it will be loaded from the database. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -1732,7 +1658,7 @@ func (m *Manager) LastExternalAddress(account uint32) (ManagedAddress, error) { // This function will return an error if the provided account number is greater // than the MaxAccountNum constant or there is no account information for the // passed account. Any other errors returned are generally unexpected. -func (m *Manager) LastInternalAddress(account uint32) (ManagedAddress, error) { +func (m *Manager) LastInternalAddress(ns walletdb.ReadBucket, account uint32) (ManagedAddress, error) { // Enforce maximum account number. if account > MaxAccountNum { err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) @@ -1744,7 +1670,7 @@ func (m *Manager) LastInternalAddress(account uint32) (ManagedAddress, error) { // Load account information for the passed account. It is typically // cached, but if not it will be loaded from the database. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -1774,7 +1700,7 @@ func ValidateAccountName(name string) error { // ErrDuplicateAccount will be returned. Since creating a new account requires // access to the cointype keys (from which extended account keys are derived), // it requires the manager to be unlocked. -func (m *Manager) NewAccount(name string) (uint32, error) { +func (m *Manager) NewAccount(ns walletdb.ReadWriteBucket, name string) (uint32, error) { if m.watchingOnly { return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil) } @@ -1792,87 +1718,81 @@ func (m *Manager) NewAccount(name string) (uint32, error) { } // Check that account with the same name does not exist - _, err := m.lookupAccount(name) + _, err := m.lookupAccount(ns, name) if err == nil { str := fmt.Sprintf("account with the same name already exists") return 0, managerError(ErrDuplicateAccount, str, err) } - var account uint32 - var coinTypePrivEnc []byte - // Fetch latest account, and create a new account in the same transaction - err = m.namespace.Update(func(tx walletdb.Tx) error { - var err error - // Fetch the latest account number to generate the next account number - account, err = fetchLastAccount(tx) - if err != nil { - return err - } - account++ - // Fetch the cointype key which will be used to derive the next account - // extended keys - _, coinTypePrivEnc, err = fetchCoinTypeKeys(tx) - if err != nil { - return err - } + // Fetch the latest account number to generate the next account number + account, err := fetchLastAccount(ns) + if err != nil { + return 0, err + } + account++ + // Fetch the cointype key which will be used to derive the next account + // extended keys + _, coinTypePrivEnc, err := fetchCoinTypeKeys(ns) + if err != nil { + return 0, err + } - // Decrypt the cointype key - serializedKeyPriv, err := m.cryptoKeyPriv.Decrypt(coinTypePrivEnc) - if err != nil { - str := fmt.Sprintf("failed to decrypt cointype serialized private key") - return managerError(ErrLocked, str, err) - } - coinTypeKeyPriv, err := hdkeychain.NewKeyFromString(string(serializedKeyPriv)) - zero.Bytes(serializedKeyPriv) - if err != nil { - str := fmt.Sprintf("failed to create cointype extended private key") - return managerError(ErrKeyChain, str, err) - } + // Decrypt the cointype key + serializedKeyPriv, err := m.cryptoKeyPriv.Decrypt(coinTypePrivEnc) + if err != nil { + str := fmt.Sprintf("failed to decrypt cointype serialized private key") + return 0, managerError(ErrLocked, str, err) + } + coinTypeKeyPriv, err := hdkeychain.NewKeyFromString(string(serializedKeyPriv)) + zero.Bytes(serializedKeyPriv) + if err != nil { + str := fmt.Sprintf("failed to create cointype extended private key") + return 0, managerError(ErrKeyChain, str, err) + } - // Derive the account key using the cointype key - acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account) - coinTypeKeyPriv.Zero() - if err != nil { - str := "failed to convert private key for account" - return managerError(ErrKeyChain, str, err) - } - acctKeyPub, err := acctKeyPriv.Neuter() - if err != nil { - str := "failed to convert public key for account" - return managerError(ErrKeyChain, str, err) - } - // Encrypt the default account keys with the associated crypto keys. - acctPubEnc, err := m.cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) - if err != nil { - str := "failed to encrypt public key for account" - return managerError(ErrCrypto, str, err) - } - acctPrivEnc, err := m.cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String())) - if err != nil { - str := "failed to encrypt private key for account" - return managerError(ErrCrypto, str, err) - } - // We have the encrypted account extended keys, so save them to the - // database - err = putAccountInfo(tx, account, acctPubEnc, acctPrivEnc, 0, 0, name) - if err != nil { - return err - } + // Derive the account key using the cointype key + acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account) + coinTypeKeyPriv.Zero() + if err != nil { + str := "failed to convert private key for account" + return 0, managerError(ErrKeyChain, str, err) + } + acctKeyPub, err := acctKeyPriv.Neuter() + if err != nil { + str := "failed to convert public key for account" + return 0, managerError(ErrKeyChain, str, err) + } + // Encrypt the default account keys with the associated crypto keys. + acctPubEnc, err := m.cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) + if err != nil { + str := "failed to encrypt public key for account" + return 0, managerError(ErrCrypto, str, err) + } + acctPrivEnc, err := m.cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String())) + if err != nil { + str := "failed to encrypt private key for account" + return 0, managerError(ErrCrypto, str, err) + } + // We have the encrypted account extended keys, so save them to the + // database + err = putAccountInfo(ns, account, acctPubEnc, acctPrivEnc, 0, 0, name) + if err != nil { + return 0, err + } - // Save last account metadata - if err := putLastAccount(tx, account); err != nil { - return err - } - return nil - }) - return account, err + // Save last account metadata + if err := putLastAccount(ns, account); err != nil { + return 0, err + } + + return account, nil } // RenameAccount renames an account stored in the manager based on the // given account number with the given name. If an account with the same name // already exists, ErrDuplicateAccount will be returned. -func (m *Manager) RenameAccount(account uint32, name string) error { +func (m *Manager) RenameAccount(ns walletdb.ReadWriteBucket, account uint32, name string) error { m.mtx.Lock() defer m.mtx.Unlock() @@ -1883,7 +1803,7 @@ func (m *Manager) RenameAccount(account uint32, name string) error { } // Check that account with the new name does not exist - _, err := m.lookupAccount(name) + _, err := m.lookupAccount(ns, name) if err == nil { str := fmt.Sprintf("account with the same name already exists") return managerError(ErrDuplicateAccount, str, err) @@ -1893,31 +1813,30 @@ func (m *Manager) RenameAccount(account uint32, name string) error { return err } - var rowInterface interface{} - err = m.namespace.Update(func(tx walletdb.Tx) error { - var err error - rowInterface, err = fetchAccountInfo(tx, account) - if err != nil { - return err - } - // Ensure the account type is a BIP0044 account. - row, ok := rowInterface.(*dbBIP0044AccountRow) - if !ok { - str := fmt.Sprintf("unsupported account type %T", row) - err = managerError(ErrDatabase, str, nil) - } - // Remove the old name key from the accout id index - if err = deleteAccountIDIndex(tx, account); err != nil { - return err - } - // Remove the old name key from the account name index - if err = deleteAccountNameIndex(tx, row.name); err != nil { - return err - } - err = putAccountInfo(tx, account, row.pubKeyEncrypted, - row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, name) + rowInterface, err := fetchAccountInfo(ns, account) + if err != nil { return err - }) + } + + // Ensure the account type is a BIP0044 account. + row, ok := rowInterface.(*dbBIP0044AccountRow) + if !ok { + str := fmt.Sprintf("unsupported account type %T", row) + err = managerError(ErrDatabase, str, nil) + } + // Remove the old name key from the accout id index + if err = deleteAccountIDIndex(ns, account); err != nil { + return err + } + // Remove the old name key from the account name index + if err = deleteAccountNameIndex(ns, row.name); err != nil { + return err + } + err = putAccountInfo(ns, account, row.pubKeyEncrypted, + row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, name) + if err != nil { + return err + } // Update in-memory account info with new name if cached and the db // write was successful. @@ -1932,56 +1851,35 @@ func (m *Manager) RenameAccount(account uint32, name string) error { // AccountName returns the account name for the given account number // stored in the manager. -func (m *Manager) AccountName(account uint32) (string, error) { - var acctName string - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - acctName, err = fetchAccountName(tx, account) - return err - }) - if err != nil { - return "", err - } - - return acctName, nil +func (m *Manager) AccountName(ns walletdb.ReadBucket, account uint32) (string, error) { + return fetchAccountName(ns, account) } // ForEachAccount calls the given function with each account stored in the // manager, breaking early on error. -func (m *Manager) ForEachAccount(fn func(account uint32) error) error { - return m.namespace.View(func(tx walletdb.Tx) error { - return forEachAccount(tx, fn) - }) +func (m *Manager) ForEachAccount(ns walletdb.ReadBucket, fn func(account uint32) error) error { + return forEachAccount(ns, fn) } // LastAccount returns the last account stored in the manager. -func (m *Manager) LastAccount() (uint32, error) { - var account uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - account, err = fetchLastAccount(tx) - return err - }) - return account, err +func (m *Manager) LastAccount(ns walletdb.ReadBucket) (uint32, error) { + return fetchLastAccount(ns) } // ForEachAccountAddress calls the given function with each address of // the given account stored in the manager, breaking early on error. -func (m *Manager) ForEachAccountAddress(account uint32, fn func(maddr ManagedAddress) error) error { +func (m *Manager) ForEachAccountAddress(ns walletdb.ReadBucket, account uint32, fn func(maddr ManagedAddress) error) error { m.mtx.Lock() defer m.mtx.Unlock() addrFn := func(rowInterface interface{}) error { - managedAddr, err := m.rowInterfaceToManaged(rowInterface) + managedAddr, err := m.rowInterfaceToManaged(ns, rowInterface) if err != nil { return err } return fn(managedAddr) } - - err := m.namespace.View(func(tx walletdb.Tx) error { - return forEachAccountAddress(tx, account, addrFn) - }) + err := forEachAccountAddress(ns, account, addrFn) if err != nil { return maybeConvertDbError(err) } @@ -1992,27 +1890,25 @@ func (m *Manager) ForEachAccountAddress(account uint32, fn func(maddr ManagedAdd // address of the given account stored in the manager, breaking early on // error. // TODO(tuxcanfly): actually return only active addresses -func (m *Manager) ForEachActiveAccountAddress(account uint32, fn func(maddr ManagedAddress) error) error { - return m.ForEachAccountAddress(account, fn) +func (m *Manager) ForEachActiveAccountAddress(ns walletdb.ReadBucket, account uint32, fn func(maddr ManagedAddress) error) error { + return m.ForEachAccountAddress(ns, account, fn) } // ForEachActiveAddress calls the given function with each active address // stored in the manager, breaking early on error. -func (m *Manager) ForEachActiveAddress(fn func(addr btcutil.Address) error) error { +func (m *Manager) ForEachActiveAddress(ns walletdb.ReadBucket, fn func(addr btcutil.Address) error) error { m.mtx.Lock() defer m.mtx.Unlock() addrFn := func(rowInterface interface{}) error { - managedAddr, err := m.rowInterfaceToManaged(rowInterface) + managedAddr, err := m.rowInterfaceToManaged(ns, rowInterface) if err != nil { return err } return fn(managedAddr.Address()) } - err := m.namespace.View(func(tx walletdb.Tx) error { - return forEachActiveAddress(tx, addrFn) - }) + err := forEachActiveAddress(ns, addrFn) if err != nil { return maybeConvertDbError(err) } @@ -2087,14 +1983,12 @@ func (m *Manager) Decrypt(keyType CryptoKeyType, in []byte) ([]byte, error) { } // newManager returns a new locked address manager with the given parameters. -func newManager(namespace walletdb.Namespace, chainParams *chaincfg.Params, - masterKeyPub *snacl.SecretKey, masterKeyPriv *snacl.SecretKey, - cryptoKeyPub EncryptorDecryptor, cryptoKeyPrivEncrypted, - cryptoKeyScriptEncrypted []byte, syncInfo *syncState, +func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey, + masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor, + cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState, privPassphraseSalt [saltSize]byte) *Manager { return &Manager{ - namespace: namespace, chainParams: chainParams, addrs: make(map[addrKey]ManagedAddress), syncState: *syncInfo, @@ -2188,49 +2082,51 @@ func checkBranchKeys(acctKey *hdkeychain.ExtendedKey) error { // loadManager returns a new address manager that results from loading it from // the passed opened database. The public passphrase is required to decrypt the // public keys. -func loadManager(namespace walletdb.Namespace, pubPassphrase []byte, chainParams *chaincfg.Params) (*Manager, error) { - // Perform all database lookups in a read-only view. - var watchingOnly bool - var masterKeyPubParams, masterKeyPrivParams []byte - var cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc []byte - var syncedTo, startBlock *BlockStamp - var recentHeight int32 - var recentHashes []chainhash.Hash - err := namespace.View(func(tx walletdb.Tx) error { - // Load whether or not the manager is watching-only from the db. - var err error - watchingOnly, err = fetchWatchingOnly(tx) - if err != nil { - return err - } +func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte, chainParams *chaincfg.Params) (*Manager, error) { + // Verify the version is neither too old or too new. + version, err := fetchManagerVersion(ns) + if err != nil { + str := "failed to fetch version for update" + return nil, managerError(ErrDatabase, str, err) + } + if version < latestMgrVersion { + str := "database upgrade required" + return nil, managerError(ErrUpgrade, str, nil) + } else if version > latestMgrVersion { + str := "database version is greater than latest understood version" + return nil, managerError(ErrUpgrade, str, nil) + } - // Load the master key params from the db. - masterKeyPubParams, masterKeyPrivParams, err = - fetchMasterKeyParams(tx) - if err != nil { - return err - } + // Load whether or not the manager is watching-only from the db. + watchingOnly, err := fetchWatchingOnly(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } - // Load the crypto keys from the db. - cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc, err = - fetchCryptoKeys(tx) - if err != nil { - return err - } + // Load the master key params from the db. + masterKeyPubParams, masterKeyPrivParams, err := fetchMasterKeyParams(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } - // Load the sync state from the db. - syncedTo, err = fetchSyncedTo(tx) - if err != nil { - return err - } - startBlock, err = fetchStartBlock(tx) - if err != nil { - return err - } + // Load the crypto keys from the db. + cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc, err := + fetchCryptoKeys(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } - recentHeight, recentHashes, err = fetchRecentBlocks(tx) - return err - }) + // Load the sync state from the db. + syncedTo, err := fetchSyncedTo(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } + startBlock, err := fetchStartBlock(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } + + recentHeight, recentHashes, err := fetchRecentBlocks(ns) if err != nil { return nil, maybeConvertDbError(err) } @@ -2282,7 +2178,7 @@ func loadManager(namespace walletdb.Namespace, pubPassphrase []byte, chainParams // Create new address manager with the given parameters. Also, override // the defaults for the additional fields which are not specified in the // call to new with the values loaded from the database. - mgr := newManager(namespace, chainParams, &masterKeyPub, &masterKeyPriv, + mgr := newManager(chainParams, &masterKeyPub, &masterKeyPriv, cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo, privPassphraseSalt) mgr.watchingOnly = watchingOnly @@ -2299,24 +2195,24 @@ func loadManager(namespace walletdb.Namespace, pubPassphrase []byte, chainParams // // A ManagerError with an error code of ErrNoExist will be returned if the // passed manager does not exist in the specified namespace. -func Open(namespace walletdb.Namespace, pubPassphrase []byte, chainParams *chaincfg.Params, cbs *OpenCallbacks) (*Manager, error) { +func Open(ns walletdb.ReadBucket, pubPassphrase []byte, chainParams *chaincfg.Params) (*Manager, error) { // Return an error if the manager has NOT already been created in the // given database namespace. - exists, err := managerExists(namespace) - if err != nil { - return nil, err - } + exists := managerExists(ns) if !exists { str := "the specified address manager does not exist" return nil, managerError(ErrNoExist, str, nil) } - // Upgrade the manager to the latest version as needed. - if err := upgradeManager(namespace, pubPassphrase, chainParams, cbs); err != nil { - return nil, err - } + return loadManager(ns, pubPassphrase, chainParams) +} - return loadManager(namespace, pubPassphrase, chainParams) +// DoUpgrades performs any necessary upgrades to the address manager contained +// in the wallet database, namespaced by the top level bucket key namespaceKey. +func DoUpgrades(db walletdb.DB, namespaceKey []byte, pubPassphrase []byte, + chainParams *chaincfg.Params, cbs *OpenCallbacks) error { + + return upgradeManager(db, namespaceKey, pubPassphrase, chainParams, cbs) } // Create creates a new address manager in the given namespace. The seed must @@ -2336,243 +2232,239 @@ func Open(namespace walletdb.Namespace, pubPassphrase []byte, chainParams *chain // // A ManagerError with an error code of ErrAlreadyExists will be returned the // address manager already exists in the specified namespace. -func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []byte, chainParams *chaincfg.Params, config *ScryptOptions) error { - // Return an error if the manager has already been created in the given - // database namespace. - exists, err := managerExists(namespace) - if err != nil { - return err - } - if exists { - return managerError(ErrAlreadyExists, errAlreadyExists, nil) - } - - // Ensure the private passphrase is not empty. - if len(privPassphrase) == 0 { - str := "private passphrase may not be empty" - return managerError(ErrEmptyPassphrase, str, nil) - } - - // Perform the initial bucket creation and database namespace setup. - if err := createManagerNS(namespace); err != nil { - return err - } - - if config == nil { - config = &DefaultScryptOptions - } - - // Generate the BIP0044 HD key structure to ensure the provided seed - // can generate the required structure with no issues. - - // Derive the master extended key from the seed. - root, err := hdkeychain.NewMaster(seed, chainParams) - if err != nil { - str := "failed to derive master extended key" - return managerError(ErrKeyChain, str, err) - } - - // Derive the cointype key according to BIP0044. - coinTypeKeyPriv, err := deriveCoinTypeKey(root, chainParams.HDCoinType) - if err != nil { - str := "failed to derive cointype extended key" - return managerError(ErrKeyChain, str, err) - } - defer coinTypeKeyPriv.Zero() - - // Derive the account key for the first account according to BIP0044. - acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0) - if err != nil { - // The seed is unusable if the any of the children in the - // required hierarchy can't be derived due to invalid child. - if err == hdkeychain.ErrInvalidChild { - str := "the provided seed is unusable" - return managerError(ErrKeyChain, str, - hdkeychain.ErrUnusableSeed) +func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []byte, chainParams *chaincfg.Params, config *ScryptOptions) error { + err := func() error { + // Return an error if the manager has already been created in the given + // database namespace. + exists := managerExists(ns) + if exists { + return managerError(ErrAlreadyExists, errAlreadyExists, nil) } - return err - } - - // Ensure the branch keys can be derived for the provided seed according - // to BIP0044. - if err := checkBranchKeys(acctKeyPriv); err != nil { - // The seed is unusable if the any of the children in the - // required hierarchy can't be derived due to invalid child. - if err == hdkeychain.ErrInvalidChild { - str := "the provided seed is unusable" - return managerError(ErrKeyChain, str, - hdkeychain.ErrUnusableSeed) + // Ensure the private passphrase is not empty. + if len(privPassphrase) == 0 { + str := "private passphrase may not be empty" + return managerError(ErrEmptyPassphrase, str, nil) } - return err - } + // Perform the initial bucket creation and database namespace setup. + if err := createManagerNS(ns); err != nil { + return err + } - // The address manager needs the public extended key for the account. - acctKeyPub, err := acctKeyPriv.Neuter() - if err != nil { - str := "failed to convert private key for account 0" - return managerError(ErrKeyChain, str, err) - } + if config == nil { + config = &DefaultScryptOptions + } - // Generate new master keys. These master keys are used to protect the - // crypto keys that will be generated next. - masterKeyPub, err := newSecretKey(&pubPassphrase, config) - if err != nil { - str := "failed to master public key" - return managerError(ErrCrypto, str, err) - } - masterKeyPriv, err := newSecretKey(&privPassphrase, config) - if err != nil { - str := "failed to master private key" - return managerError(ErrCrypto, str, err) - } - defer masterKeyPriv.Zero() + // Generate the BIP0044 HD key structure to ensure the provided seed + // can generate the required structure with no issues. - // Generate the private passphrase salt. This is used when hashing - // passwords to detect whether an unlock can be avoided when the manager - // is already unlocked. - var privPassphraseSalt [saltSize]byte - _, err = rand.Read(privPassphraseSalt[:]) - if err != nil { - str := "failed to read random source for passphrase salt" - return managerError(ErrCrypto, str, err) - } + // Derive the master extended key from the seed. + root, err := hdkeychain.NewMaster(seed, chainParams) + if err != nil { + str := "failed to derive master extended key" + return managerError(ErrKeyChain, str, err) + } - // Generate new crypto public, private, and script keys. These keys are - // used to protect the actual public and private data such as addresses, - // extended keys, and scripts. - cryptoKeyPub, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto public key" - return managerError(ErrCrypto, str, err) - } - cryptoKeyPriv, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto private key" - return managerError(ErrCrypto, str, err) - } - defer cryptoKeyPriv.Zero() - cryptoKeyScript, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto script key" - return managerError(ErrCrypto, str, err) - } - defer cryptoKeyScript.Zero() + // Derive the cointype key according to BIP0044. + coinTypeKeyPriv, err := deriveCoinTypeKey(root, chainParams.HDCoinType) + if err != nil { + str := "failed to derive cointype extended key" + return managerError(ErrKeyChain, str, err) + } + defer coinTypeKeyPriv.Zero() - // Encrypt the crypto keys with the associated master keys. - cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes()) - if err != nil { - str := "failed to encrypt crypto public key" - return managerError(ErrCrypto, str, err) - } - cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes()) - if err != nil { - str := "failed to encrypt crypto private key" - return managerError(ErrCrypto, str, err) - } - cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript.Bytes()) - if err != nil { - str := "failed to encrypt crypto script key" - return managerError(ErrCrypto, str, err) - } + // Derive the account key for the first account according to BIP0044. + acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0) + if err != nil { + // The seed is unusable if the any of the children in the + // required hierarchy can't be derived due to invalid child. + if err == hdkeychain.ErrInvalidChild { + str := "the provided seed is unusable" + return managerError(ErrKeyChain, str, + hdkeychain.ErrUnusableSeed) + } - // Encrypt the cointype keys with the associated crypto keys. - coinTypeKeyPub, err := coinTypeKeyPriv.Neuter() - if err != nil { - str := "failed to convert cointype private key" - return managerError(ErrKeyChain, str, err) - } - coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(coinTypeKeyPub.String())) - if err != nil { - str := "failed to encrypt cointype public key" - return managerError(ErrCrypto, str, err) - } - coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(coinTypeKeyPriv.String())) - if err != nil { - str := "failed to encrypt cointype private key" - return managerError(ErrCrypto, str, err) - } + return err + } - // Encrypt the default account keys with the associated crypto keys. - acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) - if err != nil { - str := "failed to encrypt public key for account 0" - return managerError(ErrCrypto, str, err) - } - acctPrivEnc, err := cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String())) - if err != nil { - str := "failed to encrypt private key for account 0" - return managerError(ErrCrypto, str, err) - } + // Ensure the branch keys can be derived for the provided seed according + // to BIP0044. + if err := checkBranchKeys(acctKeyPriv); err != nil { + // The seed is unusable if the any of the children in the + // required hierarchy can't be derived due to invalid child. + if err == hdkeychain.ErrInvalidChild { + str := "the provided seed is unusable" + return managerError(ErrKeyChain, str, + hdkeychain.ErrUnusableSeed) + } - // Use the genesis block for the passed chain as the created at block - // for the default. - createdAt := &BlockStamp{Hash: *chainParams.GenesisHash, Height: 0} + return err + } - // Create the initial sync state. - recentHashes := []chainhash.Hash{createdAt.Hash} - recentHeight := createdAt.Height - syncInfo := newSyncState(createdAt, createdAt, recentHeight, recentHashes) + // The address manager needs the public extended key for the account. + acctKeyPub, err := acctKeyPriv.Neuter() + if err != nil { + str := "failed to convert private key for account 0" + return managerError(ErrKeyChain, str, err) + } + + // Generate new master keys. These master keys are used to protect the + // crypto keys that will be generated next. + masterKeyPub, err := newSecretKey(&pubPassphrase, config) + if err != nil { + str := "failed to master public key" + return managerError(ErrCrypto, str, err) + } + masterKeyPriv, err := newSecretKey(&privPassphrase, config) + if err != nil { + str := "failed to master private key" + return managerError(ErrCrypto, str, err) + } + defer masterKeyPriv.Zero() + + // Generate the private passphrase salt. This is used when hashing + // passwords to detect whether an unlock can be avoided when the manager + // is already unlocked. + var privPassphraseSalt [saltSize]byte + _, err = rand.Read(privPassphraseSalt[:]) + if err != nil { + str := "failed to read random source for passphrase salt" + return managerError(ErrCrypto, str, err) + } + + // Generate new crypto public, private, and script keys. These keys are + // used to protect the actual public and private data such as addresses, + // extended keys, and scripts. + cryptoKeyPub, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto public key" + return managerError(ErrCrypto, str, err) + } + cryptoKeyPriv, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto private key" + return managerError(ErrCrypto, str, err) + } + defer cryptoKeyPriv.Zero() + cryptoKeyScript, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto script key" + return managerError(ErrCrypto, str, err) + } + defer cryptoKeyScript.Zero() + + // Encrypt the crypto keys with the associated master keys. + cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes()) + if err != nil { + str := "failed to encrypt crypto public key" + return managerError(ErrCrypto, str, err) + } + cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes()) + if err != nil { + str := "failed to encrypt crypto private key" + return managerError(ErrCrypto, str, err) + } + cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript.Bytes()) + if err != nil { + str := "failed to encrypt crypto script key" + return managerError(ErrCrypto, str, err) + } + + // Encrypt the cointype keys with the associated crypto keys. + coinTypeKeyPub, err := coinTypeKeyPriv.Neuter() + if err != nil { + str := "failed to convert cointype private key" + return managerError(ErrKeyChain, str, err) + } + coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(coinTypeKeyPub.String())) + if err != nil { + str := "failed to encrypt cointype public key" + return managerError(ErrCrypto, str, err) + } + coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(coinTypeKeyPriv.String())) + if err != nil { + str := "failed to encrypt cointype private key" + return managerError(ErrCrypto, str, err) + } + + // Encrypt the default account keys with the associated crypto keys. + acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) + if err != nil { + str := "failed to encrypt public key for account 0" + return managerError(ErrCrypto, str, err) + } + acctPrivEnc, err := cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String())) + if err != nil { + str := "failed to encrypt private key for account 0" + return managerError(ErrCrypto, str, err) + } + + // Use the genesis block for the passed chain as the created at block + // for the default. + createdAt := &BlockStamp{Hash: *chainParams.GenesisHash, Height: 0} + + // Create the initial sync state. + recentHashes := []chainhash.Hash{createdAt.Hash} + recentHeight := createdAt.Height + syncInfo := newSyncState(createdAt, createdAt, recentHeight, recentHashes) - // Perform all database updates in a single transaction. - err = namespace.Update(func(tx walletdb.Tx) error { // Save the master key params to the database. pubParams := masterKeyPub.Marshal() privParams := masterKeyPriv.Marshal() - err = putMasterKeyParams(tx, pubParams, privParams) + err = putMasterKeyParams(ns, pubParams, privParams) if err != nil { return err } // Save the encrypted crypto keys to the database. - err = putCryptoKeys(tx, cryptoKeyPubEnc, cryptoKeyPrivEnc, + err = putCryptoKeys(ns, cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc) if err != nil { return err } // Save the encrypted cointype keys to the database. - err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc) + err = putCoinTypeKeys(ns, coinTypePubEnc, coinTypePrivEnc) if err != nil { return err } // Save the fact this is not a watching-only address manager to // the database. - err = putWatchingOnly(tx, false) + err = putWatchingOnly(ns, false) if err != nil { return err } // Save the initial synced to state. - err = putSyncedTo(tx, &syncInfo.syncedTo) + err = putSyncedTo(ns, &syncInfo.syncedTo) if err != nil { return err } - err = putStartBlock(tx, &syncInfo.startBlock) + err = putStartBlock(ns, &syncInfo.startBlock) if err != nil { return err } // Save the initial recent blocks state. - err = putRecentBlocks(tx, recentHeight, recentHashes) + err = putRecentBlocks(ns, recentHeight, recentHashes) if err != nil { return err } // Save the information for the imported account to the database. - err = putAccountInfo(tx, ImportedAddrAccount, nil, + err = putAccountInfo(ns, ImportedAddrAccount, nil, nil, 0, 0, ImportedAddrAccountName) if err != nil { return err } // Save the information for the default account to the database. - err = putAccountInfo(tx, DefaultAccountNum, acctPubEnc, + err = putAccountInfo(ns, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0, defaultAccountName) return err - }) + }() if err != nil { return maybeConvertDbError(err) } diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index c9b025f..687c7fd 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -293,9 +293,13 @@ func testExternalAddresses(tc *testContext) bool { var addrs []waddrmgr.ManagedAddress if tc.create { prefix := prefix + " NextExternalAddresses" - var err error - addrs, err = tc.manager.NextExternalAddresses(tc.account, 5, - waddrmgr.PubKeyHash) + var addrs []waddrmgr.ManagedAddress + err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + addrs, err = tc.manager.NextExternalAddresses(ns, tc.account, 5) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", prefix, err) return false @@ -324,7 +328,13 @@ func testExternalAddresses(tc *testContext) bool { // Ensure the last external address is the expected one. leaPrefix := prefix + " LastExternalAddress" - lastAddr, err := tc.manager.LastExternalAddress(tc.account) + var lastAddr waddrmgr.ManagedAddress + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + lastAddr, err = tc.manager.LastExternalAddress(ns, tc.account) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", leaPrefix, err) return false @@ -347,7 +357,13 @@ func testExternalAddresses(tc *testContext) bool { } prefix := fmt.Sprintf("%s Address #%d", prefix, i) - addr, err := tc.manager.Address(utilAddr) + var addr waddrmgr.ManagedAddress + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = tc.manager.Address(ns, utilAddr) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", prefix, err) @@ -378,7 +394,11 @@ func testExternalAddresses(tc *testContext) bool { // Unlock the manager and retest all of the addresses to ensure the // private information is valid as well. - if err := tc.manager.Unlock(privPassphrase); err != nil { + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, privPassphrase) + }) + if err != nil { tc.t.Errorf("Unlock: unexpected error: %v", err) return false } @@ -410,7 +430,11 @@ func testInternalAddresses(tc *testContext) bool { if !tc.watchingOnly { // Unlock the manager and retest all of the addresses to ensure the // private information is valid as well. - if err := tc.manager.Unlock(privPassphrase); err != nil { + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, privPassphrase) + }) + if err != nil { tc.t.Errorf("Unlock: unexpected error: %v", err) return false } @@ -421,9 +445,12 @@ func testInternalAddresses(tc *testContext) bool { var addrs []waddrmgr.ManagedAddress if tc.create { prefix := prefix + " NextInternalAddress" - var err error - addrs, err = tc.manager.NextInternalAddresses(tc.account, 5, - waddrmgr.PubKeyHash) + err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + addrs, err = tc.manager.NextInternalAddresses(ns, tc.account, 5) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", prefix, err) return false @@ -452,7 +479,13 @@ func testInternalAddresses(tc *testContext) bool { // Ensure the last internal address is the expected one. liaPrefix := prefix + " LastInternalAddress" - lastAddr, err := tc.manager.LastInternalAddress(tc.account) + var lastAddr waddrmgr.ManagedAddress + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + lastAddr, err = tc.manager.LastInternalAddress(ns, tc.account) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", liaPrefix, err) return false @@ -475,7 +508,13 @@ func testInternalAddresses(tc *testContext) bool { } prefix := fmt.Sprintf("%s Address #%d", prefix, i) - addr, err := tc.manager.Address(utilAddr) + var addr waddrmgr.ManagedAddress + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = tc.manager.Address(ns, utilAddr) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", prefix, err) @@ -552,7 +591,10 @@ func testLocking(tc *testContext) bool { // unexpected errors and the manager properly reports it is unlocked. // Since watching-only address managers can't be unlocked, also ensure // the correct error for that case. - err = tc.manager.Unlock(privPassphrase) + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, privPassphrase) + }) if tc.watchingOnly { if !checkManagerError(tc.t, "Unlock", err, waddrmgr.ErrWatchingOnly) { return false @@ -569,7 +611,10 @@ func testLocking(tc *testContext) bool { // Unlocking the manager again is allowed. Since watching-only address // managers can't be unlocked, also ensure the correct error for that // case. - err = tc.manager.Unlock(privPassphrase) + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, privPassphrase) + }) if tc.watchingOnly { if !checkManagerError(tc.t, "Unlock2", err, waddrmgr.ErrWatchingOnly) { return false @@ -585,7 +630,10 @@ func testLocking(tc *testContext) bool { // Unlocking the manager with an invalid passphrase must result in an // error and a locked manager. - err = tc.manager.Unlock([]byte("invalidpassphrase")) + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, []byte("invalidpassphrase")) + }) wantErrCode = waddrmgr.ErrWrongPassphrase if tc.watchingOnly { wantErrCode = waddrmgr.ErrWatchingOnly @@ -651,7 +699,11 @@ func testImportPrivateKey(tc *testContext) bool { // The manager must be unlocked to import a private key, however a // watching-only manager can't be unlocked. if !tc.watchingOnly { - if err := tc.manager.Unlock(privPassphrase); err != nil { + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, privPassphrase) + }) + if err != nil { tc.t.Errorf("Unlock: unexpected error: %v", err) return false } @@ -670,8 +722,13 @@ func testImportPrivateKey(tc *testContext) bool { "error: %v", prefix, i, test.name, err) continue } - addr, err := tc.manager.ImportPrivateKey(wif, - &test.blockstamp) + var addr waddrmgr.ManagedPubKeyAddress + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + addr, err = tc.manager.ImportPrivateKey(ns, wif, &test.blockstamp) + return err + }) if err != nil { tc.t.Errorf("%s ImportPrivateKey #%d (%s): "+ "unexpected error: %v", prefix, i, @@ -706,7 +763,13 @@ func testImportPrivateKey(tc *testContext) bool { } taPrefix := fmt.Sprintf("%s Address #%d (%s)", prefix, i, test.name) - ma, err := tc.manager.Address(utilAddr) + var ma waddrmgr.ManagedAddress + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + ma, err = tc.manager.Address(ns, utilAddr) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", taPrefix, err) @@ -809,7 +872,11 @@ func testImportScript(tc *testContext) bool { // testing private data. However, a watching-only manager can't be // unlocked. if !tc.watchingOnly { - if err := tc.manager.Unlock(privPassphrase); err != nil { + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, privPassphrase) + }) + if err != nil { tc.t.Errorf("Unlock: unexpected error: %v", err) return false } @@ -825,8 +892,13 @@ func testImportScript(tc *testContext) bool { prefix := fmt.Sprintf("%s ImportScript #%d (%s)", prefix, i, test.name) - addr, err := tc.manager.ImportScript(test.in, - &test.blockstamp) + var addr waddrmgr.ManagedScriptAddress + err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + addr, err = tc.manager.ImportScript(ns, test.in, &test.blockstamp) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", prefix, err) @@ -859,7 +931,13 @@ func testImportScript(tc *testContext) bool { } taPrefix := fmt.Sprintf("%s Address #%d (%s)", prefix, i, test.name) - ma, err := tc.manager.Address(utilAddr) + var ma waddrmgr.ManagedAddress + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + ma, err = tc.manager.Address(ns, utilAddr) + return err + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", taPrefix, err) @@ -946,36 +1024,36 @@ func testMarkUsed(tc *testContext) bool { continue } - maddr, err := tc.manager.Address(addr) - if err != nil { - tc.t.Errorf("%s #%d: Address unexpected error: %v", prefix, i, err) - continue - } - if tc.create { - // Test that initially the address is not flagged as used - used, err := maddr.Used() + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + + maddr, err := tc.manager.Address(ns, addr) if err != nil { - tc.t.Errorf("%s #%d: Used unexpected error: %v", prefix, i, err) - continue + tc.t.Errorf("%s #%d: Address unexpected error: %v", prefix, i, err) + return nil } - if used != false { + if tc.create { + // Test that initially the address is not flagged as used + used := maddr.Used(ns) + if used != false { + tc.t.Errorf("%s #%d: unexpected used flag -- got "+ + "%v, want %v", prefix, i, used, false) + } + } + err = tc.manager.MarkUsed(ns, addr) + if err != nil { + tc.t.Errorf("%s #%d: unexpected error: %v", prefix, i, err) + return nil + } + used := maddr.Used(ns) + if used != true { tc.t.Errorf("%s #%d: unexpected used flag -- got "+ - "%v, want %v", prefix, i, used, false) + "%v, want %v", prefix, i, used, true) } - } - err = tc.manager.MarkUsed(addr) + return nil + }) if err != nil { - tc.t.Errorf("%s #%d: unexpected error: %v", prefix, i, err) - continue - } - used, err := maddr.Used() - if err != nil { - tc.t.Errorf("%s #%d: Used unexpected error: %v", prefix, i, err) - continue - } - if used != true { - tc.t.Errorf("%s #%d: unexpected used flag -- got "+ - "%v, want %v", prefix, i, used, true) + tc.t.Errorf("Unexpected error %v", err) } } @@ -992,7 +1070,10 @@ func testChangePassphrase(tc *testContext) bool { var err error waddrmgr.TstRunWithReplacedNewSecretKey(func() { - err = tc.manager.ChangePassphrase(pubPassphrase, pubPassphrase2, false, fastScrypt) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.ChangePassphrase(ns, pubPassphrase, pubPassphrase2, false, fastScrypt) + }) }) if !checkManagerError(tc.t, testName, err, waddrmgr.ErrCrypto) { return false @@ -1000,14 +1081,20 @@ func testChangePassphrase(tc *testContext) bool { // Attempt to change public passphrase with invalid old passphrase. testName = "ChangePassphrase (public) with invalid old passphrase" - err = tc.manager.ChangePassphrase([]byte("bogus"), pubPassphrase2, false, fastScrypt) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.ChangePassphrase(ns, []byte("bogus"), pubPassphrase2, false, fastScrypt) + }) if !checkManagerError(tc.t, testName, err, waddrmgr.ErrWrongPassphrase) { return false } // Change the public passphrase. testName = "ChangePassphrase (public)" - err = tc.manager.ChangePassphrase(pubPassphrase, pubPassphrase2, false, fastScrypt) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.ChangePassphrase(ns, pubPassphrase, pubPassphrase2, false, fastScrypt) + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", testName, err) return false @@ -1020,7 +1107,10 @@ func testChangePassphrase(tc *testContext) bool { } // Change the private passphrase back to what it was. - err = tc.manager.ChangePassphrase(pubPassphrase2, pubPassphrase, false, fastScrypt) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.ChangePassphrase(ns, pubPassphrase2, pubPassphrase, false, fastScrypt) + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", testName, err) return false @@ -1030,7 +1120,10 @@ func testChangePassphrase(tc *testContext) bool { // The error should be ErrWrongPassphrase or ErrWatchingOnly depending // on the type of the address manager. testName = "ChangePassphrase (private) with invalid old passphrase" - err = tc.manager.ChangePassphrase([]byte("bogus"), privPassphrase2, true, fastScrypt) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.ChangePassphrase(ns, []byte("bogus"), privPassphrase2, true, fastScrypt) + }) wantErrCode := waddrmgr.ErrWrongPassphrase if tc.watchingOnly { wantErrCode = waddrmgr.ErrWatchingOnly @@ -1049,7 +1142,10 @@ func testChangePassphrase(tc *testContext) bool { // Change the private passphrase. testName = "ChangePassphrase (private)" - err = tc.manager.ChangePassphrase(privPassphrase, privPassphrase2, true, fastScrypt) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.ChangePassphrase(ns, privPassphrase, privPassphrase2, true, fastScrypt) + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", testName, err) return false @@ -1057,7 +1153,11 @@ func testChangePassphrase(tc *testContext) bool { // Unlock the manager with the new passphrase to ensure it changed as // expected. - if err := tc.manager.Unlock(privPassphrase2); err != nil { + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.Unlock(ns, privPassphrase2) + }) + if err != nil { tc.t.Errorf("%s: failed to unlock with new private "+ "passphrase: %v", testName, err) return false @@ -1066,7 +1166,10 @@ func testChangePassphrase(tc *testContext) bool { // Change the private passphrase back to what it was while the manager // is unlocked to ensure that path works properly as well. - err = tc.manager.ChangePassphrase(privPassphrase2, privPassphrase, true, fastScrypt) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.ChangePassphrase(ns, privPassphrase2, privPassphrase, true, fastScrypt) + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", testName, err) return false @@ -1091,7 +1194,11 @@ func testChangePassphrase(tc *testContext) bool { func testNewAccount(tc *testContext) bool { if tc.watchingOnly { // Creating new accounts in watching-only mode should return ErrWatchingOnly - _, err := tc.manager.NewAccount("test") + err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + _, err := tc.manager.NewAccount(ns, "test") + return err + }) if !checkManagerError(tc.t, "Create account in watching-only mode", err, waddrmgr.ErrWatchingOnly) { tc.manager.Close() @@ -1100,7 +1207,11 @@ func testNewAccount(tc *testContext) bool { return true } // Creating new accounts when wallet is locked should return ErrLocked - _, err := tc.manager.NewAccount("test") + err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + _, err := tc.manager.NewAccount(ns, "test") + return err + }) if !checkManagerError(tc.t, "Create account when wallet is locked", err, waddrmgr.ErrLocked) { tc.manager.Close() @@ -1108,7 +1219,12 @@ func testNewAccount(tc *testContext) bool { } // Unlock the wallet to decrypt cointype keys required // to derive account keys - if err := tc.manager.Unlock(privPassphrase); err != nil { + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + err := tc.manager.Unlock(ns, privPassphrase) + return err + }) + if err != nil { tc.t.Errorf("Unlock: unexpected error: %v", err) return false } @@ -1121,7 +1237,13 @@ func testNewAccount(tc *testContext) bool { testName = "acct-open" expectedAccount++ } - account, err := tc.manager.NewAccount(testName) + var account uint32 + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + account, err = tc.manager.NewAccount(ns, testName) + return err + }) if err != nil { tc.t.Errorf("NewAccount: unexpected error: %v", err) return false @@ -1134,20 +1256,32 @@ func testNewAccount(tc *testContext) bool { } // Test duplicate account name error - _, err = tc.manager.NewAccount(testName) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + _, err := tc.manager.NewAccount(ns, testName) + return err + }) wantErrCode := waddrmgr.ErrDuplicateAccount if !checkManagerError(tc.t, testName, err, wantErrCode) { return false } // Test account name validation testName = "" // Empty account names are not allowed - _, err = tc.manager.NewAccount(testName) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + _, err := tc.manager.NewAccount(ns, testName) + return err + }) wantErrCode = waddrmgr.ErrInvalidAccount if !checkManagerError(tc.t, testName, err, wantErrCode) { return false } testName = "imported" // A reserved account name - _, err = tc.manager.NewAccount(testName) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + _, err := tc.manager.NewAccount(ns, testName) + return err + }) wantErrCode = waddrmgr.ErrInvalidAccount if !checkManagerError(tc.t, testName, err, wantErrCode) { return false @@ -1164,7 +1298,13 @@ func testLookupAccount(tc *testContext) bool { waddrmgr.ImportedAddrAccountName: waddrmgr.ImportedAddrAccount, } for acctName, expectedAccount := range expectedAccounts { - account, err := tc.manager.LookupAccount(acctName) + var account uint32 + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + account, err = tc.manager.LookupAccount(ns, acctName) + return err + }) if err != nil { tc.t.Errorf("LookupAccount: unexpected error: %v", err) return false @@ -1178,14 +1318,24 @@ func testLookupAccount(tc *testContext) bool { } // Test account not found error testName := "non existent account" - _, err := tc.manager.LookupAccount(testName) + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := tc.manager.LookupAccount(ns, testName) + return err + }) wantErrCode := waddrmgr.ErrAccountNotFound if !checkManagerError(tc.t, testName, err, wantErrCode) { return false } // Test last account - lastAccount, err := tc.manager.LastAccount() + var lastAccount uint32 + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + lastAccount, err = tc.manager.LastAccount(ns) + return err + }) var expectedLastAccount uint32 expectedLastAccount = 1 if !tc.create { @@ -1208,7 +1358,13 @@ func testLookupAccount(tc *testContext) bool { tc.t.Errorf("AddrAccount #%d: unexpected error: %v", i, err) return false } - account, err := tc.manager.AddrAccount(addr) + var account uint32 + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + account, err = tc.manager.AddrAccount(ns, addr) + return err + }) if err != nil { tc.t.Errorf("AddrAccount #%d: unexpected error: %v", i, err) return false @@ -1226,18 +1382,33 @@ func testLookupAccount(tc *testContext) bool { // testRenameAccount tests the rename account func of the address manager works // as expected. func testRenameAccount(tc *testContext) bool { - acctName, err := tc.manager.AccountName(tc.account) + var acctName string + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + acctName, err = tc.manager.AccountName(ns, tc.account) + return err + }) if err != nil { tc.t.Errorf("AccountName: unexpected error: %v", err) return false } testName := acctName + "-renamed" - err = tc.manager.RenameAccount(tc.account, testName) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.RenameAccount(ns, tc.account, testName) + }) if err != nil { tc.t.Errorf("RenameAccount: unexpected error: %v", err) return false } - newName, err := tc.manager.AccountName(tc.account) + var newName string + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + newName, err = tc.manager.AccountName(ns, tc.account) + return err + }) if err != nil { tc.t.Errorf("AccountName: unexpected error: %v", err) return false @@ -1249,13 +1420,20 @@ func testRenameAccount(tc *testContext) bool { return false } // Test duplicate account name error - err = tc.manager.RenameAccount(tc.account, testName) + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.RenameAccount(ns, tc.account, testName) + }) wantErrCode := waddrmgr.ErrDuplicateAccount if !checkManagerError(tc.t, testName, err, wantErrCode) { return false } // Test old account name is no longer valid - _, err = tc.manager.LookupAccount(acctName) + err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := tc.manager.LookupAccount(ns, acctName) + return err + }) wantErrCode = waddrmgr.ErrAccountNotFound if !checkManagerError(tc.t, testName, err, wantErrCode) { return false @@ -1275,9 +1453,12 @@ func testForEachAccount(tc *testContext) bool { // Imported account expectedAccounts = append(expectedAccounts, waddrmgr.ImportedAddrAccount) var accounts []uint32 - err := tc.manager.ForEachAccount(func(account uint32) error { - accounts = append(accounts, account) - return nil + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.ForEachAccount(ns, func(account uint32) error { + accounts = append(accounts, account) + return nil + }) }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", prefix, err) @@ -1310,11 +1491,14 @@ func testForEachAccountAddress(tc *testContext) bool { } var addrs []waddrmgr.ManagedAddress - err := tc.manager.ForEachAccountAddress(tc.account, - func(maddr waddrmgr.ManagedAddress) error { - addrs = append(addrs, maddr) - return nil - }) + err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return tc.manager.ForEachAccountAddress(ns, tc.account, + func(maddr waddrmgr.ManagedAddress) error { + addrs = append(addrs, maddr) + return nil + }) + }) if err != nil { tc.t.Errorf("%s: unexpected error: %v", prefix, err) return false @@ -1385,7 +1569,7 @@ func testWatchingOnly(tc *testContext) bool { defer os.Remove(woMgrName) // Open the new database copy and get the address manager namespace. - db, namespace, err := openDbNamespace(woMgrName) + db, err := walletdb.Open("bdb", woMgrName) if err != nil { tc.t.Errorf("openDbNamespace: unexpected error: %v", err) return false @@ -1393,13 +1577,22 @@ func testWatchingOnly(tc *testContext) bool { defer db.Close() // Open the manager using the namespace and convert it to watching-only. - mgr, err := waddrmgr.Open(namespace, pubPassphrase, - &chaincfg.MainNetParams, nil) + var mgr *waddrmgr.Manager + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + mgr, err = waddrmgr.Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) if err != nil { tc.t.Errorf("%v", err) return false } - if err := mgr.ConvertToWatchingOnly(); err != nil { + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return mgr.ConvertToWatchingOnly(ns) + }) + if err != nil { tc.t.Errorf("%v", err) return false } @@ -1417,8 +1610,12 @@ func testWatchingOnly(tc *testContext) bool { mgr.Close() // Open the watching-only manager and run all the tests again. - mgr, err = waddrmgr.Open(namespace, pubPassphrase, &chaincfg.MainNetParams, - nil) + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + mgr, err = waddrmgr.Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) if err != nil { tc.t.Errorf("Open Watching-Only: unexpected error: %v", err) return false @@ -1543,7 +1740,11 @@ func testSync(tc *testContext) bool { Height: int32(i) + 1, Hash: *test.hash, } - if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.SetSyncedTo(ns, &blockStamp) + }) + if err != nil { tc.t.Errorf("SetSyncedTo unexpected err: %v", err) return false } @@ -1599,7 +1800,11 @@ func testSync(tc *testContext) bool { Height: 10, Hash: *tests[9].hash, } - if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.SetSyncedTo(ns, &blockStamp) + }) + if err != nil { tc.t.Errorf("SetSyncedTo unexpected err on rollback to block "+ "in recent history: %v", err) return false @@ -1617,7 +1822,11 @@ func testSync(tc *testContext) bool { Height: 100, Hash: *newHash("000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a"), } - if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.SetSyncedTo(ns, &blockStamp) + }) + if err != nil { tc.t.Errorf("SetSyncedTo unexpected err on future block stamp: "+ "%v", err) return false @@ -1639,7 +1848,11 @@ func testSync(tc *testContext) bool { Height: 1, Hash: *tests[0].hash, } - if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.SetSyncedTo(ns, &blockStamp) + }) + if err != nil { tc.t.Errorf("SetSyncedTo unexpected err on rollback to block "+ "not in recent history: %v", err) return false @@ -1665,7 +1878,11 @@ func testSync(tc *testContext) bool { // Ensure syncing the manager to nil results in the synced to state // being the earliest block (genesis block in this case). - if err := tc.manager.SetSyncedTo(nil); err != nil { + err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return tc.manager.SetSyncedTo(ns, nil) + }) + if err != nil { tc.t.Errorf("SetSyncedTo unexpected err on nil: %v", err) return false } @@ -1689,35 +1906,37 @@ func testSync(tc *testContext) bool { func TestManager(t *testing.T) { t.Parallel() - dbName := "mgrtest.bin" - _ = os.Remove(dbName) - db, mgrNamespace, err := createDbNamespace(dbName) - if err != nil { - t.Errorf("createDbNamespace: unexpected error: %v", err) - return - } - defer os.Remove(dbName) - defer db.Close() + teardown, db := emptyDB(t) + defer teardown() // Open manager that does not exist to ensure the expected error is // returned. - _, err = waddrmgr.Open(mgrNamespace, pubPassphrase, - &chaincfg.MainNetParams, nil) + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := waddrmgr.Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) if !checkManagerError(t, "Open non-existant", err, waddrmgr.ErrNoExist) { return } // Create a new manager. - err = waddrmgr.Create(mgrNamespace, seed, pubPassphrase, - privPassphrase, &chaincfg.MainNetParams, fastScrypt) + var mgr *waddrmgr.Manager + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + err = waddrmgr.Create(ns, seed, pubPassphrase, privPassphrase, + &chaincfg.MainNetParams, fastScrypt) + if err != nil { + return err + } + mgr, err = waddrmgr.Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) if err != nil { - t.Errorf("Create: unexpected error: %v", err) - return - } - mgr, err := waddrmgr.Open(mgrNamespace, pubPassphrase, - &chaincfg.MainNetParams, nil) - if err != nil { - t.Errorf("Open: unexpected error: %v", err) + t.Errorf("Create/Open: unexpected error: %v", err) return } @@ -1726,8 +1945,11 @@ func TestManager(t *testing.T) { // Attempt to create the manager again to ensure the expected error is // returned. - err = waddrmgr.Create(mgrNamespace, seed, pubPassphrase, - privPassphrase, &chaincfg.MainNetParams, fastScrypt) + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return waddrmgr.Create(ns, seed, pubPassphrase, privPassphrase, + &chaincfg.MainNetParams, fastScrypt) + }) if !checkManagerError(t, "Create existing", err, waddrmgr.ErrAlreadyExists) { mgr.Close() return @@ -1748,8 +1970,11 @@ func TestManager(t *testing.T) { // Ensure the expected error is returned if the latest manager version // constant is bumped without writing code to actually do the upgrade. *waddrmgr.TstLatestMgrVersion++ - _, err = waddrmgr.Open(mgrNamespace, pubPassphrase, - &chaincfg.MainNetParams, nil) + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := waddrmgr.Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) if !checkManagerError(t, "Upgrade needed", err, waddrmgr.ErrUpgrade) { return } @@ -1757,8 +1982,12 @@ func TestManager(t *testing.T) { // Open the manager and run all the tests again in open mode which // avoids reinserting new addresses like the create mode tests do. - mgr, err = waddrmgr.Open(mgrNamespace, pubPassphrase, - &chaincfg.MainNetParams, nil) + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + mgr, err = waddrmgr.Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) if err != nil { t.Errorf("Open: unexpected error: %v", err) return @@ -1784,7 +2013,11 @@ func TestManager(t *testing.T) { // Unlock the manager so it can be closed with it unlocked to ensure // it works without issue. - if err := mgr.Unlock(privPassphrase); err != nil { + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return mgr.Unlock(ns, privPassphrase) + }) + if err != nil { t.Errorf("Unlock: unexpected error: %v", err) } } @@ -1792,7 +2025,7 @@ func TestManager(t *testing.T) { // TestEncryptDecryptErrors ensures that errors which occur while encrypting and // decrypting data return the expected errors. func TestEncryptDecryptErrors(t *testing.T) { - teardown, mgr := setupManager(t) + teardown, db, mgr := setupManager(t) defer teardown() invalidKeyType := waddrmgr.CryptoKeyType(0xff) @@ -1820,7 +2053,11 @@ func TestEncryptDecryptErrors(t *testing.T) { err, waddrmgr.ErrLocked) // Unlock the manager for these tests - if err = mgr.Unlock(privPassphrase); err != nil { + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return mgr.Unlock(ns, privPassphrase) + }) + if err != nil { t.Fatal("Attempted to unlock the manager, but failed:", err) } @@ -1840,13 +2077,17 @@ func TestEncryptDecryptErrors(t *testing.T) { // TestEncryptDecrypt ensures that encrypting and decrypting data with the // the various crypto key types works as expected. func TestEncryptDecrypt(t *testing.T) { - teardown, mgr := setupManager(t) + teardown, db, mgr := setupManager(t) defer teardown() plainText := []byte("this is a plaintext") // Make sure address manager is unlocked - if err := mgr.Unlock(privPassphrase); err != nil { + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return mgr.Unlock(ns, privPassphrase) + }) + if err != nil { t.Fatal("Attempted to unlock the manager, but failed:", err) } diff --git a/waddrmgr/sync.go b/waddrmgr/sync.go index f81cfa9..60a313c 100644 --- a/waddrmgr/sync.go +++ b/waddrmgr/sync.go @@ -137,7 +137,7 @@ func (m *Manager) NewIterateRecentBlocks() *BlockIterator { // imported addresses will be used. This effectively allows the manager to be // marked as unsynced back to the oldest known point any of the addresses have // appeared in the block chain. -func (m *Manager) SetSyncedTo(bs *BlockStamp) error { +func (m *Manager) SetSyncedTo(ns walletdb.ReadWriteBucket, bs *BlockStamp) error { m.mtx.Lock() defer m.mtx.Unlock() @@ -199,14 +199,11 @@ func (m *Manager) SetSyncedTo(bs *BlockStamp) error { } // Update the database. - err := m.namespace.Update(func(tx walletdb.Tx) error { - err := putSyncedTo(tx, bs) - if err != nil { - return err - } - - return putRecentBlocks(tx, recentHeight, recentHashes) - }) + err := putSyncedTo(ns, bs) + if err != nil { + return err + } + err = putRecentBlocks(ns, recentHeight, recentHashes) if err != nil { return err } diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index c09a5d3..2a18eb6 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -5,10 +5,11 @@ package wallet import ( - "github.com/roasbeef/btcd/txscript" - "github.com/roasbeef/btcwallet/chain" - "github.com/roasbeef/btcwallet/waddrmgr" - "github.com/roasbeef/btcwallet/wtxmgr" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) func (w *Wallet) handleChainNotifications() { @@ -31,16 +32,26 @@ func (w *Wallet) handleChainNotifications() { } for n := range chainClient.Notifications() { + var notificationName string var err error switch n := n.(type) { case chain.ClientConnected: go sync(w) case chain.BlockConnected: - w.connectBlock(wtxmgr.BlockMeta(n)) + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + return w.connectBlock(tx, wtxmgr.BlockMeta(n)) + }) + notificationName = "blockconnected" case chain.BlockDisconnected: - err = w.disconnectBlock(wtxmgr.BlockMeta(n)) + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + return w.disconnectBlock(tx, wtxmgr.BlockMeta(n)) + }) + notificationName = "blockdisconnected" case chain.RelevantTx: - err = w.addRelevantTx(n.TxRecord, n.Block) + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + return w.addRelevantTx(tx, n.TxRecord, n.Block) + }) + notificationName = "recvtx/redeemingtx" // The following are handled by the wallet's rescan // goroutines, so just pass them there. @@ -48,8 +59,8 @@ func (w *Wallet) handleChainNotifications() { w.rescanNotifications <- n } if err != nil { - log.Errorf("Cannot handle chain server "+ - "notification: %v", err) + log.Errorf("Failed to process consensus server notification "+ + "(name: `%s`, detail: `%v`)", notificationName, err) } } w.wg.Done() @@ -58,25 +69,32 @@ func (w *Wallet) handleChainNotifications() { // connectBlock handles a chain server notification by marking a wallet // that's currently in-sync with the chain server as being synced up to // the passed block. -func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) { +func (w *Wallet) connectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) error { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + bs := waddrmgr.BlockStamp{ Height: b.Height, Hash: b.Hash, } - if err := w.Manager.SetSyncedTo(&bs); err != nil { - log.Errorf("Failed to update address manager sync state in "+ - "connect block for hash %v (height %d): %v", b.Hash, - b.Height, err) + err := w.Manager.SetSyncedTo(addrmgrNs, &bs) + if err != nil { + return err } // Notify interested clients of the connected block. - w.NtfnServer.notifyAttachedBlock(&b) + // + // TODO: move all notifications outside of the database transaction. + w.NtfnServer.notifyAttachedBlock(dbtx, &b) + return nil } // disconnectBlock handles a chain server reorganize by rolling back all // block history from the reorged block for a wallet in-sync with the chain // server. -func (w *Wallet) disconnectBlock(b wtxmgr.BlockMeta) error { +func (w *Wallet) disconnectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) error { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + if !w.ChainSynced() { return nil } @@ -87,8 +105,8 @@ func (w *Wallet) disconnectBlock(b wtxmgr.BlockMeta) error { if iter != nil && iter.BlockStamp().Hash == b.Hash { if iter.Prev() { prev := iter.BlockStamp() - w.Manager.SetSyncedTo(&prev) - err := w.TxStore.Rollback(prev.Height + 1) + w.Manager.SetSyncedTo(addrmgrNs, &prev) + err := w.TxStore.Rollback(txmgrNs, prev.Height+1) if err != nil { return err } @@ -98,9 +116,9 @@ func (w *Wallet) disconnectBlock(b wtxmgr.BlockMeta) error { // will in turn lead to a rescan from either the // earliest blockstamp the addresses in the manager are // known to have been created. - w.Manager.SetSyncedTo(nil) + w.Manager.SetSyncedTo(addrmgrNs, nil) // Rollback everything but the genesis block. - err := w.TxStore.Rollback(1) + err := w.TxStore.Rollback(txmgrNs, 1) if err != nil { return err } @@ -113,45 +131,15 @@ func (w *Wallet) disconnectBlock(b wtxmgr.BlockMeta) error { return nil } -func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error { - // TODO: The transaction store and address manager need to be updated - // together, but each operate under different namespaces and are changed - // under new transactions. This is not error safe as we lose - // transaction semantics. - // - // I'm unsure of the best way to solve this. Some possible solutions - // and drawbacks: - // - // 1. Open write transactions here and pass the handle to every - // waddrmr and wtxmgr method. This complicates the caller code - // everywhere, however. - // - // 2. Move the wtxmgr namespace into the waddrmgr namespace, likely - // under its own bucket. This entire function can then be moved - // into the waddrmgr package, which updates the nested wtxmgr. - // This removes some of separation between the components. - // - // 3. Use multiple wtxmgrs, one for each account, nested in the - // waddrmgr namespace. This still provides some sort of logical - // separation (transaction handling remains in another package, and - // is simply used by waddrmgr), but may result in duplicate - // transactions being saved if they are relevant to multiple - // accounts. - // - // 4. Store wtxmgr-related details under the waddrmgr namespace, but - // solve the drawback of #3 by splitting wtxmgr to save entire - // transaction records globally for all accounts, with - // credit/debit/balance tracking per account. Each account would - // also save the relevant transaction hashes and block incidence so - // the full transaction can be loaded from the waddrmgr - // transactions bucket. This currently seems like the best - // solution. +func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) // At the moment all notified transactions are assumed to actually be // relevant. This assumption will not hold true when SPV support is // added, but until then, simply insert the transaction because there // should either be one or more relevant inputs or outputs. - err := w.TxStore.InsertTx(rec, block) + err := w.TxStore.InsertTx(txmgrNs, rec, block) if err != nil { return err } @@ -166,17 +154,17 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) er continue } for _, addr := range addrs { - ma, err := w.Manager.Address(addr) + ma, err := w.Manager.Address(addrmgrNs, addr) if err == nil { // TODO: Credits should be added with the // account they belong to, so wtxmgr is able to // track per-account balances. - err = w.TxStore.AddCredit(rec, block, uint32(i), + err = w.TxStore.AddCredit(txmgrNs, rec, block, uint32(i), ma.Internal()) if err != nil { return err } - err = w.Manager.MarkUsed(addr) + err = w.Manager.MarkUsed(addrmgrNs, addr) if err != nil { return err } @@ -197,18 +185,18 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) er // // TODO: Avoid the extra db hits. if block == nil { - details, err := w.TxStore.UniqueTxDetails(&rec.Hash, nil) + details, err := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, nil) if err != nil { log.Errorf("Cannot query transaction details for notifiation: %v", err) } else { - w.NtfnServer.notifyUnminedTransaction(details) + w.NtfnServer.notifyUnminedTransaction(dbtx, details) } } else { - details, err := w.TxStore.UniqueTxDetails(&rec.Hash, &block.Block) + details, err := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, &block.Block) if err != nil { log.Errorf("Cannot query transaction details for notifiation: %v", err) } else { - w.NtfnServer.notifyMinedTransaction(details, block) + w.NtfnServer.notifyMinedTransaction(dbtx, details, block) } } diff --git a/wallet/common.go b/wallet/common.go new file mode 100644 index 0000000..15a0296 --- /dev/null +++ b/wallet/common.go @@ -0,0 +1,88 @@ +// Copyright (c) 2016 The Decred developers +// Copyright (c) 2017 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 ( + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +// Note: The following common types should never reference the Wallet type. +// Long term goal is to move these to their own package so that the database +// access APIs can create them directly for the wallet to return. + +// BlockIdentity identifies a block, or the lack of one (used to describe an +// unmined transaction). +type BlockIdentity struct { + Hash chainhash.Hash + Height int32 +} + +// None returns whether there is no block described by the instance. When +// associated with a transaction, this indicates the transaction is unmined. +func (b *BlockIdentity) None() bool { + // BUG: Because dcrwallet uses both 0 and -1 in various places to refer + // to an unmined transaction this must check against both and may not + // ever be usable to represent the genesis block. + return *b == BlockIdentity{Height: -1} || *b == BlockIdentity{} +} + +// OutputKind describes a kind of transaction output. This is used to +// differentiate between coinbase, stakebase, and normal outputs. +type OutputKind byte + +// Defined OutputKind constants +const ( + OutputKindNormal OutputKind = iota + OutputKindCoinbase +) + +// TransactionOutput describes an output that was or is at least partially +// controlled by the wallet. Depending on context, this could refer to an +// unspent output, or a spent one. +type TransactionOutput struct { + OutPoint wire.OutPoint + Output wire.TxOut + OutputKind OutputKind + // These should be added later when the DB can return them more + // efficiently: + //TxLockTime uint32 + //TxExpiry uint32 + ContainingBlock BlockIdentity + ReceiveTime time.Time +} + +// OutputRedeemer identifies the transaction input which redeems an output. +type OutputRedeemer struct { + TxHash chainhash.Hash + InputIndex uint32 +} + +// P2SHMultiSigOutput describes a transaction output with a pay-to-script-hash +// output script and an imported redemption script. Along with common details +// of the output, this structure also includes the P2SH address the script was +// created from and the number of signatures required to redeem it. +// +// TODO: Could be useful to return how many of the required signatures can be +// created by this wallet. +type P2SHMultiSigOutput struct { + // TODO: Add a TransactionOutput member to this struct and remove these + // fields which are duplicated by it. This improves consistency. Only + // not done now because wtxmgr APIs don't support an efficient way of + // fetching other Transactionoutput data together with the rest of the + // multisig info. + OutPoint wire.OutPoint + OutputAmount btcutil.Amount + ContainingBlock BlockIdentity + + P2SHAddress *btcutil.AddressScriptHash + RedeemScript []byte + M, N uint8 // M of N signatures required to redeem + Redeemer *OutputRedeemer // nil unless spent +} diff --git a/wallet/createtx.go b/wallet/createtx.go index 31130b4..53d7669 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -1,4 +1,5 @@ -// Copyright (c) 2013-2016 The btcsuite developers +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2016 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -14,6 +15,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/wtxmgr" ) @@ -57,10 +59,11 @@ func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { // address manager. type secretSource struct { *waddrmgr.Manager + addrmgrNs walletdb.ReadBucket } func (s secretSource) GetKey(addr btcutil.Address) (*btcec.PrivateKey, bool, error) { - ma, err := s.Address(addr) + ma, err := s.Address(s.addrmgrNs, addr) if err != nil { return nil, false, err } @@ -79,7 +82,7 @@ func (s secretSource) GetKey(addr btcutil.Address) (*btcec.PrivateKey, bool, err } func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) { - ma, err := s.Address(addr) + ma, err := s.Address(s.addrmgrNs, addr) if err != nil { return nil, err } @@ -98,63 +101,57 @@ 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. -func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int32) (*txauthor.AuthoredTx, error) { - // Address manager must be unlocked to compose transaction. Grab - // the unlock if possible (to prevent future unlocks), or return the - // error if already locked. - heldUnlock, err := w.HoldUnlock() - if err != nil { - return nil, err - } - defer heldUnlock.Release() - +func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int32) (tx *txauthor.AuthoredTx, err error) { chainClient, err := w.requireChainClient() if err != nil { return nil, err } - // Get current block's height and hash. - bs, err := chainClient.BlockStamp() - if err != nil { - return nil, err - } + err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) - eligible, err := w.findEligibleOutputs(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 - if account == waddrmgr.ImportedAddrAccount { - changeAddr, err = w.NewChangeAddress(0, - waddrmgr.WitnessPubKey) - } else { - changeAddr, err = w.NewChangeAddress(account, - waddrmgr.WitnessPubKey) - } + // Get current block's height and hash. + bs, err := chainClient.BlockStamp() if err != nil { - return nil, err + return err } - return txscript.PayToAddrScript(changeAddr) - } - tx, err := txauthor.NewUnsignedTransaction(outputs, w.RelayFee(), - inputSource, changeSource) - if err != nil { - return nil, 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() - } + eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) + if err != nil { + return err + } - err = tx.AddAllInputScripts(secretSource{w.Manager}) + 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(0) + } else { + changeAddr, err = w.NewChangeAddress(account) + } + if err != nil { + return nil, err + } + return txscript.PayToAddrScript(changeAddr) + } + tx, err = txauthor.NewUnsignedTransaction(outputs, w.RelayFee(), + inputSource, changeSource) + 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() + } + + return tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) + }) if err != nil { return nil, err } @@ -173,8 +170,11 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int3 return tx, nil } -func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { - unspent, err := w.TxStore.UnspentOutputs() +func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) if err != nil { return nil, err } @@ -216,11 +216,10 @@ func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, bs *waddrmgr if err != nil || len(addrs) != 1 { continue } - addrAcct, err := w.Manager.AddrAccount(addrs[0]) + addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0]) if err != nil || addrAcct != account { continue } - eligible = append(eligible, *output) } return eligible, nil diff --git a/wallet/multisig.go b/wallet/multisig.go new file mode 100644 index 0000000..5337841 --- /dev/null +++ b/wallet/multisig.go @@ -0,0 +1,105 @@ +// Copyright (c) 2017 The btcsuite developers +// Copyright (c) 2016 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "errors" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" +) + +// MakeMultiSigScript creates a multi-signature script that can be redeemed with +// nRequired signatures of the passed keys and addresses. If the address is a +// P2PKH address, the associated pubkey is looked up by the wallet if possible, +// otherwise an error is returned for a missing pubkey. +// +// This function only works with pubkeys and P2PKH addresses derived from them. +func (w *Wallet) MakeMultiSigScript(addrs []btcutil.Address, nRequired int) ([]byte, error) { + pubKeys := make([]*btcutil.AddressPubKey, len(addrs)) + + var dbtx walletdb.ReadTx + var addrmgrNs walletdb.ReadBucket + defer func() { + if dbtx != nil { + dbtx.Rollback() + } + }() + + // The address list will made up either of addreseses (pubkey hash), for + // which we need to look up the keys in wallet, straight pubkeys, or a + // mixture of the two. + for i, addr := range addrs { + switch addr := addr.(type) { + default: + return nil, errors.New("cannot make multisig script for " + + "a non-secp256k1 public key or P2PKH address") + + case *btcutil.AddressPubKey: + pubKeys[i] = addr + + case *btcutil.AddressPubKeyHash: + if dbtx == nil { + var err error + dbtx, err = w.db.BeginReadTx() + if err != nil { + return nil, err + } + addrmgrNs = dbtx.ReadBucket(waddrmgrNamespaceKey) + } + addrInfo, err := w.Manager.Address(addrmgrNs, addr) + if err != nil { + return nil, err + } + serializedPubKey := addrInfo.(waddrmgr.ManagedPubKeyAddress). + PubKey().SerializeCompressed() + + pubKeyAddr, err := btcutil.NewAddressPubKey( + serializedPubKey, w.chainParams) + if err != nil { + return nil, err + } + pubKeys[i] = pubKeyAddr + } + } + + return txscript.MultiSigScript(pubKeys, nRequired) +} + +// ImportP2SHRedeemScript adds a P2SH redeem script to the wallet. +func (w *Wallet) ImportP2SHRedeemScript(script []byte) (*btcutil.AddressScriptHash, error) { + var p2shAddr *btcutil.AddressScriptHash + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + + // TODO(oga) blockstamp current block? + bs := &waddrmgr.BlockStamp{ + Hash: *w.ChainParams().GenesisHash, + Height: 0, + } + + addrInfo, err := w.Manager.ImportScript(addrmgrNs, script, bs) + if err != nil { + // Don't care if it's already there, but still have to + // set the p2shAddr since the address manager didn't + // return anything useful. + if waddrmgr.IsError(err, waddrmgr.ErrDuplicateAddress) { + // This function will never error as it always + // hashes the script to the correct length. + p2shAddr, _ = btcutil.NewAddressScriptHash(script, + w.chainParams) + return nil + } + return err + } + + p2shAddr = addrInfo.Address().(*btcutil.AddressScriptHash) + return nil + }) + return p2shAddr, err +} diff --git a/wallet/notifications.go b/wallet/notifications.go index c71c4c9..dee5ba3 100644 --- a/wallet/notifications.go +++ b/wallet/notifications.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/wtxmgr" ) @@ -46,11 +47,14 @@ func newNotificationServer(wallet *Wallet) *NotificationServer { } } -func lookupInputAccount(w *Wallet, details *wtxmgr.TxDetails, deb wtxmgr.DebitRecord) uint32 { +func lookupInputAccount(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails, deb wtxmgr.DebitRecord) uint32 { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + // TODO: Debits should record which account(s?) they // debit from so this doesn't need to be looked up. prevOP := &details.MsgTx.TxIn[deb.Index].PreviousOutPoint - prev, err := w.TxStore.TxDetails(&prevOP.Hash) + prev, err := w.TxStore.TxDetails(txmgrNs, &prevOP.Hash) if err != nil { log.Errorf("Cannot query previous transaction details for %v: %v", prevOP.Hash, err) return 0 @@ -63,7 +67,7 @@ func lookupInputAccount(w *Wallet, details *wtxmgr.TxDetails, deb wtxmgr.DebitRe _, addrs, _, err := txscript.ExtractPkScriptAddrs(prevOut.PkScript, w.chainParams) var inputAcct uint32 if err == nil && len(addrs) > 0 { - inputAcct, err = w.Manager.AddrAccount(addrs[0]) + inputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } if err != nil { log.Errorf("Cannot fetch account for previous output %v: %v", prevOP, err) @@ -72,12 +76,16 @@ func lookupInputAccount(w *Wallet, details *wtxmgr.TxDetails, deb wtxmgr.DebitRe return inputAcct } -func lookupOutputChain(w *Wallet, details *wtxmgr.TxDetails, cred wtxmgr.CreditRecord) (account uint32, internal bool) { +func lookupOutputChain(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails, + cred wtxmgr.CreditRecord) (account uint32, internal bool) { + + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + output := details.MsgTx.TxOut[cred.Index] _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) var ma waddrmgr.ManagedAddress if err == nil && len(addrs) > 0 { - ma, err = w.Manager.Address(addrs[0]) + ma, err = w.Manager.Address(addrmgrNs, addrs[0]) } if err != nil { log.Errorf("Cannot fetch account for wallet output: %v", err) @@ -88,7 +96,7 @@ func lookupOutputChain(w *Wallet, details *wtxmgr.TxDetails, cred wtxmgr.CreditR return } -func makeTxSummary(w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { +func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { serializedTx := details.SerializedTx if serializedTx == nil { var buf bytes.Buffer @@ -113,7 +121,7 @@ func makeTxSummary(w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { for i, d := range details.Debits { inputs[i] = TransactionSummaryInput{ Index: d.Index, - PreviousAccount: lookupInputAccount(w, details, d), + PreviousAccount: lookupInputAccount(dbtx, w, details, d), PreviousAmount: d.Amount, } } @@ -125,7 +133,7 @@ func makeTxSummary(w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { if !mine { continue } - acct, internal := lookupOutputChain(w, details, details.Credits[credIndex]) + acct, internal := lookupOutputChain(dbtx, w, details, details.Credits[credIndex]) output := TransactionSummaryOutput{ Index: uint32(i), Account: acct, @@ -143,8 +151,9 @@ func makeTxSummary(w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { } } -func totalBalances(w *Wallet, m map[uint32]btcutil.Amount) error { - unspent, err := w.TxStore.UnspentOutputs() +func totalBalances(dbtx walletdb.ReadTx, w *Wallet, m map[uint32]btcutil.Amount) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + unspent, err := w.TxStore.UnspentOutputs(dbtx.ReadBucket(wtxmgrNamespaceKey)) if err != nil { return err } @@ -154,7 +163,7 @@ func totalBalances(w *Wallet, m map[uint32]btcutil.Amount) error { _, addrs, _, err := txscript.ExtractPkScriptAddrs( output.PkScript, w.chainParams) if err == nil && len(addrs) > 0 { - outputAcct, err = w.Manager.AddrAccount(addrs[0]) + outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } if err == nil { _, ok := m[outputAcct] @@ -185,7 +194,7 @@ func relevantAccounts(w *Wallet, m map[uint32]btcutil.Amount, txs []TransactionS } } -func (s *NotificationServer) notifyUnminedTransaction(details *wtxmgr.TxDetails) { +func (s *NotificationServer) notifyUnminedTransaction(dbtx walletdb.ReadTx, details *wtxmgr.TxDetails) { // Sanity check: should not be currently coalescing a notification for // mined transactions at the same time that an unmined tx is notified. if s.currentTxNtfn != nil { @@ -199,15 +208,15 @@ func (s *NotificationServer) notifyUnminedTransaction(details *wtxmgr.TxDetails) return } - unminedTxs := []TransactionSummary{makeTxSummary(s.wallet, details)} - unminedHashes, err := s.wallet.TxStore.UnminedTxHashes() + unminedTxs := []TransactionSummary{makeTxSummary(dbtx, s.wallet, details)} + unminedHashes, err := s.wallet.TxStore.UnminedTxHashes(dbtx.ReadBucket(wtxmgrNamespaceKey)) if err != nil { log.Errorf("Cannot fetch unmined transaction hashes: %v", err) return } bals := make(map[uint32]btcutil.Amount) relevantAccounts(s.wallet, bals, unminedTxs) - err = totalBalances(s.wallet, bals) + err = totalBalances(dbtx, s.wallet, bals) if err != nil { log.Errorf("Cannot determine balances for relevant accounts: %v", err) return @@ -229,7 +238,7 @@ func (s *NotificationServer) notifyDetachedBlock(hash *chainhash.Hash) { s.currentTxNtfn.DetachedBlocks = append(s.currentTxNtfn.DetachedBlocks, hash) } -func (s *NotificationServer) notifyMinedTransaction(details *wtxmgr.TxDetails, block *wtxmgr.BlockMeta) { +func (s *NotificationServer) notifyMinedTransaction(dbtx walletdb.ReadTx, details *wtxmgr.TxDetails, block *wtxmgr.BlockMeta) { if s.currentTxNtfn == nil { s.currentTxNtfn = &TransactionNotifications{} } @@ -243,10 +252,11 @@ func (s *NotificationServer) notifyMinedTransaction(details *wtxmgr.TxDetails, b n++ } txs := s.currentTxNtfn.AttachedBlocks[n-1].Transactions - s.currentTxNtfn.AttachedBlocks[n-1].Transactions = append(txs, makeTxSummary(s.wallet, details)) + s.currentTxNtfn.AttachedBlocks[n-1].Transactions = + append(txs, makeTxSummary(dbtx, s.wallet, details)) } -func (s *NotificationServer) notifyAttachedBlock(block *wtxmgr.BlockMeta) { +func (s *NotificationServer) notifyAttachedBlock(dbtx walletdb.ReadTx, block *wtxmgr.BlockMeta) { if s.currentTxNtfn == nil { s.currentTxNtfn = &TransactionNotifications{} } @@ -285,7 +295,8 @@ func (s *NotificationServer) notifyAttachedBlock(block *wtxmgr.BlockMeta) { // a mined transaction in the new best chain, there is no possiblity of // a new, previously unseen transaction appearing in unconfirmed. - unminedHashes, err := s.wallet.TxStore.UnminedTxHashes() + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + unminedHashes, err := s.wallet.TxStore.UnminedTxHashes(txmgrNs) if err != nil { log.Errorf("Cannot fetch unmined transaction hashes: %v", err) return @@ -296,7 +307,7 @@ func (s *NotificationServer) notifyAttachedBlock(block *wtxmgr.BlockMeta) { for _, b := range s.currentTxNtfn.AttachedBlocks { relevantAccounts(s.wallet, bals, b.Transactions) } - err = totalBalances(s.wallet, bals) + err = totalBalances(dbtx, s.wallet, bals) if err != nil { log.Errorf("Cannot determine balances for relevant accounts: %v", err) return diff --git a/wallet/rescan.go b/wallet/rescan.go index ab95629..effb28d 100644 --- a/wallet/rescan.go +++ b/wallet/rescan.go @@ -1,15 +1,16 @@ -// Copyright (c) 2013-2014 The btcsuite developers +// Copyright (c) 2013-2017 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 ( - "github.com/roasbeef/btcd/wire" - "github.com/roasbeef/btcutil" - "github.com/roasbeef/btcwallet/chain" - "github.com/roasbeef/btcwallet/waddrmgr" - "github.com/roasbeef/btcwallet/wtxmgr" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) // RescanProgressMsg reports the current progress made by a rescan for a @@ -178,7 +179,11 @@ out: Hash: *n.Hash, Height: n.Height, } - if err := w.Manager.SetSyncedTo(&bs); err != nil { + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.SetSyncedTo(ns, &bs) + }) + if err != nil { log.Errorf("Failed to update address manager "+ "sync state for hash %v (height %d): %v", n.Hash, n.Height, err) @@ -191,15 +196,24 @@ out: log.Infof("Finished rescan for %d %s (synced to block "+ "%s, height %d)", len(addrs), noun, n.Hash, n.Height) - bs := waddrmgr.BlockStamp{Height: n.Height, Hash: *n.Hash} - if err := w.Manager.SetSyncedTo(&bs); err != nil { + + bs := waddrmgr.BlockStamp{ + Height: n.Height, + Hash: *n.Hash, + } + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.SetSyncedTo(ns, &bs) + }) + if err != nil { log.Errorf("Failed to update address manager "+ "sync state for hash %v (height %d): %v", n.Hash, n.Height, err) + continue } - w.SetChainSynced(true) - go w.ResendUnminedTxs() + w.SetChainSynced(true) + go w.resendUnminedTxs() case <-quit: break out diff --git a/wallet/sync.go b/wallet/sync.go new file mode 100644 index 0000000..91736f0 --- /dev/null +++ b/wallet/sync.go @@ -0,0 +1,710 @@ +//+build ignore + +/* + * Copyright (c) 2013-2016 The btcsuite developers + * Copyright (c) 2015 The Decred developers + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package wallet + +import ( + "bytes" + "encoding/hex" + "fmt" + "strconv" + + "github.com/btcsuite/btclog" + + "github.com/decred/bitset" + "github.com/decred/dcrutil" + "github.com/decred/dcrwallet/chain" + "github.com/decred/dcrwallet/waddrmgr" + "github.com/decred/dcrwallet/walletdb" +) + +// finalScanLength is the final length of accounts to scan for the +// function below. +var finalAcctScanLength = 50 + +// acctSeekWidth is the number of addresses for both internal and external +// branches to scan to determine whether or not an account exists and should +// be rescanned. This is the tolerance for account gaps as well. +var acctSeekWidth uint32 = 5 + +// accountIsUsed checks if an account has ever been used by scanning the +// first acctSeekWidth many addresses for usage. +func (w *Wallet) accountIsUsed(account uint32, chainClient *chain.RPCClient) bool { + // Search external branch then internal branch for a used + // address. We need to set the address function to use based + // on whether or not this is the initial sync. The function + // AddressDerivedFromCointype is able to see addresses that + // exists in accounts that have not yet been created, while + // AddressDerivedFromDbAcct can not. + addrFunc := w.Manager.AddressDerivedFromDbAcct + if w.initiallyUnlocked { + addrFunc = w.Manager.AddressDerivedFromCointype + } + + for branch := uint32(0); branch < 2; branch++ { + for i := uint32(0); i < acctSeekWidth; i++ { + var addr dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = addrFunc(addrmgrNs, i, account, branch) + return err + }) + if err != nil { + // Skip erroneous keys, which happen rarely. + continue + } + + exists, err := chainClient.ExistsAddress(addr) + if err != nil { + return false + } + if exists { + return true + } + } + } + + return false +} + +// bisectLastAcctIndex is a helper function for searching through accounts to +// find the last used account. It uses logarithmic scanning to determine if +// an account has been used. +func (w *Wallet) bisectLastAcctIndex(hi, low int) int { + chainClient, err := w.requireChainClient() + if err != nil { + return 0 + } + + offset := low + for i := hi - low - 1; i > 0; i /= 2 { + if i+offset+int(acctSeekWidth) < waddrmgr.MaxAddressesPerAccount { + for j := i + offset + int(addrSeekWidth); j >= i+offset; j-- { + if w.accountIsUsed(uint32(j), chainClient) { + return i + offset + } + } + } else { + if w.accountIsUsed(uint32(i+offset), chainClient) { + return i + offset + } + } + } + + return 0 +} + +// findAcctEnd is a helper function for searching for the last used account by +// logarithmic scanning of the account indexes. +func (w *Wallet) findAcctEnd(start, stop int) int { + indexStart := w.bisectLastAcctIndex(stop, start) + indexLast := 0 + for { + indexLastStored := indexStart + low := indexLastStored + hi := indexLast + ((indexStart - indexLast) * 2) + 1 + indexStart = w.bisectLastAcctIndex(hi, low) + indexLast = indexLastStored + + if indexStart == 0 { + break + } + } + + return indexLast +} + +// scanAccountIndex identifies the last used address in an HD keychain of public +// keys. It returns the index of the last used key, along with the address of +// this key. +func (w *Wallet) scanAccountIndex(start int, end int) (uint32, error) { + chainClient, err := w.requireChainClient() + if err != nil { + return 0, err + } + + // Find the last used account. Scan from it to the end in case there was a + // gap from that position, which is possible. Then, return the account + // in that position. + lastUsed := w.findAcctEnd(start, end) + if lastUsed != 0 { + for i := lastUsed + finalAcctScanLength; i >= lastUsed; i-- { + if w.accountIsUsed(uint32(i), chainClient) { + return uint32(i), nil + } + } + } + + // We can't find any used addresses. The account is + // unused. + return 0, nil +} + +// debugScanLength is the final length of keys to scan past the +// last index returned from the logarithmic scanning function +// when creating the debug string of used addresses. +var debugAddrScanLength = 3500 + +// addrSeekWidth is the number of new addresses to generate and add to the +// address manager when trying to sync up a wallet to the main chain. This +// is the maximum gap introduced by a resyncing as well, and should be less +// than finalScanLength above. +// TODO Optimize the scanning so that rather than overshooting the end address, +// you instead step through addresses incrementally until reaching idx so that +// you don't reach a gap. This can be done by keeping track of where the current +// cursor is and adding addresses in big chunks until you hit the end. +var addrSeekWidth uint32 = 20 + +// errDerivation is an error type signifying that the waddrmgr failed to +// derive a key. +var errDerivation = fmt.Errorf("failed to derive key") + +// scanAddressRange scans backwards from end to start many addresses in the +// account branch, and return the first index that is found on the blockchain. +// If the address doesn't exist, false is returned as the first argument. +func (w *Wallet) scanAddressRange(account uint32, branch uint32, start int, + end int, chainClient *chain.RPCClient) (bool, int, error) { + + var addresses []dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addresses, err = w.Manager.AddressesDerivedFromDbAcct(addrmgrNs, + uint32(start), uint32(end+1), account, branch) + if err != nil { + return errDerivation + } + return nil + }) + if err != nil { + return false, 0, err + } + + // Whether or not the addresses exist is encoded as a binary + // bitset. + exists, err := chainClient.ExistsAddresses(addresses) + if err != nil { + return false, 0, err + } + existsB, err := hex.DecodeString(exists) + if err != nil { + return false, 0, err + } + set := bitset.Bytes(existsB) + + // Prevent a panic when an empty message is passed as a response. + if len(set) == 0 { + return false, 0, nil + } + + // Scan backwards and return if we find an address exists. + idx := end + itr := len(addresses) - 1 + for idx >= start { + // If the address exists in the mempool or blockchain according + // to the bit set returned, return this index. + if set.Get(itr) { + return true, idx, nil + } + + itr-- + idx-- + } + + return false, 0, nil +} + +// bisectLastAddrIndex is a helper function for search through addresses. +func (w *Wallet) bisectLastAddrIndex(hi, low int, account uint32, branch uint32) int { + chainClient, err := w.requireChainClient() + if err != nil { + return 0 + } + + // Logarithmically scan address indexes to find the last used + // address index. Each time the algorithm receives an end point, + // scans a chunk of addresses at the end point, and if no + // addresses are found, divides the address index by two and + // repeats until it finds the last used index. + offset := low + for i := hi - low - 1; i > 0; i /= 2 { + if i+offset+int(addrSeekWidth) < waddrmgr.MaxAddressesPerAccount { + start := i + offset + end := i + offset + int(addrSeekWidth) + exists, idx, err := w.scanAddressRange(account, branch, start, end, + chainClient) + // Skip erroneous keys, which happen rarely. Don't skip + // other errors. + if err == errDerivation { + continue + } + if err != nil { + log.Warnf("unexpected error encountered during bisection "+ + "scan of account %v, branch %v: %s", account, branch, + err.Error()) + return 0 + } + if exists { + return idx + } + } else { + var addr dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = w.Manager.AddressDerivedFromDbAcct(addrmgrNs, + uint32(i+offset), account, branch) + return err + }) + // Skip erroneous keys, which happen rarely. + if err != nil { + continue + } + + exists, err := chainClient.ExistsAddress(addr) + if err != nil { + return 0 + } + if exists { + return i + offset + } + } + } + + return 0 +} + +// findEnd is a helper function for searching for used addresses. +func (w *Wallet) findAddrEnd(start, stop int, account uint32, branch uint32) int { + indexStart := w.bisectLastAddrIndex(stop, start, account, branch) + indexLast := 0 + for { + indexLastStored := indexStart + low := indexLastStored + hi := indexLast + ((indexStart - indexLast) * 2) + 1 + indexStart = w.bisectLastAddrIndex(hi, low, account, branch) + indexLast = indexLastStored + + if indexStart == 0 { + break + } + } + + return indexLast +} + +// debugAccountAddrGapsString is a debug function that prints a graphical outlook +// of address usage to a string, from the perspective of the daemon. +func debugAccountAddrGapsString(scanBackFrom int, account uint32, branch uint32, + w *Wallet) (string, error) { + + chainClient, err := w.requireChainClient() + if err != nil { + return "", err + } + + var buf bytes.Buffer + str := fmt.Sprintf("Begin debug address scan scanning backwards from "+ + "idx %v, account %v, branch %v\n", scanBackFrom, account, branch) + buf.WriteString(str) + firstUsedIndex := 0 + for i := scanBackFrom; i > 0; i-- { + var addr dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = w.Manager.AddressDerivedFromDbAcct(addrmgrNs, + uint32(i), account, branch) + return err + }) + // Skip erroneous keys. + if err != nil { + continue + } + + exists, err := chainClient.ExistsAddress(addr) + if err != nil { + return "", fmt.Errorf("failed to access chain server: %v", + err.Error()) + } + + if exists { + firstUsedIndex = i + break + } + } + + str = fmt.Sprintf("Last used index found: %v\n", firstUsedIndex) + buf.WriteString(str) + + batchSize := 50 + batches := (firstUsedIndex / batchSize) + 1 + lastBatchSize := 0 + if firstUsedIndex%batchSize != 0 { + lastBatchSize = firstUsedIndex - ((batches - 1) * batchSize) + } + + for i := 0; i < batches; i++ { + str = fmt.Sprintf("%8v", i*batchSize) + buf.WriteString(str) + + start := i * batchSize + end := (i + 1) * batchSize + if i == batches-1 { + // Nothing to do because last batch empty. + if lastBatchSize == 0 { + break + } + end = (i*batchSize + lastBatchSize) + 1 + } + + for j := start; j < end; j++ { + if j%10 == 0 { + buf.WriteString(" ") + } + + char := "_" + var addr dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = w.Manager.AddressDerivedFromDbAcct(addrmgrNs, + uint32(j), account, branch) + return err + }) + if err != nil { + char = "X" + } + + exists, err := chainClient.ExistsAddress(addr) + if err != nil { + return "", fmt.Errorf("failed to access chain server: %v", + err.Error()) + } + if exists { + char = "#" + } + + buf.WriteString(char) + } + + buf.WriteString("\n") + } + + return buf.String(), nil +} + +// scanAddressIndex identifies the last used address in an HD keychain of public +// keys. It returns the index of the last used key, along with the address of +// this key. +func (w *Wallet) scanAddressIndex(start int, end int, account uint32, + branch uint32) (uint32, dcrutil.Address, error) { + chainClient, err := w.requireChainClient() + if err != nil { + return 0, nil, err + } + + // Find the last used address. Scan from it to the end in case there was a + // gap from that position, which is possible. Then, return the address + // in that position. + lastUsed := w.findAddrEnd(start, end, account, branch) + + // If debug is on, do an exhaustive check and a graphical printout + // of what the used addresses currently look like. + if log.Level() == btclog.DebugLvl || log.Level() == btclog.TraceLvl { + dbgStr, err := debugAccountAddrGapsString(lastUsed+debugAddrScanLength, + account, branch, w) + if err != nil { + log.Debugf("Failed to debug address gaps for account %v, "+ + "branch %v: %v", account, branch, err) + } else { + log.Debugf("%v", dbgStr) + } + } + + // If there was a last used index, do an exhaustive final scan that + // reexamines the last used addresses and ensures that the final index + // we have found is correct. + if lastUsed != 0 { + start := lastUsed + end := lastUsed + w.addrIdxScanLen + exists, idx, err := w.scanAddressRange(account, branch, start, end, + chainClient) + if err != nil { + return 0, nil, err + } + + if exists { + lastUsed = idx + var addr dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = w.Manager.AddressDerivedFromDbAcct(addrmgrNs, + uint32(lastUsed), account, branch) + return err + }) + if err != nil { + return 0, nil, err + } + return uint32(lastUsed), addr, nil + } + } + + // In the case that 0 was returned as the last used address, + // make sure the the 0th address was not used. If it was, + // return this address to let the caller know that this + // 0th address was used. + if lastUsed == 0 { + var addr dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addr, err = w.Manager.AddressDerivedFromDbAcct(addrmgrNs, 0, + account, branch) + return err + }) + // Skip erroneous keys. + if err != nil { + return 0, nil, err + } + + exists, err := chainClient.ExistsAddress(addr) + if err != nil { + return 0, nil, fmt.Errorf("failed to access chain server: %v", + err.Error()) + } + + if exists { + return 0, addr, nil + } + } + + // We can't find any used addresses for this account's + // branch. + return 0, nil, nil +} + +// rescanActiveAddresses accesses the daemon to discover all the addresses that +// have been used by an HD keychain stemming from this wallet in the default +// account. +func (w *Wallet) rescanActiveAddresses() error { + log.Infof("Beginning a rescan of active addresses using the daemon. " + + "This may take a while.") + + // Start by rescanning the accounts and determining what the + // current account index is. This scan should only ever be + // performed if we're restoring our wallet from seed. + lastAcct := uint32(0) + var err error + if w.initiallyUnlocked { + min := 0 + max := waddrmgr.MaxAccountNum + lastAcct, err = w.scanAccountIndex(min, max) + if err != nil { + return err + } + } + + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + lastAcctMgr, err := w.Manager.LastAccount(addrmgrNs) + if err != nil { + return err + } + + // The address manager is not synced (wallet has been restored + // from seed?). In this case, spawn the accounts in the address + // manager first. The accounts are named by their respective + // index number, as strings. + if lastAcctMgr < lastAcct { + for i := lastAcctMgr + 1; i <= lastAcct; i++ { + _, err := w.Manager.NewAccount( + addrmgrNs, strconv.Itoa(int(i))) + if err != nil { + return err + } + } + } + + // The account manager has a greater index than the rescan. + // It is likely that the end user created a new account but + // did not use it yet. Rescan it anyway so that the address + // pool is created. + if lastAcctMgr > lastAcct { + lastAcct = lastAcctMgr + } + return nil + }) + if err != nil { + return err + } + + log.Infof("The last used account was %v. Beginning a rescan for "+ + "all active addresses in known accounts.", lastAcct) + + // Rescan addresses for the both the internal and external + // branches of the account. Insert a new address pool for + // the respective account and initialize it. + for acct := uint32(0); acct <= lastAcct; acct++ { + var extIdx, intIdx uint32 + min := 0 + max := waddrmgr.MaxAddressesPerAccount + + // Do this for both external (0) and internal (1) branches. + for branch := uint32(0); branch < 2; branch++ { + idx, lastAddr, err := w.scanAddressIndex(min, max, acct, branch) + if err != nil { + return err + } + + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + + // If the account is unused, buffer the initial address pool + // by syncing the address manager upstream. + unusedAcct := (lastAddr == nil) + if unusedAcct { + _, err := w.Manager.SyncAccountToAddrIndex( + addrmgrNs, acct, addressPoolBuffer, branch) + if err != nil { + // A ErrSyncToIndex error indicates that we're already + // synced to beyond the end of the account in the + // waddrmgr. + errWaddrmgr, ok := err.(waddrmgr.ManagerError) + if !ok || errWaddrmgr.ErrorCode != waddrmgr.ErrSyncToIndex { + return fmt.Errorf("failed to create initial waddrmgr "+ + "address buffer for the address pool, "+ + "account %v, branch %v: %s", acct, branch, + err.Error()) + } + } + } + + branchString := "external" + if branch == waddrmgr.InternalBranch { + branchString = "internal" + } + + // Fetch the address pool index for this account and + // branch from the database meta bucket. + isInternal := branch == waddrmgr.InternalBranch + oldIdx, err := w.Manager.NextToUseAddrPoolIndex( + addrmgrNs, isInternal, acct) + unexpectedError := false + if err != nil { + mErr, ok := err.(waddrmgr.ManagerError) + if !ok { + unexpectedError = true + } else { + // Skip errors where the account's address index + // has not been store. For this case, oldIdx will + // be the special case 0 which will always be + // skipped in the initialization step below. + if mErr.ErrorCode != waddrmgr.ErrMetaPoolIdxNoExist { + unexpectedError = true + } + } + if unexpectedError { + return fmt.Errorf("got unexpected error trying to "+ + "retrieve last known addr index for acct %v, "+ + "%s branch: %v", acct, branchString, err) + } + } + + // If the stored index is further along than the sync-to + // index determined by the contents of daemon's addrindex, + // use it to initialize the address pool instead. + nextToUseIdx := idx + if !unusedAcct { + nextToUseIdx++ + } + if oldIdx > nextToUseIdx { + nextToUseIdx = oldIdx + } + nextToUseAddr, err := w.Manager.AddressDerivedFromDbAcct( + addrmgrNs, nextToUseIdx, acct, branch) + if err != nil { + return fmt.Errorf("failed to derive next address for "+ + "account %v, branch %v: %s", acct, branch, + err.Error()) + } + + // Save these for the address pool startup later. + if isInternal { + intIdx = nextToUseIdx + } else { + extIdx = nextToUseIdx + } + + // Synchronize the account manager to our address index plus + // an extra chunk of addresses that are used as a buffer + // in the address pool. + _, err = w.Manager.SyncAccountToAddrIndex(addrmgrNs, + acct, nextToUseIdx+addressPoolBuffer, branch) + if err != nil { + // A ErrSyncToIndex error indicates that we're already + // synced to beyond the end of the account in the + // waddrmgr. + errWaddrmgr, ok := err.(waddrmgr.ManagerError) + if !ok || errWaddrmgr.ErrorCode != waddrmgr.ErrSyncToIndex { + return fmt.Errorf("couldn't sync %s addresses in "+ + "address manager: %v", branchString, err.Error()) + } + } + + // Set the next address in the waddrmgr database so that the + // address pool can synchronize properly after. + err = w.Manager.StoreNextToUseAddress( + addrmgrNs, isInternal, acct, nextToUseIdx) + if err != nil { + log.Errorf("Failed to store next to use pool idx for "+ + "%s pool in the manager on init sync: %v", + branchString, err.Error()) + } + + log.Infof("Successfully synchronized the address manager to "+ + "%s address %v (key index %v) for account %v", + branchString, + nextToUseAddr.String(), + nextToUseIdx, + acct) + return nil + }) + if err != nil { + return err + } + } + + pool, err := newAddressPools(acct, intIdx, extIdx, w) + if err != nil { + return err + } + + w.addrPoolsMtx.Lock() + w.addrPools[acct] = pool + w.addrPoolsMtx.Unlock() + } + + log.Infof("Successfully synchronized wallet accounts to account "+ + "number %v.", lastAcct) + + return nil +} diff --git a/wallet/unstable.go b/wallet/unstable.go new file mode 100644 index 0000000..c34b6d1 --- /dev/null +++ b/wallet/unstable.go @@ -0,0 +1,44 @@ +// Copyright (c) 2016 The Decred developers +// Copyright (c) 2017 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 ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +type unstableAPI struct { + w *Wallet +} + +// UnstableAPI exposes additional unstable public APIs for a Wallet. These APIs +// may be changed or removed at any time. Currently this type exists to ease +// the transation (particularly for the legacy JSON-RPC server) from using +// exported manager packages to a unified wallet package that exposes all +// functionality by itself. New code should not be written using this API. +func UnstableAPI(w *Wallet) unstableAPI { return unstableAPI{w} } + +// TxDetails calls wtxmgr.Store.TxDetails under a single database view transaction. +func (u unstableAPI) TxDetails(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { + var details *wtxmgr.TxDetails + err := walletdb.View(u.w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + var err error + details, err = u.w.TxStore.TxDetails(txmgrNs, txHash) + return err + }) + return details, err +} + +// RangeTransactions calls wtxmgr.Store.RangeTransactions under a single +// database view tranasction. +func (u unstableAPI) RangeTransactions(begin, end int32, f func([]wtxmgr.TxDetails) (bool, error)) error { + return walletdb.View(u.w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + return u.w.TxStore.RangeTransactions(txmgrNs, begin, end, f) + }) +} diff --git a/wallet/utxos.go b/wallet/utxos.go new file mode 100644 index 0000000..f1f454d --- /dev/null +++ b/wallet/utxos.go @@ -0,0 +1,90 @@ +// Copyright (c) 2016 The Decred developers +// Copyright (c) 2017 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 ( + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/walletdb" +) + +// OutputSelectionPolicy describes the rules for selecting an output from the +// wallet. +type OutputSelectionPolicy struct { + Account uint32 + RequiredConfirmations int32 +} + +func (p *OutputSelectionPolicy) meetsRequiredConfs(txHeight, curHeight int32) bool { + return confirmed(p.RequiredConfirmations, txHeight, curHeight) +} + +// UnspentOutputs fetches all unspent outputs from the wallet that match rules +// described in the passed policy. +func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOutput, error) { + var outputResults []*TransactionOutput + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + syncBlock := w.Manager.SyncedTo() + + // TODO: actually stream outputs from the db instead of fetching + // all of them at once. + outputs, err := w.TxStore.UnspentOutputs(txmgrNs) + if err != nil { + return err + } + + for _, output := range outputs { + // Ignore outputs that haven't reached the required + // number of confirmations. + if !policy.meetsRequiredConfs(output.Height, syncBlock.Height) { + continue + } + + // Ignore outputs that are not controlled by the account. + _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, + w.chainParams) + if err != nil || len(addrs) == 0 { + // Cannot determine which account this belongs + // to without a valid address. TODO: Fix this + // by saving outputs per account, or accounts + // per output. + continue + } + outputAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0]) + if err != nil { + return err + } + if outputAcct != policy.Account { + continue + } + + // Stakebase isn't exposed by wtxmgr so those will be + // OutputKindNormal for now. + outputSource := OutputKindNormal + if output.FromCoinBase { + outputSource = OutputKindCoinbase + } + + result := &TransactionOutput{ + OutPoint: output.OutPoint, + Output: wire.TxOut{ + Value: int64(output.Amount), + PkScript: output.PkScript, + }, + OutputKind: outputSource, + ContainingBlock: BlockIdentity(output.Block), + ReceiveTime: output.Received, + } + outputResults = append(outputResults, result) + } + + return nil + }) + return outputResults, err +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 4f4f55d..0cc4996 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1,18 +1,14 @@ -// Copyright (c) 2013-2016 The btcsuite developers +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package wallet import ( - "bytes" - "encoding/base64" "encoding/hex" "errors" "fmt" - "io/ioutil" - "os" - "path/filepath" "sort" "sync" "time" @@ -94,10 +90,15 @@ type Wallet struct { // Channels for the manager locker. unlockRequests chan unlockRequest lockRequests chan struct{} - holdUnlockRequests chan chan HeldUnlock + holdUnlockRequests chan chan heldUnlock lockState chan bool changePassphrase chan changePassphraseRequest + // Information for reorganization handling. + reorganizingLock sync.Mutex + reorganizeToHash chainhash.Hash + reorganizing bool + NtfnServer *NotificationServer chainParams *chaincfg.Params @@ -297,17 +298,19 @@ func (w *Wallet) SetChainSynced(synced bool) { // activeData returns the currently-active receiving addresses and all unspent // outputs. This is primarely intended to provide the parameters for a // rescan request. -func (w *Wallet) activeData() ([]btcutil.Address, []wtxmgr.Credit, error) { +func (w *Wallet) activeData(dbtx walletdb.ReadTx) ([]btcutil.Address, []wtxmgr.Credit, error) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + var addrs []btcutil.Address - // TODO(roasbeef): lookahead? - err := w.Manager.ForEachActiveAddress(func(addr btcutil.Address) error { + err := w.Manager.ForEachActiveAddress(addrmgrNs, func(addr btcutil.Address) error { addrs = append(addrs, addr) return nil }) if err != nil { return nil, nil, err } - unspent, err := w.TxStore.UnspentOutputs() + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) return addrs, unspent, err } @@ -336,8 +339,15 @@ func (w *Wallet) syncWithChain() error { // Request notifications for transactions sending to all wallet // addresses. - // TODO(roasbeef): need to check 3 versions of each key? - addrs, unspent, err := w.activeData() + var ( + addrs []btcutil.Address + unspent []wtxmgr.Credit + ) + err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + var err error + addrs, unspent, err = w.activeData(dbtx) + return err + }) if err != nil { return err } @@ -361,9 +371,12 @@ func (w *Wallet) syncWithChain() error { if err != nil { return err } - return w.Manager.SetSyncedTo(&waddrmgr.BlockStamp{ - Hash: *hash, - Height: height, + return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.SetSyncedTo(ns, &waddrmgr.BlockStamp{ + Hash: *hash, + Height: height, + }) }) } @@ -392,14 +405,18 @@ func (w *Wallet) syncWithChain() error { } if rollback { - err = w.Manager.SetSyncedTo(&syncBlock) - if err != nil { - return err - } - // Rollback unconfirms transactions at and beyond the passed - // height, so add one to the new synced-to height to prevent - // unconfirming txs from the synced-to block. - err = w.TxStore.Rollback(syncBlock.Height + 1) + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + err := w.Manager.SetSyncedTo(addrmgrNs, &syncBlock) + if err != nil { + return err + } + // Rollback unconfirms transactions at and beyond the passed + // height, so add one to the new synced-to height to prevent + // unconfirming txs from the synced-to block. + return w.TxStore.Rollback(txmgrNs, syncBlock.Height+1) + }) if err != nil { return err } @@ -437,9 +454,15 @@ out: for { select { case txr := <-w.createTxRequests: - tx, err := w.txToOutputs(txr.outputs, txr.account, txr.minconf) + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createTxResponse{nil, err} + continue + } + tx, err := w.txToOutputs(txr.outputs, txr.account, + txr.minconf) + heldUnlock.release() txr.resp <- createTxResponse{tx, err} - case <-quit: break out } @@ -479,24 +502,27 @@ type ( err chan error } - // HeldUnlock is a tool to prevent the wallet from automatically + // heldUnlock is a tool to prevent the wallet from automatically // locking after some timeout before an operation which needed - // the unlocked wallet has finished. Any aquired HeldUnlock + // the unlocked wallet has finished. Any aquired heldUnlock // *must* be released (preferably with a defer) or the wallet // will forever remain unlocked. - HeldUnlock chan struct{} + heldUnlock chan struct{} ) // walletLocker manages the locked/unlocked state of a wallet. func (w *Wallet) walletLocker() { var timeout <-chan time.Time - holdChan := make(HeldUnlock) + holdChan := make(heldUnlock) quit := w.quitChan() out: for { select { case req := <-w.unlockRequests: - err := w.Manager.Unlock(req.passphrase) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.Unlock(addrmgrNs, req.passphrase) + }) if err != nil { req.err <- err continue @@ -511,8 +537,11 @@ out: continue case req := <-w.changePassphrase: - err := w.Manager.ChangePassphrase(req.old, req.new, true, - &waddrmgr.DefaultScryptOptions) + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.ChangePassphrase(addrmgrNs, req.old, + req.new, true, &waddrmgr.DefaultScryptOptions) + }) req.err <- err continue @@ -585,14 +614,14 @@ func (w *Wallet) Locked() bool { return <-w.lockState } -// HoldUnlock prevents the wallet from being locked. The HeldUnlock object +// holdUnlock prevents the wallet from being locked. The heldUnlock object // *must* be released, or the wallet will forever remain unlocked. // // TODO: To prevent the above scenario, perhaps closures should be passed // to the walletLocker goroutine and disallow callers from explicitly // handling the locking mechanism. -func (w *Wallet) HoldUnlock() (HeldUnlock, error) { - req := make(chan HeldUnlock) +func (w *Wallet) holdUnlock() (heldUnlock, error) { + req := make(chan heldUnlock) w.holdUnlockRequests <- req hl, ok := <-req if !ok { @@ -606,18 +635,18 @@ func (w *Wallet) HoldUnlock() (HeldUnlock, error) { return hl, nil } -// Release releases the hold on the unlocked-state of the wallet and allows the +// release releases the hold on the unlocked-state of the wallet and allows the // wallet to be locked again. If a lock timeout has already expired, the -// wallet is locked again as soon as Release is called. -func (c HeldUnlock) Release() { +// wallet is locked again as soon as release is called. +func (c heldUnlock) release() { c <- struct{}{} } -// ChangePassphrase attempts to change the passphrase for a wallet from old -// to new. Changing the passphrase is synchronized with all other address +// ChangePrivatePassphrase attempts to change the passphrase for a wallet from +// old to new. Changing the passphrase is synchronized with all other address // manager locking and unlocking. The lock state will be the same as it was // before the password change. -func (w *Wallet) ChangePassphrase(old, new []byte) error { +func (w *Wallet) ChangePrivatePassphrase(old, new []byte) error { err := make(chan error, 1) w.changePassphrase <- changePassphraseRequest{ old: old, @@ -627,27 +656,45 @@ func (w *Wallet) ChangePassphrase(old, new []byte) error { return <-err } -// AccountUsed returns whether there are any recorded transactions spending to +// ChangePublicPassphrase modifies the public passphrase of the wallet. +func (w *Wallet) ChangePublicPassphrase(old, new []byte) error { + return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.ChangePassphrase(addrmgrNs, old, new, false, + &waddrmgr.DefaultScryptOptions) + }) +} + +// accountUsed returns whether there are any recorded transactions spending to // a given account. It returns true if atleast one address in the account was // used and false if no address in the account was used. -func (w *Wallet) AccountUsed(account uint32) (bool, error) { +func (w *Wallet) accountUsed(addrmgrNs walletdb.ReadWriteBucket, account uint32) (bool, error) { var used bool - var err error - merr := w.Manager.ForEachAccountAddress(account, + err := w.Manager.ForEachAccountAddress(addrmgrNs, account, func(maddr waddrmgr.ManagedAddress) error { - used, err = maddr.Used() - if err != nil { - return err - } + used = maddr.Used(addrmgrNs) if used { return waddrmgr.Break } return nil }) - if merr == waddrmgr.Break { - merr = nil + if err == waddrmgr.Break { + err = nil } - return used, merr + return used, err +} + +// AccountAddresses returns the addresses for every created address for an +// account. +func (w *Wallet) AccountAddresses(account uint32) (addrs []btcutil.Address, err error) { + err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.ForEachAccountAddress(addrmgrNs, account, func(maddr waddrmgr.ManagedAddress) error { + addrs = append(addrs, maddr.Address()) + return nil + }) + }) + return } // CalculateBalance sums the amounts of all unspent transaction @@ -659,8 +706,15 @@ func (w *Wallet) AccountUsed(account uint32) (bool, error) { // the balance will be calculated based on how many how many blocks // include a UTXO. func (w *Wallet) CalculateBalance(confirms int32) (btcutil.Amount, error) { - blk := w.Manager.SyncedTo() - return w.TxStore.Balance(confirms, blk.Height) + var balance btcutil.Amount + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + blk := w.Manager.SyncedTo() + balance, err = w.TxStore.Balance(txmgrNs, confirms, blk.Height) + return err + }) + return balance, err } // Balances records total, spendable (by policy), and immature coinbase @@ -679,37 +733,42 @@ type Balances struct { // outputs must be iterated. func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balances, error) { var bals Balances + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() - unspent, err := w.TxStore.UnspentOutputs() - if err != nil { - return bals, err - } - for i := range unspent { - output := &unspent[i] - - var outputAcct uint32 - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams) - if err == nil && len(addrs) > 0 { - outputAcct, err = w.Manager.AddrAccount(addrs[0]) - } - if err != nil || outputAcct != account { - continue + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) + if err != nil { + return err } + for i := range unspent { + output := &unspent[i] - bals.Total += output.Amount - if output.FromCoinBase && !confirmed(int32(w.chainParams.CoinbaseMaturity), - output.Height, syncBlock.Height) { - bals.ImmatureReward += output.Amount - } else if confirmed(confirms, output.Height, syncBlock.Height) { - bals.Spendable += output.Amount + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if err != nil || outputAcct != account { + continue + } + + bals.Total += output.Amount + if output.FromCoinBase && !confirmed(int32(w.chainParams.CoinbaseMaturity), + output.Height, syncBlock.Height) { + bals.ImmatureReward += output.Amount + } else if confirmed(confirms, output.Height, syncBlock.Height) { + bals.Spendable += output.Amount + } } - } - return bals, nil + return nil + }) + return bals, err } // CurrentAddress gets the most recently requested Bitcoin payment address @@ -717,63 +776,192 @@ func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balan // one transaction spending to it in the blockchain or btcd mempool), the next // chained address is returned. func (w *Wallet) CurrentAddress(account uint32) (btcutil.Address, error) { - addr, err := w.Manager.LastExternalAddress(account) - if err != nil { - // If no address exists yet, create the first external address - if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { - // TODO(roasbeef): what to default to ? - return w.NewAddress(account, waddrmgr.WitnessPubKey) + var addr btcutil.Address + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + maddr, err := w.Manager.LastExternalAddress(addrmgrNs, account) + if err != nil { + // If no address exists yet, create the first external address + if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { + addr, err = w.newAddress(addrmgrNs, account) + } + return err } - return nil, err - } - // Get next chained address if the last one has already been used. - used, err := addr.Used() - if err != nil { - return nil, err - } - if used { - return w.NewAddress(account, waddrmgr.WitnessPubKey) - } + // Get next chained address if the last one has already been used. + if maddr.Used(addrmgrNs) { + addr, err = w.newAddress(addrmgrNs, account) + return err + } - return addr.Address(), nil + addr = maddr.Address() + return nil + }) + return addr, err +} + +// PubKeyForAddress looks up the associated public key for a P2PKH address. +func (w *Wallet) PubKeyForAddress(a btcutil.Address) (*btcec.PublicKey, error) { + var pubKey *btcec.PublicKey + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, err := w.Manager.Address(addrmgrNs, a) + if err != nil { + return err + } + managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return errors.New("address does not have an associated public key") + } + pubKey = managedPubKeyAddr.PubKey() + return nil + }) + return pubKey, err +} + +// PrivKeyForAddress looks up the associated private key for a P2PKH or P2PK +// address. +func (w *Wallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) { + var privKey *btcec.PrivateKey + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, err := w.Manager.Address(addrmgrNs, a) + if err != nil { + return err + } + managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return errors.New("address does not have an associated private key") + } + privKey, err = managedPubKeyAddr.PrivKey() + return err + }) + return privKey, err +} + +// HaveAddress returns whether the wallet is the owner of the address a. +func (w *Wallet) HaveAddress(a btcutil.Address) (bool, error) { + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := w.Manager.Address(addrmgrNs, a) + return err + }) + if err == nil { + return true, nil + } + if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { + return false, nil + } + return false, err +} + +// AccountOfAddress finds the account that an address is associated with. +func (w *Wallet) AccountOfAddress(a btcutil.Address) (uint32, error) { + var account uint32 + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + account, err = w.Manager.AddrAccount(addrmgrNs, a) + return err + }) + return account, err +} + +// AddressInfo returns detailed information regarding a wallet address. +func (w *Wallet) AddressInfo(a btcutil.Address) (waddrmgr.ManagedAddress, error) { + var managedAddress waddrmgr.ManagedAddress + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + managedAddress, err = w.Manager.Address(addrmgrNs, a) + return err + }) + return managedAddress, err +} + +// AccountNumber returns the account number for an account name. +func (w *Wallet) AccountNumber(accountName string) (uint32, error) { + var account uint32 + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + account, err = w.Manager.LookupAccount(addrmgrNs, accountName) + return err + }) + return account, err +} + +// AccountName returns the name of an account. +func (w *Wallet) AccountName(accountNumber uint32) (string, error) { + var accountName string + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + accountName, err = w.Manager.AccountName(addrmgrNs, accountNumber) + return err + }) + return accountName, err +} + +// AccountProperties returns the properties of an account, including address +// indexes and name. It first fetches the desynced information from the address +// manager, then updates the indexes based on the address pools. +func (w *Wallet) AccountProperties(acct uint32) (*waddrmgr.AccountProperties, error) { + var props *waddrmgr.AccountProperties + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + props, err = w.Manager.AccountProperties(waddrmgrNs, acct) + return err + }) + return props, err } // RenameAccount sets the name for an account number to newName. func (w *Wallet) RenameAccount(account uint32, newName string) error { - err := w.Manager.RenameAccount(account, newName) - if err != nil { + var props *waddrmgr.AccountProperties + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + err := w.Manager.RenameAccount(addrmgrNs, account, newName) + if err != nil { + return err + } + props, err = w.Manager.AccountProperties(addrmgrNs, account) return err - } - - props, err := w.Manager.AccountProperties(account) - if err != nil { - log.Errorf("Cannot fetch new account properties for notification "+ - "during account rename: %v", err) - } else { + }) + if err == nil { w.NtfnServer.notifyAccountProperties(props) } - - return nil + return err } -// NextAccount creates the next account and returns its account number. The -// name must be unique to the account. -func (w *Wallet) NextAccount(name string) (uint32, error) { - account, err := w.Manager.NewAccount(name) - if err != nil { - return 0, err - } +const maxEmptyAccounts = 100 - props, err := w.Manager.AccountProperties(account) +// NextAccount creates the next account and returns its account number. The +// name must be unique to the account. In order to support automatic seed +// restoring, new accounts may not be created when all of the previous 100 +// accounts have no transaction history (this is a deviation from the BIP0044 +// spec, which allows no unused account gaps). +func (w *Wallet) NextAccount(name string) (uint32, error) { + var account uint32 + var props *waddrmgr.AccountProperties + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + account, err = w.Manager.NewAccount(addrmgrNs, name) + if err != nil { + return err + } + props, err = w.Manager.AccountProperties(addrmgrNs, account) + return err + }) if err != nil { log.Errorf("Cannot fetch new account properties for notification "+ "after account creation: %v", err) } else { w.NtfnServer.notifyAccountProperties(props) } - - return account, nil + return account, err } // CreditCategory describes the type of wallet transaction output. The category @@ -823,13 +1011,15 @@ func RecvCategory(details *wtxmgr.TxDetails, syncHeight int32, net *chaincfg.Par return CreditReceive } -// ListTransactions creates a object that may be marshalled to a response result +// listTransactions creates a object that may be marshalled to a response result // for a listtransactions RPC. // // TODO: This should be moved to the legacyrpc package. -func ListTransactions(details *wtxmgr.TxDetails, addrMgr *waddrmgr.Manager, +func listTransactions(tx walletdb.ReadTx, details *wtxmgr.TxDetails, addrMgr *waddrmgr.Manager, syncHeight int32, net *chaincfg.Params) []btcjson.ListTransactionsResult { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var ( blockHashStr string blockTime int64 @@ -891,9 +1081,9 @@ outputs: if len(addrs) == 1 { addr := addrs[0] address = addr.EncodeAddress() - account, err := addrMgr.AddrAccount(addrs[0]) + account, err := addrMgr.AddrAccount(addrmgrNs, addrs[0]) if err == nil { - accountName, err = addrMgr.AccountName(account) + accountName, err = addrMgr.AccountName(addrmgrNs, account) if err != nil { accountName = "" } @@ -955,13 +1145,19 @@ outputs: // This is intended to be used for listsinceblock RPC replies. func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) { txList := []btcjson.ListTransactionsResult{} - err := w.TxStore.RangeTransactions(start, end, func(details []wtxmgr.TxDetails) (bool, error) { - for _, detail := range details { - jsonResults := ListTransactions(&detail, w.Manager, - syncHeight, w.chainParams) - txList = append(txList, jsonResults...) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for _, detail := range details { + jsonResults := listTransactions(tx, &detail, + w.Manager, syncHeight, w.chainParams) + txList = append(txList, jsonResults...) + } + return false, nil } - return false, nil + + return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) }) return txList, err } @@ -972,89 +1168,99 @@ func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTra func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsResult, error) { txList := []btcjson.ListTransactionsResult{} - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - // Need to skip the first from transactions, and after those, only - // include the next count transactions. - skipped := 0 - n := 0 + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() - // Return newer results first by starting at mempool height and working - // down to the genesis block. - err := w.TxStore.RangeTransactions(-1, 0, func(details []wtxmgr.TxDetails) (bool, error) { - // Iterate over transactions at this height in reverse order. - // This does nothing for unmined transactions, which are - // unsorted, but it will process mined transactions in the - // reverse order they were marked mined. - for i := len(details) - 1; i >= 0; i-- { - if from > skipped { - skipped++ - continue + // Need to skip the first from transactions, and after those, only + // include the next count transactions. + skipped := 0 + n := 0 + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse order. + // This does nothing for unmined transactions, which are + // unsorted, but it will process mined transactions in the + // reverse order they were marked mined. + for i := len(details) - 1; i >= 0; i-- { + if from > skipped { + skipped++ + continue + } + + n++ + if n > count { + return true, nil + } + + jsonResults := listTransactions(tx, &details[i], + w.Manager, syncBlock.Height, w.chainParams) + txList = append(txList, jsonResults...) + + if len(jsonResults) > 0 { + n++ + } } - n++ - if n > count { - return true, nil - } - - jsonResults := ListTransactions(&details[i], - w.Manager, syncBlock.Height, w.chainParams) - txList = append(txList, jsonResults...) + return false, nil } - return false, nil + // Return newer results first by starting at mempool height and working + // down to the genesis block. + return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) }) - return txList, err } // ListAddressTransactions returns a slice of objects with details about // recorded transactions to or from any address belonging to a set. This is // intended to be used for listaddresstransactions RPC replies. -func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( - []btcjson.ListTransactionsResult, error) { - +func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ([]btcjson.ListTransactionsResult, error) { txList := []btcjson.ListTransactionsResult{} + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + loopDetails: + for i := range details { + detail := &details[i] - err := w.TxStore.RangeTransactions(0, -1, func(details []wtxmgr.TxDetails) (bool, error) { - loopDetails: - for i := range details { - detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + pkScript, w.chainParams) + if err != nil || len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) + if !ok { + continue + } + _, ok = pkHashes[string(apkh.ScriptAddress())] + if !ok { + continue + } - for _, cred := range detail.Credits { - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - pkScript, w.chainParams) - if err != nil || len(addrs) != 1 { - continue + jsonResults := listTransactions(tx, detail, + w.Manager, syncBlock.Height, w.chainParams) + if err != nil { + return false, err + } + txList = append(txList, jsonResults...) + continue loopDetails } - apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) - if !ok { - continue - } - _, ok = pkHashes[string(apkh.ScriptAddress())] - if !ok { - continue - } - - jsonResults := ListTransactions(detail, w.Manager, - syncBlock.Height, w.chainParams) - if err != nil { - return false, err - } - txList = append(txList, jsonResults...) - continue loopDetails } + return false, nil } - return false, nil - }) + return w.TxStore.RangeTransactions(txmgrNs, 0, -1, rangeFn) + }) return txList, err } @@ -1063,26 +1269,30 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( // replies. func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) { txList := []btcjson.ListTransactionsResult{} + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() - // Return newer results first by starting at mempool height and working - // down to the genesis block. - err := w.TxStore.RangeTransactions(-1, 0, func(details []wtxmgr.TxDetails) (bool, error) { - // Iterate over transactions at this height in reverse order. - // This does nothing for unmined transactions, which are - // unsorted, but it will process mined transactions in the - // reverse order they were marked mined. - for i := len(details) - 1; i >= 0; i-- { - jsonResults := ListTransactions(&details[i], w.Manager, - syncBlock.Height, w.chainParams) - txList = append(txList, jsonResults...) + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse order. + // This does nothing for unmined transactions, which are + // unsorted, but it will process mined transactions in the + // reverse order they were marked mined. + for i := len(details) - 1; i >= 0; i-- { + jsonResults := listTransactions(tx, &details[i], w.Manager, + syncBlock.Height, w.chainParams) + txList = append(txList, jsonResults...) + } + return false, nil } - return false, nil - }) + // Return newer results first by starting at mempool height and + // working down to the genesis block. + return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) + }) return txList, err } @@ -1167,36 +1377,42 @@ func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, cancel < } var res GetTransactionsResult - err := w.TxStore.RangeTransactions(start, end, func(details []wtxmgr.TxDetails) (bool, error) { - // TODO: probably should make RangeTransactions not reuse the - // details backing array memory. - dets := make([]wtxmgr.TxDetails, len(details)) - copy(dets, details) - details = dets + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) - txs := make([]TransactionSummary, 0, len(details)) - for i := range details { - txs = append(txs, makeTxSummary(w, &details[i])) + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // TODO: probably should make RangeTransactions not reuse the + // details backing array memory. + dets := make([]wtxmgr.TxDetails, len(details)) + copy(dets, details) + details = dets + + txs := make([]TransactionSummary, 0, len(details)) + for i := range details { + txs = append(txs, makeTxSummary(dbtx, w, &details[i])) + } + + if details[0].Block.Height != -1 { + blockHash := details[0].Block.Hash + res.MinedTransactions = append(res.MinedTransactions, Block{ + Hash: &blockHash, + Height: details[0].Block.Height, + Timestamp: details[0].Block.Time.Unix(), + Transactions: txs, + }) + } else { + res.UnminedTransactions = txs + } + + select { + case <-cancel: + return true, nil + default: + return false, nil + } } - if details[0].Block.Height != -1 { - blockHash := details[0].Block.Hash - res.MinedTransactions = append(res.MinedTransactions, Block{ - Hash: &blockHash, - Height: details[0].Block.Height, - Timestamp: details[0].Block.Time.Unix(), - Transactions: txs, - }) - } else { - res.UnminedTransactions = txs - } - - select { - case <-cancel: - return true, nil - default: - return false, nil - } + return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) }) return &res, err } @@ -1222,51 +1438,100 @@ type AccountsResult struct { // TODO(jrick): Is the chain tip really needed, since only the total balances // are included? func (w *Wallet) Accounts() (*AccountsResult, error) { - var accounts []AccountResult - syncBlock := w.Manager.SyncedTo() - unspent, err := w.TxStore.UnspentOutputs() - if err != nil { - return nil, err - } - err = w.Manager.ForEachAccount(func(acct uint32) error { - props, err := w.Manager.AccountProperties(acct) + var ( + accounts []AccountResult + syncBlockHash *chainhash.Hash + syncBlockHeight int32 + ) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + syncBlock := w.Manager.SyncedTo() + syncBlockHash = &syncBlock.Hash + syncBlockHeight = syncBlock.Height + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) if err != nil { return err } - accounts = append(accounts, AccountResult{ - AccountProperties: *props, - // TotalBalance set below + err = w.Manager.ForEachAccount(addrmgrNs, func(acct uint32) error { + props, err := w.Manager.AccountProperties(addrmgrNs, acct) + if err != nil { + return err + } + accounts = append(accounts, AccountResult{ + AccountProperties: *props, + // TotalBalance set below + }) + return nil }) - return nil - }) - if err != nil { - return nil, err - } - m := make(map[uint32]*btcutil.Amount) - for i := range accounts { - a := &accounts[i] - m[a.AccountNumber] = &a.TotalBalance - } - for i := range unspent { - output := &unspent[i] - var outputAcct uint32 - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams) - if err == nil && len(addrs) > 0 { - outputAcct, err = w.Manager.AddrAccount(addrs[0]) + if err != nil { + return err } - if err == nil { - amt, ok := m[outputAcct] - if ok { - *amt += output.Amount + m := make(map[uint32]*btcutil.Amount) + for i := range accounts { + a := &accounts[i] + m[a.AccountNumber] = &a.TotalBalance + } + for i := range unspent { + output := unspent[i] + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if err == nil { + amt, ok := m[outputAcct] + if ok { + *amt += output.Amount + } } } - } + return nil + }) return &AccountsResult{ Accounts: accounts, - CurrentBlockHash: &syncBlock.Hash, - CurrentBlockHeight: syncBlock.Height, - }, nil + CurrentBlockHash: syncBlockHash, + CurrentBlockHeight: syncBlockHeight, + }, err +} + +// AccountBalanceResult is a single result for the Wallet.AccountBalances method. +type AccountBalanceResult struct { + AccountNumber uint32 + AccountName string + AccountBalance btcutil.Amount +} + +// AccountBalances returns all accounts in the wallet and their balances. +// Balances are determined by excluding transactions that have not met +// requiredConfs confirmations. +func (w *Wallet) AccountBalances(requiredConfs int32) ([]AccountBalanceResult, error) { + var results []AccountBalanceResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + syncBlock := w.Manager.SyncedTo() + + return w.Manager.ForEachAccount(addrmgrNs, func(account uint32) error { + accountName, err := w.Manager.AccountName(addrmgrNs, account) + if err != nil { + return err + } + balance, err := w.TxStore.Balance(txmgrNs, requiredConfs, syncBlock.Height) + if err != nil { + return err + } + results = append(results, AccountBalanceResult{ + AccountNumber: account, + AccountName: accountName, + AccountBalance: balance, + }) + return nil + }) + }) + return results, err } // creditSlice satisifies the sort.Interface interface to provide sorting @@ -1311,167 +1576,172 @@ func (s creditSlice) Swap(i, j int) { // minconf, less than maxconf and if addresses is populated only the addresses // contained within it will be considered. If we know nothing about a // transaction an empty array will be returned. -func (w *Wallet) ListUnspent(minconf, maxconf int32, - addresses map[string]struct{}) ([]*btcjson.ListUnspentResult, error) { +func (w *Wallet) ListUnspent(minconf, maxconf int32, addresses map[string]struct{}) ([]*btcjson.ListUnspentResult, error) { + var results []*btcjson.ListUnspentResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - syncBlock := w.Manager.SyncedTo() + syncBlock := w.Manager.SyncedTo() - filter := len(addresses) != 0 + filter := len(addresses) != 0 + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) + if err != nil { + return err + } + sort.Sort(sort.Reverse(creditSlice(unspent))) - unspent, err := w.TxStore.UnspentOutputs() - if err != nil { - return nil, err - } - sort.Sort(sort.Reverse(creditSlice(unspent))) - - defaultAccountName, err := w.Manager.AccountName(waddrmgr.DefaultAccountNum) - if err != nil { - return nil, err - } - - results := make([]*btcjson.ListUnspentResult, 0, len(unspent)) - for i := range unspent { - output := &unspent[i] - - // Outputs with fewer confirmations than the minimum or more - // confs than the maximum are excluded. - confs := confirms(output.Height, syncBlock.Height) - if confs < minconf || confs > maxconf { - continue + defaultAccountName, err := w.Manager.AccountName(addrmgrNs, + waddrmgr.DefaultAccountNum) + if err != nil { + return err } - // Only mature coinbase outputs are included. - if output.FromCoinBase { - target := int32(w.chainParams.CoinbaseMaturity) - if !confirmed(target, output.Height, syncBlock.Height) { + results = make([]*btcjson.ListUnspentResult, 0, len(unspent)) + for i := range unspent { + output := unspent[i] + + // Outputs with fewer confirmations than the minimum or more + // confs than the maximum are excluded. + confs := confirms(output.Height, syncBlock.Height) + if confs < minconf || confs > maxconf { continue } - } - // Exclude locked outputs from the result set. - if w.LockedOutpoint(output.OutPoint) { - continue - } - - // Lookup the associated account for the output. Use the - // default account name in case there is no associated account - // for some reason, although this should never happen. - // - // This will be unnecessary once transactions and outputs are - // grouped under the associated account in the db. - acctName := defaultAccountName - sc, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams) - if err != nil { - continue - } - if len(addrs) > 0 { - acct, err := w.Manager.AddrAccount(addrs[0]) - if err == nil { - s, err := w.Manager.AccountName(acct) - if err == nil { - acctName = s - } - } - } - - if filter { - for _, addr := range addrs { - _, ok := addresses[addr.EncodeAddress()] - if ok { - goto include - } - } - continue - } - - include: - // At the moment watch-only addresses are not supported, so all - // recorded outputs that are not multisig are "spendable". - // Multisig outputs are only "spendable" if all keys are - // controlled by this wallet. - // - // TODO: Each case will need updates when watch-only addrs - // is added. For P2PK, P2PKH, and P2SH, the address must be - // looked up and not be watching-only. For multisig, all - // pubkeys must belong to the manager with the associated - // private key (currently it only checks whether the pubkey - // exists, since the private key is required at the moment). - var spendable bool - scSwitch: - switch sc { - case txscript.PubKeyHashTy: - spendable = true - case txscript.PubKeyTy: - spendable = true - case txscript.WitnessPubKeyHashTy: - spendable = true - case txscript.ScriptHashTy: - spendable = true - case txscript.MultiSigTy: - for _, a := range addrs { - _, err := w.Manager.Address(a) - if err == nil { + // Only mature coinbase outputs are included. + if output.FromCoinBase { + target := int32(w.ChainParams().CoinbaseMaturity) + if !confirmed(target, output.Height, syncBlock.Height) { continue } - if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { - break scSwitch - } - return nil, err } - spendable = true + + // Exclude locked outputs from the result set. + if w.LockedOutpoint(output.OutPoint) { + continue + } + + // Lookup the associated account for the output. Use the + // default account name in case there is no associated account + // for some reason, although this should never happen. + // + // This will be unnecessary once transactions and outputs are + // grouped under the associated account in the db. + acctName := defaultAccountName + sc, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) + if err != nil { + continue + } + if len(addrs) > 0 { + acct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0]) + if err == nil { + s, err := w.Manager.AccountName(addrmgrNs, acct) + if err == nil { + acctName = s + } + } + } + + if filter { + for _, addr := range addrs { + _, ok := addresses[addr.EncodeAddress()] + if ok { + goto include + } + } + continue + } + + include: + // At the moment watch-only addresses are not supported, so all + // recorded outputs that are not multisig are "spendable". + // Multisig outputs are only "spendable" if all keys are + // controlled by this wallet. + // + // TODO: Each case will need updates when watch-only addrs + // is added. For P2PK, P2PKH, and P2SH, the address must be + // looked up and not be watching-only. For multisig, all + // pubkeys must belong to the manager with the associated + // private key (currently it only checks whether the pubkey + // exists, since the private key is required at the moment). + var spendable bool + scSwitch: + switch sc { + case txscript.PubKeyHashTy: + spendable = true + case txscript.PubKeyTy: + spendable = true + case txscript.ScriptHashTy: + spendable = true + case txscript.MultiSigTy: + for _, a := range addrs { + _, err := w.Manager.Address(addrmgrNs, a) + if err == nil { + continue + } + if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { + break scSwitch + } + return err + } + spendable = true + } + + result := &btcjson.ListUnspentResult{ + TxID: output.OutPoint.Hash.String(), + Vout: output.OutPoint.Index, + Account: acctName, + ScriptPubKey: hex.EncodeToString(output.PkScript), + Amount: output.Amount.ToBTC(), + Confirmations: int64(confs), + Spendable: spendable, + } + + // BUG: this should be a JSON array so that all + // addresses can be included, or removed (and the + // caller extracts addresses from the pkScript). + if len(addrs) > 0 { + result.Address = addrs[0].EncodeAddress() + } + + results = append(results, result) } - - result := &btcjson.ListUnspentResult{ - TxID: output.OutPoint.Hash.String(), - Vout: output.OutPoint.Index, - Account: acctName, - ScriptPubKey: hex.EncodeToString(output.PkScript), - Amount: output.Amount.ToBTC(), - Confirmations: int64(confs), - Spendable: spendable, - } - - // BUG: this should be a JSON array so that all - // addresses can be included, or removed (and the - // caller extracts addresses from the pkScript). - if len(addrs) > 0 { - result.Address = addrs[0].EncodeAddress() - } - - results = append(results, result) - } - - return results, nil + return nil + }) + return results, err } // DumpPrivKeys returns the WIF-encoded private keys for all addresses with // private keys in a wallet. func (w *Wallet) DumpPrivKeys() ([]string, error) { var privkeys []string - // Iterate over each active address, appending the private key to - // privkeys. - err := w.Manager.ForEachActiveAddress(func(addr btcutil.Address) error { - ma, err := w.Manager.Address(addr) - if err != nil { - return err - } + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + // Iterate over each active address, appending the private key to + // privkeys. + return w.Manager.ForEachActiveAddress(addrmgrNs, func(addr btcutil.Address) error { + ma, err := w.Manager.Address(addrmgrNs, addr) + if err != nil { + return err + } - // Only those addresses with keys needed. - pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) - if !ok { + // Only those addresses with keys needed. + pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil + } + + wif, err := pka.ExportPrivKey() + if err != nil { + // It would be nice to zero out the array here. However, + // since strings in go are immutable, and we have no + // control over the caller I don't think we can. :( + return err + } + privkeys = append(privkeys, wif.String()) return nil - } - - wif, err := pka.ExportPrivKey() - if err != nil { - // It would be nice to zero out the array here. However, - // since strings in go are immutable, and we have no - // control over the caller I don't think we can. :( - return err - } - privkeys = append(privkeys, wif.String()) - return nil + }) }) return privkeys, err } @@ -1479,13 +1749,19 @@ func (w *Wallet) DumpPrivKeys() ([]string, error) { // DumpWIFPrivateKey returns the WIF encoded private key for a // single wallet address. func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { - // Get private key from wallet if it exists. - address, err := w.Manager.Address(addr) + var maddr waddrmgr.ManagedAddress + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + // Get private key from wallet if it exists. + var err error + maddr, err = w.Manager.Address(waddrmgrNs, addr) + return err + }) if err != nil { return "", err } - pka, ok := address.(waddrmgr.ManagedPubKeyAddress) + pka, ok := maddr.(waddrmgr.ManagedPubKeyAddress) if !ok { return "", fmt.Errorf("address %s is not a key type", addr) } @@ -1512,7 +1788,18 @@ func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp, } // Attempt to import private key into wallet. - addr, err := w.Manager.ImportPrivateKey(wif, bs) + var addr btcutil.Address + var props *waddrmgr.AccountProperties + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + maddr, err := w.Manager.ImportPrivateKey(addrmgrNs, wif, bs) + if err == nil { + addr = maddr.Address() + props, err = w.Manager.AccountProperties( + addrmgrNs, waddrmgr.ImportedAddrAccount) + } + return err + }) if err != nil { return "", err } @@ -1521,7 +1808,7 @@ func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp, // imported address. if rescan { job := &RescanJob{ - Addrs: []btcutil.Address{addr.Address()}, + Addrs: []btcutil.Address{addr}, OutPoints: nil, BlockStamp: *bs, } @@ -1531,87 +1818,21 @@ func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp, // or failure is logged elsewhere, and the channel is not // required to be read, so discard the return value. _ = w.SubmitRescan(job) - } - - addrStr := addr.Address().EncodeAddress() - log.Infof("Imported payment address %s", addrStr) - - props, err := w.Manager.AccountProperties(waddrmgr.ImportedAddrAccount) - if err != nil { - log.Errorf("Cannot fetch account properties for imported "+ - "account after importing key: %v", err) } else { - w.NtfnServer.notifyAccountProperties(props) - } - - // Return the payment address string of the imported private key. - return addrStr, nil -} - -// ExportWatchingWallet returns a watching-only version of the wallet serialized -// database as a base64-encoded string. -func (w *Wallet) ExportWatchingWallet() (string, error) { - tmpDir, err := ioutil.TempDir("", "btcwallet") - if err != nil { - return "", err - } - defer os.RemoveAll(tmpDir) - - // Create a new file and write a copy of the current database into it. - woDbPath := filepath.Join(tmpDir, walletDbWatchingOnlyName) - fi, err := os.OpenFile(woDbPath, os.O_CREATE|os.O_RDWR, 0600) - if err != nil { - return "", err - } - if err := w.db.Copy(fi); err != nil { - fi.Close() - return "", err - } - fi.Close() - defer os.Remove(woDbPath) - - // Open the new database, get the address manager namespace, and open - // it. - woDb, err := walletdb.Open("bdb", woDbPath) - if err != nil { - _ = os.Remove(woDbPath) - return "", err - } - defer woDb.Close() - - namespace, err := woDb.Namespace(waddrmgrNamespaceKey) - if err != nil { - return "", err - } - woMgr, err := waddrmgr.Open(namespace, w.publicPassphrase, - w.chainParams, nil) - if err != nil { - return "", err - } - defer woMgr.Close() - - // Convert the namespace to watching only if needed. - if err := woMgr.ConvertToWatchingOnly(); err != nil { - // Only return the error is it's not because it's already - // watching-only. When it is already watching-only, the code - // just falls through to the export below. - if !waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly) { - return "", err + err := w.chainClient.NotifyReceived([]btcutil.Address{addr}) + if err != nil { + return "", fmt.Errorf("Failed to subscribe for address ntfns for "+ + "address %s: %s", addr.EncodeAddress(), err) } } - // Export the watching only wallet's serialized data. - return exportBase64DB(woDb) -} + addrStr := addr.EncodeAddress() + log.Infof("Imported payment address %s", addrStr) -// exportBase64DB exports a wallet's serialized database as a base64-encoded -// string. -func exportBase64DB(db walletdb.DB) (string, error) { - var buf bytes.Buffer - if err := db.Copy(&buf); err != nil { - return "", err - } - return base64.StdEncoding.EncodeToString(buf.Bytes()), nil + w.NtfnServer.notifyAccountProperties(props) + + // Return the payment address string of the imported private key. + return addrStr, nil } // LockedOutpoint returns whether an outpoint has been marked as locked and @@ -1655,21 +1876,28 @@ func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput { return locked } -// ResendUnminedTxs iterates through all transactions that spend from wallet +// resendUnminedTxs iterates through all transactions that spend from wallet // credits that are not known to have been mined into a block, and attempts // to send each to the chain server for relay. -func (w *Wallet) ResendUnminedTxs() { +func (w *Wallet) resendUnminedTxs() { chainClient, err := w.requireChainClient() if err != nil { log.Errorf("No chain server available to resend unmined transactions") return } - txs, err := w.TxStore.UnminedTxs() + var txs []*wire.MsgTx + err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + txs, err = w.TxStore.UnminedTxs(txmgrNs) + return err + }) if err != nil { log.Errorf("Cannot load unmined transactions for resending: %v", err) return } + for _, tx := range txs { resp, err := chainClient.SendRawTransaction(tx, false) if err != nil { @@ -1687,9 +1915,12 @@ func (w *Wallet) ResendUnminedTxs() { // addresses in a wallet. func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { var addrStrs []string - err := w.Manager.ForEachActiveAddress(func(addr btcutil.Address) error { - addrStrs = append(addrStrs, addr.EncodeAddress()) - return nil + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.ForEachActiveAddress(addrmgrNs, func(addr btcutil.Address) error { + addrStrs = append(addrStrs, addr.EncodeAddress()) + return nil + }) }) if err != nil { return nil, err @@ -1700,11 +1931,19 @@ func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { } // NewAddress returns the next external chained address for a wallet. -func (w *Wallet) NewAddress(account uint32, - addrType waddrmgr.AddressType) (btcutil.Address, error) { +func (w *Wallet) NewAddress(account uint32) (addr btcutil.Address, err error) { + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + addr, err = w.newAddress(addrmgrNs, account) + return err + }) + return +} +func (w *Wallet) newAddress(addrmgrNs walletdb.ReadWriteBucket, account uint32) (btcutil.Address, error) { // Get next address from wallet. - addrs, err := w.Manager.NextExternalAddresses(account, 1, addrType) + addrs, err := w.Manager.NextExternalAddresses(addrmgrNs, account, 1) if err != nil { return nil, err } @@ -1724,7 +1963,7 @@ func (w *Wallet) NewAddress(account uint32, } } - props, err := w.Manager.AccountProperties(account) + props, err := w.Manager.AccountProperties(addrmgrNs, account) if err != nil { log.Errorf("Cannot fetch account properties for notification "+ "after deriving next external address: %v", err) @@ -1736,11 +1975,19 @@ func (w *Wallet) NewAddress(account uint32, } // NewChangeAddress returns a new change address for a wallet. -func (w *Wallet) NewChangeAddress(account uint32, - addrType waddrmgr.AddressType) (btcutil.Address, error) { +func (w *Wallet) NewChangeAddress(account uint32) (addr btcutil.Address, err error) { + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + var err error + addr, err = w.newChangeAddress(addrmgrNs, account) + return err + }) + return +} +func (w *Wallet) newChangeAddress(addrmgrNs walletdb.ReadWriteBucket, account uint32) (btcutil.Address, error) { // Get next chained change address from wallet for account. - addrs, err := w.Manager.NextInternalAddresses(account, 1, addrType) + addrs, err := w.Manager.NextInternalAddresses(addrmgrNs, account, 1) if err != nil { return nil, err } @@ -1780,84 +2027,120 @@ func confirms(txHeight, curHeight int32) int32 { } } -// TotalReceivedForAccount iterates through a wallet's transaction history, -// returning the total amount of bitcoins received for a single wallet -// account. -func (w *Wallet) TotalReceivedForAccount(account uint32, minConf int32) (btcutil.Amount, int32, error) { - syncBlock := w.Manager.SyncedTo() +// AccountTotalReceivedResult is a single result for the +// Wallet.TotalReceivedForAccounts method. +type AccountTotalReceivedResult struct { + AccountNumber uint32 + AccountName string + TotalReceived btcutil.Amount + LastConfirmation int32 +} - var ( - amount btcutil.Amount - lastConf int32 // Confs of the last matching transaction. - stopHeight int32 - ) +// TotalReceivedForAccounts iterates through a wallet's transaction history, +// returning the total amount of decred received for all accounts. +func (w *Wallet) TotalReceivedForAccounts(minConf int32) ([]AccountTotalReceivedResult, error) { + var results []AccountTotalReceivedResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - if minConf > 0 { - stopHeight = syncBlock.Height - minConf + 1 - } else { - stopHeight = -1 - } - err := w.TxStore.RangeTransactions(0, stopHeight, func(details []wtxmgr.TxDetails) (bool, error) { - for i := range details { - detail := &details[i] - for _, cred := range detail.Credits { - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript - var outputAcct uint32 - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - pkScript, w.chainParams) - if err == nil && len(addrs) > 0 { - outputAcct, err = w.Manager.AddrAccount(addrs[0]) - } - if err == nil && outputAcct == account { - amount += cred.Amount - lastConf = confirms(detail.Block.Height, syncBlock.Height) + syncBlock := w.Manager.SyncedTo() + + err := w.Manager.ForEachAccount(addrmgrNs, func(account uint32) error { + accountName, err := w.Manager.AccountName(addrmgrNs, account) + if err != nil { + return err + } + results = append(results, AccountTotalReceivedResult{ + AccountNumber: account, + AccountName: accountName, + }) + return nil + }) + if err != nil { + return err + } + + var stopHeight int32 + + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if err == nil { + acctIndex := int(outputAcct) + if outputAcct == waddrmgr.ImportedAddrAccount { + acctIndex = len(results) - 1 + } + res := &results[acctIndex] + res.TotalReceived += cred.Amount + res.LastConfirmation = confirms( + detail.Block.Height, syncBlock.Height) + } } } + return false, nil } - return false, nil + return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) }) - - return amount, lastConf, err + return results, err } // TotalReceivedForAddr iterates through a wallet's transaction history, // returning the total amount of bitcoins received for a single wallet // address. func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) { - syncBlock := w.Manager.SyncedTo() + var amount btcutil.Amount + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - var ( - addrStr = addr.EncodeAddress() - amount btcutil.Amount - stopHeight int32 - ) + syncBlock := w.Manager.SyncedTo() - if minConf > 0 { - stopHeight = syncBlock.Height - minConf + 1 - } else { - stopHeight = -1 - } - err := w.TxStore.RangeTransactions(0, stopHeight, func(details []wtxmgr.TxDetails) (bool, error) { - for i := range details { - detail := &details[i] - for _, cred := range detail.Credits { - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - pkScript, w.chainParams) - // An error creating addresses from the output script only - // indicates a non-standard script, so ignore this credit. - if err != nil { - continue - } - for _, a := range addrs { - if addrStr == a.EncodeAddress() { - amount += cred.Amount - break + var ( + addrStr = addr.EncodeAddress() + stopHeight int32 + ) + + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, + w.chainParams) + // An error creating addresses from the output script only + // indicates a non-standard script, so ignore this credit. + if err != nil { + continue + } + for _, a := range addrs { + if addrStr == a.EncodeAddress() { + amount += cred.Amount + break + } } } } + return false, nil } - return false, nil + return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) }) return amount, err } @@ -1893,19 +2176,25 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32, log.Errorf("Cannot create record for created transaction: %v", err) return nil, err } - err = w.TxStore.InsertTx(rec, nil) - if err != nil { - log.Errorf("Error adding sent tx history: %v", err) - return nil, err - } - - if createdTx.ChangeIndex >= 0 { - err = w.TxStore.AddCredit(rec, nil, uint32(createdTx.ChangeIndex), true) + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + err := w.TxStore.InsertTx(txmgrNs, rec, nil) if err != nil { - log.Errorf("Error adding change address for sent "+ - "tx: %v", err) - return nil, err + return err } + + if createdTx.ChangeIndex >= 0 { + err = w.TxStore.AddCredit(txmgrNs, rec, nil, uint32(createdTx.ChangeIndex), true) + if err != nil { + log.Errorf("Error adding change address for sent "+ + "tx: %v", err) + return err + } + } + return nil + }) + if err != nil { + return nil, err } // TODO: The record already has the serialized tx, so no need to @@ -1935,119 +2224,119 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, p2shRedeemScriptsByAddress map[string][]byte) ([]SignatureError, error) { var signErrors []SignatureError - for i, txIn := range tx.TxIn { - prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] - if !ok { - prevHash := &txIn.PreviousOutPoint.Hash - prevIndex := txIn.PreviousOutPoint.Index - txDetails, err := w.TxStore.TxDetails(prevHash) - if err != nil { - return nil, fmt.Errorf("Cannot query previous transaction "+ - "details for %v: %v", txIn.PreviousOutPoint, err) - } - if txDetails == nil { - return nil, fmt.Errorf("%v not found", - txIn.PreviousOutPoint) - } - prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript - } + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) - // Set up our callbacks that we pass to txscript so it can - // look up the appropriate keys and scripts by address. - getKey := txscript.KeyClosure(func(addr btcutil.Address) ( - *btcec.PrivateKey, bool, error) { - if len(additionalKeysByAddress) != 0 { - addrStr := addr.EncodeAddress() - wif, ok := additionalKeysByAddress[addrStr] - if !ok { - return nil, false, - errors.New("no key for address") - } - return wif.PrivKey, wif.CompressPubKey, nil - } - address, err := w.Manager.Address(addr) - if err != nil { - return nil, false, err - } - - pka, ok := address.(waddrmgr.ManagedPubKeyAddress) + for i, txIn := range tx.TxIn { + prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] if !ok { - return nil, false, errors.New("address is not " + - "a pubkey address") - } - - key, err := pka.PrivKey() - if err != nil { - return nil, false, err - } - - return key, pka.Compressed(), nil - }) - getScript := txscript.ScriptClosure(func( - addr btcutil.Address) ([]byte, error) { - // If keys were provided then we can only use the - // redeem scripts provided with our inputs, too. - if len(additionalKeysByAddress) != 0 { - addrStr := addr.EncodeAddress() - script, ok := p2shRedeemScriptsByAddress[addrStr] - if !ok { - return nil, errors.New("no script for " + - "address") + prevHash := &txIn.PreviousOutPoint.Hash + prevIndex := txIn.PreviousOutPoint.Index + txDetails, err := w.TxStore.TxDetails(txmgrNs, prevHash) + if err != nil { + return fmt.Errorf("cannot query previous transaction "+ + "details for %v: %v", txIn.PreviousOutPoint, err) } - return script, nil - } - address, err := w.Manager.Address(addr) - if err != nil { - return nil, err - } - sa, ok := address.(waddrmgr.ManagedScriptAddress) - if !ok { - return nil, errors.New("address is not a script" + - " address") + if txDetails == nil { + return fmt.Errorf("%v not found", + txIn.PreviousOutPoint) + } + prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript } - return sa.Script() - }) + // Set up our callbacks that we pass to txscript so it can + // look up the appropriate keys and scripts by address. + getKey := txscript.KeyClosure(func(addr btcutil.Address) (*btcec.PrivateKey, bool, error) { + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + wif, ok := additionalKeysByAddress[addrStr] + if !ok { + return nil, false, + errors.New("no key for address") + } + return wif.PrivKey, wif.CompressPubKey, nil + } + address, err := w.Manager.Address(addrmgrNs, addr) + if err != nil { + return nil, false, err + } - // SigHashSingle inputs can only be signed if there's a - // corresponding output. However this could be already signed, - // so we always verify the output. - if (hashType&txscript.SigHashSingle) != - txscript.SigHashSingle || i < len(tx.TxOut) { + pka, ok := address.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil, false, fmt.Errorf("address %v is not "+ + "a pubkey address", address.Address().EncodeAddress()) + } - // TODO(roasbeef): make aware of witness, and nested p2sh + key, err := pka.PrivKey() + if err != nil { + return nil, false, err + } - script, err := txscript.SignTxOutput(w.ChainParams(), - tx, i, prevOutScript, hashType, getKey, - getScript, txIn.SignatureScript) - // Failure to sign isn't an error, it just means that - // the tx isn't complete. + return key, pka.Compressed(), nil + }) + getScript := txscript.ScriptClosure(func(addr btcutil.Address) ([]byte, error) { + // If keys were provided then we can only use the + // redeem scripts provided with our inputs, too. + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + script, ok := p2shRedeemScriptsByAddress[addrStr] + if !ok { + return nil, errors.New("no script for address") + } + return script, nil + } + address, err := w.Manager.Address(addrmgrNs, addr) + if err != nil { + return nil, err + } + sa, ok := address.(waddrmgr.ManagedScriptAddress) + if !ok { + return nil, errors.New("address is not a script" + + " address") + } + + return sa.Script() + }) + + // SigHashSingle inputs can only be signed if there's a + // corresponding output. However this could be already signed, + // so we always verify the output. + if (hashType&txscript.SigHashSingle) != + txscript.SigHashSingle || i < len(tx.TxOut) { + + script, err := txscript.SignTxOutput(w.ChainParams(), + tx, i, prevOutScript, hashType, getKey, + getScript, txIn.SignatureScript) + // Failure to sign isn't an error, it just means that + // the tx isn't complete. + if err != nil { + signErrors = append(signErrors, SignatureError{ + InputIndex: uint32(i), + Error: err, + }) + continue + } + txIn.SignatureScript = script + } + + // Either it was already signed or we just signed it. + // Find out if it is completely satisfied or still needs more. + vm, err := txscript.NewEngine(prevOutScript, tx, i, + txscript.StandardVerifyFlags, nil) + if err == nil { + err = vm.Execute() + } if err != nil { signErrors = append(signErrors, SignatureError{ InputIndex: uint32(i), Error: err, }) - continue } - txIn.SignatureScript = script } - - // Either it was already signed or we just signed it. - // Find out if it is completely satisfied or still needs more. - vm, err := txscript.NewEngine(prevOutScript, tx, i, - txscript.StandardVerifyFlags, nil, nil, 0) - if err == nil { - err = vm.Execute() - } - if err != nil { - signErrors = append(signErrors, SignatureError{ - InputIndex: uint32(i), - Error: err, - }) - } - } - - return signErrors, nil + return nil + }) + return signErrors, err } // PublishTransaction sends the transaction to the consensus RPC server so it @@ -2098,55 +2387,74 @@ func Create(db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Par return hdkeychain.ErrInvalidSeedLen } - // Create the address manager. - addrMgrNamespace, err := db.Namespace(waddrmgrNamespaceKey) - if err != nil { - return err - } - err = waddrmgr.Create(addrMgrNamespace, seed, pubPass, privPass, - params, nil) - if err != nil { - return err - } + return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + txmgrNs, err := tx.CreateTopLevelBucket(wtxmgrNamespaceKey) + if err != nil { + return err + } - // Create empty transaction manager. - txMgrNamespace, err := db.Namespace(wtxmgrNamespaceKey) - if err != nil { - return err - } - return wtxmgr.Create(txMgrNamespace) + err = waddrmgr.Create(addrmgrNs, seed, pubPass, privPass, + params, nil) + if err != nil { + return err + } + return wtxmgr.Create(txmgrNs) + }) } // Open loads an already-created wallet from the passed database and namespaces. func Open(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, params *chaincfg.Params) (*Wallet, error) { - addrMgrNS, err := db.Namespace(waddrmgrNamespaceKey) - if err != nil { - return nil, err - } - txMgrNS, err := db.Namespace(wtxmgrNamespaceKey) - if err != nil { - return nil, err - } - addrMgr, err := waddrmgr.Open(addrMgrNS, pubPass, params, cbs) - if err != nil { - return nil, err - } - noTxMgr, err := walletdb.NamespaceIsEmpty(txMgrNS) - if err != nil { - return nil, err - } - if noTxMgr { - log.Info("No recorded transaction history -- needs full rescan") - err = addrMgr.SetSyncedTo(nil) - if err != nil { - return nil, err + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + waddrmgrBucket := tx.ReadBucket(waddrmgrNamespaceKey) + if waddrmgrBucket == nil { + return errors.New("missing address manager namespace") } - err = wtxmgr.Create(txMgrNS) - if err != nil { - return nil, err + wtxmgrBucket := tx.ReadBucket(wtxmgrNamespaceKey) + if wtxmgrBucket == nil { + return errors.New("missing transaction manager namespace") } + return nil + }) + if err != nil { + return nil, err } - txMgr, err := wtxmgr.Open(txMgrNS, params) + + // Perform upgrades as necessary. Each upgrade is done under its own + // transaction, which is managed by each package itself, so the entire + // DB is passed instead of passing already opened write transaction. + // + // This will need to change later when upgrades in one package depend on + // data in another (such as removing chain synchronization from address + // manager). + err = waddrmgr.DoUpgrades(db, waddrmgrNamespaceKey, pubPass, params, cbs) + if err != nil { + return nil, err + } + err = wtxmgr.DoUpgrades(db, wtxmgrNamespaceKey) + if err != nil { + return nil, err + } + + // Open database abstraction instances + var ( + addrMgr *waddrmgr.Manager + txMgr *wtxmgr.Store + ) + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + addrMgr, err = waddrmgr.Open(addrmgrNs, pubPass, params) + if err != nil { + return err + } + txMgr, err = wtxmgr.Open(txmgrNs, params) + return err + }) if err != nil { return nil, err } @@ -2167,7 +2475,7 @@ func Open(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, params *c createTxRequests: make(chan createTxRequest), unlockRequests: make(chan unlockRequest), lockRequests: make(chan struct{}), - holdUnlockRequests: make(chan chan HeldUnlock), + holdUnlockRequests: make(chan chan heldUnlock), lockState: make(chan bool), changePassphrase: make(chan changePassphraseRequest), chainParams: params, diff --git a/walletdb/bdb/db.go b/walletdb/bdb/db.go index 3cf6c0f..e1e7aee 100644 --- a/walletdb/bdb/db.go +++ b/walletdb/bdb/db.go @@ -48,34 +48,88 @@ func convertErr(err error) error { return err } -// bucket is an internal type used to represent a collection of key/value pairs -// and implements the walletdb.Bucket interface. -type bucket bolt.Bucket +// transaction represents a database transaction. It can either by read-only or +// read-write and implements the walletdb Tx interfaces. The transaction +// provides a root bucket against which all read and writes occur. +type transaction struct { + boltTx *bolt.Tx +} -// Enforce bucket implements the walletdb.Bucket interface. -var _ walletdb.Bucket = (*bucket)(nil) +func (tx *transaction) ReadBucket(key []byte) walletdb.ReadBucket { + return tx.ReadWriteBucket(key) +} -// Bucket retrieves a nested bucket with the given key. Returns nil if -// the bucket does not exist. -// -// This function is part of the walletdb.Bucket interface implementation. -func (b *bucket) Bucket(key []byte) walletdb.Bucket { - // This nil check is intentional so the return value can be checked - // against nil directly. - boltBucket := (*bolt.Bucket)(b).Bucket(key) +func (tx *transaction) ReadWriteBucket(key []byte) walletdb.ReadWriteBucket { + boltBucket := tx.boltTx.Bucket(key) if boltBucket == nil { return nil } return (*bucket)(boltBucket) } +func (tx *transaction) CreateTopLevelBucket(key []byte) (walletdb.ReadWriteBucket, error) { + boltBucket, err := tx.boltTx.CreateBucket(key) + if err != nil { + return nil, convertErr(err) + } + return (*bucket)(boltBucket), nil +} + +func (tx *transaction) DeleteTopLevelBucket(key []byte) error { + err := tx.boltTx.DeleteBucket(key) + if err != nil { + return convertErr(err) + } + return nil +} + +// 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. +func (tx *transaction) Commit() error { + return convertErr(tx.boltTx.Commit()) +} + +// 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. +func (tx *transaction) Rollback() error { + return convertErr(tx.boltTx.Rollback()) +} + +// bucket is an internal type used to represent a collection of key/value pairs +// and implements the walletdb Bucket interfaces. +type bucket bolt.Bucket + +// Enforce bucket implements the walletdb Bucket interfaces. +var _ walletdb.ReadWriteBucket = (*bucket)(nil) + +// NestedReadWriteBucket retrieves a nested bucket with the given key. Returns +// nil if the bucket does not exist. +// +// This function is part of the walletdb.ReadWriteBucket interface implementation. +func (b *bucket) NestedReadWriteBucket(key []byte) walletdb.ReadWriteBucket { + boltBucket := (*bolt.Bucket)(b).Bucket(key) + // Don't return a non-nil interface to a nil pointer. + if boltBucket == nil { + return nil + } + return (*bucket)(boltBucket) +} + +func (b *bucket) NestedReadBucket(key []byte) walletdb.ReadBucket { + return b.NestedReadWriteBucket(key) +} + // CreateBucket creates and returns a new nested bucket with the given key. // Returns ErrBucketExists if the bucket already exists, 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. -func (b *bucket) CreateBucket(key []byte) (walletdb.Bucket, error) { +func (b *bucket) CreateBucket(key []byte) (walletdb.ReadWriteBucket, error) { boltBucket, err := (*bolt.Bucket)(b).CreateBucket(key) if err != nil { return nil, convertErr(err) @@ -88,7 +142,7 @@ func (b *bucket) CreateBucket(key []byte) (walletdb.Bucket, error) { // key is empty or ErrIncompatibleValue if the key value is otherwise invalid. // // This function is part of the walletdb.Bucket interface implementation. -func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.Bucket, error) { +func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.ReadWriteBucket, error) { boltBucket, err := (*bolt.Bucket)(b).CreateBucketIfNotExists(key) if err != nil { return nil, convertErr(err) @@ -96,12 +150,12 @@ func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.Bucket, error) { return (*bucket)(boltBucket), nil } -// DeleteBucket removes a nested bucket with the given key. Returns +// DeleteNestedBucket removes a nested bucket with the given key. Returns // 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. -func (b *bucket) DeleteBucket(key []byte) error { +func (b *bucket) DeleteNestedBucket(key []byte) error { return convertErr((*bolt.Bucket)(b).DeleteBucket(key)) } @@ -118,13 +172,6 @@ func (b *bucket) ForEach(fn func(k, v []byte) error) error { return convertErr((*bolt.Bucket)(b).ForEach(fn)) } -// Writable returns whether or not the bucket is writable. -// -// This function is part of the walletdb.Bucket interface implementation. -func (b *bucket) Writable() bool { - return (*bolt.Bucket)(b).Writable() -} - // Put saves the specified key/value pair to the bucket. Keys that do not // already exist are added and keys that already exist are overwritten. Returns // ErrTxNotWritable if attempted against a read-only transaction. @@ -155,11 +202,15 @@ func (b *bucket) Delete(key []byte) error { return convertErr((*bolt.Bucket)(b).Delete(key)) } -// Cursor returns a new cursor, allowing for iteration over the bucket's +func (b *bucket) ReadCursor() walletdb.ReadCursor { + return b.ReadWriteCursor() +} + +// 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. -func (b *bucket) Cursor() walletdb.Cursor { +func (b *bucket) ReadWriteCursor() walletdb.ReadWriteCursor { return (*cursor)((*bolt.Bucket)(b).Cursor()) } @@ -172,13 +223,6 @@ func (b *bucket) Cursor() walletdb.Cursor { // and values returned may be unpredictable. type cursor bolt.Cursor -// Bucket returns the bucket the cursor was created for. -// -// This function is part of the walletdb.Cursor interface implementation. -func (c *cursor) Bucket() walletdb.Bucket { - return (*bucket)((*bolt.Cursor)(c).Bucket()) -} - // Delete removes the current key/value pair the cursor is at without // invalidating the cursor. Returns ErrTxNotWritable if attempted on a read-only // transaction, or ErrIncompatibleValue if attempted when the cursor points to a @@ -225,118 +269,6 @@ func (c *cursor) Seek(seek []byte) (key, value []byte) { return (*bolt.Cursor)(c).Seek(seek) } -// transaction represents a database transaction. It can either by read-only or -// read-write and implements the walletdb.Bucket interface. The transaction -// provides a root bucket against which all read and writes occur. -type transaction struct { - boltTx *bolt.Tx - rootBucket *bolt.Bucket -} - -// Enforce transaction implements the walletdb.Tx interface. -var _ walletdb.Tx = (*transaction)(nil) - -// RootBucket returns the top-most bucket for the namespace the transaction was -// created from. -// -// This function is part of the walletdb.Tx interface implementation. -func (tx *transaction) RootBucket() walletdb.Bucket { - return (*bucket)(tx.rootBucket) -} - -// 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. -func (tx *transaction) Commit() error { - return convertErr(tx.boltTx.Commit()) -} - -// 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. -func (tx *transaction) Rollback() error { - return convertErr(tx.boltTx.Rollback()) -} - -// namespace represents a database namespace that is inteded to support the -// concept of a single entity that controls the opening, creating, and closing -// of a database while providing other entities their own namespace to work in. -// It implements the walletdb.Namespace interface. -type namespace struct { - db *bolt.DB - key []byte -} - -// Enforce namespace implements the walletdb.Namespace interface. -var _ walletdb.Namespace = (*namespace)(nil) - -// Begin starts a transaction which is either read-only or read-write depending -// on the specified flag. Multiple read-only transactions can be started -// simultaneously while only a single read-write transaction can be started at a -// time. The call will block when starting a read-write transaction when one is -// already open. -// -// NOTE: The transaction must be closed by calling Rollback or Commit on it when -// it is no longer needed. Failure to do so will result in unclaimed memory. -// -// This function is part of the walletdb.Namespace interface implementation. -func (ns *namespace) Begin(writable bool) (walletdb.Tx, error) { - boltTx, err := ns.db.Begin(writable) - if err != nil { - return nil, convertErr(err) - } - - bucket := boltTx.Bucket(ns.key) - if bucket == nil { - boltTx.Rollback() - return nil, walletdb.ErrBucketNotFound - } - - return &transaction{boltTx: boltTx, rootBucket: bucket}, nil -} - -// View invokes the passed function in the context of a managed read-only -// transaction. Any errors returned from the user-supplied function are -// returned from this function. -// -// Calling Rollback on the transaction passed to the user-supplied function will -// result in a panic. -// -// This function is part of the walletdb.Namespace interface implementation. -func (ns *namespace) View(fn func(walletdb.Tx) error) error { - return convertErr(ns.db.View(func(boltTx *bolt.Tx) error { - bucket := boltTx.Bucket(ns.key) - if bucket == nil { - return walletdb.ErrBucketNotFound - } - - return fn(&transaction{boltTx: boltTx, rootBucket: bucket}) - })) -} - -// Update invokes the passed function in the context of a managed read-write -// transaction. Any errors returned from the user-supplied function will cause -// the transaction to be rolled back and are returned from this function. -// Otherwise, the transaction is commited when the user-supplied function -// returns a nil error. -// -// Calling Rollback on the transaction passed to the user-supplied function will -// result in a panic. -// -// This function is part of the walletdb.Namespace interface implementation. -func (ns *namespace) Update(fn func(walletdb.Tx) error) error { - return convertErr(ns.db.Update(func(boltTx *bolt.Tx) error { - bucket := boltTx.Bucket(ns.key) - if bucket == nil { - return walletdb.ErrBucketNotFound - } - - return fn(&transaction{boltTx: boltTx, rootBucket: bucket}) - })) -} - // db represents a collection of namespaces which are persisted and implements // the walletdb.Db interface. All database access is performed through // transactions which are obtained through the specific Namespace. @@ -345,51 +277,20 @@ type db bolt.DB // Enforce db implements the walletdb.Db interface. var _ walletdb.DB = (*db)(nil) -// Namespace returns a Namespace interface for the provided key. See the -// Namespace interface documentation for more details. Attempting to access a -// Namespace on a database that is not open yet or has been closed will result -// in ErrDbNotOpen. Namespaces are created in the database on first access. -// -// This function is part of the walletdb.Db interface implementation. -func (db *db) Namespace(key []byte) (walletdb.Namespace, error) { - // Check if the namespace needs to be created using a read-only - // transaction. This is done because read-only transactions are faster - // and don't block like write transactions. - var doCreate bool - err := (*bolt.DB)(db).View(func(tx *bolt.Tx) error { - boltBucket := tx.Bucket(key) - if boltBucket == nil { - doCreate = true - } - return nil - }) +func (db *db) beginTx(writable bool) (*transaction, error) { + boltTx, err := (*bolt.DB)(db).Begin(writable) if err != nil { return nil, convertErr(err) } - - // Create the namespace if needed by using an writable update - // transaction. - if doCreate { - err := (*bolt.DB)(db).Update(func(tx *bolt.Tx) error { - _, err := tx.CreateBucket(key) - return err - }) - if err != nil { - return nil, convertErr(err) - } - } - - return &namespace{db: (*bolt.DB)(db), key: key}, nil + return &transaction{boltTx: boltTx}, nil } -// DeleteNamespace deletes the namespace for the passed key. ErrBucketNotFound -// will be returned if the namespace does not exist. -// -// This function is part of the walletdb.Db interface implementation. -func (db *db) DeleteNamespace(key []byte) error { - return convertErr((*bolt.DB)(db).Update(func(tx *bolt.Tx) error { - return tx.DeleteBucket(key) - })) +func (db *db) BeginReadTx() (walletdb.ReadTx, error) { + return db.beginTx(false) +} + +func (db *db) BeginReadWriteTx() (walletdb.ReadWriteTx, error) { + return db.beginTx(true) } // Copy writes a copy of the database to the provided writer. This call will diff --git a/walletdb/bdb/interface_test.go b/walletdb/bdb/interface_test.go index 7c6f6c0..d86dcdf 100644 --- a/walletdb/bdb/interface_test.go +++ b/walletdb/bdb/interface_test.go @@ -45,7 +45,7 @@ func rollbackValues(values map[string]string) map[string]string { // testGetValues checks that all of the provided key/value pairs can be // retrieved from the database and the retrieved values match the provided // values. -func testGetValues(tc *testContext, bucket walletdb.Bucket, values map[string]string) bool { +func testGetValues(tc *testContext, bucket walletdb.ReadWriteBucket, values map[string]string) bool { for k, v := range values { var vBytes []byte if v != "" { @@ -65,7 +65,7 @@ func testGetValues(tc *testContext, bucket walletdb.Bucket, values map[string]st // testPutValues stores all of the provided key/value pairs in the provided // bucket while checking for errors. -func testPutValues(tc *testContext, bucket walletdb.Bucket, values map[string]string) bool { +func testPutValues(tc *testContext, bucket walletdb.ReadWriteBucket, values map[string]string) bool { for k, v := range values { var vBytes []byte if v != "" { @@ -82,7 +82,7 @@ func testPutValues(tc *testContext, bucket walletdb.Bucket, values map[string]st // testDeleteValues removes all of the provided key/value pairs from the // provided bucket. -func testDeleteValues(tc *testContext, bucket walletdb.Bucket, values map[string]string) bool { +func testDeleteValues(tc *testContext, bucket walletdb.ReadWriteBucket, values map[string]string) bool { for k := range values { if err := bucket.Delete([]byte(k)); err != nil { tc.t.Errorf("Delete: unexpected error: %v", err) @@ -95,7 +95,7 @@ func testDeleteValues(tc *testContext, bucket walletdb.Bucket, values map[string // testNestedBucket reruns the testBucketInterface against a nested bucket along // with a counter to only test a couple of level deep. -func testNestedBucket(tc *testContext, testBucket walletdb.Bucket) bool { +func testNestedBucket(tc *testContext, testBucket walletdb.ReadWriteBucket) bool { // Don't go more than 2 nested level deep. if tc.bucketDepth > 1 { return true @@ -114,7 +114,7 @@ func testNestedBucket(tc *testContext, testBucket walletdb.Bucket) bool { // testBucketInterface ensures the bucket interface is working properly by // exercising all of its functions. -func testBucketInterface(tc *testContext, bucket walletdb.Bucket) bool { +func testBucketInterface(tc *testContext, bucket walletdb.ReadWriteBucket) bool { if bucket.Writable() != tc.isWritable { tc.t.Errorf("Bucket writable state does not match.") return false @@ -210,7 +210,7 @@ func testBucketInterface(tc *testContext, bucket walletdb.Bucket) bool { } // Ensure retrieving and existing bucket works as expected. - testBucket = bucket.Bucket(testBucketName) + testBucket = bucket.ReadWriteBucket(testBucketName) if !testNestedBucket(tc, testBucket) { return false } @@ -220,7 +220,7 @@ func testBucketInterface(tc *testContext, bucket walletdb.Bucket) bool { tc.t.Errorf("DeleteBucket: unexpected error: %v", err) return false } - if b := bucket.Bucket(testBucketName); b != nil { + if b := bucket.ReadWriteBucket(testBucketName); b != nil { tc.t.Errorf("DeleteBucket: bucket '%s' still exists", testBucketName) return false @@ -253,7 +253,7 @@ func testBucketInterface(tc *testContext, bucket walletdb.Bucket) bool { tc.t.Errorf("DeleteBucket: unexpected error: %v", err) return false } - if b := bucket.Bucket(testBucketName); b != nil { + if b := bucket.ReadWriteBucket(testBucketName); b != nil { tc.t.Errorf("DeleteBucket: bucket '%s' still exists", testBucketName) return false diff --git a/walletdb/interface.go b/walletdb/interface.go index 079ba0a..754e08b 100644 --- a/walletdb/interface.go +++ b/walletdb/interface.go @@ -9,31 +9,47 @@ package walletdb import "io" -// Bucket represents a collection of key/value pairs. -type Bucket interface { - // Bucket retrieves a nested bucket with the given key. Returns nil if - // the bucket does not exist. - Bucket(key []byte) Bucket +// ReadTx represents a database transaction that can only be used for reads. If +// a database update must occur, use a ReadWriteTx. +type ReadTx interface { + // ReadBucket opens the root bucket for read only access. If the bucket + // described by the key does not exist, nil is returned. + ReadBucket(key []byte) ReadBucket - // CreateBucket creates and returns a new nested bucket with the given - // key. Returns ErrBucketExists if the bucket already exists, - // ErrBucketNameRequired if the key is empty, or ErrIncompatibleValue - // if the key value is otherwise invalid for the particular database - // implementation. Other errors are possible depending on the - // implementation. - CreateBucket(key []byte) (Bucket, error) + // Rollback closes the transaction, discarding changes (if any) if the + // database was modified by a write transaction. + Rollback() error +} - // CreateBucketIfNotExists creates and returns a new nested bucket with - // the given key if it does not already exist. Returns - // ErrBucketNameRequired if the key is empty or ErrIncompatibleValue - // if the key value is otherwise invalid for the particular database - // backend. Other errors are possible depending on the implementation. - CreateBucketIfNotExists(key []byte) (Bucket, error) +// ReadWriteTx represents a database transaction that can be used for both reads +// and writes. When only reads are necessary, consider using a ReadTx instead. +type ReadWriteTx interface { + ReadTx - // DeleteBucket removes a nested bucket with the given key. Returns - // ErrTxNotWritable if attempted against a read-only transaction and - // ErrBucketNotFound if the specified bucket does not exist. - DeleteBucket(key []byte) error + // ReadWriteBucket opens the root bucket for read/write access. If the + // bucket described by the key does not exist, nil is returned. + ReadWriteBucket(key []byte) ReadWriteBucket + + // CreateTopLevelBucket creates the top level bucket for a key if it + // does not exist. The newly-created bucket it returned. + CreateTopLevelBucket(key []byte) (ReadWriteBucket, error) + + // DeleteTopLevelBucket deletes the top level bucket for a key. This + // errors if the bucket can not be found or the key keys a single value + // instead of a bucket. + DeleteTopLevelBucket(key []byte) error + + // Commit commits all changes that have been on the transaction's root + // buckets and all of their sub-buckets to persistent storage. + Commit() error +} + +// ReadBucket represents a bucket (a hierarchical structure within the database) +// that is only allowed to perform read operations. +type ReadBucket interface { + // NestedReadBucket retrieves a nested bucket with the given key. + // Returns nil if the bucket does not exist. + NestedReadBucket(key []byte) ReadBucket // ForEach invokes the passed function with every key/value pair in // the bucket. This includes nested buckets, in which case the value @@ -47,15 +63,6 @@ type Bucket interface { // implementations. ForEach(func(k, v []byte) error) error - // Writable returns whether or not the bucket is writable. - Writable() bool - - // Put saves the specified key/value pair to the bucket. Keys that do - // not already exist are added and keys that already exist are - // overwritten. Returns ErrTxNotWritable if attempted against a - // read-only transaction. - Put(key, value []byte) error - // Get returns the value for the given key. Returns nil if the key does // not exist in this bucket (or nested buckets). // @@ -66,6 +73,44 @@ type Bucket interface { // implementations. Get(key []byte) []byte + ReadCursor() ReadCursor +} + +// ReadWriteBucket represents a bucket (a hierarchical structure within the +// database) that is allowed to perform both read and write operations. +type ReadWriteBucket interface { + ReadBucket + + // NestedReadWriteBucket retrieves a nested bucket with the given key. + // Returns nil if the bucket does not exist. + NestedReadWriteBucket(key []byte) ReadWriteBucket + + // CreateBucket creates and returns a new nested bucket with the given + // key. Returns ErrBucketExists if the bucket already exists, + // ErrBucketNameRequired if the key is empty, or ErrIncompatibleValue + // if the key value is otherwise invalid for the particular database + // implementation. Other errors are possible depending on the + // implementation. + CreateBucket(key []byte) (ReadWriteBucket, error) + + // CreateBucketIfNotExists creates and returns a new nested bucket with + // the given key if it does not already exist. Returns + // ErrBucketNameRequired if the key is empty or ErrIncompatibleValue + // if the key value is otherwise invalid for the particular database + // backend. Other errors are possible depending on the implementation. + CreateBucketIfNotExists(key []byte) (ReadWriteBucket, error) + + // DeleteNestedBucket removes a nested bucket with the given key. + // Returns ErrTxNotWritable if attempted against a read-only transaction + // and ErrBucketNotFound if the specified bucket does not exist. + DeleteNestedBucket(key []byte) error + + // Put saves the specified key/value pair to the bucket. Keys that do + // not already exist are added and keys that already exist are + // overwritten. Returns ErrTxNotWritable if attempted against a + // read-only transaction. + Put(key, value []byte) error + // Delete removes the specified key from the bucket. Deleting a key // that does not exist does not return an error. Returns // ErrTxNotWritable if attempted against a read-only transaction. @@ -73,26 +118,13 @@ type Bucket interface { // Cursor returns a new cursor, allowing for iteration over the bucket's // key/value pairs and nested buckets in forward or backward order. - Cursor() Cursor + ReadWriteCursor() ReadWriteCursor } -// Cursor represents a cursor over key/value pairs and nested buckets of a -// bucket. -// -// Note that open cursors are not tracked on bucket changes and any -// modifications to the bucket, with the exception of Cursor.Delete, invalidate -// the cursor. After invalidation, the cursor must be repositioned, or the keys -// and values returned may be unpredictable. -type Cursor interface { - // Bucket returns the bucket the cursor was created for. - Bucket() Bucket - - // Delete removes the current key/value pair the cursor is at without - // invalidating the cursor. Returns ErrTxNotWritable if attempted on a - // read-only transaction, or ErrIncompatibleValue if attempted when the - // cursor points to a nested bucket. - Delete() error - +// ReadCursor represents a bucket cursor that can be positioned at the start or +// end of the bucket's key/value pairs and iterate over pairs in the bucket. +// This type is only allowed to perform database read operations. +type ReadCursor interface { // First positions the cursor at the first key/value pair and returns // the pair. First() (key, value []byte) @@ -115,88 +147,34 @@ type Cursor interface { Seek(seek []byte) (key, value []byte) } -// Tx represents a database transaction. It can either by read-only or -// read-write. The transaction provides a root bucket against which all read -// and writes occur. -// -// As would be expected with a transaction, no changes will be saved to the -// database until it has been committed. The transaction will only provide a -// view of the database at the time it was created. Transactions should not be -// long running operations. -type Tx interface { - // RootBucket returns the top-most bucket for the namespace the - // transaction was created from. - RootBucket() Bucket +// ReadWriteCursor represents a bucket cursor that can be positioned at the +// start or end of the bucket's key/value pairs and iterate over pairs in the +// bucket. This abstraction is allowed to perform both database read and write +// operations. +type ReadWriteCursor interface { + ReadCursor - // Commit commits all changes that have been made through the root - // bucket and all of its sub-buckets to persistent storage. - Commit() error - - // Rollback undoes all changes that have been made to the root bucket - // and all of its sub-buckets. - Rollback() error + // Delete removes the current key/value pair the cursor is at without + // invalidating the cursor. Returns ErrIncompatibleValue if attempted + // when the cursor points to a nested bucket. + Delete() error } -// Namespace represents a database namespace that is inteded to support the -// concept of a single entity that controls the opening, creating, and closing -// of a database while providing other entities their own namespace to work in. -type Namespace interface { - // Begin starts a transaction which is either read-only or read-write - // depending on the specified flag. Multiple read-only transactions - // can be started simultaneously while only a single read-write - // transaction can be started at a time. The call will block when - // starting a read-write transaction when one is already open. - // - // NOTE: The transaction must be closed by calling Rollback or Commit on - // it when it is no longer needed. Failure to do so can result in - // unclaimed memory depending on the specific database implementation. - Begin(writable bool) (Tx, error) - - // View invokes the passed function in the context of a managed - // read-only transaction. Any errors returned from the user-supplied - // function are returned from this function. - // - // Calling Rollback on the transaction passed to the user-supplied - // function will result in a panic. - View(fn func(Tx) error) error - - // Update invokes the passed function in the context of a managed - // read-write transaction. Any errors returned from the user-supplied - // function will cause the transaction to be rolled back and are - // returned from this function. Otherwise, the transaction is commited - // when the user-supplied function returns a nil error. - // - // Calling Rollback on the transaction passed to the user-supplied - // function will result in a panic. - Update(fn func(Tx) error) error +// BucketIsEmpty returns whether the bucket is empty, that is, whether there are +// no key/value pairs or nested buckets. +func BucketIsEmpty(bucket ReadBucket) bool { + k, v := bucket.ReadCursor().First() + return k == nil && v == nil } -// NamespaceIsEmpty returns whether the namespace is empty, that is, whether there -// are no key/value pairs or nested buckets. -func NamespaceIsEmpty(namespace Namespace) (bool, error) { - var empty bool - err := namespace.View(func(tx Tx) error { - k, v := tx.RootBucket().Cursor().First() - empty = k == nil && v == nil - return nil - }) - return empty, err -} - -// DB represents a collection of namespaces which are persisted. All database -// access is performed through transactions which are obtained through the -// specific Namespace. +// DB represents an ACID database. All database access is performed through +// read or read+write transactions. type DB interface { - // Namespace returns a Namespace interface for the provided key. See - // the Namespace interface documentation for more details. Attempting - // to access a Namespace on a database that is not open yet or has been - // closed will result in ErrDbNotOpen. Namespaces are created in the - // database on first access. - Namespace(key []byte) (Namespace, error) + // BeginReadTx opens a database read transaction. + BeginReadTx() (ReadTx, error) - // DeleteNamespace deletes the namespace for the passed key. - // ErrBucketNotFound will be returned if the namespace does not exist. - DeleteNamespace(key []byte) error + // BeginReadWriteTx opens a database read+write transaction. + BeginReadWriteTx() (ReadWriteTx, error) // Copy writes a copy of the database to the provided writer. This // call will start a read-only transaction to perform all operations. @@ -206,6 +184,47 @@ type DB interface { Close() error } +// View opens a database read transaction and executes the function f with the +// transaction passed as a parameter. After f exits, the transaction is rolled +// back. If f errors, its error is returned, not a rollback error (if any +// occur). +func View(db DB, f func(tx ReadTx) error) error { + tx, err := db.BeginReadTx() + if err != nil { + return err + } + err = f(tx) + rollbackErr := tx.Rollback() + if err != nil { + return err + } + if rollbackErr != nil { + return rollbackErr + } + return nil +} + +// Update opens a database read/write transaction and executes the function f +// with the transaction passed as a parameter. After f exits, if f did not +// error, the transaction is committed. Otherwise, if f did error, the +// transaction is rolled back. If the rollback fails, the original error +// returned by f is still returned. If the commit fails, the commit error is +// returned. +func Update(db DB, f func(tx ReadWriteTx) error) error { + tx, err := db.BeginReadWriteTx() + if err != nil { + return err + } + err = f(tx) + if err != nil { + // Want to return the original error, not a rollback error if + // any occur. + _ = tx.Rollback() + return err + } + return tx.Commit() +} + // Driver defines a structure for backend drivers to use when they registered // themselves as a backend which implements the Db interface. type Driver struct { diff --git a/walletsetup.go b/walletsetup.go index 56c9a58..5206b12 100644 --- a/walletsetup.go +++ b/walletsetup.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" @@ -42,7 +43,7 @@ func networkDir(dataDir string, chainParams *chaincfg.Params) string { // convertLegacyKeystore converts all of the addresses in the passed legacy // key store to the new waddrmgr.Manager format. Both the legacy keystore and // the new manager must be unlocked. -func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Manager) error { +func convertLegacyKeystore(legacyKeyStore *keystore.Store, w *wallet.Wallet) error { netParams := legacyKeyStore.Net() blockStamp := waddrmgr.BlockStamp{ Height: 0, @@ -68,7 +69,7 @@ func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Man continue } - _, err = manager.ImportPrivateKey(wif, &blockStamp) + _, err = w.ImportPrivateKey(wif, &blockStamp, false) if err != nil { fmt.Printf("WARN: Failed to import private "+ "key for address %v: %v\n", @@ -77,7 +78,7 @@ func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Man } case keystore.ScriptAddress: - _, err := manager.ImportScript(addr.Script(), &blockStamp) + _, err := w.ImportP2SHRedeemScript(addr.Script()) if err != nil { fmt.Printf("WARN: Failed to import "+ "pay-to-script-hash script for "+ @@ -146,15 +147,18 @@ func createWallet(cfg *config) error { fmt.Println("Importing addresses from existing wallet...") - err := w.Manager.Unlock(privPass) + lockChan := make(chan time.Time, 1) + defer func() { + lockChan <- time.Time{} + }() + err := w.Unlock(privPass, lockChan) if err != nil { fmt.Printf("ERR: Failed to unlock new wallet "+ "during old wallet key import: %v", err) return } - defer w.Manager.Lock() - err = convertLegacyKeystore(legacyKeyStore, w.Manager) + err = convertLegacyKeystore(legacyKeyStore, w) if err != nil { fmt.Printf("ERR: Failed to import keys from old "+ "wallet format: %v", err) diff --git a/wtxmgr/db.go b/wtxmgr/db.go index 696dce0..0c92e17 100644 --- a/wtxmgr/db.go +++ b/wtxmgr/db.go @@ -1,4 +1,5 @@ // Copyright (c) 2015 The btcsuite developers +// Copyright (c) 2015 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -88,8 +89,7 @@ var ( // outputs spent by mempool transactions, which must be considered when // returning the actual balance for a given number of block confirmations. The // value is the amount serialized as a uint64. - -func fetchMinedBalance(ns walletdb.Bucket) (btcutil.Amount, error) { +func fetchMinedBalance(ns walletdb.ReadBucket) (btcutil.Amount, error) { v := ns.Get(rootMinedBalance) if len(v) != 8 { str := fmt.Sprintf("balance: short read (expected 8 bytes, "+ @@ -99,7 +99,7 @@ func fetchMinedBalance(ns walletdb.Bucket) (btcutil.Amount, error) { return btcutil.Amount(byteOrder.Uint64(v)), nil } -func putMinedBalance(ns walletdb.Bucket, amt btcutil.Amount) error { +func putMinedBalance(ns walletdb.ReadWriteBucket, amt btcutil.Amount) error { v := make([]byte, 8) byteOrder.PutUint64(v, uint64(amt)) err := ns.Put(rootMinedBalance, v) @@ -176,8 +176,8 @@ func appendRawBlockRecord(v []byte, txHash *chainhash.Hash) ([]byte, error) { return newv, nil } -func putRawBlockRecord(ns walletdb.Bucket, k, v []byte) error { - err := ns.Bucket(bucketBlocks).Put(k, v) +func putRawBlockRecord(ns walletdb.ReadWriteBucket, k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketBlocks).Put(k, v) if err != nil { str := "failed to store block" return storeError(ErrDatabase, str, err) @@ -185,15 +185,15 @@ func putRawBlockRecord(ns walletdb.Bucket, k, v []byte) error { return nil } -func putBlockRecord(ns walletdb.Bucket, block *BlockMeta, txHash *chainhash.Hash) error { +func putBlockRecord(ns walletdb.ReadWriteBucket, block *BlockMeta, txHash *chainhash.Hash) error { k := keyBlockRecord(block.Height) v := valueBlockRecord(block, txHash) return putRawBlockRecord(ns, k, v) } -func fetchBlockTime(ns walletdb.Bucket, height int32) (time.Time, error) { +func fetchBlockTime(ns walletdb.ReadBucket, height int32) (time.Time, error) { k := keyBlockRecord(height) - v := ns.Bucket(bucketBlocks).Get(k) + v := ns.NestedReadBucket(bucketBlocks).Get(k) if len(v) < 44 { str := fmt.Sprintf("%s: short read (expected %d bytes, read %d)", bucketBlocks, 44, len(v)) @@ -202,9 +202,9 @@ func fetchBlockTime(ns walletdb.Bucket, height int32) (time.Time, error) { return time.Unix(int64(byteOrder.Uint64(v[32:40])), 0), nil } -func existsBlockRecord(ns walletdb.Bucket, height int32) (k, v []byte) { +func existsBlockRecord(ns walletdb.ReadBucket, height int32) (k, v []byte) { k = keyBlockRecord(height) - v = ns.Bucket(bucketBlocks).Get(k) + v = ns.NestedReadBucket(bucketBlocks).Get(k) return } @@ -241,7 +241,7 @@ func readRawBlockRecord(k, v []byte, block *blockRecord) error { } type blockIterator struct { - c walletdb.Cursor + c walletdb.ReadWriteCursor seek []byte ck []byte cv []byte @@ -249,22 +249,36 @@ type blockIterator struct { err error } -func makeBlockIterator(ns walletdb.Bucket, height int32) blockIterator { +func makeBlockIterator(ns walletdb.ReadWriteBucket, height int32) blockIterator { seek := make([]byte, 4) byteOrder.PutUint32(seek, uint32(height)) - c := ns.Bucket(bucketBlocks).Cursor() + c := ns.NestedReadWriteBucket(bucketBlocks).ReadWriteCursor() return blockIterator{c: c, seek: seek} } +func makeReadBlockIterator(ns walletdb.ReadBucket, height int32) blockIterator { + seek := make([]byte, 4) + byteOrder.PutUint32(seek, uint32(height)) + c := ns.NestedReadBucket(bucketBlocks).ReadCursor() + return blockIterator{c: readCursor{c}, seek: seek} +} + // Works just like makeBlockIterator but will initially position the cursor at // the last k/v pair. Use this with blockIterator.prev. -func makeReverseBlockIterator(ns walletdb.Bucket) blockIterator { +func makeReverseBlockIterator(ns walletdb.ReadWriteBucket) blockIterator { seek := make([]byte, 4) byteOrder.PutUint32(seek, ^uint32(0)) - c := ns.Bucket(bucketBlocks).Cursor() + c := ns.NestedReadWriteBucket(bucketBlocks).ReadWriteCursor() return blockIterator{c: c, seek: seek} } +func makeReadReverseBlockIterator(ns walletdb.ReadBucket) blockIterator { + seek := make([]byte, 4) + byteOrder.PutUint32(seek, ^uint32(0)) + c := ns.NestedReadBucket(bucketBlocks).ReadCursor() + return blockIterator{c: readCursor{c}, seek: seek} +} + func (it *blockIterator) next() bool { if it.c == nil { return false @@ -378,13 +392,13 @@ func valueTxRecord(rec *TxRecord) ([]byte, error) { return v, nil } -func putTxRecord(ns walletdb.Bucket, rec *TxRecord, block *Block) error { +func putTxRecord(ns walletdb.ReadWriteBucket, rec *TxRecord, block *Block) error { k := keyTxRecord(&rec.Hash, block) v, err := valueTxRecord(rec) if err != nil { return err } - err = ns.Bucket(bucketTxRecords).Put(k, v) + err = ns.NestedReadWriteBucket(bucketTxRecords).Put(k, v) if err != nil { str := fmt.Sprintf("%s: put failed for %v", bucketTxRecords, rec.Hash) return storeError(ErrDatabase, str, err) @@ -392,8 +406,8 @@ func putTxRecord(ns walletdb.Bucket, rec *TxRecord, block *Block) error { return nil } -func putRawTxRecord(ns walletdb.Bucket, k, v []byte) error { - err := ns.Bucket(bucketTxRecords).Put(k, v) +func putRawTxRecord(ns walletdb.ReadWriteBucket, k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketTxRecords).Put(k, v) if err != nil { str := fmt.Sprintf("%s: put failed", bucketTxRecords) return storeError(ErrDatabase, str, err) @@ -429,9 +443,9 @@ func readRawTxRecordBlock(k []byte, block *Block) error { return nil } -func fetchTxRecord(ns walletdb.Bucket, txHash *chainhash.Hash, block *Block) (*TxRecord, error) { +func fetchTxRecord(ns walletdb.ReadBucket, txHash *chainhash.Hash, block *Block) (*TxRecord, error) { k := keyTxRecord(txHash, block) - v := ns.Bucket(bucketTxRecords).Get(k) + v := ns.NestedReadBucket(bucketTxRecords).Get(k) rec := new(TxRecord) err := readRawTxRecord(txHash, v, rec) @@ -454,27 +468,27 @@ func fetchRawTxRecordPkScript(k, v []byte, index uint32) ([]byte, error) { return rec.MsgTx.TxOut[index].PkScript, nil } -func existsTxRecord(ns walletdb.Bucket, txHash *chainhash.Hash, block *Block) (k, v []byte) { +func existsTxRecord(ns walletdb.ReadBucket, txHash *chainhash.Hash, block *Block) (k, v []byte) { k = keyTxRecord(txHash, block) - v = ns.Bucket(bucketTxRecords).Get(k) + v = ns.NestedReadBucket(bucketTxRecords).Get(k) return } -func existsRawTxRecord(ns walletdb.Bucket, k []byte) (v []byte) { - return ns.Bucket(bucketTxRecords).Get(k) +func existsRawTxRecord(ns walletdb.ReadBucket, k []byte) (v []byte) { + return ns.NestedReadBucket(bucketTxRecords).Get(k) } -func deleteTxRecord(ns walletdb.Bucket, txHash *chainhash.Hash, block *Block) error { +func deleteTxRecord(ns walletdb.ReadWriteBucket, txHash *chainhash.Hash, block *Block) error { k := keyTxRecord(txHash, block) - return ns.Bucket(bucketTxRecords).Delete(k) + return ns.NestedReadWriteBucket(bucketTxRecords).Delete(k) } // latestTxRecord searches for the newest recorded mined transaction record with // a matching hash. In case of a hash collision, the record from the newest // block is returned. Returns (nil, nil) if no matching transactions are found. -func latestTxRecord(ns walletdb.Bucket, txHash *chainhash.Hash) (k, v []byte) { +func latestTxRecord(ns walletdb.ReadBucket, txHash *chainhash.Hash) (k, v []byte) { prefix := txHash[:] - c := ns.Bucket(bucketTxRecords).Cursor() + c := ns.NestedReadBucket(bucketTxRecords).ReadCursor() ck, cv := c.Seek(prefix) var lastKey, lastVal []byte for bytes.HasPrefix(ck, prefix) { @@ -530,8 +544,8 @@ func valueUnspentCredit(cred *credit) []byte { return v } -func putRawCredit(ns walletdb.Bucket, k, v []byte) error { - err := ns.Bucket(bucketCredits).Put(k, v) +func putRawCredit(ns walletdb.ReadWriteBucket, k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketCredits).Put(k, v) if err != nil { str := "failed to put credit" return storeError(ErrDatabase, str, err) @@ -542,7 +556,7 @@ func putRawCredit(ns walletdb.Bucket, k, v []byte) error { // putUnspentCredit puts a credit record for an unspent credit. It may only be // used when the credit is already know to be unspent, or spent by an // unconfirmed transaction. -func putUnspentCredit(ns walletdb.Bucket, cred *credit) error { +func putUnspentCredit(ns walletdb.ReadWriteBucket, cred *credit) error { k := keyCredit(&cred.outPoint.Hash, cred.outPoint.Index, &cred.block) v := valueUnspentCredit(cred) return putRawCredit(ns, k, v) @@ -602,8 +616,8 @@ func fetchRawCreditUnspentValue(k []byte) ([]byte, error) { // spendRawCredit marks the credit with a given key as mined at some particular // block as spent by the input at some transaction incidence. The debited // amount is returned. -func spendCredit(ns walletdb.Bucket, k []byte, spender *indexedIncidence) (btcutil.Amount, error) { - v := ns.Bucket(bucketCredits).Get(k) +func spendCredit(ns walletdb.ReadWriteBucket, k []byte, spender *indexedIncidence) (btcutil.Amount, error) { + v := ns.NestedReadBucket(bucketCredits).Get(k) newv := make([]byte, 81) copy(newv, v) v = newv @@ -619,8 +633,8 @@ func spendCredit(ns walletdb.Bucket, k []byte, spender *indexedIncidence) (btcut // unspendRawCredit rewrites the credit for the given key as unspent. The // output amount of the credit is returned. It returns without error if no // credit exists for the key. -func unspendRawCredit(ns walletdb.Bucket, k []byte) (btcutil.Amount, error) { - b := ns.Bucket(bucketCredits) +func unspendRawCredit(ns walletdb.ReadWriteBucket, k []byte) (btcutil.Amount, error) { + b := ns.NestedReadWriteBucket(bucketCredits) v := b.Get(k) if v == nil { return 0, nil @@ -637,18 +651,18 @@ func unspendRawCredit(ns walletdb.Bucket, k []byte) (btcutil.Amount, error) { return btcutil.Amount(byteOrder.Uint64(v[0:8])), nil } -func existsCredit(ns walletdb.Bucket, txHash *chainhash.Hash, index uint32, block *Block) (k, v []byte) { +func existsCredit(ns walletdb.ReadBucket, txHash *chainhash.Hash, index uint32, block *Block) (k, v []byte) { k = keyCredit(txHash, index, block) - v = ns.Bucket(bucketCredits).Get(k) + v = ns.NestedReadBucket(bucketCredits).Get(k) return } -func existsRawCredit(ns walletdb.Bucket, k []byte) []byte { - return ns.Bucket(bucketCredits).Get(k) +func existsRawCredit(ns walletdb.ReadBucket, k []byte) []byte { + return ns.NestedReadBucket(bucketCredits).Get(k) } -func deleteRawCredit(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketCredits).Delete(k) +func deleteRawCredit(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketCredits).Delete(k) if err != nil { str := "failed to delete credit" return storeError(ErrDatabase, str, err) @@ -677,7 +691,7 @@ func deleteRawCredit(ns walletdb.Bucket, k []byte) error { // k := canonicalOutPoint(&txHash, it.elem.Index) // it.elem.Spent = existsRawUnminedInput(ns, k) != nil type creditIterator struct { - c walletdb.Cursor // Set to nil after final iteration + c walletdb.ReadWriteCursor // Set to nil after final iteration prefix []byte ck []byte cv []byte @@ -685,11 +699,16 @@ type creditIterator struct { err error } -func makeCreditIterator(ns walletdb.Bucket, prefix []byte) creditIterator { - c := ns.Bucket(bucketCredits).Cursor() +func makeCreditIterator(ns walletdb.ReadWriteBucket, prefix []byte) creditIterator { + c := ns.NestedReadWriteBucket(bucketCredits).ReadWriteCursor() return creditIterator{c: c, prefix: prefix} } +func makeReadCreditIterator(ns walletdb.ReadBucket, prefix []byte) creditIterator { + c := ns.NestedReadBucket(bucketCredits).ReadCursor() + return creditIterator{c: readCursor{c}, prefix: prefix} +} + func (it *creditIterator) readElem() error { if len(it.ck) < 72 { str := fmt.Sprintf("%s: short key (expected %d bytes, read %d)", @@ -752,10 +771,10 @@ func valueUnspent(block *Block) []byte { return v } -func putUnspent(ns walletdb.Bucket, outPoint *wire.OutPoint, block *Block) error { +func putUnspent(ns walletdb.ReadWriteBucket, outPoint *wire.OutPoint, block *Block) error { k := canonicalOutPoint(&outPoint.Hash, outPoint.Index) v := valueUnspent(block) - err := ns.Bucket(bucketUnspent).Put(k, v) + err := ns.NestedReadWriteBucket(bucketUnspent).Put(k, v) if err != nil { str := "cannot put unspent" return storeError(ErrDatabase, str, err) @@ -763,8 +782,8 @@ func putUnspent(ns walletdb.Bucket, outPoint *wire.OutPoint, block *Block) error return nil } -func putRawUnspent(ns walletdb.Bucket, k, v []byte) error { - err := ns.Bucket(bucketUnspent).Put(k, v) +func putRawUnspent(ns walletdb.ReadWriteBucket, k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketUnspent).Put(k, v) if err != nil { str := "cannot put unspent" return storeError(ErrDatabase, str, err) @@ -785,7 +804,7 @@ func readUnspentBlock(v []byte, block *Block) error { // existsUnspent returns the key for the unspent output and the corresponding // key for the credits bucket. If there is no unspent output recorded, the // credit key is nil. -func existsUnspent(ns walletdb.Bucket, outPoint *wire.OutPoint) (k, credKey []byte) { +func existsUnspent(ns walletdb.ReadBucket, outPoint *wire.OutPoint) (k, credKey []byte) { k = canonicalOutPoint(&outPoint.Hash, outPoint.Index) credKey = existsRawUnspent(ns, k) return k, credKey @@ -793,11 +812,11 @@ func existsUnspent(ns walletdb.Bucket, outPoint *wire.OutPoint) (k, credKey []by // existsRawUnspent returns the credit key if there exists an output recorded // for the raw unspent key. It returns nil if the k/v pair does not exist. -func existsRawUnspent(ns walletdb.Bucket, k []byte) (credKey []byte) { +func existsRawUnspent(ns walletdb.ReadBucket, k []byte) (credKey []byte) { if len(k) < 36 { return nil } - v := ns.Bucket(bucketUnspent).Get(k) + v := ns.NestedReadBucket(bucketUnspent).Get(k) if len(v) < 36 { return nil } @@ -808,8 +827,8 @@ func existsRawUnspent(ns walletdb.Bucket, k []byte) (credKey []byte) { return credKey } -func deleteRawUnspent(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketUnspent).Delete(k) +func deleteRawUnspent(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketUnspent).Delete(k) if err != nil { str := "failed to delete unspent" return storeError(ErrDatabase, str, err) @@ -845,14 +864,14 @@ func keyDebit(txHash *chainhash.Hash, index uint32, block *Block) []byte { return k } -func putDebit(ns walletdb.Bucket, txHash *chainhash.Hash, index uint32, amount btcutil.Amount, block *Block, credKey []byte) error { +func putDebit(ns walletdb.ReadWriteBucket, txHash *chainhash.Hash, index uint32, amount btcutil.Amount, block *Block, credKey []byte) error { k := keyDebit(txHash, index, block) v := make([]byte, 80) byteOrder.PutUint64(v, uint64(amount)) copy(v[8:80], credKey) - err := ns.Bucket(bucketDebits).Put(k, v) + err := ns.NestedReadWriteBucket(bucketDebits).Put(k, v) if err != nil { str := fmt.Sprintf("failed to update debit %s input %d", txHash, index) @@ -868,9 +887,9 @@ func extractRawDebitCreditKey(v []byte) []byte { // existsDebit checks for the existance of a debit. If found, the debit and // previous credit keys are returned. If the debit does not exist, both keys // are nil. -func existsDebit(ns walletdb.Bucket, txHash *chainhash.Hash, index uint32, block *Block) (k, credKey []byte, err error) { +func existsDebit(ns walletdb.ReadBucket, txHash *chainhash.Hash, index uint32, block *Block) (k, credKey []byte, err error) { k = keyDebit(txHash, index, block) - v := ns.Bucket(bucketDebits).Get(k) + v := ns.NestedReadBucket(bucketDebits).Get(k) if v == nil { return nil, nil, nil } @@ -882,8 +901,8 @@ func existsDebit(ns walletdb.Bucket, txHash *chainhash.Hash, index uint32, block return k, v[8:80], nil } -func deleteRawDebit(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketDebits).Delete(k) +func deleteRawDebit(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketDebits).Delete(k) if err != nil { str := "failed to delete debit" return storeError(ErrDatabase, str, err) @@ -906,7 +925,7 @@ func deleteRawDebit(ns walletdb.Bucket, k []byte) error { // // Handle error // } type debitIterator struct { - c walletdb.Cursor // Set to nil after final iteration + c walletdb.ReadWriteCursor // Set to nil after final iteration prefix []byte ck []byte cv []byte @@ -914,11 +933,16 @@ type debitIterator struct { err error } -func makeDebitIterator(ns walletdb.Bucket, prefix []byte) debitIterator { - c := ns.Bucket(bucketDebits).Cursor() +func makeDebitIterator(ns walletdb.ReadWriteBucket, prefix []byte) debitIterator { + c := ns.NestedReadWriteBucket(bucketDebits).ReadWriteCursor() return debitIterator{c: c, prefix: prefix} } +func makeReadDebitIterator(ns walletdb.ReadBucket, prefix []byte) debitIterator { + c := ns.NestedReadBucket(bucketDebits).ReadCursor() + return debitIterator{c: readCursor{c}, prefix: prefix} +} + func (it *debitIterator) readElem() error { if len(it.ck) < 72 { str := fmt.Sprintf("%s: short key (expected %d bytes, read %d)", @@ -964,8 +988,8 @@ func (it *debitIterator) next() bool { // [0:8] Received time (8 bytes) // [8:] Serialized transaction (varies) -func putRawUnmined(ns walletdb.Bucket, k, v []byte) error { - err := ns.Bucket(bucketUnmined).Put(k, v) +func putRawUnmined(ns walletdb.ReadWriteBucket, k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketUnmined).Put(k, v) if err != nil { str := "failed to put unmined record" return storeError(ErrDatabase, str, err) @@ -982,12 +1006,12 @@ func readRawUnminedHash(k []byte, txHash *chainhash.Hash) error { return nil } -func existsRawUnmined(ns walletdb.Bucket, k []byte) (v []byte) { - return ns.Bucket(bucketUnmined).Get(k) +func existsRawUnmined(ns walletdb.ReadBucket, k []byte) (v []byte) { + return ns.NestedReadBucket(bucketUnmined).Get(k) } -func deleteRawUnmined(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketUnmined).Delete(k) +func deleteRawUnmined(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketUnmined).Delete(k) if err != nil { str := "failed to delete unmined record" return storeError(ErrDatabase, str, err) @@ -1017,8 +1041,8 @@ func valueUnminedCredit(amount btcutil.Amount, change bool) []byte { return v } -func putRawUnminedCredit(ns walletdb.Bucket, k, v []byte) error { - err := ns.Bucket(bucketUnminedCredits).Put(k, v) +func putRawUnminedCredit(ns walletdb.ReadWriteBucket, k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketUnminedCredits).Put(k, v) if err != nil { str := "cannot put unmined credit" return storeError(ErrDatabase, str, err) @@ -1052,12 +1076,12 @@ func fetchRawUnminedCreditAmountChange(v []byte) (btcutil.Amount, bool, error) { return amt, change, nil } -func existsRawUnminedCredit(ns walletdb.Bucket, k []byte) []byte { - return ns.Bucket(bucketUnminedCredits).Get(k) +func existsRawUnminedCredit(ns walletdb.ReadBucket, k []byte) []byte { + return ns.NestedReadBucket(bucketUnminedCredits).Get(k) } -func deleteRawUnminedCredit(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketUnminedCredits).Delete(k) +func deleteRawUnminedCredit(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketUnminedCredits).Delete(k) if err != nil { str := "failed to delete unmined credit" return storeError(ErrDatabase, str, err) @@ -1085,7 +1109,7 @@ func deleteRawUnminedCredit(ns walletdb.Bucket, k []byte) error { // // spent := existsRawUnminedInput(ns, it.ck) != nil type unminedCreditIterator struct { - c walletdb.Cursor + c walletdb.ReadWriteCursor prefix []byte ck []byte cv []byte @@ -1093,11 +1117,25 @@ type unminedCreditIterator struct { err error } -func makeUnminedCreditIterator(ns walletdb.Bucket, txHash *chainhash.Hash) unminedCreditIterator { - c := ns.Bucket(bucketUnminedCredits).Cursor() +type readCursor struct { + walletdb.ReadCursor +} + +func (r readCursor) Delete() error { + str := "failed to delete current cursor item from read-only cursor" + return storeError(ErrDatabase, str, walletdb.ErrTxNotWritable) +} + +func makeUnminedCreditIterator(ns walletdb.ReadWriteBucket, txHash *chainhash.Hash) unminedCreditIterator { + c := ns.NestedReadWriteBucket(bucketUnminedCredits).ReadWriteCursor() return unminedCreditIterator{c: c, prefix: txHash[:]} } +func makeReadUnminedCreditIterator(ns walletdb.ReadBucket, txHash *chainhash.Hash) unminedCreditIterator { + c := ns.NestedReadBucket(bucketUnminedCredits).ReadCursor() + return unminedCreditIterator{c: readCursor{c}, prefix: txHash[:]} +} + func (it *unminedCreditIterator) readElem() error { index, err := fetchRawUnminedCreditIndex(it.ck) if err != nil { @@ -1161,8 +1199,8 @@ func (it *unminedCreditIterator) delete() error { // // [0:32] Transaction hash (32 bytes) -func putRawUnminedInput(ns walletdb.Bucket, k, v []byte) error { - err := ns.Bucket(bucketUnminedInputs).Put(k, v) +func putRawUnminedInput(ns walletdb.ReadWriteBucket, k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketUnminedInputs).Put(k, v) if err != nil { str := "failed to put unmined input" return storeError(ErrDatabase, str, err) @@ -1170,12 +1208,12 @@ func putRawUnminedInput(ns walletdb.Bucket, k, v []byte) error { return nil } -func existsRawUnminedInput(ns walletdb.Bucket, k []byte) (v []byte) { - return ns.Bucket(bucketUnminedInputs).Get(k) +func existsRawUnminedInput(ns walletdb.ReadBucket, k []byte) (v []byte) { + return ns.NestedReadBucket(bucketUnminedInputs).Get(k) } -func deleteRawUnminedInput(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketUnminedInputs).Delete(k) +func deleteRawUnminedInput(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketUnminedInputs).Delete(k) if err != nil { str := "failed to delete unmined input" return storeError(ErrDatabase, str, err) @@ -1183,39 +1221,24 @@ func deleteRawUnminedInput(ns walletdb.Bucket, k []byte) error { return nil } -// openStore opens an existing transaction store from the passed namespace. If -// necessary, an already existing store is upgraded to newer db format. -func openStore(namespace walletdb.Namespace) error { - var version uint32 - err := scopedView(namespace, func(ns walletdb.Bucket) error { - // Verify a store already exists and upgrade as necessary. - v := ns.Get(rootVersion) - if len(v) != 4 { - return nil - } - version = byteOrder.Uint32(v) - return nil - }) - if err != nil { - const desc = "failed to open existing store" - if serr, ok := err.(Error); ok { - serr.Desc = desc + ": " + serr.Desc - return serr - } - return storeError(ErrDatabase, desc, err) - } - - // The initial version is one. If no store exists and no version was - // saved, this variable will be zero. - if version == 0 { +// openStore opens an existing transaction store from the passed namespace. +func openStore(ns walletdb.ReadBucket) error { + v := ns.Get(rootVersion) + if len(v) != 4 { str := "no transaction store exists in namespace" return storeError(ErrNoExists, str, nil) } + version := byteOrder.Uint32(v) + + if version < LatestVersion { + str := fmt.Sprintf("a database upgrade is required to upgrade "+ + "wtxmgr from recorded version %d to the latest version %d", + version, LatestVersion) + return storeError(ErrNeedsUpgrade, str, nil) + } - // Cannot continue if the saved database is too new for this software. - // This probably indicates an outdated binary. if version > LatestVersion { - str := fmt.Sprintf("recorded version %d is newer that latest "+ + str := fmt.Sprintf("version recorded version %d is newer that latest "+ "understood version %d", version, LatestVersion) return storeError(ErrUnknownVersion, str, nil) } @@ -1238,119 +1261,106 @@ func openStore(namespace walletdb.Namespace) error { // createStore creates the tx store (with the latest db version) in the passed // namespace. If a store already exists, ErrAlreadyExists is returned. -func createStore(namespace walletdb.Namespace) error { - // Initialize the buckets and root bucket fields as needed. - err := scopedUpdate(namespace, func(ns walletdb.Bucket) error { - // Ensure that nothing currently exists in the namespace bucket. - ck, cv := ns.Cursor().First() - if ck != nil || cv != nil { - const str = "namespace is not empty" - return storeError(ErrAlreadyExists, str, nil) - } +func createStore(ns walletdb.ReadWriteBucket) error { + // Ensure that nothing currently exists in the namespace bucket. + ck, cv := ns.ReadCursor().First() + if ck != nil || cv != nil { + const str = "namespace is not empty" + return storeError(ErrAlreadyExists, str, nil) + } - // Write the latest store version. - v := make([]byte, 4) - byteOrder.PutUint32(v, LatestVersion) - err := ns.Put(rootVersion, v) - if err != nil { - str := "failed to store latest database version" - return storeError(ErrDatabase, str, err) - } - - // Save the creation date of the store. - v = make([]byte, 8) - byteOrder.PutUint64(v, uint64(time.Now().Unix())) - err = ns.Put(rootCreateDate, v) - if err != nil { - str := "failed to store database creation time" - return storeError(ErrDatabase, str, err) - } - - // Write a zero balance. - v = make([]byte, 8) - err = ns.Put(rootMinedBalance, v) - if err != nil { - str := "failed to write zero balance" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketBlocks) - if err != nil { - str := "failed to create blocks bucket" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketTxRecords) - if err != nil { - str := "failed to create tx records bucket" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketCredits) - if err != nil { - str := "failed to create credits bucket" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketDebits) - if err != nil { - str := "failed to create debits bucket" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketUnspent) - if err != nil { - str := "failed to create unspent bucket" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketUnmined) - if err != nil { - str := "failed to create unmined bucket" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketUnminedCredits) - if err != nil { - str := "failed to create unmined credits bucket" - return storeError(ErrDatabase, str, err) - } - - _, err = ns.CreateBucket(bucketUnminedInputs) - if err != nil { - str := "failed to create unmined inputs bucket" - return storeError(ErrDatabase, str, err) - } - - return nil - }) + // Write the latest store version. + v := make([]byte, 4) + byteOrder.PutUint32(v, LatestVersion) + err := ns.Put(rootVersion, v) if err != nil { - const desc = "failed to create new store" - if serr, ok := err.(Error); ok { - serr.Desc = desc + ": " + serr.Desc - return serr - } - return storeError(ErrDatabase, desc, err) + str := "failed to store latest database version" + return storeError(ErrDatabase, str, err) + } + + // Save the creation date of the store. + v = make([]byte, 8) + byteOrder.PutUint64(v, uint64(time.Now().Unix())) + err = ns.Put(rootCreateDate, v) + if err != nil { + str := "failed to store database creation time" + return storeError(ErrDatabase, str, err) + } + + // Write a zero balance. + v = make([]byte, 8) + err = ns.Put(rootMinedBalance, v) + if err != nil { + str := "failed to write zero balance" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketBlocks) + if err != nil { + str := "failed to create blocks bucket" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketTxRecords) + if err != nil { + str := "failed to create tx records bucket" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketCredits) + if err != nil { + str := "failed to create credits bucket" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketDebits) + if err != nil { + str := "failed to create debits bucket" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketUnspent) + if err != nil { + str := "failed to create unspent bucket" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketUnmined) + if err != nil { + str := "failed to create unmined bucket" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketUnminedCredits) + if err != nil { + str := "failed to create unmined credits bucket" + return storeError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucket(bucketUnminedInputs) + if err != nil { + str := "failed to create unmined inputs bucket" + return storeError(ErrDatabase, str, err) } return nil } -func scopedUpdate(ns walletdb.Namespace, f func(walletdb.Bucket) error) error { - tx, err := ns.Begin(true) +func scopedUpdate(db walletdb.DB, namespaceKey []byte, f func(walletdb.ReadWriteBucket) error) error { + tx, err := db.BeginReadWriteTx() if err != nil { str := "cannot begin update" return storeError(ErrDatabase, str, err) } - err = f(tx.RootBucket()) + err = f(tx.ReadWriteBucket(namespaceKey)) if err != nil { - rbErr := tx.Rollback() - if rbErr != nil { + rollbackErr := tx.Rollback() + if rollbackErr != nil { const desc = "rollback failed" serr, ok := err.(Error) if !ok { // This really shouldn't happen. - return storeError(ErrDatabase, desc, rbErr) + return storeError(ErrDatabase, desc, rollbackErr) } serr.Desc = desc + ": " + serr.Desc return serr @@ -1365,20 +1375,20 @@ func scopedUpdate(ns walletdb.Namespace, f func(walletdb.Bucket) error) error { return nil } -func scopedView(ns walletdb.Namespace, f func(walletdb.Bucket) error) error { - tx, err := ns.Begin(false) +func scopedView(db walletdb.DB, namespaceKey []byte, f func(walletdb.ReadBucket) error) error { + tx, err := db.BeginReadTx() if err != nil { str := "cannot begin view" return storeError(ErrDatabase, str, err) } - err = f(tx.RootBucket()) - rbErr := tx.Rollback() + err = f(tx.ReadBucket(namespaceKey)) + rollbackErr := tx.Rollback() if err != nil { return err } - if rbErr != nil { + if rollbackErr != nil { str := "cannot close view" - return storeError(ErrDatabase, str, rbErr) + return storeError(ErrDatabase, str, rollbackErr) } return nil } diff --git a/wtxmgr/error.go b/wtxmgr/error.go index 6c65a34..7e26832 100644 --- a/wtxmgr/error.go +++ b/wtxmgr/error.go @@ -1,4 +1,5 @@ -// Copyright (c) 2015 The btcsuite developers +// Copyright (c) 2015-2017 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -40,6 +41,10 @@ const ( // handled by creating a new store. ErrNoExists + // ErrNeedsUpgrade describes an error during store opening where the + // database contains an older version of the store. + ErrNeedsUpgrade + // ErrUnknownVersion describes an error where the store already exists // but the database version is newer than latest version known to this // software. This likely indicates an outdated binary. @@ -52,6 +57,7 @@ var errStrs = [...]string{ ErrInput: "ErrInput", ErrAlreadyExists: "ErrAlreadyExists", ErrNoExists: "ErrNoExists", + ErrNeedsUpgrade: "ErrNeedsUpgrade", ErrUnknownVersion: "ErrUnknownVersion", } diff --git a/wtxmgr/query.go b/wtxmgr/query.go index efb3b93..6e7f40f 100644 --- a/wtxmgr/query.go +++ b/wtxmgr/query.go @@ -1,4 +1,5 @@ -// Copyright (c) 2015 The btcsuite developers +// Copyright (c) 2015-2017 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -42,7 +43,7 @@ type TxDetails struct { // minedTxDetails fetches the TxDetails for the mined transaction with hash // txHash and the passed tx record key and value. -func (s *Store) minedTxDetails(ns walletdb.Bucket, txHash *chainhash.Hash, recKey, recVal []byte) (*TxDetails, error) { +func (s *Store) minedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, recKey, recVal []byte) (*TxDetails, error) { var details TxDetails // Parse transaction record k/v, lookup the full block record for the @@ -60,7 +61,7 @@ func (s *Store) minedTxDetails(ns walletdb.Bucket, txHash *chainhash.Hash, recKe return nil, err } - credIter := makeCreditIterator(ns, recKey) + credIter := makeReadCreditIterator(ns, recKey) for credIter.next() { if int(credIter.elem.Index) >= len(details.MsgTx.TxOut) { str := "saved credit index exceeds number of outputs" @@ -80,7 +81,7 @@ func (s *Store) minedTxDetails(ns walletdb.Bucket, txHash *chainhash.Hash, recKe return nil, credIter.err } - debIter := makeDebitIterator(ns, recKey) + debIter := makeReadDebitIterator(ns, recKey) for debIter.next() { if int(debIter.elem.Index) >= len(details.MsgTx.TxIn) { str := "saved debit index exceeds number of inputs" @@ -94,7 +95,7 @@ func (s *Store) minedTxDetails(ns walletdb.Bucket, txHash *chainhash.Hash, recKe // unminedTxDetails fetches the TxDetails for the unmined transaction with the // hash txHash and the passed unmined record value. -func (s *Store) unminedTxDetails(ns walletdb.Bucket, txHash *chainhash.Hash, v []byte) (*TxDetails, error) { +func (s *Store) unminedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, v []byte) (*TxDetails, error) { details := TxDetails{ Block: BlockMeta{Block: Block{Height: -1}}, } @@ -103,7 +104,7 @@ func (s *Store) unminedTxDetails(ns walletdb.Bucket, txHash *chainhash.Hash, v [ return nil, err } - it := makeUnminedCreditIterator(ns, txHash) + it := makeReadUnminedCreditIterator(ns, txHash) for it.next() { if int(it.elem.Index) >= len(details.MsgTx.TxOut) { str := "saved credit index exceeds number of outputs" @@ -166,30 +167,22 @@ func (s *Store) unminedTxDetails(ns walletdb.Bucket, txHash *chainhash.Hash, v [ // // Not finding a transaction with this hash is not an error. In this case, // a nil TxDetails is returned. -func (s *Store) TxDetails(txHash *chainhash.Hash) (*TxDetails, error) { - var details *TxDetails - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error +func (s *Store) TxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash) (*TxDetails, error) { + // First, check whether there exists an unmined transaction with this + // hash. Use it if found. + v := existsRawUnmined(ns, txHash[:]) + if v != nil { + return s.unminedTxDetails(ns, txHash, v) + } - // First, check whether there exists an unmined transaction with this - // hash. Use it if found. - v := existsRawUnmined(ns, txHash[:]) - if v != nil { - details, err = s.unminedTxDetails(ns, txHash, v) - return err - } - - // Otherwise, if there exists a mined transaction with this matching - // hash, skip over to the newest and begin fetching all details. - k, v := latestTxRecord(ns, txHash) - if v == nil { - // not found - return nil - } - details, err = s.minedTxDetails(ns, txHash, k, v) - return err - }) - return details, err + // Otherwise, if there exists a mined transaction with this matching + // hash, skip over to the newest and begin fetching all details. + k, v := latestTxRecord(ns, txHash) + if v == nil { + // not found + return nil, nil + } + return s.minedTxDetails(ns, txHash, k, v) } // UniqueTxDetails looks up all recorded details for a transaction recorded @@ -197,27 +190,22 @@ func (s *Store) TxDetails(txHash *chainhash.Hash) (*TxDetails, error) { // // Not finding a transaction with this hash from this block is not an error. In // this case, a nil TxDetails is returned. -func (s *Store) UniqueTxDetails(txHash *chainhash.Hash, block *Block) (*TxDetails, error) { - var details *TxDetails - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - if block == nil { - v := existsRawUnmined(ns, txHash[:]) - if v == nil { - return nil - } - details, err = s.unminedTxDetails(ns, txHash, v) - return err - } +func (s *Store) UniqueTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, + block *Block) (*TxDetails, error) { - k, v := existsTxRecord(ns, txHash, block) + if block == nil { + v := existsRawUnmined(ns, txHash[:]) if v == nil { - return nil + return nil, nil } - details, err = s.minedTxDetails(ns, txHash, k, v) - return err - }) - return details, err + return s.unminedTxDetails(ns, txHash, v) + } + + k, v := existsTxRecord(ns, txHash, block) + if v == nil { + return nil, nil + } + return s.minedTxDetails(ns, txHash, k, v) } // rangeUnminedTransactions executes the function f with TxDetails for every @@ -225,9 +213,9 @@ func (s *Store) UniqueTxDetails(txHash *chainhash.Hash, block *Block) (*TxDetail // Error returns from f (if any) are propigated to the caller. Returns true // (signaling breaking out of a RangeTransactions) iff f executes and returns // true. -func (s *Store) rangeUnminedTransactions(ns walletdb.Bucket, f func([]TxDetails) (bool, error)) (bool, error) { +func (s *Store) rangeUnminedTransactions(ns walletdb.ReadBucket, f func([]TxDetails) (bool, error)) (bool, error) { var details []TxDetails - err := ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnmined).ForEach(func(k, v []byte) error { if len(k) < 32 { str := fmt.Sprintf("%s: short key (expected %d "+ "bytes, read %d)", bucketUnmined, 32, len(k)) @@ -257,7 +245,9 @@ func (s *Store) rangeUnminedTransactions(ns walletdb.Bucket, f func([]TxDetails) // between heights begin and end (reverse order when end > begin) until f // returns true, or the transactions from block is processed. Returns true iff // f executes and returns true. -func (s *Store) rangeBlockTransactions(ns walletdb.Bucket, begin, end int32, f func([]TxDetails) (bool, error)) (bool, error) { +func (s *Store) rangeBlockTransactions(ns walletdb.ReadBucket, begin, end int32, + f func([]TxDetails) (bool, error)) (bool, error) { + // Mempool height is considered a high bound. if begin < 0 { begin = int32(^uint32(0) >> 1) @@ -270,7 +260,7 @@ func (s *Store) rangeBlockTransactions(ns walletdb.Bucket, begin, end int32, f f var advance func(*blockIterator) bool if begin < end { // Iterate in forwards order - blockIter = makeBlockIterator(ns, begin) + blockIter = makeReadBlockIterator(ns, begin) advance = func(it *blockIterator) bool { if !it.next() { return false @@ -279,7 +269,7 @@ func (s *Store) rangeBlockTransactions(ns walletdb.Bucket, begin, end int32, f f } } else { // Iterate in backwards order, from begin -> end. - blockIter = makeBlockIterator(ns, begin) + blockIter = makeReadBlockIterator(ns, begin) advance = func(it *blockIterator) bool { if !it.prev() { return false @@ -317,7 +307,7 @@ func (s *Store) rangeBlockTransactions(ns walletdb.Bucket, begin, end int32, f f return false, err } - credIter := makeCreditIterator(ns, k) + credIter := makeReadCreditIterator(ns, k) for credIter.next() { if int(credIter.elem.Index) >= len(detail.MsgTx.TxOut) { str := "saved credit index exceeds number of outputs" @@ -338,7 +328,7 @@ func (s *Store) rangeBlockTransactions(ns walletdb.Bucket, begin, end int32, f f return false, credIter.err } - debIter := makeDebitIterator(ns, k) + debIter := makeReadDebitIterator(ns, k) for debIter.next() { if int(debIter.elem.Index) >= len(detail.MsgTx.TxIn) { str := "saved debit index exceeds number of inputs" @@ -377,86 +367,89 @@ func (s *Store) rangeBlockTransactions(ns walletdb.Bucket, begin, end int32, f f // All calls to f are guaranteed to be passed a slice with more than zero // elements. The slice may be reused for multiple blocks, so it is not safe to // use it after the loop iteration it was acquired. -func (s *Store) RangeTransactions(begin, end int32, f func([]TxDetails) (bool, error)) error { - return scopedView(s.namespace, func(ns walletdb.Bucket) error { - var addedUnmined bool - if begin < 0 { - brk, err := s.rangeUnminedTransactions(ns, f) - if err != nil || brk { - return err - } - addedUnmined = true - } +func (s *Store) RangeTransactions(ns walletdb.ReadBucket, begin, end int32, + f func([]TxDetails) (bool, error)) error { - brk, err := s.rangeBlockTransactions(ns, begin, end, f) - if err == nil && !brk && !addedUnmined && end < 0 { - _, err = s.rangeUnminedTransactions(ns, f) + var addedUnmined bool + if begin < 0 { + brk, err := s.rangeUnminedTransactions(ns, f) + if err != nil || brk { + return err } - return err - }) + addedUnmined = true + } + + brk, err := s.rangeBlockTransactions(ns, begin, end, f) + if err == nil && !brk && !addedUnmined && end < 0 { + _, err = s.rangeUnminedTransactions(ns, f) + } + return err } // PreviousPkScripts returns a slice of previous output scripts for each credit // output this transaction record debits from. -func (s *Store) PreviousPkScripts(rec *TxRecord, block *Block) ([][]byte, error) { +func (s *Store) PreviousPkScripts(ns walletdb.ReadBucket, rec *TxRecord, block *Block) ([][]byte, error) { var pkScripts [][]byte - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - if block == nil { - for _, input := range rec.MsgTx.TxIn { - prevOut := &input.PreviousOutPoint - // Input may spend a previous unmined output, a - // mined output (which would still be marked - // unspent), or neither. + if block == nil { + for _, input := range rec.MsgTx.TxIn { + prevOut := &input.PreviousOutPoint - v := existsRawUnmined(ns, prevOut.Hash[:]) - if v != nil { - // Ensure a credit exists for this - // unmined transaction before including - // the output script. - k := canonicalOutPoint(&prevOut.Hash, prevOut.Index) - if existsRawUnminedCredit(ns, k) == nil { - continue - } + // Input may spend a previous unmined output, a + // mined output (which would still be marked + // unspent), or neither. - pkScript, err := fetchRawTxRecordPkScript( - prevOut.Hash[:], v, prevOut.Index) - if err != nil { - return err - } - pkScripts = append(pkScripts, pkScript) + v := existsRawUnmined(ns, prevOut.Hash[:]) + if v != nil { + // Ensure a credit exists for this + // unmined transaction before including + // the output script. + k := canonicalOutPoint(&prevOut.Hash, prevOut.Index) + if existsRawUnminedCredit(ns, k) == nil { continue } - _, credKey := existsUnspent(ns, prevOut) - if credKey != nil { - k := extractRawCreditTxRecordKey(credKey) - v = existsRawTxRecord(ns, k) - pkScript, err := fetchRawTxRecordPkScript(k, v, - prevOut.Index) - if err != nil { - return err - } - pkScripts = append(pkScripts, pkScript) + pkScript, err := fetchRawTxRecordPkScript( + prevOut.Hash[:], v, prevOut.Index) + if err != nil { + return nil, err } + pkScripts = append(pkScripts, pkScript) + continue } - return nil - } - recKey := keyTxRecord(&rec.Hash, block) - it := makeDebitIterator(ns, recKey) - for it.next() { - credKey := extractRawDebitCreditKey(it.cv) - index := extractRawCreditIndex(credKey) - k := extractRawCreditTxRecordKey(credKey) - v := existsRawTxRecord(ns, k) - pkScript, err := fetchRawTxRecordPkScript(k, v, index) - if err != nil { - return err + _, credKey := existsUnspent(ns, prevOut) + if credKey != nil { + k := extractRawCreditTxRecordKey(credKey) + v = existsRawTxRecord(ns, k) + pkScript, err := fetchRawTxRecordPkScript(k, v, + prevOut.Index) + if err != nil { + return nil, err + } + pkScripts = append(pkScripts, pkScript) + continue } - pkScripts = append(pkScripts, pkScript) } - return it.err - }) - return pkScripts, err + return pkScripts, nil + } + + recKey := keyTxRecord(&rec.Hash, block) + it := makeReadDebitIterator(ns, recKey) + for it.next() { + credKey := extractRawDebitCreditKey(it.cv) + index := extractRawCreditIndex(credKey) + k := extractRawCreditTxRecordKey(credKey) + v := existsRawTxRecord(ns, k) + pkScript, err := fetchRawTxRecordPkScript(k, v, index) + if err != nil { + return nil, err + } + pkScripts = append(pkScripts, pkScript) + } + if it.err != nil { + return nil, it.err + } + + return pkScripts, nil } diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go index 92eb9de..9efe9c7 100644 --- a/wtxmgr/tx.go +++ b/wtxmgr/tx.go @@ -1,4 +1,5 @@ -// Copyright (c) 2013-2016 The btcsuite developers +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -132,7 +133,6 @@ type Credit struct { // Store implements a transaction store for storing and managing wallet // transactions. type Store struct { - namespace walletdb.Namespace chainParams *chaincfg.Params // Event callbacks. These execute in the same goroutine as the wtxmgr @@ -140,28 +140,36 @@ type Store struct { NotifyUnspent func(hash *chainhash.Hash, index uint32) } +// DoUpgrades performs any necessary upgrades to the transaction history +// contained in the wallet database, namespaced by the top level bucket key +// namespaceKey. +func DoUpgrades(db walletdb.DB, namespaceKey []byte) error { + // No upgrades + return nil +} + // Open opens the wallet transaction store from a walletdb namespace. If the -// store does not exist, ErrNoExist is returned. Existing stores will be -// upgraded to new database formats as necessary. -func Open(namespace walletdb.Namespace, chainParams *chaincfg.Params) (*Store, error) { - // Open the store, upgrading to the latest version as needed. - err := openStore(namespace) +// store does not exist, ErrNoExist is returned. +func Open(ns walletdb.ReadBucket, chainParams *chaincfg.Params) (*Store, error) { + // Open the store. + err := openStore(ns) if err != nil { return nil, err } - return &Store{namespace, chainParams, nil}, nil // TODO: set callbacks + s := &Store{chainParams, nil} // TODO: set callbacks + return s, nil } // Create creates a new persistent transaction store in the walletdb namespace. // Creating the store when one already exists in this namespace will error with // ErrAlreadyExists. -func Create(namespace walletdb.Namespace) error { - return createStore(namespace) +func Create(ns walletdb.ReadWriteBucket) error { + return createStore(ns) } // moveMinedTx moves a transaction record from the unmined buckets to block // buckets. -func (s *Store) moveMinedTx(ns walletdb.Bucket, rec *TxRecord, recKey, recVal []byte, block *BlockMeta) error { +func (s *Store) moveMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, recKey, recVal []byte, block *BlockMeta) error { log.Infof("Marking unconfirmed transaction %v mined in block %d", &rec.Hash, block.Height) @@ -282,66 +290,19 @@ func (s *Store) moveMinedTx(ns walletdb.Bucket, rec *TxRecord, recKey, recVal [] // InsertTx records a transaction as belonging to a wallet's transaction // history. If block is nil, the transaction is considered unspent, and the // transaction's index must be unset. -func (s *Store) InsertTx(rec *TxRecord, block *BlockMeta) error { - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - if block == nil { - return s.insertMemPoolTx(ns, rec) - } - return s.insertMinedTx(ns, rec, block) - }) +func (s *Store) InsertTx(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta) error { + if block == nil { + return s.insertMemPoolTx(ns, rec) + } + return s.insertMinedTx(ns, rec, block) } // insertMinedTx inserts a new transaction record for a mined transaction into // the database. It is expected that the exact transation does not already // exist in the unmined buckets, but unmined double spends (including mutations) // are removed. -func (s *Store) insertMinedTx(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta) error { - // If a transaction record for this tx hash and block already exist, - // there is nothing left to do. - k, v := existsTxRecord(ns, &rec.Hash, &block.Block) - if v != nil { - return nil - } - - // If the exact tx (not a double spend) is already included but - // unconfirmed, move it to a block. - v = existsRawUnmined(ns, rec.Hash[:]) - if v != nil { - return s.moveMinedTx(ns, rec, k, v, block) - } - - // As there may be unconfirmed transactions that are invalidated by this - // transaction (either being duplicates, or double spends), remove them - // from the unconfirmed set. This also handles removing unconfirmed - // transaction spend chains if any other unconfirmed transactions spend - // outputs of the removed double spend. - err := s.removeDoubleSpends(ns, rec) - if err != nil { - return err - } - - // If a block record does not yet exist for any transactions from this - // block, insert the record. Otherwise, update it by adding the - // transaction hash to the set of transactions from this block. - blockKey, blockValue := existsBlockRecord(ns, block.Height) - if blockValue == nil { - err = putBlockRecord(ns, block, &rec.Hash) - } else { - blockValue, err = appendRawBlockRecord(blockValue, &rec.Hash) - if err != nil { - return err - } - err = putRawBlockRecord(ns, blockKey, blockValue) - } - if err != nil { - return err - } - - err = putTxRecord(ns, rec, &block.Block) - if err != nil { - return err - } - +func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta) error { + // Fetch the mined balance in case we need to update it. minedBalance, err := fetchMinedBalance(ns) if err != nil { return err @@ -395,7 +356,60 @@ func (s *Store) insertMinedTx(ns walletdb.Bucket, rec *TxRecord, block *BlockMet } } - return putMinedBalance(ns, minedBalance) + // TODO only update if we actually modified the + // mined balance. + err = putMinedBalance(ns, minedBalance) + if err != nil { + return nil + } + + // If a transaction record for this tx hash and block already exist, + // there is nothing left to do. + k, v := existsTxRecord(ns, &rec.Hash, &block.Block) + if v != nil { + return nil + } + + // If the exact tx (not a double spend) is already included but + // unconfirmed, move it to a block. + v = existsRawUnmined(ns, rec.Hash[:]) + if v != nil { + return s.moveMinedTx(ns, rec, k, v, block) + } + + // As there may be unconfirmed transactions that are invalidated by this + // transaction (either being duplicates, or double spends), remove them + // from the unconfirmed set. This also handles removing unconfirmed + // transaction spend chains if any other unconfirmed transactions spend + // outputs of the removed double spend. + err = s.removeDoubleSpends(ns, rec) + if err != nil { + return err + } + + // If a block record does not yet exist for any transactions from this + // block, insert the record. Otherwise, update it by adding the + // transaction hash to the set of transactions from this block. + blockKey, blockValue := existsBlockRecord(ns, block.Height) + if blockValue == nil { + err = putBlockRecord(ns, block, &rec.Hash) + } else { + blockValue, err = appendRawBlockRecord(blockValue, &rec.Hash) + if err != nil { + return err + } + err = putRawBlockRecord(ns, blockKey, blockValue) + } + if err != nil { + return err + } + + err = putTxRecord(ns, rec, &block.Block) + if err != nil { + return err + } + + return nil } // AddCredit marks a transaction record as containing a transaction output @@ -405,18 +419,13 @@ func (s *Store) insertMinedTx(ns walletdb.Bucket, rec *TxRecord, block *BlockMet // TODO(jrick): This should not be necessary. Instead, pass the indexes // that are known to contain credits when a transaction or merkleblock is // inserted into the store. -func (s *Store) AddCredit(rec *TxRecord, block *BlockMeta, index uint32, change bool) error { +func (s *Store) AddCredit(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) error { if int(index) >= len(rec.MsgTx.TxOut) { str := "transaction output does not exist" return storeError(ErrInput, str, nil) } - var isNew bool - err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - var err error - isNew, err = s.addCredit(ns, rec, block, index, change) - return err - }) + isNew, err := s.addCredit(ns, rec, block, index, change) if err == nil && isNew && s.NotifyUnspent != nil { s.NotifyUnspent(&rec.Hash, index) } @@ -426,7 +435,7 @@ func (s *Store) AddCredit(rec *TxRecord, block *BlockMeta, index uint32, change // addCredit is an AddCredit helper that runs in an update transaction. The // bool return specifies whether the unspent output is newly added (true) or a // duplicate (false). -func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) (bool, error) { +func (s *Store) addCredit(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) (bool, error) { if block == nil { k := canonicalOutPoint(&rec.Hash, index) if existsRawUnminedCredit(ns, k) != nil { @@ -475,13 +484,11 @@ func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, i // Rollback removes all blocks at height onwards, moving any transactions within // each block to the unconfirmed pool. -func (s *Store) Rollback(height int32) error { - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - return s.rollback(ns, height) - }) +func (s *Store) Rollback(ns walletdb.ReadWriteBucket, height int32) error { + return s.rollback(ns, height) } -func (s *Store) rollback(ns walletdb.Bucket, height int32) error { +func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { minedBalance, err := fetchMinedBalance(ns) if err != nil { return err @@ -693,22 +700,12 @@ func (s *Store) rollback(ns walletdb.Bucket, height int32) error { // UnspentOutputs returns all unspent received transaction outputs. // The order is undefined. -func (s *Store) UnspentOutputs() ([]Credit, error) { - var credits []Credit - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - credits, err = s.unspentOutputs(ns) - return err - }) - return credits, err -} - -func (s *Store) unspentOutputs(ns walletdb.Bucket) ([]Credit, error) { +func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { var unspent []Credit var op wire.OutPoint var block Block - err := ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error { err := readCanonicalOutPoint(k, &op) if err != nil { return err @@ -757,7 +754,7 @@ func (s *Store) unspentOutputs(ns walletdb.Bucket) ([]Credit, error) { return nil, storeError(ErrDatabase, str, err) } - err = ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { + err = ns.NestedReadBucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { if existsRawUnminedInput(ns, k) != nil { // Output is spent by an unmined transaction. // Skip to next unmined credit. @@ -810,17 +807,7 @@ func (s *Store) unspentOutputs(ns walletdb.Bucket) ([]Credit, error) { // // Balance may return unexpected results if syncHeight is lower than the block // height of the most recent mined transaction in the store. -func (s *Store) Balance(minConf, syncHeight int32) (btcutil.Amount, error) { - var amt btcutil.Amount - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - amt, err = s.balance(ns, minConf, syncHeight) - return err - }) - return amt, err -} - -func (s *Store) balance(ns walletdb.Bucket, minConf int32, syncHeight int32) (btcutil.Amount, error) { +func (s *Store) Balance(ns walletdb.ReadBucket, minConf int32, syncHeight int32) (btcutil.Amount, error) { bal, err := fetchMinedBalance(ns) if err != nil { return 0, err @@ -830,7 +817,7 @@ func (s *Store) balance(ns walletdb.Bucket, minConf int32, syncHeight int32) (bt // transaction. var op wire.OutPoint var block Block - err = ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + err = ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error { err := readCanonicalOutPoint(k, &op) if err != nil { return err @@ -865,7 +852,7 @@ func (s *Store) balance(ns walletdb.Bucket, minConf int32, syncHeight int32) (bt stopConf = coinbaseMaturity } lastHeight := syncHeight - stopConf - blockIt := makeReverseBlockIterator(ns) + blockIt := makeReadReverseBlockIterator(ns) for blockIt.prev() { block := &blockIt.elem @@ -915,7 +902,7 @@ func (s *Store) balance(ns walletdb.Bucket, minConf int32, syncHeight int32) (bt // If unmined outputs are included, increment the balance for each // output that is unspent. if minConf == 0 { - err = ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { + err = ns.NestedReadBucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { if existsRawUnminedInput(ns, k) != nil { // Output is spent by an unmined transaction. // Skip to next unmined credit. diff --git a/wtxmgr/unconfirmed.go b/wtxmgr/unconfirmed.go index 8653fca..be68dba 100644 --- a/wtxmgr/unconfirmed.go +++ b/wtxmgr/unconfirmed.go @@ -1,4 +1,5 @@ -// Copyright (c) 2013-2016 The btcsuite developers +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -12,7 +13,7 @@ import ( // insertMemPoolTx inserts the unmined transaction record. It also marks // previous outputs referenced by the inputs as spent. -func (s *Store) insertMemPoolTx(ns walletdb.Bucket, rec *TxRecord) error { +func (s *Store) insertMemPoolTx(ns walletdb.ReadWriteBucket, rec *TxRecord) error { v := existsRawUnmined(ns, rec.Hash[:]) if v != nil { // TODO: compare serialized txs to ensure this isn't a hash collision? @@ -48,7 +49,7 @@ func (s *Store) insertMemPoolTx(ns walletdb.Bucket, rec *TxRecord) error { // a double spend if tx was added to the store (either as a confirmed or unmined // transaction). Each conflicting transaction and all transactions which spend // it are recursively removed. -func (s *Store) removeDoubleSpends(ns walletdb.Bucket, rec *TxRecord) error { +func (s *Store) removeDoubleSpends(ns walletdb.ReadWriteBucket, rec *TxRecord) error { for _, input := range rec.MsgTx.TxIn { prevOut := &input.PreviousOutPoint prevOutKey := canonicalOutPoint(&prevOut.Hash, prevOut.Index) @@ -78,7 +79,7 @@ func (s *Store) removeDoubleSpends(ns walletdb.Bucket, rec *TxRecord) error { // deriving from it from the store. This is designed to remove transactions // that would otherwise result in double spend conflicts if left in the store, // and to remove transactions that spend coinbase transactions on reorgs. -func (s *Store) removeConflict(ns walletdb.Bucket, rec *TxRecord) error { +func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error { // For each potential credit for this record, each spender (if any) must // be recursively removed as well. Once the spenders are removed, the // credit is deleted. @@ -126,13 +127,8 @@ func (s *Store) removeConflict(ns walletdb.Bucket, rec *TxRecord) error { // UnminedTxs returns the underlying transactions for all unmined transactions // which are not known to have been mined in a block. Transactions are // guaranteed to be sorted by their dependency order. -func (s *Store) UnminedTxs() ([]*wire.MsgTx, error) { - var recSet map[chainhash.Hash]*TxRecord - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - recSet, err = s.unminedTxRecords(ns) - return err - }) +func (s *Store) UnminedTxs(ns walletdb.ReadBucket) ([]*wire.MsgTx, error) { + recSet, err := s.unminedTxRecords(ns) if err != nil { return nil, err } @@ -145,9 +141,9 @@ func (s *Store) UnminedTxs() ([]*wire.MsgTx, error) { return txs, nil } -func (s *Store) unminedTxRecords(ns walletdb.Bucket) (map[chainhash.Hash]*TxRecord, error) { +func (s *Store) unminedTxRecords(ns walletdb.ReadBucket) (map[chainhash.Hash]*TxRecord, error) { unmined := make(map[chainhash.Hash]*TxRecord) - err := ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnmined).ForEach(func(k, v []byte) error { var txHash chainhash.Hash err := readRawUnminedHash(k, &txHash) if err != nil { @@ -167,19 +163,13 @@ func (s *Store) unminedTxRecords(ns walletdb.Bucket) (map[chainhash.Hash]*TxReco // UnminedTxHashes returns the hashes of all transactions not known to have been // mined in a block. -func (s *Store) UnminedTxHashes() ([]*chainhash.Hash, error) { - var hashes []*chainhash.Hash - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - hashes, err = s.unminedTxHashes(ns) - return err - }) - return hashes, err +func (s *Store) UnminedTxHashes(ns walletdb.ReadBucket) ([]*chainhash.Hash, error) { + return s.unminedTxHashes(ns) } -func (s *Store) unminedTxHashes(ns walletdb.Bucket) ([]*chainhash.Hash, error) { +func (s *Store) unminedTxHashes(ns walletdb.ReadBucket) ([]*chainhash.Hash, error) { var hashes []*chainhash.Hash - err := ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnmined).ForEach(func(k, v []byte) error { hash := new(chainhash.Hash) err := readRawUnminedHash(k, hash) if err == nil {