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.
This commit is contained in:
Josh Rickmar 2017-01-16 19:19:02 -05:00 committed by Olaoluwa Osuntokun
parent 521b2123de
commit 4656a00705
30 changed files with 4069 additions and 2788 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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{

View file

@ -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
}

View file

@ -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.

View file

@ -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
}

View file

@ -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:
// <blockheight><blockhash>
@ -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:
// <blockheight><blockhash>
@ -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:
// <blockheight><blockhash>
@ -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:
// <blockheight><blockhash>
@ -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:
// <blockheight><numhashes><blockhashes>
@ -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:
// <blockheight><numhashes><blockhashes>
@ -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)
}

File diff suppressed because it is too large Load diff

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
}

88
wallet/common.go Normal file
View file

@ -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
}

View file

@ -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

105
wallet/multisig.go Normal file
View file

@ -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
}

View file

@ -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

View file

@ -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

710
wallet/sync.go Normal file
View file

@ -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
}

44
wallet/unstable.go Normal file
View file

@ -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)
})
}

90
wallet/utxos.go Normal file
View file

@ -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
}

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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
}

View file

@ -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",
}

View file

@ -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
}

View file

@ -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.

View file

@ -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 {