diff --git a/chain/chain.go b/chain/chain.go index a794d83f1..2ece5dbfa 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -33,11 +33,6 @@ type RPCClient struct { dequeueVotingNotification chan interface{} currentBlock chan *waddrmgr.BlockStamp - // Information for reorganization handling. - reorganizingLock sync.Mutex - reorganizeToHash chainhash.Hash - reorganizing bool - quit chan struct{} wg sync.WaitGroup started bool @@ -68,8 +63,6 @@ func NewRPCClient(chainParams *chaincfg.Params, connect, user, pass string, cert DisableConnectOnNew: true, DisableTLS: disableTLS, }, - reorganizeToHash: chainhash.Hash{}, - reorganizing: false, chainParams: chainParams, reconnectAttempts: reconnectAttempts, enqueueNotification: make(chan interface{}), @@ -229,25 +222,6 @@ type ( } ) -// SetReorganizingState allows you to set the flag for blockchain -// reorganization. -func (c *RPCClient) SetReorganizingState(is bool, hash chainhash.Hash) { - c.reorganizingLock.Lock() - defer c.reorganizingLock.Unlock() - - c.reorganizing = is - c.reorganizeToHash = hash -} - -// GetReorganizing allows you to get the value of the flag for blockchain -// reorganization. -func (c *RPCClient) GetReorganizing() (bool, chainhash.Hash) { - c.reorganizingLock.Lock() - defer c.reorganizingLock.Unlock() - - return c.reorganizing, c.reorganizeToHash -} - // Notifications returns a channel of parsed notifications sent by the remote // decred RPC server. This channel must be continually read or the process // may abort for running out memory, as unread notifications are queued for diff --git a/cmd/dropwtxmgr/main.go b/cmd/dropwtxmgr/main.go index 6eae1cbb7..b6b27b78a 100644 --- a/cmd/dropwtxmgr/main.go +++ b/cmd/dropwtxmgr/main.go @@ -104,7 +104,9 @@ func mainInt() int { } defer db.Close() fmt.Println("Dropping wtxmgr namespace") - err = db.DeleteNamespace(wtxmgrNamespace) + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + return tx.DeleteTopLevelBucket(wtxmgrNamespace) + }) if err != nil && err != walletdb.ErrBucketNotFound { fmt.Println("Failed to drop namespace:", err) return 1 diff --git a/goclean.sh b/goclean.sh index b771b9359..9deadacb2 100755 --- a/goclean.sh +++ b/goclean.sh @@ -10,7 +10,7 @@ set -ex # Automatic checks test -z "$(go fmt $(glide novendor) | tee /dev/stderr)" # test -z "$(goimports -l -w . | tee /dev/stderr)" -test -z "$(for package in $(glide novendor); do golint $package; done | grep -v 'ALL_CAPS\|OP_\|NewFieldVal\|RpcCommand\|RpcRawCommand\|RpcSend\|Dns\|api.pb.go\|StartConsensusRpc\|factory_test.go\|legacy' | tee /dev/stderr)" +test -z "$(for package in $(glide novendor); do golint $package; done | grep -v 'ALL_CAPS\|OP_\|NewFieldVal\|RpcCommand\|RpcRawCommand\|RpcSend\|Dns\|api.pb.go\|StartConsensusRpc\|factory_test.go\|legacy\|UnstableAPI' | tee /dev/stderr)" test -z "$(go vet $(glide novendor) 2>&1 | grep -v '^exit status \|Example\|newestSha\| not a string in call to Errorf$' | tee /dev/stderr)" ############RE-ENABLE THIS WHEN TESTS ARE FIXED!!!!!!!!!!!############### #env GORACE="halt_on_error=1" go test -v -race $(glide novendor) diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index 628755a43..34d0ffdd8 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -35,13 +35,6 @@ import ( "github.com/decred/dcrwallet/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 { @@ -328,7 +321,7 @@ func jsonError(err error) *dcrjson.RPCError { // account and branch. func accountAddressIndex(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.AccountAddressIndexCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } @@ -351,7 +344,7 @@ func accountAddressIndex(icmd interface{}, w *wallet.Wallet) (interface{}, error // passed account and branch. func accountFetchAddresses(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.AccountFetchAddressesCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } @@ -369,7 +362,7 @@ func accountFetchAddresses(icmd interface{}, w *wallet.Wallet) (interface{}, err cmd.End) } - addrs, err := w.Manager.AddressesDerivedFromDbAcct(uint32(cmd.Start), + addrs, err := w.AccountBranchAddressRange(uint32(cmd.Start), uint32(cmd.End), account, branch) if err != nil { return nil, err @@ -389,7 +382,7 @@ func accountFetchAddresses(icmd interface{}, w *wallet.Wallet) (interface{}, err // is successful, nothing is returned. func accountSyncAddressIndex(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.AccountSyncAddressIndexCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } @@ -418,10 +411,6 @@ func accountSyncAddressIndex(icmd interface{}, w *wallet.Wallet) (interface{}, e return nil, w.SyncAddressPoolIndex(account, branch, index) } -// makeMultiSigScript is a helper function to combine common logic for -// AddMultiSig and CreateMultiSig. -// all error codes are rpc parse error here to match bitcoind which just throws -// a runtime exception. *sigh*. func makeMultiSigScript(w *wallet.Wallet, keys []string, nRequired int) ([]byte, error) { keysesPrecious := make([]*dcrutil.AddressSecpPubKey, len(keys)) @@ -439,25 +428,21 @@ func makeMultiSigScript(w *wallet.Wallet, keys []string, switch addr := a.(type) { case *dcrutil.AddressSecpPubKey: keysesPrecious[i] = addr - case *dcrutil.AddressPubKeyHash: - ainfo, err := w.Manager.Address(addr) + default: + pubKey, err := w.PubKeyForAddress(addr) if err != nil { return nil, err } - - apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) - - // This will be an addresspubkey - a, err := decodeAddress(apkinfo.ExportPubKey(), - w.ChainParams()) + if pubKey.GetType() != chainec.ECTypeSecp256k1 { + return nil, errors.New("only secp256k1 " + + "pubkeys are currently supported") + } + pubKeyAddr, err := dcrutil.NewAddressSecpPubKey( + pubKey.Serialize(), w.ChainParams()) if err != nil { return nil, err } - - apk := a.(*dcrutil.AddressSecpPubKey) - keysesPrecious[i] = apk - default: - return nil, err + keysesPrecious[i] = pubKeyAddr } } @@ -474,34 +459,31 @@ func addMultiSigAddress(icmd interface{}, w *wallet.Wallet, chainClient *chain.R return nil, &ErrNotImportedAccount } - script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) - if err != nil { - return nil, ParseError{err} + secp256k1Addrs := make([]dcrutil.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 } - // Insert into the tx store. - err = w.TxStore.InsertTxScript(script) + script, err := w.MakeSecp256k1MultiSigScript(secp256k1Addrs, cmd.NRequired) if err != nil { return nil, err } - // TODO(oga) blockstamp current block? - bs := &waddrmgr.BlockStamp{ - Hash: *w.ChainParams().GenesisHash, - Height: 0, - } - - addr, err := w.Manager.ImportScript(script, bs) + p2shAddr, err := w.ImportP2SHRedeemScript(script) if err != nil { return nil, err } - err = chainClient.NotifyReceived([]dcrutil.Address{addr.Address()}) + err = chainClient.NotifyReceived([]dcrutil.Address{p2shAddr}) if err != nil { return nil, err } - return addr.Address().EncodeAddress(), nil + return p2shAddr.EncodeAddress(), nil } // addTicket adds a ticket to the stake manager manually. @@ -518,10 +500,7 @@ func addTicket(icmd interface{}, w *wallet.Wallet) (interface{}, error) { if err != nil { return nil, err } - tx := dcrutil.NewTx(mtx) - - err = w.StakeMgr.InsertSStx(tx, w.VoteBits) - + err = w.AddTicket(dcrutil.NewTx(mtx)) return nil, err } @@ -533,7 +512,7 @@ func consolidate(icmd interface{}, w *wallet.Wallet) (interface{}, error) { account := uint32(waddrmgr.DefaultAccountNum) var err error if cmd.Account != nil { - account, err = w.Manager.LookupAccount(*cmd.Account) + account, err = w.AccountNumber(*cmd.Account) if err != nil { return nil, err } @@ -621,7 +600,7 @@ func dumpWallet(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func getAddressesByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.GetAddressesByAccountCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } @@ -643,7 +622,7 @@ func getAddressesByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, err // Derive the addresses. addrsStr := make([]string, endInt+endExt) - addrsExt, err := w.Manager.AddressesDerivedFromDbAcct(0, endExt, + addrsExt, err := w.AccountBranchAddressRange(0, endExt, account, waddrmgr.ExternalBranch) if err != nil { return nil, err @@ -651,7 +630,7 @@ func getAddressesByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, err for i := range addrsExt { addrsStr[i] = addrsExt[i].EncodeAddress() } - addrsInt, err := w.Manager.AddressesDerivedFromDbAcct(0, endInt, + addrsInt, err := w.AccountBranchAddressRange(0, endInt, account, waddrmgr.InternalBranch) if err != nil { return nil, err @@ -696,7 +675,7 @@ func getBalance(icmd interface{}, w *wallet.Wallet) (interface{}, error) { balType) } else { var account uint32 - account, err = w.Manager.LookupAccount(accountName) + account, err = w.AccountNumber(accountName) if err != nil { return nil, err } @@ -821,12 +800,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 } @@ -842,7 +821,7 @@ func getAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func getAccountAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.GetAccountAddressCmd) - account, err := w.Manager.LookupAccount(cmd.Account) + account, err := w.AccountNumber(cmd.Account) if err != nil { return nil, err } @@ -863,7 +842,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 } @@ -967,23 +946,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, &dcrjson.RPCError{ Code: dcrjson.ErrRPCWalletUnlockNeeded, @@ -1006,7 +969,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 } @@ -1029,43 +992,17 @@ func getMultisigOutInfo(icmd interface{}, w *wallet.Wallet, chainClient *chain.R Index: cmd.Index, Tree: dcrutil.TxTreeRegular, } - mso, err := w.TxStore.GetMultisigOutput(op) - if err != nil { - return nil, err - } - - scriptAddr, err := dcrutil.NewAddressScriptHashFromHash(mso.ScriptHash[:], - w.ChainParams()) - if err != nil { - return nil, err - } - redeemScript, err := w.TxStore.GetTxScript(mso.ScriptHash[:]) + p2shOutput, err := w.FetchP2SHMultiSigOutput(op) if err != nil { return nil, err } - // Couldn't find it, look in the manager too. - if redeemScript == nil { - address, err := w.Manager.Address(scriptAddr) - if err != nil { - return nil, err - } - sa, ok := address.(waddrmgr.ManagedScriptAddress) - if !ok { - return nil, errors.New("address is not a script" + - " address") - } - - redeemScript, err = sa.Script() - if err != nil { - return nil, err - } - } // Get the list of pubkeys required to sign. var pubkeys []string _, pubkeyAddrs, _, err := txscript.ExtractPkScriptAddrs( - txscript.DefaultScriptVersion, redeemScript, w.ChainParams()) + txscript.DefaultScriptVersion, p2shOutput.RedeemScript, + w.ChainParams()) if err != nil { return nil, err } @@ -1073,20 +1010,25 @@ func getMultisigOutInfo(icmd interface{}, w *wallet.Wallet, chainClient *chain.R pubkeys = append(pubkeys, hex.EncodeToString(pka.ScriptAddress())) } - return dcrjson.GetMultisigOutInfoResult{ - Address: scriptAddr.EncodeAddress(), - RedeemScript: hex.EncodeToString(redeemScript), - M: mso.M, - N: mso.N, + result := &dcrjson.GetMultisigOutInfoResult{ + Address: p2shOutput.P2SHAddress.EncodeAddress(), + RedeemScript: hex.EncodeToString(p2shOutput.RedeemScript), + M: p2shOutput.M, + N: p2shOutput.N, Pubkeys: pubkeys, - TxHash: mso.TxHash.String(), - BlockHeight: mso.BlockHeight, - BlockHash: mso.BlockHash.String(), - Spent: mso.Spent, - SpentBy: mso.SpentBy.String(), - SpentByIndex: mso.SpentByIndex, - Amount: mso.Amount.ToCoin(), - }, nil + TxHash: p2shOutput.OutPoint.Hash.String(), + Amount: p2shOutput.OutputAmount.ToCoin(), + } + if !p2shOutput.ContainingBlock.None() { + result.BlockHeight = uint32(p2shOutput.ContainingBlock.Height) + result.BlockHash = p2shOutput.ContainingBlock.Hash.String() + } + if p2shOutput.Redeemer != nil { + result.Spent = true + result.SpentBy = p2shOutput.Redeemer.TxHash.String() + result.SpentByIndex = p2shOutput.Redeemer.InputIndex + } + return result, nil } // getNewAddress handles a getnewaddress request by returning a new @@ -1101,7 +1043,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 } @@ -1115,21 +1057,16 @@ func getNewAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { toReturn := make(map[string]string) toReturn["address"] = addr.EncodeAddress() - ainfo, err := w.Manager.Address(addr) + pubKey, err := w.PubKeyForAddress(addr) if err != nil { return nil, err } - - apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) - - // This will be an addresspubkey. - a, err := decodeAddress(apkinfo.ExportPubKey(), - w.ChainParams()) + pubKeyAddr, err := dcrutil.NewAddressSecpPubKey( + pubKey.Serialize(), w.ChainParams()) if err != nil { return nil, err } - apk := a.(*dcrutil.AddressSecpPubKey) - toReturn["pubkey"] = apk.String() + toReturn["pubkey"] = pubKeyAddr.String() // Return the new payment address string along with the pubkey. return toReturn, nil @@ -1150,7 +1087,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 } @@ -1170,17 +1107,23 @@ func getRawChangeAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error func getReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.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.ToCoin(), nil + acctIndex := int(account) + if account == waddrmgr.ImportedAddrAccount { + acctIndex = len(results) - 1 + } + return results[acctIndex].TotalReceived.ToCoin(), nil } // getReceivedByAddress handles a getreceivedbyaddress request by returning @@ -1217,29 +1160,19 @@ func getMasterPubkey(icmd interface{}, w *wallet.Wallet) (interface{}, error) { account := uint32(waddrmgr.DefaultAccountNum) if cmd.Account != nil { var err error - account, err = w.Manager.LookupAccount(*cmd.Account) + account, err = w.AccountNumber(*cmd.Account) if err != nil { return nil, err } } - pkString, err := w.Manager.GetMasterPubkey(account) - if err != nil { - return nil, err - } - - return pkString, nil + return w.MasterPubKey(account) } // getSeed handles a getseed request by returning the wallet seed encoded as // a string. func getSeed(icmd interface{}, w *wallet.Wallet) (interface{}, error) { - seedStr, err := w.Manager.GetSeed() - if err != nil { - return nil, err - } - - return seedStr, nil + return w.Seed() } // getStakeInfo gets a large amounts of information about the stake environment @@ -1296,81 +1229,23 @@ func getTicketMaxPrice(icmd interface{}, w *wallet.Wallet) (interface{}, error) return w.GetTicketMaxPrice().ToCoin(), nil } -// hashInSlice returns whether a hash exists in a slice or not. -func hashInSlice(h chainhash.Hash, list []chainhash.Hash) bool { - for _, hash := range list { - if h == hash { - return true - } - } - - return false -} - // getTickets handles a gettickets request by returning the hashes of the tickets // currently owned by wallet, encoded as strings. func getTickets(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { cmd := icmd.(*dcrjson.GetTicketsCmd) - blk := w.Manager.SyncedTo() - - // UnspentTickets collects all the tickets that pay out to a - // public key hash for a public key owned by this wallet. - tickets, err := w.TxStore.UnspentTickets(blk.Height, cmd.IncludeImmature) - if err != nil { - return nil, err - } - // Access the stake manager and see if there are any extra tickets - // there. Likely they were either pruned because they failed to get - // into the blockchain or they are P2SH for some script we own. - var extraTickets []chainhash.Hash - stakeMgrTickets, err := w.StakeMgr.DumpSStxHashes() + ticketHashes, err := w.LiveTicketHashes(chainClient, cmd.IncludeImmature) if err != nil { return nil, err } - for _, h := range stakeMgrTickets { - if !hashInSlice(h, tickets) { - extraTickets = append(extraTickets, h) - } - } - for _, h := range extraTickets { - // Get the raw transaction information from daemon and add - // any relevant tickets. The ticket output is always the - // zeroeth output. - spent, err := chainClient.GetTxOut(&h, 0, true) - if err != nil { - continue - } - // This returns nil if the output is spent. - if spent == nil { - continue - } - - ticketTx, err := chainClient.GetRawTransactionVerbose(&h) - if err != nil { - continue - } - - txHeight := ticketTx.BlockHeight - unconfirmed := (txHeight == 0) - immature := (blk.Height-int32(txHeight) < - int32(w.ChainParams().TicketMaturity)) - if cmd.IncludeImmature { - tickets = append(tickets, h) - } else { - if !(unconfirmed || immature) { - tickets = append(tickets, h) - } - } - } // Compose a slice of strings to return. - ticketsStr := make([]string, len(tickets), len(tickets)) - for i, ticket := range tickets { - ticketsStr[i] = ticket.String() + ticketHashStrs := make([]string, 0, len(ticketHashes)) + for i := range ticketHashes { + ticketHashStrs = append(ticketHashStrs, ticketHashes[i].String()) } - return &dcrjson.GetTicketsResult{Hashes: ticketsStr}, nil + return &dcrjson.GetTicketsResult{Hashes: ticketHashStrs}, nil } // getTicketVoteBits fetches the per-ticket voteBits for a given ticket from @@ -1384,19 +1259,10 @@ func getTicketVoteBits(icmd interface{}, w *wallet.Wallet) (interface{}, error) return nil, err } - set, voteBits, err := w.StakeMgr.SStxVoteBits(ticket) + voteBits, err := w.VoteBitsForTicket(ticket) if err != nil { return nil, err } - if !set { - return &dcrjson.GetTicketVoteBitsResult{ - VoteBitsData: dcrjson.VoteBitsData{ - VoteBits: w.VoteBits, - VoteBitsExt: "", - }, - }, nil - } - return &dcrjson.GetTicketVoteBitsResult{ VoteBitsData: dcrjson.VoteBitsData{ VoteBits: voteBits, @@ -1423,19 +1289,14 @@ func getTicketsVoteBits(icmd interface{}, w *wallet.Wallet) (interface{}, error) voteBitsData := make([]dcrjson.VoteBitsData, 0, ticketsLen) for _, th := range ticketHashes { - set, voteBits, err := w.StakeMgr.SStxVoteBits(th) + voteBits, err := w.VoteBitsForTicket(th) if err != nil { return nil, err } - var vbr dcrjson.VoteBitsData - if !set { - vbr.VoteBits = w.VoteBits - vbr.VoteBitsExt = "" - } else { - vbr.VoteBits = voteBits - vbr.VoteBitsExt = "" - } - voteBitsData = append(voteBitsData, vbr) + voteBitsData = append(voteBitsData, dcrjson.VoteBitsData{ + VoteBits: voteBits, + VoteBitsExt: "", + }) } return &dcrjson.GetTicketsVoteBitsResult{VoteBitsList: voteBitsData}, nil @@ -1454,7 +1315,7 @@ func getTransaction(icmd interface{}, w *wallet.Wallet) (interface{}, error) { } } - details, err := w.TxStore.TxDetails(txSha) + details, err := wallet.UnstableAPI(w).TxDetails(txSha) if err != nil { return nil, err } @@ -1560,11 +1421,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 } } } @@ -1716,26 +1577,12 @@ func listAccounts(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.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), wtxmgr.BFBalanceFullScan) 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 - } - bal, err := w.CalculateAccountBalance(account, minConf, - wtxmgr.BFBalanceFullScan) - if err != nil { - return nil, err - } - accountBalances[acctName] = bal.ToCoin() + for _, result := range results { + accountBalances[result.AccountName] = result.AccountBalance.ToCoin() } // Return the map. This will be marshaled into a JSON object. return accountBalances, nil @@ -1760,34 +1607,20 @@ func listLockUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func listReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.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([]dcrjson.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, dcrjson.ListReceivedByAccountResult{ - Account: acctName, - Amount: bal.ToCoin(), - Confirmations: uint64(confirmations), + jsonResults := make([]dcrjson.ListReceivedByAccountResult, 0, len(results)) + for _, result := range results { + jsonResults = append(jsonResults, dcrjson.ListReceivedByAccountResult{ + Account: result.AccountName, + Amount: result.TotalReceived.ToCoin(), + Confirmations: uint64(result.LastConfirmation), }) } - return ret, nil + return jsonResults, nil } // listReceivedByAddress handles a listreceivedbyaddress request by returning @@ -1838,7 +1671,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 { @@ -1944,67 +1777,23 @@ type scriptInfo struct { // listScripts handles a listscripts request by returning an // array of script details for all scripts in the wallet. func listScripts(icmd interface{}, w *wallet.Wallet) (interface{}, error) { - scriptList := make(map[[20]byte]scriptInfo) - - // Fetch all the address manager scripts first. - importedAcct, err := w.Manager.LookupAccount(waddrmgr.ImportedAddrAccountName) + redeemScripts, err := w.FetchAllRedeemScripts() if err != nil { return nil, err } - - var managerScriptAddrs []waddrmgr.ManagedScriptAddress - err = w.Manager.ForEachAccountAddress(importedAcct, - func(maddr waddrmgr.ManagedAddress) error { - msa, is := maddr.(waddrmgr.ManagedScriptAddress) - if is { - managerScriptAddrs = append(managerScriptAddrs, msa) - } - return nil - }) - if err != nil { - log.Errorf("failed to iterate through the addrmgr scripts: %v", err) - } - for _, msa := range managerScriptAddrs { - h := msa.Address().Hash160() - scr, err := msa.Script() + listScriptsResultSIs := make([]dcrjson.ScriptInfo, len(redeemScripts)) + for i, redeemScript := range redeemScripts { + p2shAddr, err := dcrutil.NewAddressScriptHash(redeemScript, + w.ChainParams()) if err != nil { return nil, err } - scriptList[*h] = scriptInfo{ - redeemScript: scr, - address: msa.Address(), - } - } - - // Fetch all the scripts from the transaction manager. - txsScripts, err := w.TxStore.StoredTxScripts() - if err != nil { - return nil, fmt.Errorf("failed to access stored txmgr scripts") - } - for _, scr := range txsScripts { - addr, err := dcrutil.NewAddressScriptHash(scr, w.ChainParams()) - if err != nil { - log.Errorf("failed to parse txstore script: %v", err) - continue - } - h := addr.Hash160() - scriptList[*h] = scriptInfo{ - redeemScript: scr, - address: addr, + listScriptsResultSIs[i] = dcrjson.ScriptInfo{ + Hash160: hex.EncodeToString(p2shAddr.Hash160()[:]), + Address: p2shAddr.EncodeAddress(), + RedeemScript: hex.EncodeToString(redeemScript), } } - - // Generate the JSON struct result. - listScriptsResultSIs := make([]dcrjson.ScriptInfo, len(scriptList)) - itr := 0 - for h, si := range scriptList { - listScriptsResultSIs[itr].Hash160 = hex.EncodeToString(h[:]) - listScriptsResultSIs[itr].RedeemScript = - hex.EncodeToString(si.redeemScript) - listScriptsResultSIs[itr].Address = si.address.EncodeAddress() - itr++ - } - return &dcrjson.ListScriptsResult{Scripts: listScriptsResultSIs}, nil } @@ -2139,7 +1928,7 @@ func purchaseTicket(icmd interface{}, w *wallet.Wallet) (interface{}, error) { return nil, ErrNeedPositiveSpendLimit } - account, err := w.Manager.LookupAccount(cmd.FromAccount) + account, err := w.AccountNumber(cmd.FromAccount) if err != nil { return nil, err } @@ -2312,12 +2101,12 @@ func redeemMultiSigOut(icmd interface{}, w *wallet.Wallet, chainClient *chain.RP Index: cmd.Index, Tree: cmd.Tree, } - msCredit, err := w.TxStore.GetMultisigCredit(&op) + p2shOutput, err := w.FetchP2SHMultiSigOutput(&op) if err != nil { return nil, err } sc := txscript.GetScriptClass(txscript.DefaultScriptVersion, - msCredit.MSScript) + p2shOutput.RedeemScript) if sc != txscript.MultiSigTy { return nil, fmt.Errorf("invalid P2SH script: not multisig") } @@ -2328,11 +2117,11 @@ func redeemMultiSigOut(icmd interface{}, w *wallet.Wallet, chainClient *chain.RP // Then produce the txout. size := wallet.EstimateTxSize(1, 1) feeEst := wallet.FeeForSize(w.RelayFee(), size) - if feeEst >= msCredit.Amount { + if feeEst >= p2shOutput.OutputAmount { return nil, fmt.Errorf("multisig out amt is too small "+ - "(have %v, %v fee suggested)", msCredit.Amount, feeEst) + "(have %v, %v fee suggested)", p2shOutput.OutputAmount, feeEst) } - toReceive := msCredit.Amount - feeEst + toReceive := p2shOutput.OutputAmount - feeEst pkScript, err := txscript.PayToAddrScript(addr) if err != nil { return nil, fmt.Errorf("cannot create txout script: %s", err) @@ -2340,7 +2129,7 @@ func redeemMultiSigOut(icmd interface{}, w *wallet.Wallet, chainClient *chain.RP msgTx.AddTxOut(wire.NewTxOut(int64(toReceive), pkScript)) // Start creating the SignRawTransactionCmd. - outpointScript, err := txscript.PayToScriptHashScript(msCredit.ScriptHash[:]) + outpointScript, err := txscript.PayToScriptHashScript(p2shOutput.P2SHAddress.Hash160()[:]) if err != nil { return nil, err } @@ -2397,7 +2186,11 @@ func redeemMultiSigOuts(icmd interface{}, w *wallet.Wallet, chainClient *chain.R if err != nil { return nil, err } - msos, err := w.TxStore.UnspentMultisigCreditsForAddress(addr) + p2shAddr, ok := addr.(*dcrutil.AddressScriptHash) + if !ok { + return nil, errors.New("address is not P2SH") + } + msos, err := wallet.UnstableAPI(w).UnspentMultisigCreditsForAddress(p2shAddr) if err != nil { return nil, err } @@ -2441,15 +2234,7 @@ func stakePoolUserInfo(icmd interface{}, w *wallet.Wallet) (interface{}, error) if err != nil { return nil, err } - _, isScriptHash := userAddr.(*dcrutil.AddressScriptHash) - _, isP2PKH := userAddr.(*dcrutil.AddressPubKeyHash) - if !(isScriptHash || isP2PKH) { - return nil, fmt.Errorf("invalid user address %v, must be P2SH or "+ - "P2PKH", userAddr.EncodeAddress()) - } - - // Access the stake manager and fetch the user information. - spui, err := w.StakeMgr.StakePoolUserInfo(userAddr) + spui, err := w.StakePoolUserInfo(userAddr) if err != nil { return nil, err } @@ -2496,30 +2281,17 @@ func ticketsForAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) return nil, err } - ticketsAll, err := w.StakeMgr.DumpSStxHashesForAddress(addr) + ticketHashes, err := w.TicketHashesForVotingAddress(addr) if err != nil { return nil, err } - // Check the wallet database. - tickets := make([]chainhash.Hash, 0, len(ticketsAll)) - for i := range ticketsAll { - exists, err := w.TxStore.ExistsTx(&ticketsAll[i]) - if err != nil { - log.Errorf("Enountered database error while retrieving "+ - "tickets for address: %v", err.Error()) - } - if exists { - tickets = append(tickets, ticketsAll[i]) - } - } - - ticketsStr := make([]string, len(tickets), len(tickets)) - for i, h := range tickets { - ticketsStr[i] = h.String() + ticketHashStrs := make([]string, 0, len(ticketHashes)) + for _, hash := range ticketHashes { + ticketHashStrs = append(ticketHashStrs, hash.String()) } - return dcrjson.TicketsForAddressResult{Tickets: ticketsStr}, nil + return dcrjson.TicketsForAddressResult{Tickets: ticketHashStrs}, nil } func isNilOrEmpty(s *string) bool { @@ -2543,7 +2315,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 } @@ -2585,7 +2357,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 } @@ -2680,25 +2452,21 @@ func sendToMultiSig(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCCl switch addr := a.(type) { case *dcrutil.AddressSecpPubKey: pubkeys[i] = addr - case *dcrutil.AddressPubKeyHash: - ainfo, err := w.Manager.Address(addr) + default: + pubKey, err := w.PubKeyForAddress(addr) if err != nil { return nil, err } - - apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) - - // This will be an addresspubkey. - a, err := decodeAddress(apkinfo.ExportPubKey(), - w.ChainParams()) + if pubKey.GetType() != chainec.ECTypeSecp256k1 { + return nil, errors.New("only secp256k1 " + + "pubkeys are currently supported") + } + pubKeyAddr, err := dcrutil.NewAddressSecpPubKey( + pubKey.Serialize(), w.ChainParams()) if err != nil { return nil, err } - - apk := a.(*dcrutil.AddressSecpPubKey) - pubkeys[i] = apk - default: - return nil, err + pubkeys[i] = pubKeyAddr } } @@ -2734,7 +2502,7 @@ func sendToSStx(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient cmd := icmd.(*dcrjson.SendToSStxCmd) minconf := int32(*cmd.MinConf) - account, err := w.Manager.LookupAccount(cmd.FromAccount) + account, err := w.AccountNumber(cmd.FromAccount) if err != nil { return nil, err } @@ -2800,7 +2568,7 @@ func sendToSStx(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient func sendToSSGen(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.SendToSSGenCmd) - _, err := w.Manager.LookupAccount(cmd.FromAccount) + _, err := w.AccountNumber(cmd.FromAccount) if err != nil { return nil, err } @@ -2843,7 +2611,7 @@ func sendToSSGen(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func sendToSSRtx(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { cmd := icmd.(*dcrjson.SendToSSRtxCmd) - _, err := w.Manager.LookupAccount(cmd.FromAccount) + _, err := w.AccountNumber(cmd.FromAccount) if err != nil { return nil, err } @@ -2913,12 +2681,8 @@ func setTicketVoteBits(icmd interface{}, w *wallet.Wallet) (interface{}, error) return nil, err } - err = w.StakeMgr.UpdateSStxVoteBits(ticket, cmd.VoteBits) - if err != nil { - return nil, err - } - - return nil, nil + err = w.SetVoteBitsForTicket(ticket, cmd.VoteBits) + return nil, err } // setBalanceToMaintain sets the balance to maintain for automatic ticket pur. @@ -2994,19 +2758,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, &dcrjson.RPCError{ - Code: dcrjson.ErrRPCInvalidAddressOrKey, - Message: msg, - } - } - privKey, err := pka.PrivKey() + privKey, err := w.PrivKeyForAddress(addr) if err != nil { return nil, err } @@ -3138,66 +2890,41 @@ func signRawTransaction(icmd interface{}, w *wallet.Wallet, chainClient *chain.R isRelevant := false for _, addr := range addrs { - _, err := w.Manager.Address(addr) - if err == nil { + haveAddr, err := w.HaveAddress(addr) + if err != nil { + return nil, err + } + if haveAddr { isRelevant = true - err = w.Manager.MarkUsed(addr) - if err != nil { - return nil, err - } - log.Debugf("Marked address %v used", addr) - } else { - // Missing addresses are skipped. Other errors should - // be propagated. - if !waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { - return nil, err - } } } // Add the script to the script databases. if isRelevant { - err = w.TxStore.InsertTxScript(rs) + p2shAddr, err := w.ImportP2SHRedeemScript(rs) if err != nil { return nil, err } - // Get current block's height and hash. - bs, err := chainClient.BlockStamp() - if err != nil { - return nil, err + // This is the first time seeing this script + // address belongs to us, so do a rescan and see + // if there are any other outputs to this + // address. + job := &wallet.RescanJob{ + Addrs: []dcrutil.Address{p2shAddr}, + OutPoints: nil, + BlockStamp: waddrmgr.BlockStamp{ + Height: 0, + Hash: *w.ChainParams().GenesisHash, + }, } - mscriptaddr, err := w.Manager.ImportScript(rs, bs) - if err != nil { - switch { - // Don't care if it's already there. - case waddrmgr.IsError(err, waddrmgr.ErrDuplicateAddress): - break - case waddrmgr.IsError(err, waddrmgr.ErrLocked): - log.Debugf("failed to attempt script importation " + - "of incoming tx because addrmgr was locked") - break - default: - return nil, err - } - } else { - // This is the first time seeing this script address - // belongs to us, so do a rescan and see if there are - // any other outputs to this address. - job := &wallet.RescanJob{ - Addrs: []dcrutil.Address{mscriptaddr.Address()}, - OutPoints: nil, - BlockStamp: waddrmgr.BlockStamp{ - Height: 0, - Hash: *w.ChainParams().GenesisHash, - }, - } - // Submit rescan job and log when the import has completed. - // Do not block on finishing the rescan. The rescan success - // or failure is logged elsewhere, and the channel is not - // required to be read, so discard the return value. - _ = w.SubmitRescan(job) - } + // Submit rescan job and log when the import has + // completed. Do not block on finishing the + // rescan. The rescan success or failure is + // logged elsewhere, and the channel is not + // required to be read, so discard the return + // value. + _ = w.SubmitRescan(job) } } } @@ -3421,7 +3148,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. @@ -3433,7 +3160,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 } @@ -3613,7 +3340,7 @@ func walletPassphrase(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func walletPassphraseChange(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.WalletPassphraseChangeCmd) - err := w.ChangePassphrase([]byte(cmd.OldPassphrase), + err := w.ChangePrivatePassphrase([]byte(cmd.OldPassphrase), []byte(cmd.NewPassphrase)) if waddrmgr.IsError(err, waddrmgr.ErrWrongPassphrase) { return nil, &dcrjson.RPCError{ diff --git a/rpc/rpcserver/server.go b/rpc/rpcserver/server.go index 064184e37..3303517ae 100644 --- a/rpc/rpcserver/server.go +++ b/rpc/rpcserver/server.go @@ -18,7 +18,6 @@ package rpcserver import ( "bytes" - "encoding/hex" "errors" "sync" "time" @@ -154,7 +153,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) } @@ -250,24 +249,13 @@ func (s *walletServer) NextAddress(ctx context.Context, req *pb.NextAddressReque return nil, translateError(err) } - ainfo, err := s.wallet.Manager.Address(addr) + pubKey, err := s.wallet.PubKeyForAddress(addr) if err != nil { - return nil, grpc.Errorf(codes.Internal, - "Unable to find generated address in address manager") - } - mpka, ok := ainfo.(waddrmgr.ManagedPubKeyAddress) - if !ok { - return nil, grpc.Errorf(codes.Internal, - "Unable to cast returned address data as ManagedPubKeyAddress") - } - pubKeyBytes, err := hex.DecodeString(mpka.ExportPubKey()) - if err != nil { - return nil, err + return nil, translateError(err) } - pubKeyAddr, err := dcrutil.NewAddressSecpPubKey(pubKeyBytes, - s.wallet.ChainParams()) + pubKeyAddr, err := dcrutil.NewAddressSecpPubKey(pubKey.Serialize(), s.wallet.ChainParams()) if err != nil { - return nil, err + return nil, translateError(err) } return &pb.NextAddressResponse{ @@ -436,62 +424,32 @@ 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 dcrutil.Amount - for i := range outputs { - output := outputs[i] - - if !confirmed(req.RequiredConfirmations, output.Height, syncBlock.Height) { - continue - } - if !req.IncludeImmatureCoinbases && output.FromCoinBase && - !confirmed(int32(s.wallet.ChainParams().CoinbaseMaturity), output.Height, syncBlock.Height) { - continue - } - - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - txscript.DefaultScriptVersion, 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, - Tree: int32(output.Tree), + OutputIndex: output.OutPoint.Index, + Amount: output.Output.Value, + PkScript: output.Output.PkScript, + ReceiveTime: output.ReceiveTime.Unix(), + FromCoinbase: output.OutputKind == wallet.OutputKindCoinbase, + Tree: int32(output.OutPoint.Tree), }) - totalAmount += output.Amount + totalAmount += dcrutil.Amount(output.Output.Value) if req.TargetAmount != 0 && totalAmount > dcrutil.Amount(req.TargetAmount) { break } - } var changeScript []byte @@ -588,12 +546,18 @@ func (s *walletServer) ChangePassphrase(ctx context.Context, req *pb.ChangePassp zero.Bytes(req.NewPassphrase) }() - err := s.wallet.Manager.ChangePassphrase(req.OldPassphrase, req.NewPassphrase, - req.Key != pb.ChangePassphraseRequest_PUBLIC, &waddrmgr.DefaultScryptOptions) + var err error + switch req.Key { + case pb.ChangePassphraseRequest_PRIVATE: + err = s.wallet.ChangePrivatePassphrase(req.OldPassphrase, req.NewPassphrase) + case pb.ChangePassphraseRequest_PUBLIC: + err = s.wallet.ChangePublicPassphrase(req.OldPassphrase, req.NewPassphrase) + default: + return nil, grpc.Errorf(codes.InvalidArgument, "Unknown key type (%d)", req.Key) + } if err != nil { return nil, translateError(err) } - return &pb.ChangePassphraseResponse{}, nil } diff --git a/waddrmgr/address.go b/waddrmgr/address.go index 51035c10e..2b2635895 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -14,6 +14,7 @@ import ( "github.com/decred/dcrutil" "github.com/decred/dcrutil/hdkeychain" "github.com/decred/dcrwallet/internal/zero" + "github.com/decred/dcrwallet/walletdb" ) // ManagedAddress is an interface that provides acces to information regarding @@ -46,7 +47,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 @@ -191,8 +192,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. @@ -473,8 +474,8 @@ func (a *scriptAddress) Compressed() bool { // Used returns true if the address has been used in a transaction. // // This is part of the ManagedAddress interface implementation. -func (a *scriptAddress) Used() (bool, error) { - return a.manager.fetchUsed(a.AddrHash()) +func (a *scriptAddress) Used(ns walletdb.ReadBucket) bool { + return a.manager.fetchUsed(ns, a.AddrHash()) } // Script returns the script associated with the address. diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 43c9f450e..3550f7ed2 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -254,8 +254,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" @@ -266,8 +266,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) @@ -280,8 +280,8 @@ func putManagerVersion(tx walletdb.Tx, version uint32) error { // fetchSeed loads the encrypted seed needed to restore the wallet. This will // return an error for watching only wallets. -func fetchSeed(tx walletdb.Tx) ([]byte, error) { - bucket := tx.RootBucket().Bucket(mainBucketName) +func fetchSeed(ns walletdb.ReadBucket) ([]byte, error) { + bucket := ns.NestedReadBucket(mainBucketName) // Load the master private key parameters if they were stored. var seed []byte @@ -298,8 +298,8 @@ func fetchSeed(tx walletdb.Tx) ([]byte, error) { } // putSeed inserts the encrypted seed needed to restore the wallet. -func putSeed(tx walletdb.Tx, seed []byte) error { - bucket := tx.RootBucket().Bucket(mainBucketName) +func putSeed(ns walletdb.ReadWriteBucket, seed []byte) error { + bucket := ns.NestedReadWriteBucket(mainBucketName) if seed != nil { err := bucket.Put(seedName, seed) @@ -319,8 +319,8 @@ func putSeed(tx walletdb.Tx, seed []byte) 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) @@ -346,8 +346,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) @@ -370,8 +370,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 { @@ -390,8 +390,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) @@ -416,8 +416,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) @@ -450,8 +450,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) @@ -481,8 +481,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 { @@ -494,8 +494,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 { @@ -627,8 +627,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. @@ -640,8 +640,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 { @@ -655,8 +655,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 { @@ -672,8 +672,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 { @@ -686,8 +686,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) @@ -711,8 +711,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)) @@ -724,8 +724,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)) @@ -737,8 +737,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)) @@ -750,8 +750,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)) @@ -763,8 +763,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)) @@ -787,8 +787,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)) @@ -800,7 +800,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 { @@ -811,15 +811,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 } @@ -827,8 +827,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 { @@ -1035,8 +1035,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 { @@ -1063,16 +1063,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 := fastsha256.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 := fastsha256.Sum256(addressID) val := bucket.Get(addrHash[:]) @@ -1092,15 +1092,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 := fastsha256.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 @@ -1112,12 +1112,12 @@ 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, +func putChainedAddress(ns walletdb.ReadWriteBucket, addressID []byte, account uint32, status syncStatus, branch, index uint32) error { addrRow := dbAddressRow{ @@ -1127,14 +1127,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. @@ -1173,7 +1173,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) @@ -1184,12 +1184,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) @@ -1200,7 +1200,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 } @@ -1208,8 +1208,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 := fastsha256.Sum256(addressID) return bucket.Get(addrHash[:]) != nil @@ -1218,8 +1218,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 := fastsha256.Sum256(addressID) val := bucket.Get(addrHash[:]) @@ -1232,9 +1232,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 { @@ -1246,7 +1246,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", @@ -1267,8 +1267,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. @@ -1278,7 +1278,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) @@ -1304,8 +1304,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. @@ -1327,7 +1327,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 { @@ -1367,7 +1367,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 { @@ -1426,8 +1426,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: // @@ -1446,8 +1446,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: // @@ -1467,8 +1467,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: // @@ -1487,8 +1487,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: // @@ -1508,8 +1508,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: // @@ -1538,8 +1538,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: // @@ -1582,9 +1582,9 @@ func accountNumberToAddrPoolKey(isInternal bool, account uint32) []byte { // fetchNextToUseAddrPoolIdx retrieves an address pool address index for a // given account and branch from the meta bucket of the address manager // database. -func fetchNextToUseAddrPoolIdx(tx walletdb.Tx, isInternal bool, +func fetchNextToUseAddrPoolIdx(ns walletdb.ReadBucket, isInternal bool, account uint32) (uint32, error) { - bucket := tx.RootBucket().Bucket(metaBucketName) + bucket := ns.NestedReadBucket(metaBucketName) k := accountNumberToAddrPoolKey(isInternal, account) val := bucket.Get(k) @@ -1603,8 +1603,8 @@ func fetchNextToUseAddrPoolIdx(tx walletdb.Tx, isInternal bool, // putNextToUseAddrPoolIdx stores an address pool address index for a // given account and branch in the meta bucket of the address manager // database. -func putNextToUseAddrPoolIdx(tx walletdb.Tx, isInternal bool, account uint32, index uint32) error { - bucket := tx.RootBucket().Bucket(metaBucketName) +func putNextToUseAddrPoolIdx(ns walletdb.ReadWriteBucket, isInternal bool, account uint32, index uint32) error { + bucket := ns.NestedReadWriteBucket(metaBucketName) k := accountNumberToAddrPoolKey(isInternal, account) v := make([]byte, 4, 4) binary.LittleEndian.PutUint32(v, index) @@ -1620,102 +1620,84 @@ func putNextToUseAddrPoolIdx(tx walletdb.Tx, isInternal bool, account uint32, in // 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) - } - return exists, nil +func managerExists(ns walletdb.ReadBucket) bool { + 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) - } +func createManagerNS(ns walletdb.ReadWriteBucket) error { + mainBucket, err := ns.CreateBucket(mainBucketName) + if err != nil { + str := "failed to create main 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 = ns.CreateBucket(addrBucketName) + if err != nil { + str := "failed to create address 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 = ns.CreateBucket(acctBucketName) + if err != nil { + str := "failed to create account bucket" + return managerError(ErrDatabase, str, err) + } - _, err = rootBucket.CreateBucket(syncBucketName) - if err != nil { - str := "failed to create sync 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) + } - // 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 = ns.CreateBucket(syncBucketName) + if err != nil { + str := "failed to create sync 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) - } + // 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 = rootBucket.CreateBucket(acctIDIdxBucketName) - if err != nil { - str := "failed to create an account id index 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 = rootBucket.CreateBucket(metaBucketName) - if err != nil { - str := "failed to create a meta 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) + } - if err := putLastAccount(tx, DefaultAccountNum); err != nil { - return err - } + _, err = ns.CreateBucket(metaBucketName) + if err != nil { + str := "failed to create a meta bucket" + return managerError(ErrDatabase, str, err) + } - if err := putManagerVersion(tx, latestMgrVersion); err != nil { - return err - } + if err := putLastAccount(ns, DefaultAccountNum); 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) - } + if err := putManagerVersion(ns, latestMgrVersion); err != nil { + return err + } - return nil - }) + 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 update database" + str := "failed to store database creation time" return managerError(ErrDatabase, str, err) } @@ -1725,31 +1707,24 @@ func createManagerNS(namespace walletdb.Namespace) error { // upgradeToVersion5 upgrades the database from version 4 to version 5. // Version 5 uses the metadata bucket to store the address pool indexes, // so lastAddrs can be removed from the db. -func upgradeToVersion5(namespace walletdb.Namespace) error { - err := namespace.Update(func(tx walletdb.Tx) error { - currentMgrVersion := uint32(5) - bucket := tx.RootBucket().Bucket(mainBucketName) - bucket.Delete(lastDefaultAddsrNameLegacyV4) - - if err := putManagerVersion(tx, currentMgrVersion); err != nil { - return err - } +func upgradeToVersion5(ns walletdb.ReadWriteBucket) error { + currentMgrVersion := uint32(5) + bucket := ns.NestedReadWriteBucket(mainBucketName) + bucket.Delete(lastDefaultAddsrNameLegacyV4) - return nil - }) - if err != nil { - return maybeConvertDbError(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 { @@ -1783,7 +1758,11 @@ func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, chainPar // } if version < 5 { - if err := upgradeToVersion5(namespace); err != nil { + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(namespaceKey) + return upgradeToVersion5(ns) + }) + if err != nil { return err } diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index aed4c2d13..26263ec74 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -257,7 +257,6 @@ var newCryptoKey = defaultNewCryptoKey type Manager struct { mtx sync.RWMutex - namespace walletdb.Namespace chainParams *chaincfg.Params addrs map[addrKey]ManagedAddress syncState syncState @@ -458,24 +457,18 @@ func (m *Manager) deriveKey(acctInfo *accountInfo, branch, index uint32, // GetSeed gives the encoded string version of the seed if the // wallet is unlocked. -func (m *Manager) GetSeed() (string, error) { +func (m *Manager) GetSeed(ns walletdb.ReadBucket) (string, error) { if m.locked { str := "manager is locked" return "", managerError(ErrLocked, str, nil) } - var seedEnc []byte - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - var localSeed []byte - localSeed, err = fetchSeed(tx) - seedEnc = make([]byte, len(localSeed), len(localSeed)) - copy(seedEnc, localSeed) - return err - }) + localSeed, err := fetchSeed(ns) if err != nil { return "", maybeConvertDbError(err) } + seedEnc := make([]byte, len(localSeed), len(localSeed)) + copy(seedEnc, localSeed) seed, err := m.cryptoKeyPriv.Decrypt(seedEnc) if err != nil { @@ -495,15 +488,10 @@ func (m *Manager) GetSeed() (string, error) { // for the default account of the wallet. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) getMasterPubkey(account uint32) (string, error) { +func (m *Manager) getMasterPubkey(ns walletdb.ReadBucket, account uint32) (string, error) { // The account is either invalid or just wasn't cached, so attempt to // load the information from the database. - var rowInterface interface{} - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - rowInterface, err = fetchAccountInfo(tx, account) - return err - }) + rowInterface, err := fetchAccountInfo(ns, account) if err != nil { return "", maybeConvertDbError(err) } @@ -527,11 +515,11 @@ func (m *Manager) getMasterPubkey(account uint32) (string, error) { } // GetMasterPubkey is the exported, concurrency safe version of getMasterPubkey. -func (m *Manager) GetMasterPubkey(account uint32) (string, error) { +func (m *Manager) GetMasterPubkey(ns walletdb.ReadBucket, account uint32) (string, error) { m.mtx.Lock() defer m.mtx.Unlock() - return m.getMasterPubkey(account) + return m.getMasterPubkey(ns, account) } // loadAccountInfo attempts to load and cache information about the given @@ -539,7 +527,7 @@ func (m *Manager) GetMasterPubkey(account uint32) (string, error) { // keys for it and track the state of the internal and external branches. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { +func (m *Manager) loadAccountInfo(ns walletdb.ReadBucket, account uint32) (*accountInfo, error) { // Return the account info from cache if it's available. if acctInfo, ok := m.acctInfo[account]; ok { return acctInfo, nil @@ -547,12 +535,7 @@ func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { // The account is either invalid or just wasn't cached, so attempt to // load the information from the database. - var rowInterface interface{} - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - rowInterface, err = fetchAccountInfo(tx, account) - return err - }) + rowInterface, err := fetchAccountInfo(ns, account) if err != nil { return nil, maybeConvertDbError(err) } @@ -649,7 +632,7 @@ func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { // then fetching the account properties with a new read tx, this can be made // more performant by simply returning the new account properties during the // change. -func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) { +func (m *Manager) AccountProperties(ns walletdb.ReadBucket, account uint32) (*AccountProperties, error) { defer m.mtx.RUnlock() m.mtx.RLock() @@ -667,7 +650,7 @@ func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) // imported account cannot contain non-imported keys, the external and // internal key counts for it are zero. if account != ImportedAddrAccount { - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -679,14 +662,11 @@ func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) // Could be more efficient if this was tracked by the db. var importedKeyCount uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - count := func(interface{}) error { - importedKeyCount++ - return nil - } - return forEachAccountAddress(tx, ImportedAddrAccount, - count) - }) + count := func(interface{}) error { + importedKeyCount++ + return nil + } + err := forEachAccountAddress(ns, ImportedAddrAccount, count) if err != nil { return nil, err } @@ -700,10 +680,10 @@ func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) // based on the private flag for the given an account, branch, and index. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) deriveKeyFromPath(account, branch, index uint32, +func (m *Manager) deriveKeyFromPath(ns walletdb.ReadBucket, account, branch, index uint32, private bool) (*hdkeychain.ExtendedKey, error) { // Look up the account key information. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -715,9 +695,9 @@ func (m *Manager) deriveKeyFromPath(account, branch, index uint32, // address data loaded from the database. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) chainAddressRowToManaged( +func (m *Manager) chainAddressRowToManaged(ns walletdb.ReadBucket, row *dbChainAddressRow) (ManagedAddress, error) { - addressKey, err := m.deriveKeyFromPath(row.account, row.branch, + addressKey, err := m.deriveKeyFromPath(ns, row.account, row.branch, row.index, !m.locked) if err != nil { return nil, err @@ -774,10 +754,10 @@ func (m *Manager) scriptAddressRowToManaged( // appropriate type. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) rowInterfaceToManaged(rowInterface interface{}) (ManagedAddress, error) { +func (m *Manager) rowInterfaceToManaged(ns walletdb.ReadBucket, rowInterface interface{}) (ManagedAddress, error) { switch row := rowInterface.(type) { case *dbChainAddressRow: - return m.chainAddressRowToManaged(row) + return m.chainAddressRowToManaged(ns, row) case *dbImportedAddressRow: return m.importedAddressRowToManaged(row) @@ -794,15 +774,10 @@ func (m *Manager) rowInterfaceToManaged(rowInterface interface{}) (ManagedAddres // caches the associated managed address. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) loadAndCacheAddress( +func (m *Manager) loadAndCacheAddress(ns walletdb.ReadBucket, address dcrutil.Address) (ManagedAddress, error) { // Attempt to load the raw address information from the database. - var rowInterface interface{} - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - rowInterface, err = fetchAddress(tx, address.ScriptAddress()) - return err - }) + rowInterface, err := fetchAddress(ns, address.ScriptAddress()) if err != nil { if merr, ok := err.(*ManagerError); ok { desc := fmt.Sprintf("failed to fetch address '%s': %v", @@ -815,7 +790,7 @@ func (m *Manager) loadAndCacheAddress( // Create a new managed address for the specific type of address based // on type. - managedAddr, err := m.rowInterfaceToManaged(rowInterface) + managedAddr, err := m.rowInterfaceToManaged(ns, rowInterface) if err != nil { return nil, err } @@ -831,7 +806,7 @@ func (m *Manager) loadAndCacheAddress( // transactions such as the associated private key for pay-to-pubkey and // pay-to-pubkey-hash addresses and the script associated with // pay-to-script-hash addresses. -func (m *Manager) Address(address dcrutil.Address) (ManagedAddress, error) { +func (m *Manager) Address(ns walletdb.ReadBucket, address dcrutil.Address) (ManagedAddress, error) { // ScriptAddress will only return a script hash if we're // accessing an address that is either PKH or SH. In // the event we're passed a PK address, convert the @@ -856,17 +831,12 @@ func (m *Manager) Address(address dcrutil.Address) (ManagedAddress, error) { defer m.mtx.Unlock() // Attempt to load the address from the database. - return m.loadAndCacheAddress(address) + return m.loadAndCacheAddress(ns, address) } // AddrAccount returns the account to which the given address belongs. -func (m *Manager) AddrAccount(address dcrutil.Address) (uint32, error) { - var account uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - account, err = fetchAddrAccount(tx, address.ScriptAddress()) - return err - }) +func (m *Manager) AddrAccount(ns walletdb.ReadBucket, address dcrutil.Address) (uint32, error) { + account, err := fetchAddrAccount(ns, address.ScriptAddress()) if err != nil { return 0, maybeConvertDbError(err) } @@ -879,7 +849,7 @@ func (m *Manager) AddrAccount(address dcrutil.Address) (uint32, error) { // keys are derived using the scrypt parameters in the options, so changing the // passphrase may be used to bump the computational difficulty needed to brute // force the passphrase. -func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, +func (m *Manager) ChangePassphrase(ns walletdb.ReadWriteBucket, oldPassphrase, newPassphrase []byte, private bool, config *ScryptOptions) error { // No private passphrase to change for a watching-only address manager. if private && m.watchingOnly { @@ -984,14 +954,12 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, // Save the new keys and params to the the db in a single // transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putCryptoKeys(tx, nil, encPriv, encScript) - if err != nil { - return err - } + err = putCryptoKeys(ns, nil, encPriv, encScript) + if err != nil { + return maybeConvertDbError(err) + } - return putMasterKeyParams(tx, nil, newKeyParams) - }) + err = putMasterKeyParams(ns, nil, newKeyParams) if err != nil { return maybeConvertDbError(err) } @@ -1015,14 +983,12 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, // Save the new keys and params to the the db in a single // transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putCryptoKeys(tx, encryptedPub, nil, nil) - if err != nil { - return err - } + err = putCryptoKeys(ns, encryptedPub, nil, nil) + if err != nil { + return maybeConvertDbError(err) + } - return putMasterKeyParams(tx, newKeyParams, nil) - }) + err = putMasterKeyParams(ns, newKeyParams, nil) if err != nil { return maybeConvertDbError(err) } @@ -1046,7 +1012,7 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, // // Executing this function on a manager that is already watching-only will have // no effect. -func (m *Manager) ConvertToWatchingOnly() error { +func (m *Manager) ConvertToWatchingOnly(ns walletdb.ReadWriteBucket) error { m.mtx.Lock() defer m.mtx.Unlock() @@ -1057,13 +1023,12 @@ func (m *Manager) ConvertToWatchingOnly() error { // Remove all private key material and mark the new database as watching // only. - err := m.namespace.Update(func(tx walletdb.Tx) error { - if err := deletePrivateKeys(tx); err != nil { - return err - } + err := deletePrivateKeys(ns) + if err != nil { + return maybeConvertDbError(err) + } - return putWatchingOnly(tx, true) - }) + err = putWatchingOnly(ns, true) if err != nil { return maybeConvertDbError(err) } @@ -1121,50 +1086,38 @@ func (m *Manager) ConvertToWatchingOnly() error { // address manager. // // This function MUST be called with the manager lock held for reads. -func (m *Manager) existsAddress(addressID []byte) (bool, error) { +func (m *Manager) existsAddress(ns walletdb.ReadBucket, addressID []byte) bool { // Check the in-memory map first since it's faster than a db access. if _, ok := m.addrs[addrKey(addressID)]; ok { - return true, nil + return true } // Check the database if not already found above. - var exists bool - err := m.namespace.View(func(tx walletdb.Tx) error { - exists = existsAddress(tx, addressID) - return nil - }) - if err != nil { - return false, maybeConvertDbError(err) - } - - return exists, nil + return existsAddress(ns, addressID) } // ExistsAddress is the exported version of existsAddress. It is only used // to check for the existence of a PKH derived from an extended key. // // This function is safe for concurrent access. -func (m *Manager) ExistsAddress(addressID []byte) (bool, error) { +func (m *Manager) ExistsAddress(ns walletdb.ReadBucket, addressID []byte) bool { m.mtx.Lock() defer m.mtx.Unlock() - return m.existsAddress(addressID) + return m.existsAddress(ns, addressID) } // storeNextToUseAddress is used to store the next to use address index for a // given account's internal or external branch to the database. // // This function MUST be called with the manager lock held for reads. -func (m *Manager) storeNextToUseAddress(isInternal bool, account uint32, - index uint32) error { - err := m.namespace.Update(func(tx walletdb.Tx) error { - errLocal := putNextToUseAddrPoolIdx(tx, isInternal, account, index) - return errLocal - }) +func (m *Manager) storeNextToUseAddress(ns walletdb.ReadWriteBucket, + isInternal bool, account uint32, index uint32) error { + + err := putNextToUseAddrPoolIdx(ns, isInternal, account, index) if err != nil { return maybeConvertDbError(err) } - return nil } @@ -1173,24 +1126,18 @@ func (m *Manager) storeNextToUseAddress(isInternal bool, account uint32, // or external branch to the database. // // This function is safe for concurrent access. -func (m *Manager) StoreNextToUseAddress(isInternal bool, account uint32, - index uint32) error { +func (m *Manager) StoreNextToUseAddress(ns walletdb.ReadWriteBucket, isInternal bool, account uint32, index uint32) error { m.mtx.Lock() defer m.mtx.Unlock() - return m.storeNextToUseAddress(isInternal, account, index) + return m.storeNextToUseAddress(ns, isInternal, account, index) } // nextToUseAddrPoolIndex returns the next to use address index for a given // account's internal or external branch. -func (m *Manager) nextToUseAddrPoolIndex(isInternal bool, +func (m *Manager) nextToUseAddrPoolIndex(ns walletdb.ReadBucket, isInternal bool, account uint32) (uint32, error) { - var index uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - var errLocal error - index, errLocal = fetchNextToUseAddrPoolIdx(tx, isInternal, account) - return errLocal - }) + index, err := fetchNextToUseAddrPoolIdx(ns, isInternal, account) if err != nil { return 0, maybeConvertDbError(err) } @@ -1203,11 +1150,11 @@ func (m *Manager) nextToUseAddrPoolIndex(isInternal bool, // external branch. // // This function is safe for concurrent access. -func (m *Manager) NextToUseAddrPoolIndex(isInternal bool, account uint32) (uint32, error) { +func (m *Manager) NextToUseAddrPoolIndex(ns walletdb.ReadBucket, isInternal bool, account uint32) (uint32, error) { m.mtx.Lock() defer m.mtx.Unlock() - return m.nextToUseAddrPoolIndex(isInternal, account) + return m.nextToUseAddrPoolIndex(ns, isInternal, account) } // ImportPrivateKey imports a WIF private key into the address manager. The @@ -1227,8 +1174,7 @@ func (m *Manager) NextToUseAddrPoolIndex(isInternal bool, account uint32) (uint3 // watching-only, or not for the same network as the key trying to be imported. // It will also return an error if the address already exists. Any other errors // returned are generally unexpected. -func (m *Manager) ImportPrivateKey(wif *dcrutil.WIF, - bs *BlockStamp) (ManagedPubKeyAddress, error) { +func (m *Manager) ImportPrivateKey(ns walletdb.ReadWriteBucket, wif *dcrutil.WIF, bs *BlockStamp) (ManagedPubKeyAddress, error) { // Ensure the address is intended for network the address manager is // associated with. if !wif.IsForNet(m.chainParams) { @@ -1249,10 +1195,7 @@ func (m *Manager) ImportPrivateKey(wif *dcrutil.WIF, // Prevent duplicates. serializedPubKey := wif.SerializePubKey() pubKeyHash := dcrutil.Hash160(serializedPubKey) - alreadyExists, err := m.existsAddress(pubKeyHash) - if err != nil { - return nil, err - } + alreadyExists := m.existsAddress(ns, pubKeyHash) if alreadyExists { str := fmt.Sprintf("address for public key %x already exists", serializedPubKey) @@ -1286,23 +1229,19 @@ func (m *Manager) ImportPrivateKey(wif *dcrutil.WIF, // Save the new imported address to the db and update start block (if // needed) in a single transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putImportedAddress(tx, pubKeyHash, ImportedAddrAccount, - ssNone, encryptedPubKey, encryptedPrivKey) - if err != nil { - return err - } - - if updateStartBlock { - return putStartBlock(tx, bs) - } - - return nil - }) + err = putImportedAddress(ns, pubKeyHash, ImportedAddrAccount, ssNone, + encryptedPubKey, encryptedPrivKey) if err != nil { return nil, err } + if updateStartBlock { + err := putStartBlock(ns, bs) + if err != nil { + return nil, err + } + } + // Now that the database has been updated, update the start block in // memory too if needed. if updateStartBlock { @@ -1343,7 +1282,7 @@ func (m *Manager) ImportPrivateKey(wif *dcrutil.WIF, // This function will return an error if the address manager is locked and not // watching-only, or the address already exists. Any other errors returned are // generally unexpected. -func (m *Manager) ImportScript(script []byte, +func (m *Manager) ImportScript(ns walletdb.ReadWriteBucket, script []byte, bs *BlockStamp) (ManagedScriptAddress, error) { m.mtx.Lock() defer m.mtx.Unlock() @@ -1355,10 +1294,7 @@ func (m *Manager) ImportScript(script []byte, // Prevent duplicates. scriptHash := dcrutil.Hash160(script) - alreadyExists, err := m.existsAddress(scriptHash) - if err != nil { - return nil, err - } + alreadyExists := m.existsAddress(ns, scriptHash) if alreadyExists { str := fmt.Sprintf("address for script hash %x already exists", scriptHash) @@ -1397,23 +1333,19 @@ func (m *Manager) ImportScript(script []byte, // Save the new imported address to the db and update start block (if // needed) in a single transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - err := putScriptAddress(tx, scriptHash, ImportedAddrAccount, - ssNone, encryptedHash, encryptedScript) - if err != nil { - return err - } - - if updateStartBlock { - return putStartBlock(tx, bs) - } - - return nil - }) + err = putScriptAddress(ns, scriptHash, ImportedAddrAccount, + ssNone, encryptedHash, encryptedScript) if err != nil { return nil, maybeConvertDbError(err) } + if updateStartBlock { + err := putStartBlock(ns, bs) + if err != nil { + return nil, maybeConvertDbError(err) + } + } + // Now that the database has been updated, update the start block in // memory too if needed. if updateStartBlock { @@ -1477,23 +1409,17 @@ func (m *Manager) Lock() error { // account name // // This function MUST be called with the manager lock held for reads. -func (m *Manager) lookupAccount(name string) (uint32, error) { - var account uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - account, err = fetchAccountByName(tx, name) - return err - }) - return account, err +func (m *Manager) lookupAccount(ns walletdb.ReadBucket, name string) (uint32, error) { + return fetchAccountByName(ns, name) } // LookupAccount loads account number stored in the manager for the given // account name -func (m *Manager) LookupAccount(name string) (uint32, error) { +func (m *Manager) LookupAccount(ns walletdb.ReadBucket, name string) (uint32, error) { m.mtx.RLock() defer m.mtx.RUnlock() - return m.lookupAccount(name) + return m.lookupAccount(ns, name) } // Unlock derives the master private key from the specified passphrase. An @@ -1504,7 +1430,7 @@ func (m *Manager) LookupAccount(name string) (uint32, error) { // // This function will return an error if invoked on a watching-only address // manager. -func (m *Manager) Unlock(passphrase []byte) error { +func (m *Manager) Unlock(ns walletdb.ReadBucket, passphrase []byte) error { // A watching-only address manager can't be unlocked. if m.watchingOnly { return managerError(ErrWatchingOnly, errWatchingOnly, nil) @@ -1575,7 +1501,7 @@ func (m *Manager) Unlock(passphrase []byte) error { // Derive any private keys that are pending due to them being created // while the address manager was locked. for _, info := range m.deriveOnUnlock { - addressKey, err := m.deriveKeyFromPath(info.managedAddr.account, + addressKey, err := m.deriveKeyFromPath(ns, info.managedAddr.account, info.branch, info.index, true) if err != nil { m.lock() @@ -1617,21 +1543,14 @@ func (m *Manager) Unlock(passphrase []byte) error { } // fetchUsed returns true if the provided address id was flagged used. -func (m *Manager) fetchUsed(addressID []byte) (bool, error) { - var used bool - err := m.namespace.View(func(tx walletdb.Tx) error { - used = fetchAddressUsed(tx, addressID) - return nil - }) - return used, err +func (m *Manager) fetchUsed(ns walletdb.ReadBucket, addressID []byte) bool { + return fetchAddressUsed(ns, addressID) } // MarkUsed updates the used flag for the provided address. -func (m *Manager) MarkUsed(address dcrutil.Address) error { +func (m *Manager) MarkUsed(ns walletdb.ReadWriteBucket, address dcrutil.Address) error { addressID := address.ScriptAddress() - err := m.namespace.Update(func(tx walletdb.Tx) error { - return markAddressUsed(tx, addressID) - }) + err := markAddressUsed(ns, addressID) if err != nil { return maybeConvertDbError(err) } @@ -1653,16 +1572,11 @@ func (m *Manager) ChainParams() *chaincfg.Params { // AddressDerivedFromCointype loads the cointype private key and derives an address for // an account even if the account has not yet been created in the address // manager database. -func (m *Manager) AddressDerivedFromCointype(index uint32, account uint32, +func (m *Manager) AddressDerivedFromCointype(ns walletdb.ReadBucket, index uint32, account uint32, branch uint32) (dcrutil.Address, error) { // Fetch the cointype key which will be used to derive the next account // extended keys - var coinTypePrivEnc []byte - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - _, coinTypePrivEnc, err = fetchCoinTypeKeys(tx) - return err - }) + _, coinTypePrivEnc, err := fetchCoinTypeKeys(ns) if err != nil { return nil, err } @@ -1723,7 +1637,7 @@ func (m *Manager) AddressDerivedFromCointype(index uint32, account uint32, // NextAddresses function, this function does NOT add this address to the // address manager. It is used for rescanning the actively used addresses // in the wallet. -func (m *Manager) AddressDerivedFromDbAcct(index uint32, account uint32, +func (m *Manager) AddressDerivedFromDbAcct(ns walletdb.ReadBucket, index uint32, account uint32, branch uint32) (dcrutil.Address, error) { // Enforce maximum account number. if account > MaxAccountNum { @@ -1736,7 +1650,7 @@ func (m *Manager) AddressDerivedFromDbAcct(index uint32, account uint32, // The next address can only be generated for accounts that have already // been created. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -1807,7 +1721,7 @@ func AddressesDerivedFromExtPub(start uint32, end uint32, // addresses for some given account, branch, start index and end index. // In contrast to the NextAddresses function, this function does NOT add // these addresses to the address manager. -func (m *Manager) AddressesDerivedFromDbAcct(start uint32, end uint32, +func (m *Manager) AddressesDerivedFromDbAcct(ns walletdb.ReadBucket, start uint32, end uint32, account uint32, branch uint32) ([]dcrutil.Address, error) { // Enforce maximum account number. if account > MaxAccountNum { @@ -1823,7 +1737,7 @@ func (m *Manager) AddressesDerivedFromDbAcct(start uint32, end uint32, // The next address can only be generated for accounts that have already // been created. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { m.mtx.Unlock() return nil, err @@ -1838,11 +1752,11 @@ func (m *Manager) AddressesDerivedFromDbAcct(start uint32, end uint32, // the waddrmgr account to it. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) syncAccountToAddrIndex(account uint32, syncToIndex uint32, +func (m *Manager) syncAccountToAddrIndex(ns walletdb.ReadWriteBucket, account uint32, syncToIndex uint32, branch uint32) ([]ManagedAddress, error) { // The next address can only be generated for accounts that have already // been created. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -1957,21 +1871,14 @@ func (m *Manager) syncAccountToAddrIndex(account uint32, syncToIndex uint32, // Now that all addresses have been successfully generated, update the // database in a single transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - for _, info := range addressInfo { - ma := info.managedAddr - addressID := ma.Address().ScriptAddress() - err := putChainedAddress(tx, addressID, account, ssFull, - info.branch, info.index) - if err != nil { - return err - } + for _, info := range addressInfo { + ma := info.managedAddr + addressID := ma.Address().ScriptAddress() + err := putChainedAddress(ns, addressID, account, ssFull, + info.branch, info.index) + if err != nil { + return nil, maybeConvertDbError(err) } - - return nil - }) - if err != nil { - return nil, maybeConvertDbError(err) } // Finally update the next address tracking and add the addresses to the @@ -2007,8 +1914,9 @@ func (m *Manager) syncAccountToAddrIndex(account uint32, syncToIndex uint32, // SyncAccountToAddrIndex returns the specified number of next chained addresses // that are intended for internal use such as change from the address manager. -func (m *Manager) SyncAccountToAddrIndex(account uint32, syncToIndex uint32, - branch uint32) ([]ManagedAddress, error) { +func (m *Manager) SyncAccountToAddrIndex(ns walletdb.ReadWriteBucket, account uint32, + syncToIndex uint32, branch uint32) ([]ManagedAddress, error) { + // Enforce maximum account number. if account > MaxAccountNum { err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) @@ -2018,18 +1926,18 @@ func (m *Manager) SyncAccountToAddrIndex(account uint32, syncToIndex uint32, m.mtx.Lock() defer m.mtx.Unlock() - return m.syncAccountToAddrIndex(account, syncToIndex, branch) + return m.syncAccountToAddrIndex(ns, account, syncToIndex, branch) } // nextAddresses returns the specified number of next chained address from the // branch indicated by the internal flag. // // This function MUST be called with the manager lock held for writes. -func (m *Manager) nextAddresses(account uint32, numAddresses uint32, +func (m *Manager) nextAddresses(ns walletdb.ReadWriteBucket, account uint32, numAddresses uint32, internal bool) ([]ManagedAddress, error) { // The next address can only be generated for accounts that have already // been created. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, err } @@ -2118,21 +2026,14 @@ func (m *Manager) nextAddresses(account uint32, numAddresses uint32, // Now that all addresses have been successfully generated, update the // database in a single transaction. - err = m.namespace.Update(func(tx walletdb.Tx) error { - for _, info := range addressInfo { - ma := info.managedAddr - addressID := ma.Address().ScriptAddress() - err := putChainedAddress(tx, addressID, account, ssFull, - info.branch, info.index) - if err != nil { - return err - } + for _, info := range addressInfo { + ma := info.managedAddr + addressID := ma.Address().ScriptAddress() + err := putChainedAddress(ns, addressID, account, ssFull, + info.branch, info.index) + if err != nil { + return nil, maybeConvertDbError(err) } - - return nil - }) - if err != nil { - return nil, maybeConvertDbError(err) } // Finally update the next address tracking and add the addresses to the @@ -2168,7 +2069,7 @@ func (m *Manager) nextAddresses(account uint32, numAddresses uint32, // NextExternalAddresses returns the specified number of next chained addresses // that are intended for external use from the address manager. -func (m *Manager) NextExternalAddresses(account uint32, +func (m *Manager) NextExternalAddresses(ns walletdb.ReadWriteBucket, account uint32, numAddresses uint32) ([]ManagedAddress, error) { // Enforce maximum account number. if account > MaxAccountNum { @@ -2179,12 +2080,12 @@ func (m *Manager) NextExternalAddresses(account uint32, m.mtx.Lock() defer m.mtx.Unlock() - return m.nextAddresses(account, numAddresses, false) + return m.nextAddresses(ns, account, numAddresses, false) } // NextInternalAddresses returns the specified number of next chained addresses // that are intended for internal use such as change from the address manager. -func (m *Manager) NextInternalAddresses(account uint32, +func (m *Manager) NextInternalAddresses(ns walletdb.ReadWriteBucket, account uint32, numAddresses uint32) ([]ManagedAddress, error) { // Enforce maximum account number. if account > MaxAccountNum { @@ -2195,7 +2096,7 @@ func (m *Manager) NextInternalAddresses(account uint32, m.mtx.Lock() defer m.mtx.Unlock() - return m.nextAddresses(account, numAddresses, true) + return m.nextAddresses(ns, account, numAddresses, true) } // LastExternalAddress returns the most recently requested chained external @@ -2206,7 +2107,7 @@ func (m *Manager) NextInternalAddresses(account uint32, // This function will return an error if the provided account number is greater // than the MaxAccountNum constant or there is no account information for the // passed account. Any other errors returned are generally unexpected. -func (m *Manager) LastExternalAddress(account uint32) (ManagedAddress, uint32, +func (m *Manager) LastExternalAddress(ns walletdb.ReadBucket, account uint32) (ManagedAddress, uint32, error) { // Enforce maximum account number. if account > MaxAccountNum { @@ -2219,7 +2120,7 @@ func (m *Manager) LastExternalAddress(account uint32) (ManagedAddress, uint32, // Load account information for the passed account. It is typically // cached, but if not it will be loaded from the database. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, 0, err } @@ -2240,8 +2141,7 @@ func (m *Manager) LastExternalAddress(account uint32) (ManagedAddress, uint32, // This function will return an error if the provided account number is greater // than the MaxAccountNum constant or there is no account information for the // passed account. Any other errors returned are generally unexpected. -func (m *Manager) LastInternalAddress(account uint32) (ManagedAddress, uint32, - error) { +func (m *Manager) LastInternalAddress(ns walletdb.ReadBucket, account uint32) (ManagedAddress, uint32, error) { // Enforce maximum account number. if account > MaxAccountNum { err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) @@ -2253,7 +2153,7 @@ func (m *Manager) LastInternalAddress(account uint32) (ManagedAddress, uint32, // Load account information for the passed account. It is typically // cached, but if not it will be loaded from the database. - acctInfo, err := m.loadAccountInfo(account) + acctInfo, err := m.loadAccountInfo(ns, account) if err != nil { return nil, 0, err } @@ -2285,7 +2185,7 @@ func ValidateAccountName(name string) error { // ErrDuplicateAccount will be returned. Since creating a new account requires // access to the cointype keys (from which extended account keys are derived), // it requires the manager to be unlocked. -func (m *Manager) NewAccount(name string) (uint32, error) { +func (m *Manager) NewAccount(ns walletdb.ReadWriteBucket, name string) (uint32, error) { if m.watchingOnly { return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil) } @@ -2303,97 +2203,90 @@ func (m *Manager) NewAccount(name string) (uint32, error) { } // Check that account with the same name does not exist - _, err := m.lookupAccount(name) + _, err := m.lookupAccount(ns, name) if err == nil { str := fmt.Sprintf("account with the same name already exists") return 0, managerError(ErrDuplicateAccount, str, err) } - var account uint32 - var coinTypePrivEnc []byte - // Fetch latest account, and create a new account in the same transaction - err = m.namespace.Update(func(tx walletdb.Tx) error { - var err error - // Fetch the latest account number to generate the next account number - account, err = fetchLastAccount(tx) - if err != nil { - return err - } - account++ - // Fetch the cointype key which will be used to derive the next account - // extended keys - _, coinTypePrivEnc, err = fetchCoinTypeKeys(tx) - if err != nil { - return err - } + // Fetch the latest account number to generate the next account number + account, err := fetchLastAccount(ns) + if err != nil { + return 0, err + } + account++ + // Fetch the cointype key which will be used to derive the next account + // extended keys + _, coinTypePrivEnc, err := fetchCoinTypeKeys(ns) + if err != nil { + return 0, err + } - // Decrypt the cointype key - serializedKeyPriv, err := m.cryptoKeyPriv.Decrypt(coinTypePrivEnc) - if err != nil { - str := fmt.Sprintf("failed to decrypt cointype serialized private key") - return managerError(ErrLocked, str, err) - } - coinTypeKeyPriv, err := - hdkeychain.NewKeyFromString(string(serializedKeyPriv)) - zero.Bytes(serializedKeyPriv) - if err != nil { - str := fmt.Sprintf("failed to create cointype extended private key") - return managerError(ErrKeyChain, str, err) - } + // Decrypt the cointype key + serializedKeyPriv, err := m.cryptoKeyPriv.Decrypt(coinTypePrivEnc) + if err != nil { + str := fmt.Sprintf("failed to decrypt cointype serialized private key") + return 0, managerError(ErrLocked, str, err) + } + coinTypeKeyPriv, err := + hdkeychain.NewKeyFromString(string(serializedKeyPriv)) + zero.Bytes(serializedKeyPriv) + if err != nil { + str := fmt.Sprintf("failed to create cointype extended private key") + return 0, managerError(ErrKeyChain, str, err) + } - // Derive the account key using the cointype key - acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account) - coinTypeKeyPriv.Zero() - if err != nil { - str := "failed to convert private key for account" - return managerError(ErrKeyChain, str, err) - } - acctKeyPub, err := acctKeyPriv.Neuter() - if err != nil { - str := "failed to convert public key for account" - return managerError(ErrKeyChain, str, err) - } - // Encrypt the default account keys with the associated crypto keys. - apes, err := acctKeyPub.String() - if err != nil { - str := "failed to get public key string for account" - return managerError(ErrCrypto, str, err) - } - acctPubEnc, err := m.cryptoKeyPub.Encrypt([]byte(apes)) - if err != nil { - str := "failed to encrypt public key for account" - return managerError(ErrCrypto, str, err) - } - apes, err = acctKeyPriv.String() - if err != nil { - str := "failed to get private key string for account" - return managerError(ErrCrypto, str, err) - } - acctPrivEnc, err := m.cryptoKeyPriv.Encrypt([]byte(apes)) - if err != nil { - str := "failed to encrypt private key for account" - return managerError(ErrCrypto, str, err) - } - // We have the encrypted account extended keys, so save them to the - // database - err = putAccountInfo(tx, account, acctPubEnc, acctPrivEnc, 0, 0, name) - if err != nil { - return err - } + // Derive the account key using the cointype key + acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account) + coinTypeKeyPriv.Zero() + if err != nil { + str := "failed to convert private key for account" + return 0, managerError(ErrKeyChain, str, err) + } + acctKeyPub, err := acctKeyPriv.Neuter() + if err != nil { + str := "failed to convert public key for account" + return 0, managerError(ErrKeyChain, str, err) + } + // Encrypt the default account keys with the associated crypto keys. + apes, err := acctKeyPub.String() + if err != nil { + str := "failed to get public key string for account" + return 0, managerError(ErrCrypto, str, err) + } + acctPubEnc, err := m.cryptoKeyPub.Encrypt([]byte(apes)) + if err != nil { + str := "failed to encrypt public key for account" + return 0, managerError(ErrCrypto, str, err) + } + apes, err = acctKeyPriv.String() + if err != nil { + str := "failed to get private key string for account" + return 0, managerError(ErrCrypto, str, err) + } + acctPrivEnc, err := m.cryptoKeyPriv.Encrypt([]byte(apes)) + if err != nil { + str := "failed to encrypt private key for account" + return 0, managerError(ErrCrypto, str, err) + } + // We have the encrypted account extended keys, so save them to the + // database + err = putAccountInfo(ns, account, acctPubEnc, acctPrivEnc, 0, 0, name) + if err != nil { + return 0, err + } - // Save last account metadata - if err := putLastAccount(tx, account); err != nil { - return err - } - return nil - }) + // Save last account metadata + if err := putLastAccount(ns, account); err != nil { + return 0, err + } // Create a database entry for the address pool for the account. // The pool will be synced to the zeroeth index for both // branches. - m.storeNextToUseAddress(false, account, 0) - m.storeNextToUseAddress(true, account, 0) + m.storeNextToUseAddress(ns, false, account, 0) + m.storeNextToUseAddress(ns, true, account, 0) return account, err } @@ -2401,7 +2294,7 @@ func (m *Manager) NewAccount(name string) (uint32, error) { // RenameAccount renames an account stored in the manager based on the // given account number with the given name. If an account with the same name // already exists, ErrDuplicateAccount will be returned. -func (m *Manager) RenameAccount(account uint32, name string) error { +func (m *Manager) RenameAccount(ns walletdb.ReadWriteBucket, account uint32, name string) error { m.mtx.Lock() defer m.mtx.Unlock() @@ -2412,7 +2305,7 @@ func (m *Manager) RenameAccount(account uint32, name string) error { } // Check that account with the new name does not exist - _, err := m.lookupAccount(name) + _, err := m.lookupAccount(ns, name) if err == nil { str := fmt.Sprintf("account with the same name already exists") return managerError(ErrDuplicateAccount, str, err) @@ -2422,32 +2315,30 @@ func (m *Manager) RenameAccount(account uint32, name string) error { return err } - var rowInterface interface{} - err = m.namespace.Update(func(tx walletdb.Tx) error { - var err error - rowInterface, err = fetchAccountInfo(tx, account) - if err != nil { - return err - } - // Ensure the account type is a BIP0044 account. - row, ok := rowInterface.(*dbBIP0044AccountRow) - if !ok { - str := fmt.Sprintf("unsupported account type %T", row) - err = managerError(ErrDatabase, str, nil) - } - // Remove the old name key from the accout id index - if err = deleteAccountIDIndex(tx, account); err != nil { - return err - } - // Remove the old name key from the account name index - if err = deleteAccountNameIndex(tx, row.name); err != nil { - return err - } - err = putAccountInfo(tx, account, row.pubKeyEncrypted, - row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, - name) + rowInterface, err := fetchAccountInfo(ns, account) + if err != nil { return err - }) + } + // Ensure the account type is a BIP0044 account. + row, ok := rowInterface.(*dbBIP0044AccountRow) + if !ok { + str := fmt.Sprintf("unsupported account type %T", row) + err = managerError(ErrDatabase, str, nil) + } + // Remove the old name key from the accout id index + if err = deleteAccountIDIndex(ns, account); err != nil { + return err + } + // Remove the old name key from the account name index + if err = deleteAccountNameIndex(ns, row.name); err != nil { + return err + } + err = putAccountInfo(ns, account, row.pubKeyEncrypted, + row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, + name) + if err != nil { + return err + } // Update in-memory account info with new name if cached and the db // write was successful. @@ -2462,57 +2353,37 @@ func (m *Manager) RenameAccount(account uint32, name string) error { // AccountName returns the account name for the given account number // stored in the manager. -func (m *Manager) AccountName(account uint32) (string, error) { - var acctName string - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - acctName, err = fetchAccountName(tx, account) - return err - }) - if err != nil { - return "", err - } - - return acctName, nil +func (m *Manager) AccountName(ns walletdb.ReadBucket, account uint32) (string, error) { + return fetchAccountName(ns, account) } // ForEachAccount calls the given function with each account stored in the // manager, breaking early on error. -func (m *Manager) ForEachAccount(fn func(account uint32) error) error { - return m.namespace.View(func(tx walletdb.Tx) error { - return forEachAccount(tx, fn) - }) +func (m *Manager) ForEachAccount(ns walletdb.ReadBucket, fn func(account uint32) error) error { + return forEachAccount(ns, fn) } // LastAccount returns the last account stored in the manager. -func (m *Manager) LastAccount() (uint32, error) { - var account uint32 - err := m.namespace.View(func(tx walletdb.Tx) error { - var err error - account, err = fetchLastAccount(tx) - return err - }) - return account, err +func (m *Manager) LastAccount(ns walletdb.ReadBucket) (uint32, error) { + return fetchLastAccount(ns) } // ForEachAccountAddress calls the given function with each address of // the given account stored in the manager, breaking early on error. -func (m *Manager) ForEachAccountAddress(account uint32, +func (m *Manager) ForEachAccountAddress(ns walletdb.ReadBucket, account uint32, fn func(maddr ManagedAddress) error) error { + m.mtx.Lock() defer m.mtx.Unlock() addrFn := func(rowInterface interface{}) error { - managedAddr, err := m.rowInterfaceToManaged(rowInterface) + managedAddr, err := m.rowInterfaceToManaged(ns, rowInterface) if err != nil { return err } return fn(managedAddr) } - - err := m.namespace.View(func(tx walletdb.Tx) error { - return forEachAccountAddress(tx, account, addrFn) - }) + err := forEachAccountAddress(ns, account, addrFn) if err != nil { return maybeConvertDbError(err) } @@ -2523,28 +2394,27 @@ func (m *Manager) ForEachAccountAddress(account uint32, // address of the given account stored in the manager, breaking early on // error. // TODO(tuxcanfly): actually return only active addresses -func (m *Manager) ForEachActiveAccountAddress(account uint32, +func (m *Manager) ForEachActiveAccountAddress(ns walletdb.ReadBucket, account uint32, fn func(maddr ManagedAddress) error) error { - return m.ForEachAccountAddress(account, fn) + + return m.ForEachAccountAddress(ns, account, fn) } // ForEachActiveAddress calls the given function with each active address // stored in the manager, breaking early on error. -func (m *Manager) ForEachActiveAddress(fn func(addr dcrutil.Address) error) error { +func (m *Manager) ForEachActiveAddress(ns walletdb.ReadBucket, fn func(addr dcrutil.Address) error) error { m.mtx.Lock() defer m.mtx.Unlock() addrFn := func(rowInterface interface{}) error { - managedAddr, err := m.rowInterfaceToManaged(rowInterface) + managedAddr, err := m.rowInterfaceToManaged(ns, rowInterface) if err != nil { return err } return fn(managedAddr.Address()) } - err := m.namespace.View(func(tx walletdb.Tx) error { - return forEachActiveAddress(tx, addrFn) - }) + err := forEachActiveAddress(ns, addrFn) if err != nil { return maybeConvertDbError(err) } @@ -2620,14 +2490,12 @@ func (m *Manager) Decrypt(keyType CryptoKeyType, in []byte) ([]byte, error) { } // newManager returns a new locked address manager with the given parameters. -func newManager(namespace walletdb.Namespace, chainParams *chaincfg.Params, - masterKeyPub *snacl.SecretKey, masterKeyPriv *snacl.SecretKey, - cryptoKeyPub EncryptorDecryptor, cryptoKeyPrivEncrypted, - cryptoKeyScriptEncrypted []byte, syncInfo *syncState, +func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey, + masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor, + cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState, privPassphraseSalt [saltSize]byte) *Manager { return &Manager{ - namespace: namespace, chainParams: chainParams, addrs: make(map[addrKey]ManagedAddress), syncState: *syncInfo, @@ -2721,50 +2589,39 @@ func checkBranchKeys(acctKey *hdkeychain.ExtendedKey) error { // loadManager returns a new address manager that results from loading it from // the passed opened database. The public passphrase is required to decrypt the // public keys. -func loadManager(namespace walletdb.Namespace, pubPassphrase []byte, +func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte, chainParams *chaincfg.Params) (*Manager, error) { - // Perform all database lookups in a read-only view. - var watchingOnly bool - var masterKeyPubParams, masterKeyPrivParams []byte - var cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc []byte - var syncedTo, startBlock *BlockStamp - var recentHeight int32 - var recentHashes []chainhash.Hash - err := namespace.View(func(tx walletdb.Tx) error { - // Load whether or not the manager is watching-only from the db. - var err error - watchingOnly, err = fetchWatchingOnly(tx) - if err != nil { - return err - } - // Load the master key params from the db. - masterKeyPubParams, masterKeyPrivParams, err = - fetchMasterKeyParams(tx) - if err != nil { - return err - } + // Load whether or not the manager is watching-only from the db. + watchingOnly, err := fetchWatchingOnly(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } - // Load the crypto keys from the db. - cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc, err = - fetchCryptoKeys(tx) - if err != nil { - return err - } + // Load the master key params from the db. + masterKeyPubParams, masterKeyPrivParams, err := fetchMasterKeyParams(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } - // Load the sync state from the db. - syncedTo, err = fetchSyncedTo(tx) - if err != nil { - return err - } - startBlock, err = fetchStartBlock(tx) - if err != nil { - return err - } + // Load the crypto keys from the db. + cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc, err := + fetchCryptoKeys(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } - recentHeight, recentHashes, err = fetchRecentBlocks(tx) - return err - }) + // Load the sync state from the db. + syncedTo, err := fetchSyncedTo(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } + startBlock, err := fetchStartBlock(ns) + if err != nil { + return nil, maybeConvertDbError(err) + } + + recentHeight, recentHashes, err := fetchRecentBlocks(ns) if err != nil { return nil, maybeConvertDbError(err) } @@ -2816,7 +2673,7 @@ func loadManager(namespace walletdb.Namespace, pubPassphrase []byte, // Create new address manager with the given parameters. Also, override // the defaults for the additional fields which are not specified in the // call to new with the values loaded from the database. - mgr := newManager(namespace, chainParams, &masterKeyPub, &masterKeyPriv, + mgr := newManager(chainParams, &masterKeyPub, &masterKeyPriv, cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo, privPassphraseSalt) mgr.watchingOnly = watchingOnly @@ -2833,26 +2690,24 @@ func loadManager(namespace walletdb.Namespace, pubPassphrase []byte, // // A ManagerError with an error code of ErrNoExist will be returned if the // passed manager does not exist in the specified namespace. -func Open(namespace walletdb.Namespace, pubPassphrase []byte, - chainParams *chaincfg.Params, cbs *OpenCallbacks) (*Manager, error) { +func Open(ns walletdb.ReadBucket, pubPassphrase []byte, chainParams *chaincfg.Params) (*Manager, error) { // Return an error if the manager has NOT already been created in the // given database namespace. - exists, err := managerExists(namespace) - if err != nil { - return nil, err - } + exists := managerExists(ns) if !exists { str := "the specified address manager does not exist" return nil, managerError(ErrNoExist, str, nil) } - // Upgrade the manager to the latest version as needed. - if err := upgradeManager(namespace, pubPassphrase, chainParams, - cbs); err != nil { - return nil, err - } + return loadManager(ns, pubPassphrase, chainParams) +} + +// DoUpgrades performs any necessary upgrades to the address manager contained +// in the wallet database, namespaced by the top level bucket key namespaceKey. +func DoUpgrades(db walletdb.DB, namespaceKey []byte, pubPassphrase []byte, + chainParams *chaincfg.Params, cbs *OpenCallbacks) error { - return loadManager(namespace, pubPassphrase, chainParams) + return upgradeManager(db, namespaceKey, pubPassphrase, chainParams, cbs) } // Create creates a new address manager in the given namespace. The seed must @@ -2872,223 +2727,219 @@ func Open(namespace walletdb.Namespace, pubPassphrase []byte, // // A ManagerError with an error code of ErrAlreadyExists will be returned the // address manager already exists in the specified namespace. -func Create(namespace walletdb.Namespace, seed, pubPassphrase, - privPassphrase []byte, chainParams *chaincfg.Params, - config *ScryptOptions, unsafeMainNet bool) error { - // Return an error if the manager has already been created in the given - // database namespace. - exists, err := managerExists(namespace) - if err != nil { - return err - } - if exists { - return managerError(ErrAlreadyExists, errAlreadyExists, nil) - } - - // Ensure the private passphrase is not empty. - if len(privPassphrase) == 0 { - str := "private passphrase may not be empty" - return managerError(ErrEmptyPassphrase, str, nil) - } +func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []byte, + chainParams *chaincfg.Params, config *ScryptOptions, unsafeMainNet bool) error { - // Perform the initial bucket creation and database namespace setup. - if err := createManagerNS(namespace); err != nil { - return err - } + err := func() error { + // Return an error if the manager has already been created in the given + // database namespace. + exists := managerExists(ns) + if exists { + return managerError(ErrAlreadyExists, errAlreadyExists, nil) + } - if config == nil { - config = &DefaultScryptOptions - } + // Ensure the private passphrase is not empty. + if len(privPassphrase) == 0 { + str := "private passphrase may not be empty" + return managerError(ErrEmptyPassphrase, str, nil) + } - // Generate the BIP0044 HD key structure to ensure the provided seed - // can generate the required structure with no issues. + // Perform the initial bucket creation and database namespace setup. + if err := createManagerNS(ns); err != nil { + return err + } - // Derive the master extended key from the seed. - root, err := hdkeychain.NewMaster(seed, chainParams) - if err != nil { - str := "failed to derive master extended key" - return managerError(ErrKeyChain, str, err) - } + if config == nil { + config = &DefaultScryptOptions + } - // Derive the cointype key according to BIP0044. - coinTypeKeyPriv, err := deriveCoinTypeKey(root, chainParams.HDCoinType) - if err != nil { - str := "failed to derive cointype extended key" - return managerError(ErrKeyChain, str, err) - } - defer coinTypeKeyPriv.Zero() + // Generate the BIP0044 HD key structure to ensure the provided seed + // can generate the required structure with no issues. - // Derive the account key for the first account according to BIP0044. - acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0) - if err != nil { - // The seed is unusable if the any of the children in the - // required hierarchy can't be derived due to invalid child. - if err == hdkeychain.ErrInvalidChild { - str := "the provided seed is unusable" - return managerError(ErrKeyChain, str, - hdkeychain.ErrUnusableSeed) + // Derive the master extended key from the seed. + root, err := hdkeychain.NewMaster(seed, chainParams) + if err != nil { + str := "failed to derive master extended key" + return managerError(ErrKeyChain, str, err) } - return err - } + // Derive the cointype key according to BIP0044. + coinTypeKeyPriv, err := deriveCoinTypeKey(root, chainParams.HDCoinType) + if err != nil { + str := "failed to derive cointype extended key" + return managerError(ErrKeyChain, str, err) + } + defer coinTypeKeyPriv.Zero() - // Ensure the branch keys can be derived for the provided seed according - // to BIP0044. - if err := checkBranchKeys(acctKeyPriv); err != nil { - // The seed is unusable if the any of the children in the - // required hierarchy can't be derived due to invalid child. - if err == hdkeychain.ErrInvalidChild { - str := "the provided seed is unusable" - return managerError(ErrKeyChain, str, - hdkeychain.ErrUnusableSeed) + // Derive the account key for the first account according to BIP0044. + acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0) + if err != nil { + // The seed is unusable if the any of the children in the + // required hierarchy can't be derived due to invalid child. + if err == hdkeychain.ErrInvalidChild { + str := "the provided seed is unusable" + return managerError(ErrKeyChain, str, + hdkeychain.ErrUnusableSeed) + } + + return err } - return err - } + // Ensure the branch keys can be derived for the provided seed according + // to BIP0044. + if err := checkBranchKeys(acctKeyPriv); err != nil { + // The seed is unusable if the any of the children in the + // required hierarchy can't be derived due to invalid child. + if err == hdkeychain.ErrInvalidChild { + str := "the provided seed is unusable" + return managerError(ErrKeyChain, str, + hdkeychain.ErrUnusableSeed) + } - // The address manager needs the public extended key for the account. - acctKeyPub, err := acctKeyPriv.Neuter() - if err != nil { - str := "failed to convert private key for account 0" - return managerError(ErrKeyChain, str, err) - } + return err + } - // Generate new master keys. These master keys are used to protect the - // crypto keys that will be generated next. - masterKeyPub, err := newSecretKey(&pubPassphrase, config) - if err != nil { - str := "failed to master public key" - return managerError(ErrCrypto, str, err) - } - masterKeyPriv, err := newSecretKey(&privPassphrase, config) - if err != nil { - str := "failed to master private key" - return managerError(ErrCrypto, str, err) - } - defer masterKeyPriv.Zero() + // The address manager needs the public extended key for the account. + acctKeyPub, err := acctKeyPriv.Neuter() + if err != nil { + str := "failed to convert private key for account 0" + return managerError(ErrKeyChain, str, err) + } - // Generate the private passphrase salt. This is used when hashing - // passwords to detect whether an unlock can be avoided when the manager - // is already unlocked. - var privPassphraseSalt [saltSize]byte - _, err = rand.Read(privPassphraseSalt[:]) - if err != nil { - str := "failed to read random source for passphrase salt" - return managerError(ErrCrypto, str, err) - } + // Generate new master keys. These master keys are used to protect the + // crypto keys that will be generated next. + masterKeyPub, err := newSecretKey(&pubPassphrase, config) + if err != nil { + str := "failed to master public key" + return managerError(ErrCrypto, str, err) + } + masterKeyPriv, err := newSecretKey(&privPassphrase, config) + if err != nil { + str := "failed to master private key" + return managerError(ErrCrypto, str, err) + } + defer masterKeyPriv.Zero() - // Generate new crypto public, private, and script keys. These keys are - // used to protect the actual public and private data such as addresses, - // extended keys, and scripts. - cryptoKeyPub, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto public key" - return managerError(ErrCrypto, str, err) - } - cryptoKeyPriv, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto private key" - return managerError(ErrCrypto, str, err) - } - defer cryptoKeyPriv.Zero() + // Generate the private passphrase salt. This is used when hashing + // passwords to detect whether an unlock can be avoided when the manager + // is already unlocked. + var privPassphraseSalt [saltSize]byte + _, err = rand.Read(privPassphraseSalt[:]) + if err != nil { + str := "failed to read random source for passphrase salt" + return managerError(ErrCrypto, str, err) + } - // For SimNet and TestNet wallets, store the seed. For MainNet - // wallets, encrypt and store a zeroed 32-byte slice instead. - if (chainParams == &chaincfg.MainNetParams) && !unsafeMainNet { - seed = nullSeed - } - seedEnc, err := cryptoKeyPriv.Encrypt(seed) - if err != nil { - str := "failed to encrypt seed" - return managerError(ErrCrypto, str, err) - } + // Generate new crypto public, private, and script keys. These keys are + // used to protect the actual public and private data such as addresses, + // extended keys, and scripts. + cryptoKeyPub, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto public key" + return managerError(ErrCrypto, str, err) + } + cryptoKeyPriv, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto private key" + return managerError(ErrCrypto, str, err) + } + defer cryptoKeyPriv.Zero() - cryptoKeyScript, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto script key" - return managerError(ErrCrypto, str, err) - } - defer cryptoKeyScript.Zero() + // For SimNet and TestNet wallets, store the seed. For MainNet + // wallets, encrypt and store a zeroed 32-byte slice instead. + if (chainParams == &chaincfg.MainNetParams) && !unsafeMainNet { + seed = nullSeed + } + seedEnc, err := cryptoKeyPriv.Encrypt(seed) + if err != nil { + str := "failed to encrypt seed" + return managerError(ErrCrypto, str, err) + } - // Encrypt the crypto keys with the associated master keys. - cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes()) - if err != nil { - str := "failed to encrypt crypto public key" - return managerError(ErrCrypto, str, err) - } - cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes()) - if err != nil { - str := "failed to encrypt crypto private key" - return managerError(ErrCrypto, str, err) - } - cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript.Bytes()) - if err != nil { - str := "failed to encrypt crypto script key" - return managerError(ErrCrypto, str, err) - } + cryptoKeyScript, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto script key" + return managerError(ErrCrypto, str, err) + } + defer cryptoKeyScript.Zero() - // Encrypt the cointype keys with the associated crypto keys. - coinTypeKeyPub, err := coinTypeKeyPriv.Neuter() - if err != nil { - str := "failed to convert cointype private key" - return managerError(ErrKeyChain, str, err) - } - ctpes, err := coinTypeKeyPub.String() - if err != nil { - str := "failed to convert cointype public key string" - return managerError(ErrKeyChain, str, err) - } - coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(ctpes)) - if err != nil { - str := "failed to encrypt cointype public key" - return managerError(ErrCrypto, str, err) - } - ctpes, err = coinTypeKeyPriv.String() - if err != nil { - str := "failed to convert cointype private key string" - return managerError(ErrKeyChain, str, err) - } - coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(ctpes)) - if err != nil { - str := "failed to encrypt cointype private key" - return managerError(ErrCrypto, str, err) - } + // Encrypt the crypto keys with the associated master keys. + cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes()) + if err != nil { + str := "failed to encrypt crypto public key" + return managerError(ErrCrypto, str, err) + } + cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes()) + if err != nil { + str := "failed to encrypt crypto private key" + return managerError(ErrCrypto, str, err) + } + cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript.Bytes()) + if err != nil { + str := "failed to encrypt crypto script key" + return managerError(ErrCrypto, str, err) + } - // Encrypt the default account keys with the associated crypto keys. - apes, err := acctKeyPub.String() - if err != nil { - str := "failed to convert public key string for account 0" - return managerError(ErrKeyChain, str, err) - } - acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(apes)) - if err != nil { - str := "failed to encrypt public key for account 0" - return managerError(ErrCrypto, str, err) - } - apes, err = acctKeyPriv.String() - if err != nil { - str := "failed to convert private key string for account 0" - return managerError(ErrKeyChain, str, err) - } - acctPrivEnc, err := cryptoKeyPriv.Encrypt([]byte(apes)) - if err != nil { - str := "failed to encrypt private key for account 0" - return managerError(ErrCrypto, str, err) - } + // Encrypt the cointype keys with the associated crypto keys. + coinTypeKeyPub, err := coinTypeKeyPriv.Neuter() + if err != nil { + str := "failed to convert cointype private key" + return managerError(ErrKeyChain, str, err) + } + ctpes, err := coinTypeKeyPub.String() + if err != nil { + str := "failed to convert cointype public key string" + return managerError(ErrKeyChain, str, err) + } + coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(ctpes)) + if err != nil { + str := "failed to encrypt cointype public key" + return managerError(ErrCrypto, str, err) + } + ctpes, err = coinTypeKeyPriv.String() + if err != nil { + str := "failed to convert cointype private key string" + return managerError(ErrKeyChain, str, err) + } + coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(ctpes)) + if err != nil { + str := "failed to encrypt cointype private key" + return managerError(ErrCrypto, str, err) + } - // Use the genesis block for the passed chain as the created at block - // for the default. - createdAt := &BlockStamp{Hash: *chainParams.GenesisHash, Height: 0} + // Encrypt the default account keys with the associated crypto keys. + apes, err := acctKeyPub.String() + if err != nil { + str := "failed to convert public key string for account 0" + return managerError(ErrKeyChain, str, err) + } + acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(apes)) + if err != nil { + str := "failed to encrypt public key for account 0" + return managerError(ErrCrypto, str, err) + } + apes, err = acctKeyPriv.String() + if err != nil { + str := "failed to convert private key string for account 0" + return managerError(ErrKeyChain, str, err) + } + acctPrivEnc, err := cryptoKeyPriv.Encrypt([]byte(apes)) + if err != nil { + str := "failed to encrypt private key for account 0" + return managerError(ErrCrypto, str, err) + } - // Create the initial sync state. - recentHashes := []chainhash.Hash{createdAt.Hash} - recentHeight := createdAt.Height - syncInfo := newSyncState(createdAt, createdAt, recentHeight, recentHashes) + // Use the genesis block for the passed chain as the created at block + // for the default. + createdAt := &BlockStamp{Hash: *chainParams.GenesisHash, Height: 0} + + // Create the initial sync state. + recentHashes := []chainhash.Hash{createdAt.Hash} + recentHeight := createdAt.Height + syncInfo := newSyncState(createdAt, createdAt, recentHeight, recentHashes) - // Perform all database updates in a single transaction. - err = namespace.Update(func(tx walletdb.Tx) error { // Save the encrypted seed. - err = putSeed(tx, seedEnc) + err = putSeed(ns, seedEnc) if err != nil { return err } @@ -3096,69 +2947,69 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, // Save the master key params to the database. pubParams := masterKeyPub.Marshal() privParams := masterKeyPriv.Marshal() - err = putMasterKeyParams(tx, pubParams, privParams) + err = putMasterKeyParams(ns, pubParams, privParams) if err != nil { return err } // Save the encrypted crypto keys to the database. - err = putCryptoKeys(tx, cryptoKeyPubEnc, cryptoKeyPrivEnc, + err = putCryptoKeys(ns, cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc) if err != nil { return err } // Save the encrypted cointype keys to the database. - err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc) + err = putCoinTypeKeys(ns, coinTypePubEnc, coinTypePrivEnc) if err != nil { return err } // Save the fact this is a watching-only address manager to // the database. - err = putWatchingOnly(tx, false) + err = putWatchingOnly(ns, false) if err != nil { return err } // Save the initial synced to state. - err = putSyncedTo(tx, &syncInfo.syncedTo) + err = putSyncedTo(ns, &syncInfo.syncedTo) if err != nil { return err } - err = putStartBlock(tx, &syncInfo.startBlock) + err = putStartBlock(ns, &syncInfo.startBlock) if err != nil { return err } // Save the initial recent blocks state. - err = putRecentBlocks(tx, recentHeight, recentHashes) + err = putRecentBlocks(ns, recentHeight, recentHashes) if err != nil { return err } // Set the next to use addresses as empty for the address pool. - err = putNextToUseAddrPoolIdx(tx, false, DefaultAccountNum, 0) + err = putNextToUseAddrPoolIdx(ns, false, DefaultAccountNum, 0) if err != nil { return err } - err = putNextToUseAddrPoolIdx(tx, true, DefaultAccountNum, 0) + err = putNextToUseAddrPoolIdx(ns, true, DefaultAccountNum, 0) if err != nil { return err } // Save the information for the imported account to the database. - err = putAccountInfo(tx, ImportedAddrAccount, nil, + err = putAccountInfo(ns, ImportedAddrAccount, nil, nil, 0, 0, ImportedAddrAccountName) if err != nil { return err } // Save the information for the default account to the database. - err = putAccountInfo(tx, DefaultAccountNum, acctPubEnc, + err = putAccountInfo(ns, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0, defaultAccountName) return err - }) + }() if err != nil { return maybeConvertDbError(err) } @@ -3178,21 +3029,25 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, // // A ManagerError with an error code of ErrAlreadyExists will be returned the // address manager already exists in the specified namespace. -func CreateWatchOnly(namespace walletdb.Namespace, hdPubKey string, +func CreateWatchOnly(ns walletdb.ReadWriteBucket, hdPubKey string, pubPassphrase []byte, chainParams *chaincfg.Params, - config *ScryptOptions) error { + config *ScryptOptions) (err error) { + + defer func() { + if err != nil { + err = maybeConvertDbError(err) + } + }() + // Return an error if the manager has already been created in the given // database namespace. - exists, err := managerExists(namespace) - if err != nil { - return err - } + exists := managerExists(ns) if exists { return managerError(ErrAlreadyExists, errAlreadyExists, nil) } // Perform the initial bucket creation and database namespace setup. - if err := createManagerNS(namespace); err != nil { + if err := createManagerNS(ns); err != nil { return err } @@ -3328,71 +3183,63 @@ func CreateWatchOnly(namespace walletdb.Namespace, hdPubKey string, recentHeight := createdAt.Height syncInfo := newSyncState(createdAt, createdAt, recentHeight, recentHashes) - // Perform all database updates in a single transaction. - err = namespace.Update(func(tx walletdb.Tx) error { - // Save the master key params to the database. - pubParams := masterKeyPub.Marshal() - privParams := masterKeyPriv.Marshal() - err = putMasterKeyParams(tx, pubParams, privParams) - if err != nil { - return err - } - - // Save the encrypted crypto keys to the database. - err = putCryptoKeys(tx, cryptoKeyPubEnc, cryptoKeyPrivEnc, - cryptoKeyScriptEnc) - if err != nil { - return err - } - - // Save the fact this is not a watching-only address manager to - // the database. - err = putWatchingOnly(tx, true) - if err != nil { - return err - } + // Save the master key params to the database. + pubParams := masterKeyPub.Marshal() + privParams := masterKeyPriv.Marshal() + err = putMasterKeyParams(ns, pubParams, privParams) + if err != nil { + return err + } - // Save the initial synced to state. - err = putSyncedTo(tx, &syncInfo.syncedTo) - if err != nil { - return err - } - err = putStartBlock(tx, &syncInfo.startBlock) - if err != nil { - return err - } + // Save the encrypted crypto keys to the database. + err = putCryptoKeys(ns, cryptoKeyPubEnc, cryptoKeyPrivEnc, + cryptoKeyScriptEnc) + if err != nil { + return err + } - // Save the initial recent blocks state. - err = putRecentBlocks(tx, recentHeight, recentHashes) - if err != nil { - return err - } + // Save the fact this is not a watching-only address manager to + // the database. + err = putWatchingOnly(ns, true) + if err != nil { + return err + } - // Set the next to use addresses as empty for the address pool. - err = putNextToUseAddrPoolIdx(tx, false, DefaultAccountNum, 0) - if err != nil { - return err - } - err = putNextToUseAddrPoolIdx(tx, true, DefaultAccountNum, 0) - if err != nil { - return err - } + // Save the initial synced to state. + err = putSyncedTo(ns, &syncInfo.syncedTo) + if err != nil { + return err + } + err = putStartBlock(ns, &syncInfo.startBlock) + if err != nil { + return err + } - // Save the information for the imported account to the database. - err = putAccountInfo(tx, ImportedAddrAccount, nil, - nil, 0, 0, ImportedAddrAccountName) - if err != nil { - return err - } + // Save the initial recent blocks state. + err = putRecentBlocks(ns, recentHeight, recentHashes) + if err != nil { + return err + } - // Save the information for the default account to the database. - err = putAccountInfo(tx, DefaultAccountNum, acctPubEnc, - acctPrivEnc, 0, 0, defaultAccountName) + // Set the next to use addresses as empty for the address pool. + err = putNextToUseAddrPoolIdx(ns, false, DefaultAccountNum, 0) + if err != nil { return err - }) + } + err = putNextToUseAddrPoolIdx(ns, true, DefaultAccountNum, 0) if err != nil { - return maybeConvertDbError(err) + return err } - return nil + // Save the information for the imported account to the database. + err = putAccountInfo(ns, ImportedAddrAccount, nil, + nil, 0, 0, ImportedAddrAccountName) + if err != nil { + return err + } + + // Save the information for the default account to the database. + err = putAccountInfo(ns, DefaultAccountNum, acctPubEnc, + acctPrivEnc, 0, 0, defaultAccountName) + return err } diff --git a/waddrmgr/sync.go b/waddrmgr/sync.go index 11340e46b..f83405a15 100644 --- a/waddrmgr/sync.go +++ b/waddrmgr/sync.go @@ -139,7 +139,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() @@ -201,14 +201,11 @@ func (m *Manager) SetSyncedTo(bs *BlockStamp) error { } // Update the database. - err := m.namespace.Update(func(tx walletdb.Tx) error { - err := putSyncedTo(tx, bs) - if err != nil { - return err - } - - return putRecentBlocks(tx, recentHeight, recentHashes) - }) + err := putSyncedTo(ns, bs) + if err != nil { + return err + } + err = putRecentBlocks(ns, recentHeight, recentHashes) if err != nil { return err } diff --git a/wallet/addresspool.go b/wallet/addresspool.go index ae6c6f1c5..5eb103db8 100644 --- a/wallet/addresspool.go +++ b/wallet/addresspool.go @@ -23,6 +23,7 @@ import ( "github.com/decred/dcrutil" "github.com/decred/dcrwallet/waddrmgr" + "github.com/decred/dcrwallet/walletdb" ) // addressPoolBuffer is the number of addresses to fetch when the address pool @@ -83,8 +84,7 @@ func newAddressPools(account uint32, intIdx, extIdx uint32, // initialize initializes an address pool for the passed account and branch // to the address index given. It will automatically load a buffer of addresses // from the address manager to use for upcoming calls. -func (a *addressPool) initialize(account uint32, branch uint32, index uint32, - w *Wallet) error { +func (a *addressPool) initialize(account uint32, branch uint32, index uint32, w *Wallet) error { a.mutex.Lock() defer a.mutex.Unlock() @@ -104,37 +104,47 @@ func (a *addressPool) initialize(account uint32, branch uint32, index uint32, // Access the manager and get the synced to index, then insert all // the unused addresses into the address pool. - lastAddrFunc := w.Manager.LastExternalAddress - if branch == waddrmgr.InternalBranch { - lastAddrFunc = w.Manager.LastInternalAddress - } - _, mgrIdx, err := lastAddrFunc(account) - if err != nil { - return fmt.Errorf("failed to retrieve the last used addr index "+ - "from the address manager for branch %v, acct %v: %s", branch, - account, err.Error()) - } + var mgrIdx uint32 + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + waddrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) - if mgrIdx < index { - return fmt.Errorf("manager is out of sync with the passed index "+ - "(index %v, mgr index %v)", index, mgrIdx) - } + lastAddrFunc := w.Manager.LastExternalAddress + if branch == waddrmgr.InternalBranch { + lastAddrFunc = w.Manager.LastInternalAddress + } + var err error + _, mgrIdx, err = lastAddrFunc(waddrmgrNs, account) + if err != nil { + return fmt.Errorf("failed to retrieve the last used addr index "+ + "from the address manager for branch %v, acct %v: %s", branch, + account, err.Error()) + } - if mgrIdx == index { - a.addresses = make([]string, 0) - } else { - fetchNum := mgrIdx - index + 1 - a.addresses = make([]string, fetchNum) - for i := uint32(0); i < fetchNum; i++ { - addr, err := w.Manager.AddressDerivedFromDbAcct(index+i, account, - branch) - if err != nil { - return fmt.Errorf("failed to get the address at index %v "+ - "for account %v, branch %v: %s", index+i, account, branch, - err.Error()) + if mgrIdx < index { + return fmt.Errorf("manager is out of sync with the passed index "+ + "(index %v, mgr index %v)", index, mgrIdx) + } + + if mgrIdx == index { + a.addresses = make([]string, 0) + } else { + fetchNum := mgrIdx - index + 1 + a.addresses = make([]string, fetchNum) + for i := uint32(0); i < fetchNum; i++ { + addr, err := w.Manager.AddressDerivedFromDbAcct(waddrmgrNs, + index+i, account, branch) + if err != nil { + return fmt.Errorf("failed to get the address at index %v "+ + "for account %v, branch %v: %s", index+i, account, branch, + err.Error()) + } + a.addresses[i] = addr.EncodeAddress() } - a.addresses[i] = addr.EncodeAddress() } + return nil + }) + if err != nil { + return fmt.Errorf("failed to initialize address pool: %v", err) } a.wallet = w @@ -161,7 +171,7 @@ func (a *addressPool) initialize(account uint32, branch uint32, index uint32, // // This function MUST be called with the address pool mutex held and batch // finish or rollback must be called after. -func (a *addressPool) getNewAddress() (dcrutil.Address, error) { +func (a *addressPool) getNewAddress(waddrmgrNs walletdb.ReadWriteBucket) (dcrutil.Address, error) { if !a.started { return nil, fmt.Errorf("failed to getNewAddress; pool not started") } @@ -173,7 +183,7 @@ func (a *addressPool) getNewAddress() (dcrutil.Address, error) { // Replenish the pool if we're at the last address. if a.cursor == len(a.addresses)-1 || len(a.addresses) == 0 { - var nextAddrFunc func(uint32, uint32) ([]waddrmgr.ManagedAddress, error) + var nextAddrFunc func(walletdb.ReadWriteBucket, uint32, uint32) ([]waddrmgr.ManagedAddress, error) switch a.branch { case waddrmgr.InternalBranch: nextAddrFunc = a.wallet.Manager.NextInternalAddresses @@ -183,12 +193,10 @@ func (a *addressPool) getNewAddress() (dcrutil.Address, error) { return nil, fmt.Errorf("unknown default account branch %v", a.branch) } - addrs, err := - nextAddrFunc(a.account, addressPoolBuffer) + addrs, err := nextAddrFunc(waddrmgrNs, a.account, addressPoolBuffer) if err != nil { return nil, err } - for _, addr := range addrs { a.addresses = append(a.addresses, addr.Address().EncodeAddress()) } @@ -206,9 +214,6 @@ func (a *addressPool) getNewAddress() (dcrutil.Address, error) { log.Debugf("Get new address for branch %v returned %s (idx %v) from "+ "the address pool", a.branch, curAddressStr, a.index) - a.cursor++ - a.index++ - // Add the address to the notifications watcher. addrs := make([]dcrutil.Address, 1) addrs[0] = curAddress @@ -216,6 +221,9 @@ func (a *addressPool) getNewAddress() (dcrutil.Address, error) { return nil, err } + a.cursor++ + a.index++ + return curAddress, nil } @@ -225,9 +233,9 @@ func (a *addressPool) getNewAddress() (dcrutil.Address, error) { // rolled back after in the event of failure. It should mainly be // used in calls that provide a single new address to the user for // them to use externally. -func (a *addressPool) GetNewAddress() (dcrutil.Address, error) { +func (a *addressPool) GetNewAddress(waddrmgrNs walletdb.ReadWriteBucket) (dcrutil.Address, error) { defer func() { - errNotify := a.wallet.notifyAccountAddrIdxs(a.account) + errNotify := a.wallet.notifyAccountAddrIdxs(waddrmgrNs, a.account) if errNotify != nil { log.Errorf("Failed to push account update notification "+ "for account %v", a.account) @@ -236,9 +244,9 @@ func (a *addressPool) GetNewAddress() (dcrutil.Address, error) { a.mutex.Lock() defer a.mutex.Unlock() - address, err := a.getNewAddress() + address, err := a.getNewAddress(waddrmgrNs) if err == nil { - a.BatchFinish() + a.BatchFinish(waddrmgrNs) } else { a.BatchRollback() } @@ -256,8 +264,8 @@ func (w *Wallet) getAddressPools(account uint32) *addressPools { // notifyAccountAddrIdxs sends out an account notification when the address index // for some account branch has changed. -func (w *Wallet) notifyAccountAddrIdxs(account uint32) error { - name, err := w.Manager.AccountName(account) +func (w *Wallet) notifyAccountAddrIdxs(waddrmgrNs walletdb.ReadBucket, account uint32) error { + name, err := w.Manager.AccountName(waddrmgrNs, account) if err != nil { return err } @@ -286,12 +294,12 @@ func (w *Wallet) notifyAccountAddrIdxs(account uint32) error { // BatchFinish must be run after every successful series of usages of // GetNewAddress to purge the addresses from the unused map. -func (a *addressPool) BatchFinish() { +func (a *addressPool) BatchFinish(waddrmgrNs walletdb.ReadWriteBucket) { log.Debugf("Closing address batch for pool branch %v, next index %v", a.branch, a.index) isInternal := a.branch == waddrmgr.InternalBranch - err := a.wallet.Manager.StoreNextToUseAddress(isInternal, a.account, + err := a.wallet.Manager.StoreNextToUseAddress(waddrmgrNs, isInternal, a.account, a.index) if err != nil { log.Errorf("Failed to store next to use address idx for "+ @@ -321,7 +329,7 @@ func (a *addressPool) BatchRollback() { // Close writes the next to use index for the address pool to disk, then sets // the address pool as closed. -func (a *addressPool) Close() error { +func (a *addressPool) Close(waddrmgrNs walletdb.ReadWriteBucket) error { a.mutex.Lock() defer a.mutex.Unlock() @@ -330,7 +338,7 @@ func (a *addressPool) Close() error { } isInternal := a.branch == waddrmgr.InternalBranch - err := a.wallet.Manager.StoreNextToUseAddress(isInternal, a.account, + err := a.wallet.Manager.StoreNextToUseAddress(waddrmgrNs, isInternal, a.account, a.index) if err != nil { return fmt.Errorf("Failed to store next to use address idx for "+ @@ -346,10 +354,9 @@ func (a *addressPool) Close() error { // acounts. Then it inserts them into the address manager database, so that // the address manager can be used upon startup to restore the cursor position // in the address pool. -func (w *Wallet) CloseAddressPools() { +func (w *Wallet) CloseAddressPools(waddrmgrNs walletdb.ReadWriteBucket) { w.addrPoolsMtx.Lock() defer w.addrPoolsMtx.Unlock() - for _, addressPools := range w.addrPools { if addressPools.internal == nil { return @@ -361,12 +368,12 @@ func (w *Wallet) CloseAddressPools() { return } - err := addressPools.internal.Close() + err := addressPools.internal.Close(waddrmgrNs) if err != nil { log.Errorf("failed to close default acct internal addr pool: %v", err) } - err = addressPools.external.Close() + err = addressPools.external.Close(waddrmgrNs) if err != nil { log.Errorf("failed to close default acct external addr pool: %v", err) @@ -411,6 +418,17 @@ func (w *Wallet) CheckAddressPoolsInitialized(account uint32) error { // AddressPoolIndex returns the next to use address index for the passed // branch of the passed account. func (w *Wallet) AddressPoolIndex(account uint32, branch uint32) (uint32, error) { + var index uint32 + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + index, err = w.addressPoolIndex(waddrmgrNs, account, branch) + return err + }) + return index, err +} + +func (w *Wallet) addressPoolIndex(waddrmgrNs walletdb.ReadBucket, account uint32, branch uint32) (uint32, error) { err := w.CheckAddressPoolsInitialized(account) if err != nil { log.Tracef("Error on fetch of address pool account %v, branch %v from "+ @@ -421,7 +439,7 @@ func (w *Wallet) AddressPoolIndex(account uint32, branch uint32) (uint32, error) // try to load the last saved address index from the meta bucket // of the database. If that fails, give up and return an error. isInternal := branch == waddrmgr.InternalBranch - idx, err := w.Manager.NextToUseAddrPoolIndex(isInternal, account) + idx, err := w.Manager.NextToUseAddrPoolIndex(waddrmgrNs, isInternal, account) if err != nil { return 0, err } @@ -447,91 +465,96 @@ func (w *Wallet) AddressPoolIndex(account uint32, branch uint32) (uint32, error) // SyncAddressPoolIndex synchronizes an account's branch to the given address // by iteratively calling getNewAddress on the respective address pool. -func (w *Wallet) SyncAddressPoolIndex(account uint32, branch uint32, - index uint32) error { +func (w *Wallet) SyncAddressPoolIndex(account uint32, branch uint32, index uint32) error { // Sanity checks. err := w.CheckAddressPoolsInitialized(account) if err != nil { return err } - defer func() { - errNotify := w.notifyAccountAddrIdxs(account) - if errNotify != nil { - log.Errorf("Failed to push account update notification "+ - "for account %v", account) + return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + waddrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + + defer func() { + errNotify := w.notifyAccountAddrIdxs(waddrmgrNs, account) + if errNotify != nil { + log.Errorf("Failed to push account update notification "+ + "for account %v", account) + } + }() + + var addrPool *addressPool + switch branch { + case waddrmgr.ExternalBranch: + addrPool = w.getAddressPools(account).external + addrPool.mutex.Lock() + defer addrPool.mutex.Unlock() + case waddrmgr.InternalBranch: + addrPool = w.getAddressPools(account).internal + addrPool.mutex.Lock() + defer addrPool.mutex.Unlock() + default: + return fmt.Errorf("unknown branch number %v", branch) + } + if index < addrPool.index { + return fmt.Errorf("the passed index, %v, is before the "+ + "currently synced to address index %v", index, + addrPool.index) + } + if index == addrPool.index { + return nil } - }() - var addrPool *addressPool - switch branch { - case waddrmgr.ExternalBranch: - addrPool = w.getAddressPools(account).external - addrPool.mutex.Lock() - defer addrPool.mutex.Unlock() - case waddrmgr.InternalBranch: - addrPool = w.getAddressPools(account).internal - addrPool.mutex.Lock() - defer addrPool.mutex.Unlock() - default: - return fmt.Errorf("unknown branch number %v", branch) - } - if index < addrPool.index { - return fmt.Errorf("the passed index, %v, is before the "+ - "currently synced to address index %v", index, - addrPool.index) - } - if index == addrPool.index { - return nil - } - // Synchronize our address pool by calling getNewAddress - // iteratively until the next to use index is synced to - // where we need it. - toFetch := index - addrPool.index - for i := uint32(0); i < toFetch; i++ { - _, err := addrPool.getNewAddress() - if err != nil { - addrPool.BatchRollback() - return err + // Synchronize our address pool by calling getNewAddress + // iteratively until the next to use index is synced to + // where we need it. + toFetch := index - addrPool.index + for i := uint32(0); i < toFetch; i++ { + _, err := addrPool.getNewAddress(waddrmgrNs) + if err != nil { + addrPool.BatchRollback() + return err + } } - } - addrPool.BatchFinish() + addrPool.BatchFinish(waddrmgrNs) - return nil + return nil + }) } // NewAddress checks the address pools and then attempts to return a new // address for the account and branch requested. -func (w *Wallet) NewAddress(account uint32, branch uint32) (dcrutil.Address, - error) { - err := w.CheckAddressPoolsInitialized(account) - if err != nil { - return nil, err - } +func (w *Wallet) NewAddress(account uint32, branch uint32) (dcrutil.Address, error) { + var address dcrutil.Address + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + waddrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) - var addrPool *addressPool - switch branch { - case waddrmgr.ExternalBranch: - addrPool = w.getAddressPools(account).external - case waddrmgr.InternalBranch: - addrPool = w.getAddressPools(account).internal - default: - return nil, fmt.Errorf("new address failed; unknown branch number %v", - branch) - } + err := w.CheckAddressPoolsInitialized(account) + if err != nil { + return err + } - return addrPool.GetNewAddress() + var addrPool *addressPool + switch branch { + case waddrmgr.ExternalBranch: + addrPool = w.getAddressPools(account).external + case waddrmgr.InternalBranch: + addrPool = w.getAddressPools(account).internal + default: + return fmt.Errorf("new address failed; unknown branch number %v", + branch) + } + + address, err = addrPool.GetNewAddress(waddrmgrNs) + return err + }) + return address, err } -// ReusedAddress returns an address that is reused from the external +// reusedAddress returns an address that is reused from the external // branch of the wallet, to cut down on new address usage for wallets. // Should be used judiciously. -func (w *Wallet) ReusedAddress() (dcrutil.Address, error) { - addr, err := w.Manager.AddressDerivedFromDbAcct(0, +func (w *Wallet) reusedAddress(waddrmgrNs walletdb.ReadBucket) (dcrutil.Address, error) { + return w.Manager.AddressDerivedFromDbAcct(waddrmgrNs, 0, waddrmgr.DefaultAccountNum, waddrmgr.ExternalBranch) - if err != nil { - return nil, err - } - - return addr, err } diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index c073169b3..a2a7b90fe 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -16,6 +16,7 @@ import ( "github.com/decred/dcrwallet/waddrmgr" "github.com/decred/dcrwallet/wallet/txauthor" "github.com/decred/dcrwallet/wallet/txrules" + "github.com/decred/dcrwallet/walletdb" "github.com/decred/dcrwallet/wstakemgr" "github.com/decred/dcrwallet/wtxmgr" ) @@ -48,15 +49,21 @@ func (w *Wallet) handleChainNotifications() { log.Infof("The client has successfully connected to dcrd and " + "is now handling websocket notifications") case chain.BlockConnected: - err = w.connectBlock(wtxmgr.BlockMeta(n)) + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + return w.connectBlock(tx, wtxmgr.BlockMeta(n)) + }) strErrType = "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)) + }) strErrType = "BlockDisconnected" case chain.Reorganization: w.handleReorganizing(n.OldHash, n.OldHeight, n.NewHash, n.NewHeight) 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) + }) strErrType = "RelevantTx" // The following are handled by the wallet's rescan @@ -74,9 +81,20 @@ func (w *Wallet) handleChainNotifications() { // handleTicketPurchases autopurchases stake tickets for the wallet // if stake mining is enabled. -func (w *Wallet) handleTicketPurchases() { - purchased := 0 - attempts := 0 +func (w *Wallet) handleTicketPurchases(dbtx walletdb.ReadWriteTx) error { + // Nothing to do when stake mining is disabled. + if !w.StakeMiningEnabled { + return nil + } + + // Tickets are not purchased if the just there are still more blocks to + // connect to the best chain add as part of a reorg. + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { + return nil + } // Parse the ticket purchase frequency. Positive numbers mean // that many tickets per block. Negative numbers mean to only @@ -84,111 +102,87 @@ func (w *Wallet) handleTicketPurchases() { maxTickets := 1 switch { case w.ticketBuyFreq == 0: - return + return nil case w.ticketBuyFreq > 1: maxTickets = w.ticketBuyFreq case w.ticketBuyFreq < 0: bs := w.Manager.SyncedTo() if int(bs.Height)%w.ticketBuyFreq != 0 { - return + return nil } } sdiff, err := w.StakeDifficulty() if err != nil { - return + return err } maxToPay := w.GetTicketMaxPrice() minBalance := w.BalanceToMaintain() if sdiff > maxToPay { - return + log.Debugf("No tickets will be auto-purchased: current stake "+ + "difficulty %v is above maximum allowed price %v", sdiff, + maxToPay) + return nil } -ticketPurchaseLoop: - for { - if purchased >= maxTickets { - break - } - - _, err := w.PurchaseTickets(minBalance, - maxToPay, - 0, // No minconf - w.TicketAddress(), - waddrmgr.DefaultAccountNum, - 1, // One ticket at a time - w.PoolAddress(), - w.PoolFees(), - 0, // No expiry - w.RelayFee(), - w.TicketFeeIncrement()) - if err != nil { - _, insufficientFunds := err.(txauthor.InsufficientFundsError) - switch { - case insufficientFunds: - break ticketPurchaseLoop - case waddrmgr.IsError(err, waddrmgr.ErrLocked): - log.Warnf("Ticket purchase for stake mining is enabled, " + - "but tickets could not be purchased because the " + - "wallet is currently locked!") - break ticketPurchaseLoop - case err == ErrTicketPriceNotSet: - log.Warnf("Tickets could not be purchased because the " + - "ticket price could not be established") - break ticketPurchaseLoop - default: - log.Errorf("PurchaseTicket error returned: %v", err) - break ticketPurchaseLoop - } - } else { - purchased++ - } - - attempts++ - } + _, err = w.purchaseTicketsInternal(dbtx, purchaseTicketRequest{ + minBalance: minBalance, + spendLimit: maxToPay, + minConf: 0, // No minconf + ticketAddr: w.ticketAddress, + account: waddrmgr.DefaultAccountNum, + numTickets: maxTickets, + poolAddress: w.poolAddress, + poolFees: w.poolFees, + expiry: 0, // No expiry + txFee: w.RelayFee(), + ticketFee: w.TicketFeeIncrement(), + resp: nil, // not used, error is returned + }) + return err } // 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) error { +func (w *Wallet) connectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) error { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + + chainClient, err := w.requireChainClient() + if err != nil { + return err + } + 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) - } log.Infof("Connecting block %v, height %v", bs.Hash, bs.Height) - chainClient, err := w.requireChainClient() + err = w.Manager.SetSyncedTo(addrmgrNs, &bs) if err != nil { return err } - isReorganizing, topHash := chainClient.GetReorganizing() - - // If we've made it to the height where the reorganization is finished, - // revert our reorganization state. - if isReorganizing { - if bs.Hash.IsEqual(&topHash) { - log.Infof("Wallet reorganization to block %v complete", - topHash) - chainClient.SetReorganizingState(false, chainhash.Hash{}) - } - } - - if bs.Height >= int32(w.chainParams.CoinbaseMaturity) && - w.StakeMiningEnabled && - !isReorganizing { - w.handleTicketPurchases() + // Handle automatic ticket purchasing if enabled. This function should + // not error due to an error purchasing tickets (several tickets may be + // have been purhcased and successfully published, as well as addresses + // created and used), so just log it instead. + err = w.handleTicketPurchases(dbtx) + switch err.(type) { + case nil: + case txauthor.InsufficientFundsError: + log.Debugf("Insufficient funds to auto-purchase maximum number " + + "of tickets") + default: + log.Errorf("Failed to perform automatic picket purchasing: %v", err) } // Insert the block if we haven't already through a relevant tx. - err = w.TxStore.InsertBlock(&b) + err = w.TxStore.InsertBlock(txmgrNs, &b) if err != nil { err = fmt.Errorf("Couldn't insert block %v into database: %v", b.Hash, err) @@ -197,7 +191,7 @@ func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) error { // Rollback testing for simulation network, if enabled. if b.Height < rollbackTestHeight && w.rollbackTesting { - dbd, err := w.TxStore.DatabaseDump(b.Height, nil) + dbd, err := w.TxStore.DatabaseDump(txmgrNs, addrmgrNs, b.Height, nil) if err != nil { panicStr := fmt.Sprintf("Failed to dump database at connection "+ "of block %v (height %v): %v", @@ -224,13 +218,13 @@ func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) error { "database evaluations.") finalHeight := rollbackTestHeight - rollbackTestDepth for i := rollbackTestHeight; i >= finalHeight; i-- { - err := w.TxStore.Rollback(int32(i)) + err := w.TxStore.Rollback(txmgrNs, addrmgrNs, int32(i)) if err != nil { log.Errorf("Error rolling back block at height %v: %v", i, err) } - rolledbackDb, err := w.TxStore.DatabaseDump(int32(i-1), + rolledbackDb, err := w.TxStore.DatabaseDump(txmgrNs, addrmgrNs, int32(i-1), w.rollbackBlockDB[uint32(i-1)].BucketUnminedInputs) if err != nil { panicStr := fmt.Sprintf("Failed to dump database at "+ @@ -258,27 +252,42 @@ func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) error { // Prune all expired transactions and all stake tickets that no longer // meet the minimum stake difficulty. - stakeDifficulty, err := w.StakeDifficulty() + block, err := chainClient.GetBlock(&b.Hash) if err != nil { - return fmt.Errorf("Failed to get stake difficulty for pruning: %s", - err.Error()) + return err } - err = w.TxStore.PruneUnconfirmed(bs.Height, int64(stakeDifficulty)) + stakeDifficulty := dcrutil.Amount(block.MsgBlock().Header.SBits) + err = w.TxStore.PruneUnconfirmed(txmgrNs, bs.Height, int64(stakeDifficulty)) if err != nil { err = fmt.Errorf("Failed to prune unconfirmed transactions when "+ "connecting block height %v: %s", bs.Height, err.Error()) return err } + w.reorganizingLock.Lock() + isReorganizing, topHash := w.reorganizing, w.reorganizeToHash + // If we've made it to the height where the reorganization is finished, + // revert our reorganization state. + if isReorganizing && bs.Hash == topHash { + log.Infof("Wallet reorganization to block %v complete", topHash) + w.reorganizing = false + } + w.reorganizingLock.Unlock() + // 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 } @@ -290,15 +299,19 @@ func (w *Wallet) disconnectBlock(b wtxmgr.BlockMeta) error { // Disconnect the last seen block from the manager if it matches the // removed block. - err := w.TxStore.Rollback(b.Height) + err := w.TxStore.Rollback(txmgrNs, addrmgrNs, b.Height) if err != nil { return err } - prev, err := w.TxStore.GetBlockHash(b.Height - 1) + prev, err := w.TxStore.GetBlockHash(txmgrNs, b.Height-1) + if err != nil { + return err + } + prevBlock := &waddrmgr.BlockStamp{Hash: prev, Height: b.Height - 1} + err = w.Manager.SetSyncedTo(addrmgrNs, prevBlock) if err != nil { return err } - w.Manager.SetSyncedTo(&waddrmgr.BlockStamp{Hash: prev, Height: b.Height - 1}) // Notify interested clients of the disconnected block. w.NtfnServer.notifyDetachedBlock(&b.Hash) @@ -317,13 +330,10 @@ func (w *Wallet) handleReorganizing(oldHash *chainhash.Hash, oldHeight int64, log.Infof("New top block hash: %v", newHash) log.Infof("New top block height: %v", newHeight) - chainClient, err := w.requireChainClient() - if err != nil { - log.Error(err) - return - } - - chainClient.SetReorganizingState(true, *newHash) + w.reorganizingLock.Lock() + w.reorganizing = true + w.reorganizeToHash = *newHash + w.reorganizingLock.Unlock() } // evaluateStakePoolTicket evaluates a stake pool ticket to see if it's @@ -402,40 +412,12 @@ func (w *Wallet) evaluateStakePoolTicket(rec *wtxmgr.TxRecord, return true, nil } -func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, +func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, 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. + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + stakemgrNs := dbtx.ReadWriteBucket(wstakemgrNamespaceKey) + 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 @@ -462,7 +444,7 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, txOut.PkScript, w.chainParams) insert := false for _, addr := range addrs { - _, err := w.Manager.Address(addr) + _, err := w.Manager.Address(addrmgrNs, addr) if err == nil { // We own the voting output pubkey or script and we're // not operating as a stake pool, so simply insert this @@ -489,8 +471,8 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, HeightTicket: uint32(block.Height), Status: wstakemgr.TSImmatureOrLive, } - errUpdate := w.StakeMgr.UpdateStakePoolUserTickets(addr, - poolTicket) + errUpdate := w.StakeMgr.UpdateStakePoolUserTickets( + stakemgrNs, addrmgrNs, addr, poolTicket) if errUpdate != nil { log.Warnf("Failed to insert stake pool "+ "user ticket: %s", err.Error()) @@ -509,8 +491,8 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, log.Warnf("Ticket %v failed ticket evaluation for "+ "the stake pool: %s", rec.Hash, err.Error()) } - errUpdate := w.StakeMgr.UpdateStakePoolUserInvalTickets(addr, - &rec.Hash) + errUpdate := w.StakeMgr.UpdateStakePoolUserInvalTickets( + stakemgrNs, addr, &rec.Hash) if errUpdate != nil { log.Warnf("Failed to update pool user %v with "+ "invalid ticket %v", addr.EncodeAddress(), @@ -521,7 +503,7 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, } if insert { - err := w.StakeMgr.InsertSStx(tx, w.VoteBits) + err := w.StakeMgr.InsertSStx(stakemgrNs, tx, w.VoteBits) if err != nil { log.Errorf("Failed to insert SStx %v"+ "into the stake store.", tx.Sha()) @@ -535,7 +517,7 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, if block != nil { txInHash := tx.MsgTx().TxIn[1].PreviousOutPoint.Hash if w.StakeMgr.CheckHashInStore(&txInHash) { - w.StakeMgr.InsertSSGen(&block.Hash, + w.StakeMgr.InsertSSGen(stakemgrNs, &block.Hash, int64(block.Height), &txHash, w.VoteBits, @@ -554,13 +536,13 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, HeightSpent: uint32(block.Height), } - poolUser, err := w.StakeMgr.SStxAddress(&txInHash) + poolUser, err := w.StakeMgr.SStxAddress(stakemgrNs, &txInHash) if err != nil { log.Warnf("Failed to fetch stake pool user for "+ "ticket %v (voted ticket)", txInHash) } else { - err = w.StakeMgr.UpdateStakePoolUserTickets(poolUser, - poolTicket) + err = w.StakeMgr.UpdateStakePoolUserTickets( + stakemgrNs, addrmgrNs, poolUser, poolTicket) if err != nil { log.Warnf("Failed to update stake pool ticket for "+ "stake pool user %s after voting", @@ -587,7 +569,7 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, txInHash := tx.MsgTx().TxIn[0].PreviousOutPoint.Hash if w.StakeMgr.CheckHashInStore(&txInHash) { - w.StakeMgr.InsertSSRtx(&block.Hash, + w.StakeMgr.InsertSSRtx(stakemgrNs, &block.Hash, int64(block.Height), &txHash, &txInHash) @@ -605,13 +587,13 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, HeightSpent: uint32(block.Height), } - poolUser, err := w.StakeMgr.SStxAddress(&txInHash) + poolUser, err := w.StakeMgr.SStxAddress(stakemgrNs, &txInHash) if err != nil { log.Warnf("failed to fetch stake pool user for "+ "ticket %v (missed ticket)", txInHash) } else { - err = w.StakeMgr.UpdateStakePoolUserTickets(poolUser, - poolTicket) + err = w.StakeMgr.UpdateStakePoolUserTickets( + stakemgrNs, addrmgrNs, poolUser, poolTicket) if err != nil { log.Warnf("failed to update stake pool ticket for "+ "stake pool user %s after revoking", @@ -626,7 +608,7 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, } } - err := w.TxStore.InsertTx(rec, block) + err := w.TxStore.InsertTx(txmgrNs, addrmgrNs, rec, block) if err != nil { return err } @@ -654,10 +636,10 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, isRelevant := false for _, addr := range addrs { - _, err := w.Manager.Address(addr) + _, err := w.Manager.Address(addrmgrNs, addr) if err == nil { isRelevant = true - err = w.Manager.MarkUsed(addr) + err = w.Manager.MarkUsed(addrmgrNs, addr) if err != nil { return err } @@ -674,7 +656,7 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, // Add the script to the script databases. // TODO Markused script address? cj if isRelevant { - err = w.TxStore.InsertTxScript(rs) + err = w.TxStore.InsertTxScript(txmgrNs, rs) if err != nil { return err } @@ -685,7 +667,7 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, Hash: block.Hash, } } - mscriptaddr, err := w.Manager.ImportScript(rs, blockToUse) + mscriptaddr, err := w.Manager.ImportScript(addrmgrNs, rs, blockToUse) if err != nil { switch { // Don't care if it's already there. @@ -729,9 +711,9 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, // example, the wallet might be rescanning as called from // the above function and so does not have the output // included yet. - mso, err := w.TxStore.GetMultisigOutput(&input.PreviousOutPoint) + mso, err := w.TxStore.GetMultisigOutput(txmgrNs, &input.PreviousOutPoint) if mso != nil && err == nil { - w.TxStore.SpendMultisigOut(&input.PreviousOutPoint, + w.TxStore.SpendMultisigOut(txmgrNs, &input.PreviousOutPoint, rec.Hash, uint32(i)) } @@ -766,17 +748,17 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, } 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), - ma.Internal(), ma.Account()) + err = w.TxStore.AddCredit(txmgrNs, rec, block, + uint32(i), ma.Internal(), ma.Account()) if err != nil { return err } - err = w.Manager.MarkUsed(addr) + err = w.Manager.MarkUsed(addrmgrNs, addr) if err != nil { return err } @@ -800,13 +782,14 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, // and the address manager for the redeem script. var err error expandedScript, err = - w.TxStore.GetTxScript(addr.ScriptAddress()) + w.TxStore.GetTxScript(txmgrNs, + addr.ScriptAddress()) if err != nil { return err } if expandedScript == nil { - scrAddr, err := w.Manager.Address(addr) + scrAddr, err := w.Manager.Address(addrmgrNs, addr) if err == nil { sa, ok := scrAddr.(waddrmgr.ManagedScriptAddress) if !ok { @@ -852,10 +835,11 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, } for _, maddr := range multisigAddrs { - _, err := w.Manager.Address(maddr) + _, err := w.Manager.Address(addrmgrNs, maddr) // An address we own; handle accordingly. if err == nil { - errStore := w.TxStore.AddMultisigOut(rec, block, uint32(i)) + errStore := w.TxStore.AddMultisigOut( + txmgrNs, rec, block, uint32(i)) if errStore != nil { // This will throw if there are multiple private keys // for this multisignature output owned by the wallet, @@ -873,20 +857,20 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, // // 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) } } @@ -906,10 +890,14 @@ func (w *Wallet) handleChainVotingNotifications() { switch n := n.(type) { case chain.WinningTickets: - err = w.handleWinningTickets(n.BlockHash, n.BlockHeight, n.Tickets) + err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + return w.handleWinningTickets(dbtx, n.BlockHash, n.BlockHeight, n.Tickets) + }) strErrType = "WinningTickets" case chain.MissedTickets: - err = w.handleMissedTickets(n.BlockHash, n.BlockHeight, n.Tickets) + err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + return w.handleMissedTickets(dbtx, n.BlockHash, n.BlockHeight, n.Tickets) + }) strErrType = "MissedTickets" default: err = fmt.Errorf("voting handler received unknown ntfn type") @@ -924,9 +912,12 @@ func (w *Wallet) handleChainVotingNotifications() { // handleWinningTickets receives a list of hashes and some block information // and submits it to the wstakemgr to handle SSGen production. -func (w *Wallet) handleWinningTickets(blockHash *chainhash.Hash, - blockHeight int64, - tickets []*chainhash.Hash) error { +func (w *Wallet) handleWinningTickets(dbtx walletdb.ReadWriteTx, blockHash *chainhash.Hash, + blockHeight int64, tickets []*chainhash.Hash) error { + + stakemgrNs := dbtx.ReadWriteBucket(wstakemgrNamespaceKey) + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + topBlockStamp := w.Manager.SyncedTo() // Even if stake voting is disabled, we should still store eligible @@ -943,7 +934,10 @@ func (w *Wallet) handleWinningTickets(blockHash *chainhash.Hash, if blockHeight >= w.chainParams.StakeValidationHeight-1 && w.StakeMiningEnabled { - ntfns, err := w.StakeMgr.HandleWinningTicketsNtfn(blockHash, + ntfns, err := w.StakeMgr.HandleWinningTicketsNtfn( + stakemgrNs, + addrmgrNs, + blockHash, blockHeight, tickets, w.VoteBits, @@ -971,9 +965,11 @@ func (w *Wallet) handleWinningTickets(blockHash *chainhash.Hash, // handleMissedTickets receives a list of hashes and some block information // and submits it to the wstakemgr to handle SSRtx production. -func (w *Wallet) handleMissedTickets(blockHash *chainhash.Hash, - blockHeight int64, - tickets []*chainhash.Hash) error { +func (w *Wallet) handleMissedTickets(dbtx walletdb.ReadWriteTx, blockHash *chainhash.Hash, + blockHeight int64, tickets []*chainhash.Hash) error { + + stakemgrNs := dbtx.ReadWriteBucket(wstakemgrNamespaceKey) + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) if !w.StakeMiningEnabled { return nil @@ -981,9 +977,8 @@ func (w *Wallet) handleMissedTickets(blockHash *chainhash.Hash, if blockHeight >= w.chainParams.StakeValidationHeight+1 && w.StakeMiningEnabled { - ntfns, err := w.StakeMgr.HandleMissedTicketsNtfn(blockHash, - blockHeight, - tickets, w.AllowHighFees) + ntfns, err := w.StakeMgr.HandleMissedTicketsNtfn(stakemgrNs, addrmgrNs, + blockHash, blockHeight, tickets, w.AllowHighFees) if ntfns != nil { // Send notifications for newly created revocations by the RPC. diff --git a/wallet/common.go b/wallet/common.go new file mode 100644 index 000000000..6fe48dfae --- /dev/null +++ b/wallet/common.go @@ -0,0 +1,88 @@ +// 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 ( + "time" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrutil" +) + +// 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 + OutputKindStakebase // not returned by all APIs yet +) + +// 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 dcrutil.Amount + ContainingBlock BlockIdentity + + P2SHAddress *dcrutil.AddressScriptHash + RedeemScript []byte + M, N uint8 // M of N signatures required to redeem + Redeemer *OutputRedeemer // nil unless spent +} diff --git a/wallet/createtx.go b/wallet/createtx.go index 926e7b5b2..e984b15ec 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -25,6 +25,7 @@ import ( "github.com/decred/dcrwallet/waddrmgr" "github.com/decred/dcrwallet/wallet/txauthor" "github.com/decred/dcrwallet/wallet/txrules" + "github.com/decred/dcrwallet/walletdb" "github.com/decred/dcrwallet/wtxmgr" ) @@ -194,10 +195,11 @@ var ErrTicketPriceNotSet = errors.New("ticket price not yet established") // address manager. type secretSource struct { *waddrmgr.Manager + addrmgrNs walletdb.ReadBucket } func (s secretSource) GetKey(addr dcrutil.Address) (chainec.PrivateKey, bool, error) { - ma, err := s.Address(addr) + ma, err := s.Address(s.addrmgrNs, addr) if err != nil { return nil, false, err } @@ -215,7 +217,7 @@ func (s secretSource) GetKey(addr dcrutil.Address) (chainec.PrivateKey, bool, er } func (s secretSource) GetScript(addr dcrutil.Address) ([]byte, error) { - ma, err := s.Address(addr) + ma, err := s.Address(s.addrmgrNs, addr) if err != nil { return nil, err } @@ -239,18 +241,24 @@ type CreatedTx struct { // insertIntoTxMgr inserts a newly created transaction into the tx store // as unconfirmed. -func (w *Wallet) insertIntoTxMgr(msgTx *wire.MsgTx) (*wtxmgr.TxRecord, error) { +func (w *Wallet) insertIntoTxMgr(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, + msgTx *wire.MsgTx) (*wtxmgr.TxRecord, error) { + // Create transaction record and insert into the db. rec, err := wtxmgr.NewTxRecordFromMsgTx(msgTx, time.Now()) if err != nil { return nil, dcrjson.ErrInternal } - return rec, w.TxStore.InsertTx(rec, nil) + return rec, w.TxStore.InsertTx(ns, addrmgrNs, rec, nil) } -func (w *Wallet) insertCreditsIntoTxMgr(msgTx *wire.MsgTx, - rec *wtxmgr.TxRecord) error { +func (w *Wallet) insertCreditsIntoTxMgr(tx walletdb.ReadWriteTx, + msgTx *wire.MsgTx, rec *wtxmgr.TxRecord) error { + + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + // Check every output to determine whether it is controlled by a wallet // key. If so, mark the output as a credit. for i, output := range msgTx.TxOut { @@ -261,17 +269,17 @@ func (w *Wallet) insertCreditsIntoTxMgr(msgTx *wire.MsgTx, 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, nil, uint32(i), - ma.Internal(), ma.Account()) + err = w.TxStore.AddCredit(txmgrNs, rec, nil, + uint32(i), ma.Internal(), ma.Account()) if err != nil { return err } - err = w.Manager.MarkUsed(addr) + err = w.Manager.MarkUsed(addrmgrNs, addr) if err != nil { return err } @@ -293,124 +301,103 @@ func (w *Wallet) insertCreditsIntoTxMgr(msgTx *wire.MsgTx, // insertMultisigOutIntoTxMgr inserts a multisignature output into the // transaction store database. -func (w *Wallet) insertMultisigOutIntoTxMgr(msgTx *wire.MsgTx, +func (w *Wallet) insertMultisigOutIntoTxMgr(ns walletdb.ReadWriteBucket, msgTx *wire.MsgTx, index uint32) error { // Create transaction record and insert into the db. rec, err := wtxmgr.NewTxRecordFromMsgTx(msgTx, time.Now()) if err != nil { - return dcrjson.ErrInternal + return err } - return w.TxStore.AddMultisigOut(rec, nil, index) + return w.TxStore.AddMultisigOut(ns, rec, nil, index) } -// TxToOutputs is the exported version of txToOutputs that does all relevant -// locking to ensure the safety of the wallet state to generate a transaction. -// It then calls txToOutputs to generate a transaction. -func (w *Wallet) TxToOutputs(outputs []*wire.TxOut, account uint32, minconf int32, +// txToOutputs creates a transaction, selecting previous outputs from an account +// with no less than minconf confirmations, and creates a signed transaction +// that pays to each of the outputs. +func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int32, randomizeChangeIdx bool) (atx *txauthor.AuthoredTx, err 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() chainClient, err := w.requireChainClient() if err != nil { return nil, err } - isReorganizing, _ := chainClient.GetReorganizing() - if isReorganizing { + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { return nil, ErrBlockchainReorganizing } - // Initialize the address pool for use. If we - // are using an imported account, loopback to - // the default account to create change. - var pool *addressPool + // Initialize the address pool for use. If we are using an imported + // account, loopback to the default account to create change. + var internalPool *addressPool if account == waddrmgr.ImportedAddrAccount { err := w.CheckAddressPoolsInitialized(waddrmgr.DefaultAccountNum) if err != nil { return nil, err } - pool = w.getAddressPools(waddrmgr.DefaultAccountNum).internal + internalPool = w.getAddressPools(waddrmgr.DefaultAccountNum).internal } else { err := w.CheckAddressPoolsInitialized(account) if err != nil { return nil, err } - pool = w.getAddressPools(account).internal + internalPool = w.getAddressPools(account).internal } - changeAddrUsed := false - txSucceeded := false - pool.mutex.Lock() - defer pool.mutex.Unlock() - defer func() { - if txSucceeded && changeAddrUsed { - pool.BatchFinish() + + err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + internalPool.mutex.Lock() + var err error + atx, err = w.txToOutputsInternal(dbtx, outputs, account, + minconf, internalPool, chainClient, randomizeChangeIdx, + w.RelayFee()) + if err == nil { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + internalPool.BatchFinish(addrmgrNs) } else { - pool.BatchRollback() + internalPool.BatchRollback() } - }() - - atx, err = w.txToOutputs(outputs, account, minconf, pool, chainClient, - randomizeChangeIdx, w.RelayFee()) - txSucceeded = atx != nil - + internalPool.mutex.Unlock() + return err + }) return atx, err } -// txToOutputs creates a signed transaction which includes each output from -// outputs. Previous outputs to reedeem are chosen from the passed account's -// 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. -// The address pool passed must be locked and engaged in an address pool -// batch call. +// txToOutputsInternal creates a signed transaction which includes each output +// from outputs. Previous outputs to reedeem are chosen from the passed +// account's 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. The address pool passed must be locked and engaged in an +// address pool batch call. // // Decred: This func also sends the transaction, and if successful, inserts it // into the database, rather than delegating this work to the caller as // btcwallet does. -func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int32, - pool *addressPool, chainClient *chain.RPCClient, randomizeChangeIdx bool, - txFee dcrutil.Amount) (atx *txauthor.AuthoredTx, err error) { - - // The change address is pulled here rather than after - // MakeInputSource is called because MakeInputSource - // accesses the database in a way that deadlocks other - // packages that also access the database like waddmgr. - // Because the address pool occasionally makes calls - // to the address manager to replenish the address pool, - // calling the address function after MakeInputSource - // and before inputSource.CloseTransaction() will - // sometimes cause a lockup. - changeAddrUsed := false - changeAddr, err := pool.getNewAddress() - if err != nil { - return nil, err - } - changeSource := func() ([]byte, error) { - changeAddrUsed = true - return txscript.PayToAddrScript(changeAddr) - } +func (w *Wallet) txToOutputsInternal(dbtx walletdb.ReadWriteTx, outputs []*wire.TxOut, + account uint32, minconf int32, internalPool *addressPool, chainClient *chain.RPCClient, + randomizeChangeIdx bool, txFee dcrutil.Amount) (atx *txauthor.AuthoredTx, err error) { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) // Get current block's height and hash. - bs, err := chainClient.BlockStamp() - if err != nil { - return nil, err - } + bs := w.Manager.SyncedTo() - inputSource := w.TxStore.MakeInputSource(account, minconf, bs.Height) + inputSource := w.TxStore.MakeInputSource(txmgrNs, addrmgrNs, account, + minconf, bs.Height) + changeSource := func() ([]byte, error) { + // Derive the change output script. + changeAddress, err := internalPool.getNewAddress(addrmgrNs) + if err != nil { + return nil, err + } + return txscript.PayToAddrScript(changeAddress) + } tx, err := txauthor.NewUnsignedTransaction(outputs, txFee, inputSource.SelectInputs, changeSource) - closeErr := inputSource.CloseTransaction() - if closeErr != nil { - log.Errorf("Failed to close view: %v", closeErr) - } if err != nil { return nil, err } @@ -422,7 +409,7 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int3 tx.RandomizeChangePosition() } - err = tx.AddAllInputScripts(secretSource{w.Manager}) + err = tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) if err != nil { return nil, err } @@ -443,17 +430,13 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int3 return nil, err } - // Create transaction record and insert into the db. + // Save the new transaction rec, err := wtxmgr.NewTxRecordFromMsgTx(tx.Tx, time.Now()) if err != nil { return nil, fmt.Errorf("Cannot create record for created transaction: %v", err) } - err = w.TxStore.InsertTx(rec, nil) - if err != nil { - return nil, fmt.Errorf("Error adding sent tx history: %v", err) - } - err = w.insertCreditsIntoTxMgr(tx.Tx, rec) + err = w.addRelevantTx(dbtx, rec, nil) if err != nil { return nil, err } @@ -475,58 +458,68 @@ func constructMultiSigScript(keys []dcrutil.AddressSecpPubKey, func (w *Wallet) txToMultisig(account uint32, amount dcrutil.Amount, pubkeys []*dcrutil.AddressSecpPubKey, nRequired int8, minconf int32) (*CreatedTx, dcrutil.Address, []byte, error) { + + var ( + ctx *CreatedTx + addr dcrutil.Address + msScript []byte + ) + err := walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + var err error + ctx, addr, msScript, err = w.txToMultisigInternal(dbtx, + account, amount, pubkeys, nRequired, minconf) + return err + }) + return ctx, addr, msScript, err +} + +func (w *Wallet) txToMultisigInternal(dbtx walletdb.ReadWriteTx, account uint32, + amount dcrutil.Amount, pubkeys []*dcrutil.AddressSecpPubKey, nRequired int8, + minconf int32) (*CreatedTx, dcrutil.Address, []byte, error) { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + txToMultisigError := func(err error) (*CreatedTx, dcrutil.Address, []byte, error) { return nil, nil, nil, err } - // Initialize the address pool for use. If we - // are using an imported account, loopback to - // the default account to create change. - var pool *addressPool + // Initialize the address pool for use. If we are using an imported + // account, loopback to the default account to create change. + var internalPool *addressPool if account == waddrmgr.ImportedAddrAccount { err := w.CheckAddressPoolsInitialized(waddrmgr.DefaultAccountNum) if err != nil { return txToMultisigError(err) } - pool = w.getAddressPools(waddrmgr.DefaultAccountNum).internal + internalPool = w.getAddressPools(waddrmgr.DefaultAccountNum).internal } else { err := w.CheckAddressPoolsInitialized(account) if err != nil { return txToMultisigError(err) } - pool = w.getAddressPools(account).internal + internalPool = w.getAddressPools(account).internal } - txSucceeded := false - pool.mutex.Lock() - defer pool.mutex.Unlock() - defer func() { - if txSucceeded { - pool.BatchFinish() - } else { - pool.BatchRollback() - } - }() - addrFunc := pool.getNewAddress + + // TODO: Yes this looks suspicious but it's a simplification of what the + // code before it was doing. Stop copy pasting code carelessly. + internalPool.mutex.Lock() + defer internalPool.mutex.Unlock() + defer internalPool.BatchRollback() + addrFunc := internalPool.getNewAddress chainClient, err := w.requireChainClient() if err != nil { return txToMultisigError(err) } - isReorganizing, _ := chainClient.GetReorganizing() - if isReorganizing { - return txToMultisigError(err) - } - - // 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 txToMultisigError(err) + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { + return txToMultisigError(ErrBlockchainReorganizing) } - defer heldUnlock.Release() // Get current block's height and hash. bs, err := chainClient.BlockStamp() @@ -549,7 +542,7 @@ func (w *Wallet) txToMultisig(account uint32, amount dcrutil.Amount, // Instead of taking reward addresses by arg, just create them now and // automatically find all eligible outputs from all current utxos. - eligible, err := w.findEligibleOutputsAmount(account, minconf, + eligible, err := w.findEligibleOutputsAmount(dbtx, account, minconf, amountRequired, bs) if err != nil { return txToMultisigError(err) @@ -581,7 +574,7 @@ func (w *Wallet) txToMultisig(account uint32, amount dcrutil.Amount, if err != nil { return txToMultisigError(err) } - _, err = w.Manager.ImportScript(msScript, bs) + _, err = w.Manager.ImportScript(addrmgrNs, msScript, bs) if err != nil { // We don't care if we've already used this address. if err.(waddrmgr.ManagerError).ErrorCode != @@ -589,7 +582,7 @@ func (w *Wallet) txToMultisig(account uint32, amount dcrutil.Amount, return txToMultisigError(err) } } - err = w.TxStore.InsertTxScript(msScript) + err = w.TxStore.InsertTxScript(txmgrNs, msScript) if err != nil { return txToMultisigError(err) } @@ -619,7 +612,7 @@ func (w *Wallet) txToMultisig(account uint32, amount dcrutil.Amount, "multisig address after accounting for fees")) } if totalInput > amount+feeEst { - changeAddr, err := addrFunc() + changeAddr, err := addrFunc(addrmgrNs) if err != nil { return txToMultisigError(err) } @@ -632,7 +625,7 @@ func (w *Wallet) txToMultisig(account uint32, amount dcrutil.Amount, msgtx.AddTxOut(wire.NewTxOut(int64(change), pkScript)) } - if err = signMsgTx(msgtx, forSigning, w.Manager, + if err = signMsgTx(msgtx, forSigning, w.Manager, addrmgrNs, w.chainParams); err != nil { return txToMultisigError(err) } @@ -650,7 +643,7 @@ func (w *Wallet) txToMultisig(account uint32, amount dcrutil.Amount, return txToMultisigError(err) } - err = w.insertMultisigOutIntoTxMgr(msgtx, 0) + err = w.insertMultisigOutIntoTxMgr(txmgrNs, msgtx, 0) if err != nil { return txToMultisigError(err) } @@ -692,15 +685,31 @@ func validateMsgTxCredits(tx *wire.MsgTx, prevCredits []wtxmgr.Credit) error { // compressWallet compresses all the utxos in a wallet into a single change // address. For use when it becomes dusty. -func (w *Wallet) compressWallet(maxNumIns int, account uint32, +func (w *Wallet) compressWallet(maxNumIns int, account uint32, changeAddr dcrutil.Address) (*chainhash.Hash, error) { + var hash *chainhash.Hash + err := walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + var err error + hash, err = w.compressWalletInternal(dbtx, maxNumIns, account, changeAddr) + return err + }) + return hash, err +} + +func (w *Wallet) compressWalletInternal(dbtx walletdb.ReadWriteTx, maxNumIns int, account uint32, changeAddr dcrutil.Address) (*chainhash.Hash, error) { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + chainClient, err := w.requireChainClient() if err != nil { return nil, err } - isReorganizing, _ := chainClient.GetReorganizing() - if isReorganizing { + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { return nil, ErrBlockchainReorganizing } @@ -710,37 +719,36 @@ func (w *Wallet) compressWallet(maxNumIns int, account uint32, return nil, err } - // Initialize the address pool for use. If we - // are using an imported account, loopback to - // the default account to create change. - var pool *addressPool + // Initialize the address pool for use. If we are using an imported + // account, loopback to the default account to create change. + var internalPool *addressPool if account == waddrmgr.ImportedAddrAccount { err := w.CheckAddressPoolsInitialized(waddrmgr.DefaultAccountNum) if err != nil { return nil, err } - pool = w.getAddressPools(waddrmgr.DefaultAccountNum).internal + internalPool = w.getAddressPools(waddrmgr.DefaultAccountNum).internal } else { err := w.CheckAddressPoolsInitialized(account) if err != nil { return nil, err } - pool = w.getAddressPools(account).internal + internalPool = w.getAddressPools(account).internal } txSucceeded := false - pool.mutex.Lock() - defer pool.mutex.Unlock() + internalPool.mutex.Lock() + defer internalPool.mutex.Unlock() defer func() { if txSucceeded { - pool.BatchFinish() + internalPool.BatchFinish(addrmgrNs) } else { - pool.BatchRollback() + internalPool.BatchRollback() } }() - addrFunc := pool.getNewAddress + addrFunc := internalPool.getNewAddress minconf := int32(1) - eligible, err := w.findEligibleOutputs(account, minconf, bs) + eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) if err != nil { return nil, err } @@ -764,7 +772,7 @@ func (w *Wallet) compressWallet(maxNumIns int, account uint32, // Check if output address is default, and generate a new adress if needed if changeAddr == nil { - changeAddr, err = addrFunc() + changeAddr, err = addrFunc(addrmgrNs) if err != nil { return nil, err } @@ -803,7 +811,7 @@ func (w *Wallet) compressWallet(maxNumIns int, account uint32, msgtx.TxOut[0].Value = int64(totalAdded - feeEst) - if err = signMsgTx(msgtx, forSigning, w.Manager, + if err = signMsgTx(msgtx, forSigning, w.Manager, addrmgrNs, w.chainParams); err != nil { return nil, err } @@ -818,11 +826,11 @@ func (w *Wallet) compressWallet(maxNumIns int, account uint32, txSucceeded = true // Insert the transaction and credits into the transaction manager. - rec, err := w.insertIntoTxMgr(msgtx) + rec, err := w.insertIntoTxMgr(txmgrNs, addrmgrNs, msgtx) if err != nil { return nil, err } - err = w.insertCreditsIntoTxMgr(msgtx, rec) + err = w.insertCreditsIntoTxMgr(dbtx, msgtx, rec) if err != nil { return nil, err } @@ -832,105 +840,6 @@ func (w *Wallet) compressWallet(maxNumIns int, account uint32, return txSha, nil } -// compressEligible compresses all the utxos passed to it into a single -// output back to the wallet. -func (w *Wallet) compressEligible(eligible []wtxmgr.Credit) error { - chainClient, err := w.requireChainClient() - if err != nil { - return err - } - - // Initialize the address pool for use. - var pool *addressPool - err = w.CheckAddressPoolsInitialized(waddrmgr.DefaultAccountNum) - if err != nil { - return err - } - pool = w.getAddressPools(waddrmgr.DefaultAccountNum).internal - if pool == nil { - log.Errorf("tried to use uninitialized pool for acct %v "+ - "when attempting to make a transaction", - waddrmgr.DefaultAccountNum) - } - txSucceeded := false - pool.mutex.Lock() - defer pool.mutex.Unlock() - defer func() { - if txSucceeded { - pool.BatchFinish() - } else { - pool.BatchRollback() - } - }() - addrFunc := pool.getNewAddress - - if len(eligible) == 0 { - return ErrNoOutsToConsolidate - } - - txInCount := len(eligible) - - // Get an initial fee estimate based on the number of selected inputs - // and added outputs, with no change. - szEst := estimateTxSize(txInCount, 1) - var feeIncrement dcrutil.Amount - feeIncrement = w.RelayFee() - - feeEst := feeForSize(feeIncrement, szEst) - - msgtx := wire.NewMsgTx() - - // Add the txins using all the eligible outputs. - totalAdded := dcrutil.Amount(0) - var forSigning []wtxmgr.Credit - for _, e := range eligible { - msgtx.AddTxIn(wire.NewTxIn(&e.OutPoint, nil)) - totalAdded += e.Amount - forSigning = append(forSigning, e) - } - - outputAmt := totalAdded - feeEst - - changeAddr, err := addrFunc() - if err != nil { - return err - } - - pkScript, err := txscript.PayToAddrScript(changeAddr) - if err != nil { - return fmt.Errorf("cannot create txout script: %s", err) - } - msgtx.AddTxOut(wire.NewTxOut(int64(outputAmt), pkScript)) - - if err = signMsgTx(msgtx, forSigning, w.Manager, - w.chainParams); err != nil { - return err - } - if err := validateMsgTxCredits(msgtx, forSigning); err != nil { - return err - } - - txSha, err := chainClient.SendRawTransaction(msgtx, w.AllowHighFees) - if err != nil { - return err - } - txSucceeded = true - - // Insert the transaction and credits into the transaction manager. - rec, err := w.insertIntoTxMgr(msgtx) - if err != nil { - return err - } - err = w.insertCreditsIntoTxMgr(msgtx, rec) - if err != nil { - return err - } - - log.Infof("Successfully consolidated funds in transaction %v", txSha) - - return nil -} - // makeTicket creates a ticket from a split transaction output. It can optionally // create a ticket that pays a fee to a pool if a pool input and pool address are // passed. @@ -1040,14 +949,27 @@ func makeTicket(params *chaincfg.Params, inputPool *extendedOutPoint, return mtx, nil } -// purchaseTicket indicates to the wallet that a ticket should be purchased +// purchaseTickets indicates to the wallet that a ticket should be purchased // using all currently available funds. The ticket address parameter in the // request can be nil in which case the ticket address associated with the // wallet instance will be used. Also, when the spend limit in the request is // greater than or equal to 0, tickets that cost more than that limit will // return an error that not enough funds are available. -func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, - error) { +func (w *Wallet) purchaseTickets(req purchaseTicketRequest) ([]*chainhash.Hash, error) { + var ticketHashes []*chainhash.Hash + err := walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + var err error + ticketHashes, err = w.purchaseTicketsInternal(dbtx, req) + return err + }) + return ticketHashes, err +} + +func (w *Wallet) purchaseTicketsInternal(dbtx walletdb.ReadWriteTx, req purchaseTicketRequest) ([]*chainhash.Hash, error) { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + stakemgrNs := dbtx.ReadWriteBucket(wstakemgrNamespaceKey) + // Ensure the minimum number of required confirmations is positive. if req.minConf < 0 { return nil, fmt.Errorf("need positive minconf") @@ -1065,37 +987,30 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, } // Initialize the address pool for use. - var pool *addressPool + var internalPool *addressPool err := w.CheckAddressPoolsInitialized(req.account) if err != nil { return nil, err } - pool = w.getAddressPools(req.account).internal - - // Address manager must be unlocked to compose tickets. 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() + internalPool = w.getAddressPools(req.account).internal // Fire up the address pool for usage in generating tickets. - pool.mutex.Lock() - defer pool.mutex.Unlock() + internalPool.mutex.Lock() + defer internalPool.mutex.Unlock() txSucceeded := false defer func() { if txSucceeded { - pool.BatchFinish() + internalPool.BatchFinish(addrmgrNs) } else { - pool.BatchRollback() + internalPool.BatchRollback() } }() - addrFunc := pool.getNewAddress + addrFunc := internalPool.getNewAddress if w.addressReuse { - addrFunc = w.ReusedAddress + addrFunc = func(ns walletdb.ReadWriteBucket) (dcrutil.Address, error) { + return w.reusedAddress(ns) + } } chainClient, err := w.requireChainClient() @@ -1103,9 +1018,11 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, return nil, err } - isReorganizing, _ := chainClient.GetReorganizing() - if isReorganizing { - return "", ErrBlockchainReorganizing + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { + return nil, ErrBlockchainReorganizing } // Fetch a new address for creating a split transaction. Then, @@ -1208,7 +1125,7 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, // Fetch the single use split address to break tickets into, to // immediately be consumed as tickets. - splitTxAddr, err := pool.getNewAddress() + splitTxAddr, err := internalPool.getNewAddress(addrmgrNs) if err != nil { return nil, err } @@ -1257,7 +1174,7 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, if txFeeIncrement == 0 { txFeeIncrement = w.RelayFee() } - splitTx, err := w.txToOutputs(splitOuts, account, req.minConf, pool, + splitTx, err := w.txToOutputsInternal(dbtx, splitOuts, account, req.minConf, internalPool, chainClient, false, txFeeIncrement) if err != nil { return nil, err @@ -1327,14 +1244,14 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, if addrVote == nil { addrVote = w.ticketAddress if addrVote == nil { - addrVote, err = addrFunc() + addrVote, err = addrFunc(addrmgrNs) if err != nil { return nil, err } } } - addrSubsidy, err := addrFunc() + addrSubsidy, err := addrFunc(addrmgrNs) if err != nil { return nil, err } @@ -1370,7 +1287,7 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, // Set the expiry. ticket.Expiry = uint32(req.expiry) - if err = signMsgTx(ticket, forSigning, w.Manager, + if err = signMsgTx(ticket, forSigning, w.Manager, addrmgrNs, w.chainParams); err != nil { return nil, err } @@ -1385,11 +1302,11 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, } // Insert the transaction and credits into the transaction manager. - rec, err := w.insertIntoTxMgr(ticket) + rec, err := w.insertIntoTxMgr(txmgrNs, addrmgrNs, ticket) if err != nil { return nil, err } - err = w.insertCreditsIntoTxMgr(ticket, rec) + err = w.insertCreditsIntoTxMgr(dbtx, ticket, rec) if err != nil { return nil, err } @@ -1399,9 +1316,9 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, // ticket into the stake manager unless we actually own output zero // of it. If this is the case, the chainntfns.go handlers will // automatically insert it. - if _, err := w.Manager.Address(addrVote); err == nil { + if _, err := w.Manager.Address(addrmgrNs, addrVote); err == nil { if w.ticketAddress == nil { - err = w.StakeMgr.InsertSStx(txTemp, w.VoteBits) + err = w.StakeMgr.InsertSStx(stakemgrNs, txTemp, w.VoteBits) if err != nil { return nil, fmt.Errorf("Failed to insert SStx %v"+ "into the stake store", txTemp.Sha()) @@ -1427,30 +1344,44 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, // enough eligible unspent outputs to create the transaction. func (w *Wallet) txToSStx(pair map[string]dcrutil.Amount, inputCredits []wtxmgr.Credit, inputs []dcrjson.SStxInput, - payouts []dcrjson.SStxCommitOut, account uint32, - addrFunc func() (dcrutil.Address, error), minconf int32) (*CreatedTx, - error) { + payouts []dcrjson.SStxCommitOut, account uint32, minconf int32) (*CreatedTx, error) { - chainClient, err := w.requireChainClient() - if err != nil { - return nil, err - } + var tx *CreatedTx + err := walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + var err error + tx, err = w.txToSStxInternal(dbtx, pair, inputCredits, inputs, + payouts, account, minconf) + return err + }) + return tx, err +} + +func (w *Wallet) txToSStxInternal(dbtx walletdb.ReadWriteTx, pair map[string]dcrutil.Amount, + inputCredits []wtxmgr.Credit, inputs []dcrjson.SStxInput, + payouts []dcrjson.SStxCommitOut, account uint32, minconf int32) (tx *CreatedTx, err error) { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + + internalPool := w.getAddressPools(waddrmgr.DefaultAccountNum).internal + internalPool.mutex.Lock() + addrFunc := internalPool.getNewAddress + defer func() { + if err == nil { + internalPool.BatchFinish(addrmgrNs) + } else { + internalPool.BatchRollback() + } + internalPool.mutex.Unlock() + }() // Quit if the blockchain is reorganizing. - isReorganizing, _ := chainClient.GetReorganizing() - if isReorganizing { + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { return nil, ErrBlockchainReorganizing } - // 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() - if len(inputs) != len(payouts) { return nil, fmt.Errorf("input and payout must have the same length") } @@ -1524,7 +1455,8 @@ func (w *Wallet) txToSStx(pair map[string]dcrutil.Amount, var addr dcrutil.Address if payouts[i].Addr == "" { - addr, err = addrFunc() + var err error + addr, err = addrFunc(addrmgrNs) if err != nil { return nil, err } @@ -1565,7 +1497,8 @@ func (w *Wallet) txToSStx(pair map[string]dcrutil.Amount, // Add change to txouts. if payouts[i].ChangeAddr == "" { - changeAddr, err = addrFunc() + var err error + changeAddr, err = addrFunc(addrmgrNs) if err != nil { return nil, err } @@ -1597,8 +1530,11 @@ func (w *Wallet) txToSStx(pair map[string]dcrutil.Amount, if _, err := stake.IsSStx(dcrutil.NewTx(msgtx)); err != nil { return nil, err } - if err = signMsgTx(msgtx, inputCredits, w.Manager, - w.chainParams); err != nil { + err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + return signMsgTx(msgtx, inputCredits, w.Manager, addrmgrNs, w.chainParams) + }) + if err != nil { return nil, err } if err := validateMsgTxCredits(msgtx, inputCredits); err != nil { @@ -1629,12 +1565,10 @@ func addOutputsSStx(msgtx *wire.MsgTx, // DECRED TODO func (w *Wallet) txToSSGen(ticketHash chainhash.Hash, blockHash chainhash.Hash, height int64, votebits uint16) (*CreatedTx, error) { - chainClient, err := w.requireChainClient() - if err != nil { - return nil, err - } - isReorganizing, _ := chainClient.GetReorganizing() - if isReorganizing { + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { return nil, ErrBlockchainReorganizing } @@ -1644,12 +1578,10 @@ func (w *Wallet) txToSSGen(ticketHash chainhash.Hash, blockHash chainhash.Hash, // txToSSRtx ... // DECRED TODO func (w *Wallet) txToSSRtx(ticketHash chainhash.Hash) (*CreatedTx, error) { - chainClient, err := w.requireChainClient() - if err != nil { - return nil, err - } - isReorganizing, _ := chainClient.GetReorganizing() - if isReorganizing { + w.reorganizingLock.Lock() + reorg := w.reorganizing + w.reorganizingLock.Unlock() + if reorg { return nil, ErrBlockchainReorganizing } @@ -1669,16 +1601,14 @@ func addSStxChange(msgtx *wire.MsgTx, change dcrutil.Amount, return nil } -func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, +func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { - unspent, err := w.TxStore.UnspentOutputs() + + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) if err != nil { - errRepair := w.attemptToRepairInconsistencies() - if errRepair != nil { - log.Warnf("Wallet found database corruption but was unable to " + - "repair itself. Please restore your wallet from seed.") - return nil, errRepair - } return nil, err } @@ -1749,7 +1679,7 @@ func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, // // TODO: Handle multisig outputs by determining if enough of the // addresses are controlled. - addrAcct, err := w.Manager.AddrAccount(addrs[0]) + addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0]) if err != nil || addrAcct != account { continue } @@ -1761,25 +1691,27 @@ func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, // FindEligibleOutputs is the exported version of findEligibleOutputs (which // tried to find unspent outputs that pass a maturity check). -func (w *Wallet) FindEligibleOutputs(account uint32, minconf int32, - bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { - return w.findEligibleOutputs(account, minconf, bs) +func (w *Wallet) FindEligibleOutputs(account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { + var unspentOutputs []wtxmgr.Credit + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + var err error + unspentOutputs, err = w.findEligibleOutputs(dbtx, account, minconf, bs) + return err + }) + return unspentOutputs, err } // findEligibleOutputsAmount uses wtxmgr to find a number of unspent // outputs while doing maturity checks there. -func (w *Wallet) findEligibleOutputsAmount(account uint32, minconf int32, +func (w *Wallet) findEligibleOutputsAmount(dbtx walletdb.ReadTx, account uint32, minconf int32, amount dcrutil.Amount, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { - unspent, err := w.TxStore.UnspentOutputsForAmount(amount, bs.Height, minconf, - false, account) + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + unspent, err := w.TxStore.UnspentOutputsForAmount(txmgrNs, addrmgrNs, + amount, bs.Height, minconf, false, account) if err != nil { - errRepair := w.attemptToRepairInconsistencies() - if errRepair != nil { - log.Warnf("Wallet found database corruption but was unable to " + - "repair itself. Please restore your wallet from seed.") - return nil, errRepair - } return nil, err } @@ -1810,7 +1742,7 @@ func (w *Wallet) findEligibleOutputsAmount(account uint32, minconf int32, // Only include the output if it is associated with the passed // account. There should only be one address since this is a // P2PKH script. - addrAcct, err := w.Manager.AddrAccount(addrs[0]) + addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0]) if err != nil || addrAcct != account { continue } @@ -1825,7 +1757,7 @@ func (w *Wallet) findEligibleOutputsAmount(account uint32, minconf int32, // It must be called every time a msgtx is changed. // Only P2PKH outputs are supported at this point. func signMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit, - mgr *waddrmgr.Manager, chainParams *chaincfg.Params) error { + mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, chainParams *chaincfg.Params) error { if len(prevOutputs) != len(msgtx.TxIn) { return fmt.Errorf( "Number of prevOutputs (%d) does not match number of tx inputs (%d)", @@ -1844,7 +1776,7 @@ func signMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit, return ErrUnsupportedTransactionType } - ai, err := mgr.Address(apkh) + ai, err := mgr.Address(addrmgrNs, apkh) if err != nil { return fmt.Errorf("cannot get address info: %v", err) } diff --git a/wallet/multisig.go b/wallet/multisig.go new file mode 100644 index 000000000..b3a7bbbf2 --- /dev/null +++ b/wallet/multisig.go @@ -0,0 +1,193 @@ +// 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/decred/dcrd/chaincfg/chainec" + "github.com/decred/dcrd/txscript" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrutil" + "github.com/decred/dcrwallet/waddrmgr" + "github.com/decred/dcrwallet/walletdb" + "github.com/decred/dcrwallet/wtxmgr" +) + +// MakeSecp256k1MultiSigScript 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 secp256k1 pubkeys and P2PKH addresses derived +// from them. +func (w *Wallet) MakeSecp256k1MultiSigScript(secp256k1Addrs []dcrutil.Address, nRequired int) ([]byte, error) { + secp256k1PubKeys := make([]*dcrutil.AddressSecpPubKey, len(secp256k1Addrs)) + + 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 secp256k1Addrs { + switch addr := addr.(type) { + default: + return nil, errors.New("cannot make multisig script for " + + "a non-secp256k1 public key or P2PKH address") + + case *dcrutil.AddressSecpPubKey: + secp256k1PubKeys[i] = addr + + case *dcrutil.AddressPubKeyHash: + if addr.DSA(w.chainParams) != chainec.ECTypeSecp256k1 { + return nil, errors.New("cannot make multisig " + + "script for a non-secp256k1 P2PKH address") + } + + 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().Serialize() + + pubKeyAddr, err := dcrutil.NewAddressSecpPubKey( + serializedPubKey, w.chainParams) + if err != nil { + return nil, err + } + secp256k1PubKeys[i] = pubKeyAddr + } + } + + return txscript.MultiSigScript(secp256k1PubKeys, nRequired) +} + +// ImportP2SHRedeemScript adds a P2SH redeem script to the wallet. +func (w *Wallet) ImportP2SHRedeemScript(script []byte) (*dcrutil.AddressScriptHash, error) { + var p2shAddr *dcrutil.AddressScriptHash + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + + err := w.TxStore.InsertTxScript(txmgrNs, script) + if err != nil { + return err + } + + // 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, _ = dcrutil.NewAddressScriptHash(script, + w.chainParams) + return nil + } + return err + } + + p2shAddr = addrInfo.Address().(*dcrutil.AddressScriptHash) + return nil + }) + return p2shAddr, err +} + +// FetchP2SHMultiSigOutput fetches information regarding a wallet's P2SH +// multi-signature output. +func (w *Wallet) FetchP2SHMultiSigOutput(outPoint *wire.OutPoint) (*P2SHMultiSigOutput, error) { + var ( + mso *wtxmgr.MultisigOut + redeemScript []byte + ) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + + mso, err = w.TxStore.GetMultisigOutput(txmgrNs, outPoint) + if err != nil { + return err + } + + redeemScript, err = w.TxStore.GetTxScript(txmgrNs, mso.ScriptHash[:]) + if err != nil { + return err + } + // returns nil, nil when it successfully found no script. That error is + // only used to return early when the database is closed. + if redeemScript == nil { + return errors.New("script not found") + } + + return nil + }) + if err != nil { + return nil, err + } + + p2shAddr, err := dcrutil.NewAddressScriptHashFromHash( + mso.ScriptHash[:], w.chainParams) + if err != nil { + return nil, err + } + + multiSigOutput := P2SHMultiSigOutput{ + OutPoint: *mso.OutPoint, + OutputAmount: mso.Amount, + ContainingBlock: BlockIdentity{ + Hash: mso.BlockHash, + Height: int32(mso.BlockHeight), + }, + P2SHAddress: p2shAddr, + RedeemScript: redeemScript, + M: mso.M, + N: mso.N, + Redeemer: nil, + } + + if mso.Spent { + multiSigOutput.Redeemer = &OutputRedeemer{ + TxHash: mso.SpentBy, + InputIndex: mso.SpentByIndex, + } + } + + return &multiSigOutput, nil +} + +// FetchAllRedeemScripts returns all P2SH redeem scripts saved by the wallet. +func (w *Wallet) FetchAllRedeemScripts() ([][]byte, error) { + var redeemScripts [][]byte + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + var err error + redeemScripts, err = w.TxStore.StoredTxScripts(txmgrNs) + return err + }) + return redeemScripts, err +} diff --git a/wallet/notifications.go b/wallet/notifications.go index 1c9f41984..c2c590a92 100644 --- a/wallet/notifications.go +++ b/wallet/notifications.go @@ -14,6 +14,7 @@ import ( "github.com/decred/dcrd/wire" "github.com/decred/dcrutil" "github.com/decred/dcrwallet/waddrmgr" + "github.com/decred/dcrwallet/walletdb" "github.com/decred/dcrwallet/wtxmgr" ) @@ -47,11 +48,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 @@ -64,7 +68,7 @@ func lookupInputAccount(w *Wallet, details *wtxmgr.TxDetails, deb wtxmgr.DebitRe _, addrs, _, err := txscript.ExtractPkScriptAddrs(prevOut.Version, 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) @@ -73,12 +77,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.Version, 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) @@ -89,7 +97,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 @@ -114,7 +122,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, } } @@ -126,7 +134,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, @@ -144,8 +152,9 @@ func makeTxSummary(w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { } } -func totalBalances(w *Wallet, m map[uint32]dcrutil.Amount) error { - unspent, err := w.TxStore.UnspentOutputs() +func totalBalances(dbtx walletdb.ReadTx, w *Wallet, m map[uint32]dcrutil.Amount) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + unspent, err := w.TxStore.UnspentOutputs(dbtx.ReadBucket(wtxmgrNamespaceKey)) if err != nil { return err } @@ -155,7 +164,7 @@ func totalBalances(w *Wallet, m map[uint32]dcrutil.Amount) error { _, addrs, _, err := txscript.ExtractPkScriptAddrs( txscript.DefaultScriptVersion, 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] @@ -186,7 +195,7 @@ func relevantAccounts(w *Wallet, m map[uint32]dcrutil.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 { @@ -200,15 +209,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]dcrutil.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 @@ -230,7 +239,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{} } @@ -244,10 +253,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{} } @@ -286,7 +296,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 @@ -297,7 +308,7 @@ func (s *NotificationServer) notifyAttachedBlock(block *wtxmgr.BlockMeta) { for _, b := range s.currentTxNtfn.AttachedBlocks { relevantAccounts(s.wallet, bals, b.Transactions) } - err = totalBalances(s.wallet, bals) + err = totalBalances(dbtx, s.wallet, bals) if err != nil { log.Errorf("Cannot determine balances for relevant accounts: %v", err) return diff --git a/wallet/repair.go b/wallet/repair.go deleted file mode 100644 index be654f087..000000000 --- a/wallet/repair.go +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2013-2015 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 ( - "github.com/decred/dcrd/wire" -) - -// attemptToRepairInconsistencies is called when there is some issue indicated -// with the wtxmgr database. This function should do absolutely nothing if the -// wallet is consistent, but will otherwise try to repair the wallet and make -// it continue to be functional even in the result of some unknown database -// failing. -func (w *Wallet) attemptToRepairInconsistencies() error { - if !w.automaticRepair { - log.Warnf("Inconsistencies have been found in the wallet database! " + - "You may wish to recreate your wallet from seed or try automatic " + - "repair. To enable automatic repair, turn on the" + - " --automaticrepair flag.") - return nil - } - - chainClient, err := w.requireChainClient() - if err != nil { - return err - } - - log.Warnf("Inconsistencies have been found in the wallet database! To " + - "ensure smooth operation, they are being corrected. However, it is " + - "recommended that you restore your wallet from seed to ensure that " + - "your wallet is consistent and has all owned outputs.") - - log.Warnf("Removing orphan unspent outputs. This may take a while. Do not " + - "shut down wallet at this time.") - utxos, err := w.TxStore.RepairInconsistencies() - if err != nil { - return err - } - - var toDelete []*wire.OutPoint - for _, utxo := range utxos { - _, err := chainClient.GetRawTransaction(&utxo.Hash) - if err != nil { - toDelete = append(toDelete, utxo) - } - } - err = w.TxStore.DeleteUnspent(toDelete) - if err != nil { - return err - } - - topBlockStamp := w.Manager.SyncedTo() - err = w.TxStore.RepairMinedBalance(topBlockStamp.Height) - if err != nil { - return err - } - - log.Infof("Wallet consistency repair complete. If errors are still being " + - "returned or owned outputs are missing, try restoring the wallet " + - "from the seed.") - return nil -} diff --git a/wallet/rescan.go b/wallet/rescan.go index 05c25f9e4..52ed09eca 100644 --- a/wallet/rescan.go +++ b/wallet/rescan.go @@ -10,6 +10,7 @@ import ( "github.com/decred/dcrutil" "github.com/decred/dcrwallet/chain" "github.com/decred/dcrwallet/waddrmgr" + "github.com/decred/dcrwallet/walletdb" ) // RescanProgressMsg reports the current progress made by a rescan for a @@ -178,7 +179,11 @@ out: Hash: *n.Hash, Height: n.Height, } - if err := w.Manager.SetSyncedTo(&bs); err != nil { + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.SetSyncedTo(ns, &bs) + }) + if err != nil { log.Errorf("Failed to update address manager "+ "sync state for hash %v (height %d): %v", n.Hash, n.Height, err) @@ -191,15 +196,24 @@ out: log.Infof("Finished rescan for %d %s (synced to block "+ "%s, height %d)", len(addrs), noun, n.Hash, n.Height) - bs := waddrmgr.BlockStamp{Height: n.Height, Hash: *n.Hash} - if err := w.Manager.SetSyncedTo(&bs); err != nil { + + bs := waddrmgr.BlockStamp{ + Height: n.Height, + Hash: *n.Hash, + } + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.SetSyncedTo(ns, &bs) + }) + if err != nil { log.Errorf("Failed to update address manager "+ "sync state for hash %v (height %d): %v", n.Hash, n.Height, err) + continue } - w.SetChainSynced(true) - go w.ResendUnminedTxs() + w.SetChainSynced(true) + go w.resendUnminedTxs() case <-quit: break out diff --git a/wallet/stakepool.go b/wallet/stakepool.go new file mode 100644 index 000000000..43711c2ba --- /dev/null +++ b/wallet/stakepool.go @@ -0,0 +1,33 @@ +// 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/decred/dcrutil" + "github.com/decred/dcrwallet/walletdb" + "github.com/decred/dcrwallet/wstakemgr" +) + +// StakePoolUserInfo returns the stake pool user information for a user +// identified by their P2SH voting address. +func (w *Wallet) StakePoolUserInfo(userAddress dcrutil.Address) (*wstakemgr.StakePoolUser, error) { + switch userAddress.(type) { + case *dcrutil.AddressPubKeyHash: // ok + case *dcrutil.AddressScriptHash: // ok + default: + return nil, errors.New("stake pool user address must be P2PKH or P2SH") + } + + var user *wstakemgr.StakePoolUser + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + stakemgrNs := tx.ReadBucket(wstakemgrNamespaceKey) + var err error + user, err = w.StakeMgr.StakePoolUserInfo(stakemgrNs, userAddress) + return err + }) + return user, err +} diff --git a/wallet/sync.go b/wallet/sync.go index ef982f0e5..9bdb879d0 100644 --- a/wallet/sync.go +++ b/wallet/sync.go @@ -29,6 +29,7 @@ import ( "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 @@ -56,9 +57,15 @@ func (w *Wallet) accountIsUsed(account uint32, chainClient *chain.RPCClient) boo for branch := uint32(0); branch < 2; branch++ { for i := uint32(0); i < acctSeekWidth; i++ { - addr, err := addrFunc(i, account, branch) - // Skip erroneous keys, which happen rarely. + 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 } @@ -172,10 +179,20 @@ var errDerivation = fmt.Errorf("failed to derive key") // 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) { - addresses, err := w.Manager.AddressesDerivedFromDbAcct(uint32(start), - uint32(end+1), account, branch) + + 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, errDerivation + return false, 0, err } // Whether or not the addresses exist is encoded as a binary @@ -213,8 +230,7 @@ func (w *Wallet) scanAddressRange(account uint32, branch uint32, start int, } // bisectLastAddrIndex is a helper function for search through addresses. -func (w *Wallet) bisectLastAddrIndex(hi, low int, account uint32, - branch uint32) int { +func (w *Wallet) bisectLastAddrIndex(hi, low int, account uint32, branch uint32) int { chainClient, err := w.requireChainClient() if err != nil { return 0 @@ -247,12 +263,19 @@ func (w *Wallet) bisectLastAddrIndex(hi, low int, account uint32, return idx } } else { - addr, err := w.Manager.AddressDerivedFromDbAcct(uint32(i+offset), - account, branch) + 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 @@ -289,6 +312,7 @@ func (w *Wallet) findAddrEnd(start, stop int, account uint32, branch uint32) int // 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 @@ -300,8 +324,14 @@ func debugAccountAddrGapsString(scanBackFrom int, account uint32, branch uint32, buf.WriteString(str) firstUsedIndex := 0 for i := scanBackFrom; i > 0; i-- { - addr, err := w.Manager.AddressDerivedFromDbAcct(uint32(i), account, - branch) + 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 @@ -349,8 +379,14 @@ func debugAccountAddrGapsString(scanBackFrom int, account uint32, branch uint32, } char := "_" - addr, err := w.Manager.AddressDerivedFromDbAcct(uint32(j), - account, branch) + 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" } @@ -415,8 +451,14 @@ func (w *Wallet) scanAddressIndex(start int, end int, account uint32, if exists { lastUsed = idx - addr, err := w.Manager.AddressDerivedFromDbAcct(uint32(lastUsed), - account, branch) + 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 } @@ -429,8 +471,14 @@ func (w *Wallet) scanAddressIndex(start int, end int, account uint32, // return this address to let the caller know that this // 0th address was used. if lastUsed == 0 { - addr, err := w.Manager.AddressDerivedFromDbAcct(0, - account, branch) + 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 @@ -473,30 +521,38 @@ func (w *Wallet) rescanActiveAddresses() error { } } - lastAcctMgr, err := w.Manager.LastAccount() - 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(strconv.Itoa(int(i))) - 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 + // 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 "+ @@ -517,112 +573,122 @@ func (w *Wallet) rescanActiveAddresses() error { return err } - // 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(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()) + 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" - } + 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(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 { + // 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 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()) } - } - // 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( - 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 + } - // 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()) + } + } - // 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(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()) } - } - // Set the next address in the waddrmgr database so that the - // address pool can synchronize properly after. - err = w.Manager.StoreNextToUseAddress(isInternal, acct, nextToUseIdx) + 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 { - log.Errorf("Failed to store next to use pool idx for "+ - "%s pool in the manager on init sync: %v", - branchString, err.Error()) + return err } - - log.Infof("Successfully synchronized the address manager to "+ - "%s address %v (key index %v) for account %v", - branchString, - nextToUseAddr.String(), - nextToUseIdx, - acct) } pool, err := newAddressPools(acct, intIdx, extIdx, w) diff --git a/wallet/tickets.go b/wallet/tickets.go new file mode 100644 index 000000000..d58970f8f --- /dev/null +++ b/wallet/tickets.go @@ -0,0 +1,163 @@ +// 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 ( + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrutil" + "github.com/decred/dcrwallet/chain" + "github.com/decred/dcrwallet/waddrmgr" + "github.com/decred/dcrwallet/walletdb" +) + +func sliceContainsHash(s []chainhash.Hash, h chainhash.Hash) bool { + for _, item := range s { + if h == item { + return true + } + } + return false +} + +// LiveTicketHashes returns the hashes of live tickets that have been purchased +// by the wallet. +func (w *Wallet) LiveTicketHashes(rpcClient *chain.RPCClient, includeImmature bool) ([]chainhash.Hash, error) { + // This was mostly copied from an older version of the legacy RPC server + // implementation, hence the overall weirdness, inefficiencies, and the + // direct dependency on the consensus server RPC client. + + var blk waddrmgr.BlockStamp + var ticketHashes []chainhash.Hash + var stakeMgrTickets []chainhash.Hash + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + blk = w.Manager.SyncedTo() + + // UnspentTickets collects all the tickets that pay out to a + // public key hash for a public key owned by this wallet. + var err error + ticketHashes, err = w.TxStore.UnspentTickets(txmgrNs, blk.Height, + includeImmature) + if err != nil { + return err + } + + // Access the stake manager and see if there are any extra tickets + // there. Likely they were either pruned because they failed to get + // into the blockchain or they are P2SH for some script we own. + stakeMgrTickets, err = w.StakeMgr.DumpSStxHashes() + return err + }) + if err != nil { + return nil, err + } + + for _, h := range stakeMgrTickets { + if sliceContainsHash(ticketHashes, h) { + continue + } + + // Get the raw transaction information from daemon and add + // any relevant tickets. The ticket output is always the + // zeroeth output. + spent, err := rpcClient.GetTxOut(&h, 0, true) + if err != nil { + continue + } + // This returns nil if the output is spent. + if spent == nil { + continue + } + + ticketTx, err := rpcClient.GetRawTransactionVerbose(&h) + if err != nil { + continue + } + + txHeight := ticketTx.BlockHeight + unconfirmed := (txHeight == 0) + immature := (blk.Height-int32(txHeight) < + int32(w.ChainParams().TicketMaturity)) + if includeImmature { + ticketHashes = append(ticketHashes, h) + } else { + if !(unconfirmed || immature) { + ticketHashes = append(ticketHashes, h) + } + } + } + + return ticketHashes, nil +} + +// TicketHashesForVotingAddress returns the hashes of all tickets with voting +// rights delegated to votingAddr. This function does not return the hashes of +// pruned tickets. +func (w *Wallet) TicketHashesForVotingAddress(votingAddr dcrutil.Address) ([]chainhash.Hash, error) { + var ticketHashes []chainhash.Hash + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + var err error + ticketHashes, err = w.StakeMgr.DumpSStxHashesForAddress( + addrmgrNs, votingAddr) + if err != nil { + return err + } + + // Exclude the hash if the transaction is not saved too. No + // promises of hash order are given (and at time of writing, + // they are copies of iterators of a Go map in wstakemgr) so + // when one must be removed, replace it with the last and + // decrease the len. + for i := range ticketHashes { + if w.TxStore.ExistsTx(txmgrNs, &ticketHashes[i]) { + continue + } + + ticketHashes[i] = ticketHashes[len(ticketHashes)-1] + ticketHashes = ticketHashes[:len(ticketHashes)-1] + } + + return nil + }) + return ticketHashes, err +} + +// AddTicket adds a ticket transaction to the wallet. +func (w *Wallet) AddTicket(ticket *dcrutil.Tx) error { + return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + stakemgrNs := tx.ReadWriteBucket(wstakemgrNamespaceKey) + return w.StakeMgr.InsertSStx(stakemgrNs, ticket, w.VoteBits) + }) +} + +// VoteBitsForTicket returns the per-ticket vote bits, if any are saved, falling +// back to the wallet's default vote bits when missing. +func (w *Wallet) VoteBitsForTicket(ticketHash *chainhash.Hash) (uint16, error) { + var voteBits uint16 + var ok bool + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + stakemgrNs := tx.ReadBucket(wstakemgrNamespaceKey) + var err error + ok, voteBits, err = w.StakeMgr.SStxVoteBits(stakemgrNs, ticketHash) + return err + }) + if !ok { + voteBits = w.VoteBits + } + return voteBits, err +} + +// SetVoteBitsForTicket sets the per-ticket vote bits. These vote bits override +// the wallet's default vote bits. +func (w *Wallet) SetVoteBitsForTicket(ticketHash *chainhash.Hash, voteBits uint16) error { + return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + stakemgrNs := tx.ReadWriteBucket(wstakemgrNamespaceKey) + return w.StakeMgr.UpdateSStxVoteBits(stakemgrNs, ticketHash, voteBits) + }) +} diff --git a/wallet/txauthor/author.go b/wallet/txauthor/author.go index 6c1b2aec1..c4d9a4758 100644 --- a/wallet/txauthor/author.go +++ b/wallet/txauthor/author.go @@ -177,8 +177,7 @@ type SecretsSource interface { // are passed in prevPkScripts and the slice length must match the number of // inputs. Private keys and redeem scripts are looked up using a SecretsSource // based on the previous output script. -func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, - secrets SecretsSource) error { +func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, secrets SecretsSource) error { inputs := tx.TxIn chainParams := secrets.ChainParams() diff --git a/wallet/unstable.go b/wallet/unstable.go new file mode 100644 index 000000000..ec8626e68 --- /dev/null +++ b/wallet/unstable.go @@ -0,0 +1,59 @@ +// 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 ( + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrutil" + "github.com/decred/dcrwallet/walletdb" + "github.com/decred/dcrwallet/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) + }) +} + +// UnspentMultisigCreditsForAddress calls +// wtxmgr.Store.UnspentMultisigCreditsForAddress under a single database view +// transaction. +func (u unstableAPI) UnspentMultisigCreditsForAddress(p2shAddr *dcrutil.AddressScriptHash) ([]*wtxmgr.MultisigCredit, error) { + var multisigCredits []*wtxmgr.MultisigCredit + err := walletdb.View(u.w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + multisigCredits, err = u.w.TxStore.UnspentMultisigCreditsForAddress( + txmgrNs, p2shAddr) + return err + }) + return multisigCredits, err +} diff --git a/wallet/utxos.go b/wallet/utxos.go new file mode 100644 index 000000000..0b420eecf --- /dev/null +++ b/wallet/utxos.go @@ -0,0 +1,89 @@ +package wallet + +import ( + "github.com/decred/dcrd/txscript" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/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( + txscript.DefaultScriptVersion, 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), + // TODO: version is bogus but there is + // only version 0 at time of writing. + Version: txscript.DefaultScriptVersion, + PkScript: output.PkScript, + }, + OutputKind: outputSource, + ContainingBlock: BlockIdentity(output.Block), + ReceiveTime: output.Received, + } + outputResults = append(outputResults, result) + } + + return nil + }) + return outputResults, err +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 988445745..555b4a080 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015 The Decred 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. @@ -109,7 +109,7 @@ type Wallet struct { VoteBits uint16 StakeMiningEnabled bool CurrentVotingInfo *VotingInfo - TicketMaxPrice dcrutil.Amount + ticketMaxPrice dcrutil.Amount ticketBuyFreq int balanceToMaintain dcrutil.Amount poolAddress dcrutil.Address @@ -169,10 +169,15 @@ type Wallet struct { // Channels for the manager locker. unlockRequests chan unlockRequest lockRequests chan struct{} - holdUnlockRequests chan chan HeldUnlock + holdUnlockRequests chan chan heldUnlock lockState chan bool changePassphrase chan changePassphraseRequest + // Information for reorganization handling. + reorganizingLock sync.Mutex + reorganizeToHash chainhash.Hash + reorganizing bool + NtfnServer *NotificationServer chainParams *chaincfg.Params @@ -225,7 +230,7 @@ func newWallet(vb uint16, esm bool, btm dcrutil.Amount, addressReuse bool, addrPools: make(map[uint32]*addressPools), addressReuse: addressReuse, ticketAddress: ticketAddress, - TicketMaxPrice: tmp, + ticketMaxPrice: tmp, ticketBuyFreq: ticketBuyFreq, poolAddress: poolAddress, poolFees: pf, @@ -238,7 +243,7 @@ func newWallet(vb uint16, esm bool, btm dcrutil.Amount, addressReuse bool, rollbackBlockDB: rollbackBlockDB, unlockRequests: make(chan unlockRequest), lockRequests: make(chan struct{}), - holdUnlockRequests: make(chan chan HeldUnlock), + holdUnlockRequests: make(chan chan heldUnlock), lockState: make(chan bool), changePassphrase: make(chan changePassphraseRequest), chainParams: params, @@ -307,15 +312,21 @@ func (w *Wallet) SetGenerate(flag bool) error { // If stake mining has been enabled again, make sure to // try to submit any possible votes on the current top // block. - if w.StakeMiningEnabled && isChanged && - w.CurrentVotingInfo != nil { - _, err := w.StakeMgr.HandleWinningTicketsNtfn( - w.CurrentVotingInfo.BlockHash, - w.CurrentVotingInfo.BlockHeight, - w.CurrentVotingInfo.Tickets, - w.VoteBits, - w.AllowHighFees, - ) + if flag && isChanged && w.CurrentVotingInfo != nil { + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + stakemgrNs := tx.ReadWriteBucket(wstakemgrNamespaceKey) + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := w.StakeMgr.HandleWinningTicketsNtfn( + stakemgrNs, + waddrmgrNs, + w.CurrentVotingInfo.BlockHash, + w.CurrentVotingInfo.BlockHeight, + w.CurrentVotingInfo.Tickets, + w.VoteBits, + w.AllowHighFees, + ) + return err + }) if err != nil { return err } @@ -364,7 +375,7 @@ func (w *Wallet) GetTicketMaxPrice() dcrutil.Amount { w.stakeSettingsLock.Lock() defer w.stakeSettingsLock.Unlock() - return w.TicketMaxPrice + return w.ticketMaxPrice } // SetTicketMaxPrice sets the current maximum price the user is willing to pay @@ -373,7 +384,7 @@ func (w *Wallet) SetTicketMaxPrice(amt dcrutil.Amount) { w.stakeSettingsLock.Lock() defer w.stakeSettingsLock.Unlock() - w.TicketMaxPrice = amt + w.ticketMaxPrice = amt } // TicketAddress gets the ticket address for the wallet to give the ticket @@ -553,10 +564,15 @@ func (w *Wallet) quitChan() <-chan struct{} { // CloseDatabases triggers the wallet databases to shut down. func (w *Wallet) CloseDatabases() { - // Store the current address pool last addresses. - w.CloseAddressPools() - w.TxStore.Close() - w.StakeMgr.Close() + walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + + // Store the current address pool last addresses. + w.CloseAddressPools(addrmgrNs) + + w.StakeMgr.Close() + return nil + }) } // Stop signals all wallet goroutines to shutdown. @@ -637,10 +653,13 @@ func (w *Wallet) SetChainSynced(synced bool) { // activeData returns the currently-active receiving addresses and all unspent // outputs. This is primarely intended to provide the parameters for a // rescan request. -func (w *Wallet) activeData() ([]dcrutil.Address, []*wire.OutPoint, error) { +func (w *Wallet) activeData(dbtx walletdb.ReadTx) ([]dcrutil.Address, []*wire.OutPoint, error) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + var err error var addrs []dcrutil.Address - err = w.Manager.ForEachActiveAddress(func(addr dcrutil.Address) error { + err = w.Manager.ForEachActiveAddress(addrmgrNs, func(addr dcrutil.Address) error { addrs = append(addrs, addr) return nil }) @@ -648,7 +667,7 @@ func (w *Wallet) activeData() ([]dcrutil.Address, []*wire.OutPoint, error) { return nil, nil, err } - unspent, err := w.TxStore.UnspentOutpoints() + unspent, err := w.TxStore.UnspentOutpoints(txmgrNs) if err != nil { return nil, nil, err } @@ -687,7 +706,15 @@ func (w *Wallet) syncWithChain() error { // Request notifications for transactions sending to all wallet // addresses. - addrs, unspent, err := w.activeData() + var ( + addrs []dcrutil.Address + unspent []*wire.OutPoint + ) + err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + var err error + addrs, unspent, err = w.activeData(dbtx) + return err + }) if err != nil { return err } @@ -700,7 +727,13 @@ func (w *Wallet) syncWithChain() error { var syncBlock waddrmgr.BlockStamp for i := localBest.Height; i > 0; i-- { // Get the block hash from the transaction store. - blhLocal, err := w.TxStore.GetBlockHash(i) + var blhLocal chainhash.Hash + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + var err error + ns := tx.ReadBucket(wtxmgrNamespaceKey) + blhLocal, err = w.TxStore.GetBlockHash(ns, i) + return err + }) if err != nil { continue } @@ -725,14 +758,18 @@ func (w *Wallet) syncWithChain() error { } if rollback { log.Debugf("Rolling back blockchain to height %v.", syncBlock.Height) - err = w.Manager.SetSyncedTo(&syncBlock) - if err != nil { - return err - } - // Rollback unconfirms transactions at and beyond the passed - // height, so add one to the new synced-to height to prevent - // unconfirming txs from the synced-to block. - err = w.TxStore.Rollback(syncBlock.Height + 1) + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + err := w.Manager.SetSyncedTo(addrmgrNs, &syncBlock) + if err != nil { + return err + } + // Rollback unconfirms transactions at and beyond the passed + // height, so add one to the new synced-to height to prevent + // unconfirming txs from the synced-to block. + return w.TxStore.Rollback(txmgrNs, addrmgrNs, syncBlock.Height+1) + }) if err != nil { return err } @@ -760,33 +797,43 @@ func (w *Wallet) syncWithChain() error { // the block isn't already otherwise in the database. If it does // exist there, the function does nothing. Insertion ordering // itself is unimportant as long as the history itself is correct. - for i := bestBlockHeight; i > - (bestBlockHeight - maxBlockDistToRestore); i-- { - bl, err := chainClient.GetBlock(curBlock) - if err != nil { - return err - } - blHeight := bl.MsgBlock().Header.Height - vb := bl.MsgBlock().Header.VoteBits - wtxBm := wtxmgr.BlockMeta{ - Block: wtxmgr.Block{ - Hash: *curBlock, - Height: int32(blHeight), - }, - Time: bl.MsgBlock().Header.Timestamp, - VoteBits: vb, - } - err = w.TxStore.InsertBlock(&wtxBm) - if err != nil { - return err - } + // + // TODO: fetch all data first before updating the DB. This + // would remove the open write transaction while performing RPC. + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + for i := bestBlockHeight; i > + (bestBlockHeight - maxBlockDistToRestore); i-- { + bl, err := chainClient.GetBlock(curBlock) + if err != nil { + return err + } + blHeight := bl.MsgBlock().Header.Height + vb := bl.MsgBlock().Header.VoteBits + wtxBm := wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: *curBlock, + Height: int32(blHeight), + }, + Time: bl.MsgBlock().Header.Timestamp, + VoteBits: vb, + } + err = w.TxStore.InsertBlock(txmgrNs, &wtxBm) + if err != nil { + return err + } - curBlock = &bl.MsgBlock().Header.PrevBlock + curBlock = &bl.MsgBlock().Header.PrevBlock - // Break early if we hit the genesis block. - if i == 0 { - break + // Break early if we hit the genesis block. + if i == 0 { + break + } } + return nil + }) + if err != nil { + return err } } @@ -913,53 +960,83 @@ out: for { select { case txr := <-w.consolidateRequests: + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- consolidateResponse{nil, err} + continue + } txh, err := w.compressWallet(txr.inputs, txr.account, txr.address) + heldUnlock.release() txr.resp <- consolidateResponse{txh, err} case txr := <-w.createTxRequests: - tx, err := w.TxToOutputs(txr.outputs, txr.account, txr.minconf, true) + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createTxResponse{nil, err} + continue + } + tx, err := w.txToOutputs(txr.outputs, txr.account, + txr.minconf, true) + heldUnlock.release() txr.resp <- createTxResponse{tx, err} case txr := <-w.createMultisigTxRequests: + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createMultisigTxResponse{nil, nil, nil, err} + continue + } tx, address, redeemScript, err := w.txToMultisig(txr.account, txr.amount, txr.pubkeys, txr.nrequired, txr.minconf) + heldUnlock.release() txr.resp <- createMultisigTxResponse{tx, address, redeemScript, err} case txr := <-w.createSStxRequests: - // Initialize the address pool for use. - pool := w.getAddressPools(waddrmgr.DefaultAccountNum).internal - pool.mutex.Lock() - addrFunc := pool.getNewAddress - + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createSStxResponse{nil, err} + continue + } tx, err := w.txToSStx(txr.pair, txr.usedInputs, txr.inputs, txr.couts, waddrmgr.DefaultAccountNum, - addrFunc, txr.minconf) - if err == nil { - pool.BatchFinish() - } else { - pool.BatchRollback() - } - pool.mutex.Unlock() - + heldUnlock.release() txr.resp <- createSStxResponse{tx, err} case txr := <-w.createSSGenRequests: + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createSSGenResponse{nil, err} + continue + } tx, err := w.txToSSGen(txr.tickethash, txr.blockhash, txr.height, txr.votebits) + heldUnlock.release() txr.resp <- createSSGenResponse{tx, err} case txr := <-w.createSSRtxRequests: + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createSSRtxResponse{nil, err} + continue + } tx, err := w.txToSSRtx(txr.tickethash) + heldUnlock.release() txr.resp <- createSSRtxResponse{tx, err} case txr := <-w.purchaseTicketRequests: - data, err := w.purchaseTicket(txr) + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- purchaseTicketResponse{nil, err} + continue + } + data, err := w.purchaseTickets(txr) + heldUnlock.release() txr.resp <- purchaseTicketResponse{data, err} case <-quit: @@ -1117,24 +1194,27 @@ type ( err chan error } - // HeldUnlock is a tool to prevent the wallet from automatically + // heldUnlock is a tool to prevent the wallet from automatically // locking after some timeout before an operation which needed - // the unlocked wallet has finished. Any aquired HeldUnlock + // the unlocked wallet has finished. Any aquired heldUnlock // *must* be released (preferably with a defer) or the wallet // will forever remain unlocked. - HeldUnlock chan struct{} + heldUnlock chan struct{} ) // walletLocker manages the locked/unlocked state of a wallet. func (w *Wallet) walletLocker() { var timeout <-chan time.Time - holdChan := make(HeldUnlock) + holdChan := make(heldUnlock) quit := w.quitChan() out: for { select { case req := <-w.unlockRequests: - err := w.Manager.Unlock(req.passphrase) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.Unlock(addrmgrNs, req.passphrase) + }) if err != nil { req.err <- err continue @@ -1149,8 +1229,11 @@ out: continue case req := <-w.changePassphrase: - err := w.Manager.ChangePassphrase(req.old, req.new, true, - &waddrmgr.DefaultScryptOptions) + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.ChangePassphrase(addrmgrNs, req.old, + req.new, true, &waddrmgr.DefaultScryptOptions) + }) req.err <- err continue @@ -1225,14 +1308,14 @@ func (w *Wallet) Locked() bool { return <-w.lockState } -// HoldUnlock prevents the wallet from being locked. The HeldUnlock object +// holdUnlock prevents the wallet from being locked. The heldUnlock object // *must* be released, or the wallet will forever remain unlocked. // // TODO: To prevent the above scenario, perhaps closures should be passed // to the walletLocker goroutine and disallow callers from explicitly // handling the locking mechanism. -func (w *Wallet) HoldUnlock() (HeldUnlock, error) { - req := make(chan HeldUnlock) +func (w *Wallet) holdUnlock() (heldUnlock, error) { + req := make(chan heldUnlock) w.holdUnlockRequests <- req hl, ok := <-req if !ok { @@ -1246,18 +1329,18 @@ func (w *Wallet) HoldUnlock() (HeldUnlock, error) { return hl, nil } -// Release releases the hold on the unlocked-state of the wallet and allows the +// release releases the hold on the unlocked-state of the wallet and allows the // wallet to be locked again. If a lock timeout has already expired, the -// wallet is locked again as soon as Release is called. -func (c HeldUnlock) Release() { +// wallet is locked again as soon as release is called. +func (c heldUnlock) release() { c <- struct{}{} } -// ChangePassphrase attempts to change the passphrase for a wallet from old -// to new. Changing the passphrase is synchronized with all other address +// ChangePrivatePassphrase attempts to change the passphrase for a wallet from +// old to new. Changing the passphrase is synchronized with all other address // manager locking and unlocking. The lock state will be the same as it was // before the password change. -func (w *Wallet) ChangePassphrase(old, new []byte) error { +func (w *Wallet) ChangePrivatePassphrase(old, new []byte) error { err := make(chan error, 1) w.changePassphrase <- changePassphraseRequest{ old: old, @@ -1267,27 +1350,32 @@ func (w *Wallet) ChangePassphrase(old, new []byte) error { return <-err } -// AccountUsed returns whether there are any recorded transactions spending to +// ChangePublicPassphrase modifies the public passphrase of the wallet. +func (w *Wallet) ChangePublicPassphrase(old, new []byte) error { + return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.ChangePassphrase(addrmgrNs, old, new, false, + &waddrmgr.DefaultScryptOptions) + }) +} + +// accountUsed returns whether there are any recorded transactions spending to // a given account. It returns true if atleast one address in the account was // used and false if no address in the account was used. -func (w *Wallet) AccountUsed(account uint32) (bool, error) { +func (w *Wallet) accountUsed(addrmgrNs walletdb.ReadWriteBucket, account uint32) (bool, error) { var used bool - var err error - merr := w.Manager.ForEachAccountAddress(account, + err := w.Manager.ForEachAccountAddress(addrmgrNs, account, func(maddr waddrmgr.ManagedAddress) error { - used, err = maddr.Used() - if err != nil { - return err - } + used = maddr.Used(addrmgrNs) if used { return waddrmgr.Break } return nil }) - if merr == waddrmgr.Break { - merr = nil + if err == waddrmgr.Break { + err = nil } - return used, merr + return used, err } // CalculateBalance sums the amounts of all unspent transaction @@ -1299,43 +1387,58 @@ func (w *Wallet) AccountUsed(account uint32) (bool, error) { // the balance will be calculated based on how many how many blocks // include a UTXO. func (w *Wallet) CalculateBalance(confirms int32, balanceType wtxmgr.BehaviorFlags) (dcrutil.Amount, error) { - blk := w.Manager.SyncedTo() - return w.TxStore.Balance(confirms, blk.Height, balanceType, true, 0) + var balance dcrutil.Amount + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + + blk := w.Manager.SyncedTo() + balance, err = w.TxStore.Balance(txmgrNs, addrmgrNs, confirms, + blk.Height, balanceType, true, 0) + return err + }) + return balance, err } // CalculateAccountBalance sums the amounts of all unspent transaction // outputs to the given account of a wallet and returns the balance. func (w *Wallet) CalculateAccountBalance(account uint32, confirms int32, balanceType wtxmgr.BehaviorFlags) (dcrutil.Amount, error) { - var bal dcrutil.Amount - var err error - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() + var balance dcrutil.Amount + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error - bal, err = w.TxStore.Balance(confirms, syncBlock.Height, - balanceType, false, account) - if err != nil { - return 0, err - } + // Get current block. The block height used for calculating the + // number of tx confirmations. + syncBlock := w.Manager.SyncedTo() - return bal, nil + balance, err = w.TxStore.Balance(txmgrNs, addrmgrNs, confirms, + syncBlock.Height, balanceType, false, account) + return err + }) + return balance, err } // CalculateAccountBalances calculates the values for the wtxmgr struct Balance, // which includes the total balance, the spendable balance, and the balance // which has yet to mature. -func (w *Wallet) CalculateAccountBalances(account uint32, - confirms int32) (wtxmgr.Balances, error) { - syncBlock := w.Manager.SyncedTo() - - bals, err := w.TxStore.AccountBalances(syncBlock.Height, confirms, account) - if err != nil { - return wtxmgr.Balances{}, err - } +func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (wtxmgr.Balances, error) { + var balances wtxmgr.Balances + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error - return bals, nil + syncBlock := w.Manager.SyncedTo() + balances, err = w.TxStore.AccountBalances(txmgrNs, addrmgrNs, + syncBlock.Height, confirms, account) + return err + }) + return balances, err } // CurrentAddress gets the most recently requested payment address from a wallet. @@ -1343,24 +1446,80 @@ func (w *Wallet) CalculateAccountBalances(account uint32, // spending to it in the blockchain or dcrd mempool), the next chained address // is returned. func (w *Wallet) CurrentAddress(account uint32) (dcrutil.Address, error) { - // Access the address index to get the next to use - // address. - nextToUseIdx, err := w.AddressPoolIndex(account, waddrmgr.ExternalBranch) - if err != nil { - return nil, err - } - if nextToUseIdx <= 0 { - return nil, fmt.Errorf("there have not been any addresses used for this account") - } - lastUsedIdx := nextToUseIdx - 1 + var addr dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + // Access the address index to get the next to use + // address. + nextToUseIdx, err := w.addressPoolIndex(addrmgrNs, + account, waddrmgr.ExternalBranch) + if err != nil { + return err + } + if nextToUseIdx <= 0 { + return fmt.Errorf("there have not been any addresses used for this account") + } + lastUsedIdx := nextToUseIdx - 1 - addr, err := w.Manager.AddressDerivedFromDbAcct(lastUsedIdx, account, - waddrmgr.ExternalBranch) - if err != nil { - return nil, err - } + addr, err = w.Manager.AddressDerivedFromDbAcct(addrmgrNs, + lastUsedIdx, account, waddrmgr.ExternalBranch) + return err + }) + return addr, err +} + +// AccountBranchAddressRange returns all addresses in the left-open, +// right-closed range (start, end] belonging to the BIP0044 account and address +// branch. +func (w *Wallet) AccountBranchAddressRange(start, end uint32, account uint32, branch uint32) ([]dcrutil.Address, error) { + var addrs []dcrutil.Address + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + addrs, err = w.Manager.AddressesDerivedFromDbAcct(addrmgrNs, + start, end, account, branch) + return err + }) + return addrs, err +} + +// PubKeyForAddress looks up the associated public key for a P2PKH address. +func (w *Wallet) PubKeyForAddress(a dcrutil.Address) (chainec.PublicKey, error) { + var pubKey chainec.PublicKey + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, err := w.Manager.Address(addrmgrNs, a) + if err != nil { + return err + } + managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return errors.New("address does not have an associated public key") + } + pubKey = managedPubKeyAddr.PubKey() + return nil + }) + return pubKey, err +} - return addr, nil +// PrivKeyForAddress looks up the associated private key for a P2PKH or P2PK +// address. +func (w *Wallet) PrivKeyForAddress(a dcrutil.Address) (chainec.PrivateKey, error) { + var privKey chainec.PrivateKey + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, err := w.Manager.Address(addrmgrNs, a) + if err != nil { + return err + } + managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return errors.New("address does not have an associated private key") + } + privKey, err = managedPubKeyAddr.PrivKey() + return err + }) + return privKey, err } // existsAddressOnChain checks the chain on daemon to see if the given address @@ -1384,12 +1543,86 @@ func (w *Wallet) ExistsAddressOnChain(address dcrutil.Address) (bool, error) { return w.existsAddressOnChain(address) } +// HaveAddress returns whether the wallet is the owner of the address a. +func (w *Wallet) HaveAddress(a dcrutil.Address) (bool, error) { + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := w.Manager.Address(addrmgrNs, a) + return err + }) + if err == nil { + return true, nil + } + if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { + return false, nil + } + return false, err +} + +// AccountOfAddress finds the account that an address is associated with. +func (w *Wallet) AccountOfAddress(a dcrutil.Address) (uint32, error) { + var account uint32 + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + account, err = w.Manager.AddrAccount(addrmgrNs, a) + return err + }) + return account, err +} + +// AddressInfo returns detailed information regarding a wallet address. +func (w *Wallet) AddressInfo(a dcrutil.Address) (waddrmgr.ManagedAddress, error) { + var managedAddress waddrmgr.ManagedAddress + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + managedAddress, err = w.Manager.Address(addrmgrNs, a) + return err + }) + return managedAddress, err +} + +// AccountNumber returns the account number for an account name. +func (w *Wallet) AccountNumber(accountName string) (uint32, error) { + var account uint32 + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + account, err = w.Manager.LookupAccount(addrmgrNs, accountName) + return err + }) + return account, err +} + +// AccountName returns the name of an account. +func (w *Wallet) AccountName(accountNumber uint32) (string, error) { + var accountName string + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + accountName, err = w.Manager.AccountName(addrmgrNs, accountNumber) + return err + }) + return accountName, err +} + // AccountProperties returns the properties of an account, including address // indexes and name. It first fetches the desynced information from the address // manager, then updates the indexes based on the address pools. -func (w *Wallet) AccountProperties(acct uint32) (*waddrmgr.AccountProperties, - error) { - props, err := w.Manager.AccountProperties(acct) +func (w *Wallet) AccountProperties(acct uint32) (*waddrmgr.AccountProperties, error) { + var props *waddrmgr.AccountProperties + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + props, err = w.accountProperties(waddrmgrNs, acct) + return err + }) + return props, err +} + +func (w *Wallet) accountProperties(waddrmgrNs walletdb.ReadBucket, acct uint32) (*waddrmgr.AccountProperties, error) { + props, err := w.Manager.AccountProperties(waddrmgrNs, acct) if err != nil { return nil, err } @@ -1398,13 +1631,13 @@ func (w *Wallet) AccountProperties(acct uint32) (*waddrmgr.AccountProperties, // buffer. Skip the imported account, which is not a BIP32-like // account. if acct != waddrmgr.ImportedAddrAccount { - extIdx, err := w.AddressPoolIndex(acct, waddrmgr.ExternalBranch) + extIdx, err := w.addressPoolIndex(waddrmgrNs, acct, waddrmgr.ExternalBranch) if err != nil { return nil, err } props.ExternalKeyCount = extIdx - intIdx, err := w.AddressPoolIndex(acct, waddrmgr.InternalBranch) + intIdx, err := w.addressPoolIndex(waddrmgrNs, acct, waddrmgr.InternalBranch) if err != nil { return nil, err } @@ -1416,68 +1649,128 @@ func (w *Wallet) AccountProperties(acct uint32) (*waddrmgr.AccountProperties, // RenameAccount sets the name for an account number to newName. func (w *Wallet) RenameAccount(account uint32, newName string) error { - err := w.Manager.RenameAccount(account, newName) - if err != nil { + var props *waddrmgr.AccountProperties + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + err := w.Manager.RenameAccount(addrmgrNs, account, newName) + if err != nil { + return err + } + props, err = w.accountProperties(addrmgrNs, account) return err - } - - props, err := w.AccountProperties(account) - if err != nil { - log.Errorf("Cannot fetch new account properties for notification "+ - "during account rename: %v", err) - } else { + }) + if err == nil { w.NtfnServer.notifyAccountProperties(props) } - - return nil + return err } +const maxEmptyAccounts = 100 + // NextAccount creates the next account and returns its account number. The -// name must be unique to the account. +// name must be unique to the account. In order to support automatic seed +// restoring, new accounts may not be created when all of the previous 100 +// accounts have no transaction history (this is a deviation from the BIP0044 +// spec, which allows no unused account gaps). func (w *Wallet) NextAccount(name string) (uint32, error) { - account, err := w.Manager.NewAccount(name) - if err != nil { - return 0, err - } + var account uint32 + var props *waddrmgr.AccountProperties + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) - props, err := w.AccountProperties(account) - if err != nil { - log.Errorf("Cannot fetch new account properties for notification "+ - "after account creation: %v", err) - } else { - w.NtfnServer.notifyAccountProperties(props) - } + var err error + account, err = w.Manager.LastAccount(addrmgrNs) + if err != nil { + return err + } + if account > maxEmptyAccounts { + used, err := w.accountUsed(addrmgrNs, account) + if err != nil { + return err + } + if !used { + return errors.New("cannot create account: " + + "previous account has no transaction history") + } + } - // Start an address buffer for this account in the address - // manager for both the internal and external branches. - _, err = w.Manager.SyncAccountToAddrIndex(account, - addressPoolBuffer, waddrmgr.ExternalBranch) - if err != nil { - return 0, fmt.Errorf("failed to create initial waddrmgr "+ - "external address buffer for the address pool, "+ - "account %v during createnewaccount: %s", - account, err.Error()) - } - _, err = w.Manager.SyncAccountToAddrIndex(account, - addressPoolBuffer, waddrmgr.InternalBranch) + account, err = w.Manager.NewAccount(addrmgrNs, name) + if err != nil { + return err + } + + props, err = w.accountProperties(addrmgrNs, account) + if err != nil { + return err + } + + // Start an address buffer for this account in the address + // manager for both the internal and external branches. + _, err = w.Manager.SyncAccountToAddrIndex(addrmgrNs, account, + addressPoolBuffer, waddrmgr.ExternalBranch) + if err != nil { + return fmt.Errorf("failed to create initial waddrmgr "+ + "external address buffer for the address pool, "+ + "account %v during createnewaccount: %s", + account, err.Error()) + } + _, err = w.Manager.SyncAccountToAddrIndex(addrmgrNs, account, + addressPoolBuffer, waddrmgr.InternalBranch) + if err != nil { + return fmt.Errorf("failed to create initial waddrmgr "+ + "internal address buffer for the address pool, "+ + "account %v during createnewaccount: %s", + account, err.Error()) + } + return nil + }) if err != nil { - return 0, fmt.Errorf("failed to create initial waddrmgr "+ - "internal address buffer for the address pool, "+ - "account %v during createnewaccount: %s", - account, err.Error()) + return 0, err } + w.NtfnServer.notifyAccountProperties(props) + // Initialize a new address pool for this account. w.addrPoolsMtx.Lock() defer w.addrPoolsMtx.Unlock() - w.addrPools[account], err = newAddressPools(account, 0, 0, w) + pool, err := newAddressPools(account, 0, 0, w) if err != nil { return 0, err } - + w.addrPools[account] = pool return account, nil } +// MasterPubKey returns the BIP0044 master public key for the passed account. +// +// TODO: This should not be returning the key as a string. +func (w *Wallet) MasterPubKey(account uint32) (string, error) { + var masterPubKey string + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + masterPubKey, err = w.Manager.GetMasterPubkey(addrmgrNs, account) + return err + }) + return masterPubKey, err +} + +// Seed returns the wallet seed if it is saved by the wallet. The wallet must +// be unlocked to access the seed, and seeds are not saved by default for +// mainnet wallets. +// +// TODO: This should not be returning the seed as a string +func (w *Wallet) Seed() (string, error) { + var seed string + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var err error + seed, err = w.Manager.GetSeed(addrmgrNs) + return err + }) + return seed, err +} + // CreditCategory describes the type of wallet transaction output. The category // of "sent transactions" (debits) is always "send", and is not expressed by // this type. @@ -1526,13 +1819,15 @@ func RecvCategory(details *wtxmgr.TxDetails, syncHeight int32, return CreditReceive } -// ListTransactions creates a object that may be marshalled to a response result +// listTransactions creates a object that may be marshalled to a response result // for a listtransactions RPC. // // TODO: This should be moved to the legacyrpc package. -func ListTransactions(details *wtxmgr.TxDetails, addrMgr *waddrmgr.Manager, +func listTransactions(tx walletdb.ReadTx, details *wtxmgr.TxDetails, addrMgr *waddrmgr.Manager, syncHeight int32, net *chaincfg.Params) []dcrjson.ListTransactionsResult { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var ( blockHashStr string blockTime int64 @@ -1605,9 +1900,9 @@ outputs: if len(addrs) == 1 { addr := addrs[0] address = addr.EncodeAddress() - account, err := addrMgr.AddrAccount(addrs[0]) + account, err := addrMgr.AddrAccount(addrmgrNs, addrs[0]) if err == nil { - accountName, err = addrMgr.AccountName(account) + accountName, err = addrMgr.AccountName(addrmgrNs, account) if err != nil { accountName = "" } @@ -1670,13 +1965,19 @@ outputs: // This is intended to be used for listsinceblock RPC replies. func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ([]dcrjson.ListTransactionsResult, error) { txList := []dcrjson.ListTransactionsResult{} - err := w.TxStore.RangeTransactions(start, end, func(details []wtxmgr.TxDetails) (bool, error) { - for _, detail := range details { - jsonResults := ListTransactions(&detail, w.Manager, - syncHeight, w.chainParams) - txList = append(txList, jsonResults...) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for _, detail := range details { + jsonResults := listTransactions(tx, &detail, + w.Manager, syncHeight, w.chainParams) + txList = append(txList, jsonResults...) + } + return false, nil } - return false, nil + + return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) }) return txList, err } @@ -1686,93 +1987,98 @@ func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ([]dcrjson.ListTra // replies. func (w *Wallet) ListTransactions(from, count int) ([]dcrjson.ListTransactionsResult, error) { txList := []dcrjson.ListTransactionsResult{} + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() + + // Need to skip the first from transactions, and after those, only + // include the next count transactions. + skipped := 0 + n := 0 + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse order. + // This does nothing for unmined transactions, which are + // unsorted, but it will process mined transactions in the + // reverse order they were marked mined. + for i := len(details) - 1; i >= 0; i-- { + if n >= count { + return true, nil + } - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() - - // Need to skip the first from transactions, and after those, only - // include the next count transactions. - skipped := 0 - n := 0 - - // Return newer results first by starting at mempool height and working - // down to the genesis block. - err := w.TxStore.RangeTransactions(-1, 0, func(details []wtxmgr.TxDetails) (bool, error) { - // Iterate over transactions at this height in reverse order. - // This does nothing for unmined transactions, which are - // unsorted, but it will process mined transactions in the - // reverse order they were marked mined. - for i := len(details) - 1; i >= 0; i-- { - if n >= count { - return true, nil - } - - if from > skipped { - skipped++ - continue - } + if from > skipped { + skipped++ + continue + } - jsonResults := ListTransactions(&details[i], - w.Manager, syncBlock.Height, w.chainParams) - txList = append(txList, jsonResults...) + jsonResults := listTransactions(tx, &details[i], + w.Manager, syncBlock.Height, w.chainParams) + txList = append(txList, jsonResults...) - if len(jsonResults) > 0 { - n++ + if len(jsonResults) > 0 { + n++ + } } + + return false, nil } - return false, nil + // Return newer results first by starting at mempool height and working + // down to the genesis block. + return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) }) - return txList, err } // ListAddressTransactions returns a slice of objects with details about // recorded transactions to or from any address belonging to a set. This is // intended to be used for listaddresstransactions RPC replies. -func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( - []dcrjson.ListTransactionsResult, error) { - +func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ([]dcrjson.ListTransactionsResult, error) { txList := []dcrjson.ListTransactionsResult{} + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + loopDetails: + for i := range details { + detail := &details[i] + + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + txscript.DefaultScriptVersion, pkScript, w.chainParams) + if err != nil || len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*dcrutil.AddressPubKeyHash) + if !ok { + continue + } + _, ok = pkHashes[string(apkh.ScriptAddress())] + if !ok { + continue + } - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() - - err := w.TxStore.RangeTransactions(0, -1, func(details []wtxmgr.TxDetails) (bool, error) { - loopDetails: - for i := range details { - detail := &details[i] - - for _, cred := range detail.Credits { - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - txscript.DefaultScriptVersion, pkScript, w.chainParams) - if err != nil || len(addrs) != 1 { - continue - } - apkh, ok := addrs[0].(*dcrutil.AddressPubKeyHash) - if !ok { - continue + jsonResults := listTransactions(tx, detail, + w.Manager, syncBlock.Height, w.chainParams) + if err != nil { + return false, err + } + txList = append(txList, jsonResults...) + continue loopDetails } - _, ok = pkHashes[string(apkh.ScriptAddress())] - if !ok { - continue - } - - jsonResults := ListTransactions(detail, w.Manager, - syncBlock.Height, w.chainParams) - if err != nil { - return false, err - } - txList = append(txList, jsonResults...) - continue loopDetails } + return false, nil } - return false, nil - }) + return w.TxStore.RangeTransactions(txmgrNs, 0, -1, rangeFn) + }) return txList, err } @@ -1781,26 +2087,31 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( // replies. func (w *Wallet) ListAllTransactions() ([]dcrjson.ListTransactionsResult, error) { txList := []dcrjson.ListTransactionsResult{} - - // Get current block. The block height used for calculating - // the number of tx confirmations. - syncBlock := w.Manager.SyncedTo() - - // Return newer results first by starting at mempool height and working - // down to the genesis block. - err := w.TxStore.RangeTransactions(-1, 0, func(details []wtxmgr.TxDetails) (bool, error) { - // Iterate over transactions at this height in reverse order. - // This does nothing for unmined transactions, which are - // unsorted, but it will process mined transactions in the - // reverse order they were marked mined. - for i := len(details) - 1; i >= 0; i-- { - jsonResults := ListTransactions(&details[i], w.Manager, - syncBlock.Height, w.chainParams) - txList = append(txList, jsonResults...) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse + // order. This does nothing for unmined transactions, + // which are unsorted, but it will process mined + // transactions in the reverse order they were marked + // mined. + for i := len(details) - 1; i >= 0; i-- { + jsonResults := listTransactions(tx, &details[i], + w.Manager, syncBlock.Height, w.chainParams) + txList = append(txList, jsonResults...) + } + return false, nil } - return false, nil - }) + // Return newer results first by starting at mempool height and + // working down to the genesis block. + return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) + }) return txList, err } @@ -1885,36 +2196,42 @@ func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, cancel < } var res GetTransactionsResult - err := w.TxStore.RangeTransactions(start, end, func(details []wtxmgr.TxDetails) (bool, error) { - // TODO: probably should make RangeTransactions not reuse the - // details backing array memory. - dets := make([]wtxmgr.TxDetails, len(details)) - copy(dets, details) - details = dets - - txs := make([]TransactionSummary, 0, len(details)) - for i := range details { - txs = append(txs, makeTxSummary(w, &details[i])) - } - - if details[0].Block.Height != -1 { - blockHash := details[0].Block.Hash - res.MinedTransactions = append(res.MinedTransactions, Block{ - Hash: &blockHash, - Height: details[0].Block.Height, - Timestamp: details[0].Block.Time.Unix(), - Transactions: txs, - }) - } else { - res.UnminedTransactions = txs - } + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // TODO: probably should make RangeTransactions not reuse the + // details backing array memory. + dets := make([]wtxmgr.TxDetails, len(details)) + copy(dets, details) + details = dets + + txs := make([]TransactionSummary, 0, len(details)) + for i := range details { + txs = append(txs, makeTxSummary(dbtx, w, &details[i])) + } - select { - case <-cancel: - return true, nil - default: - return false, nil + if details[0].Block.Height != -1 { + blockHash := details[0].Block.Hash + res.MinedTransactions = append(res.MinedTransactions, Block{ + Hash: &blockHash, + Height: details[0].Block.Height, + Timestamp: details[0].Block.Time.Unix(), + Transactions: txs, + }) + } else { + res.UnminedTransactions = txs + } + + select { + case <-cancel: + return true, nil + default: + return false, nil + } } + + return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) }) return &res, err } @@ -1940,51 +2257,103 @@ type AccountsResult struct { // TODO(jrick): Is the chain tip really needed, since only the total balances // are included? func (w *Wallet) Accounts() (*AccountsResult, error) { - var accounts []AccountResult - syncBlock := w.Manager.SyncedTo() - unspent, err := w.TxStore.UnspentOutputs() - if err != nil { - return nil, err - } - err = w.Manager.ForEachAccount(func(acct uint32) error { - props, err := w.AccountProperties(acct) + var ( + accounts []AccountResult + syncBlockHash *chainhash.Hash + syncBlockHeight int32 + ) + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + syncBlock := w.Manager.SyncedTo() + syncBlockHash = &syncBlock.Hash + syncBlockHeight = syncBlock.Height + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) if err != nil { return err } - accounts = append(accounts, AccountResult{ - AccountProperties: *props, - // TotalBalance set below + err = w.Manager.ForEachAccount(addrmgrNs, func(acct uint32) error { + props, err := w.accountProperties(addrmgrNs, acct) + if err != nil { + return err + } + accounts = append(accounts, AccountResult{ + AccountProperties: *props, + // TotalBalance set below + }) + return nil }) - return nil - }) - if err != nil { - return nil, err - } - m := make(map[uint32]*dcrutil.Amount) - for i := range accounts { - a := &accounts[i] - m[a.AccountNumber] = &a.TotalBalance - } - for i := range unspent { - output := unspent[i] - var outputAcct uint32 - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - txscript.DefaultScriptVersion, output.PkScript, w.chainParams) - if err == nil && len(addrs) > 0 { - outputAcct, err = w.Manager.AddrAccount(addrs[0]) + if err != nil { + return err } - if err == nil { - amt, ok := m[outputAcct] - if ok { - *amt += output.Amount + m := make(map[uint32]*dcrutil.Amount) + for i := range accounts { + a := &accounts[i] + m[a.AccountNumber] = &a.TotalBalance + } + for i := range unspent { + output := unspent[i] + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + txscript.DefaultScriptVersion, output.PkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if err == nil { + amt, ok := m[outputAcct] + if ok { + *amt += output.Amount + } } } - } + return nil + }) return &AccountsResult{ Accounts: accounts, - CurrentBlockHash: &syncBlock.Hash, - CurrentBlockHeight: syncBlock.Height, - }, nil + CurrentBlockHash: syncBlockHash, + CurrentBlockHeight: syncBlockHeight, + }, err +} + +// AccountBalanceResult is a single result for the Wallet.AccountBalances method. +type AccountBalanceResult struct { + AccountNumber uint32 + AccountName string + AccountBalance dcrutil.Amount +} + +// AccountBalances returns all accounts in the wallet and their balances. +// Balances are determined by excluding transactions that have not met +// requiredConfs confirmations. +func (w *Wallet) AccountBalances(requiredConfs int32, balanceType wtxmgr.BehaviorFlags) ([]AccountBalanceResult, error) { + var results []AccountBalanceResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + + syncBlock := w.Manager.SyncedTo() + + return w.Manager.ForEachAccount(addrmgrNs, func(account uint32) error { + accountName, err := w.Manager.AccountName(addrmgrNs, account) + if err != nil { + return err + } + balance, err := w.TxStore.Balance(txmgrNs, addrmgrNs, + requiredConfs, syncBlock.Height, balanceType, + false, account) + if err != nil { + return err + } + results = append(results, AccountBalanceResult{ + AccountNumber: account, + AccountName: accountName, + AccountBalance: balance, + }) + return nil + }) + }) + return results, err } // creditSlice satisifies the sort.Interface interface to provide sorting @@ -2029,212 +2398,221 @@ func (s creditSlice) Swap(i, j int) { // minconf, less than maxconf and if addresses is populated only the addresses // contained within it will be considered. If we know nothing about a // transaction an empty array will be returned. -func (w *Wallet) ListUnspent(minconf, maxconf int32, - addresses map[string]struct{}) ([]*dcrjson.ListUnspentResult, error) { - - syncBlock := w.Manager.SyncedTo() - - filter := len(addresses) != 0 - unspent, err := w.TxStore.UnspentOutputs() - if err != nil { - return nil, err - } - sort.Sort(sort.Reverse(creditSlice(unspent))) - - defaultAccountName, err := w.Manager.AccountName(waddrmgr.DefaultAccountNum) - if err != nil { - return nil, err - } +func (w *Wallet) ListUnspent(minconf, maxconf int32, addresses map[string]struct{}) ([]*dcrjson.ListUnspentResult, error) { + var results []*dcrjson.ListUnspentResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - results := make([]*dcrjson.ListUnspentResult, 0, len(unspent)) - for i := range unspent { - output := unspent[i] + syncBlock := w.Manager.SyncedTo() - details, err := w.TxStore.TxDetails(&output.Hash) + filter := len(addresses) != 0 + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) if err != nil { - return nil, fmt.Errorf("Couldn't get credit details") + return err } + sort.Sort(sort.Reverse(creditSlice(unspent))) - // Outputs with fewer confirmations than the minimum or more - // confs than the maximum are excluded. - confs := confirms(output.Height, syncBlock.Height) - if confs < minconf || confs > maxconf { - continue + defaultAccountName, err := w.Manager.AccountName( + addrmgrNs, waddrmgr.DefaultAccountNum) + if err != nil { + return err } - // Only mature coinbase outputs are included. - if output.FromCoinBase { - target := int32(w.ChainParams().CoinbaseMaturity) - if !confirmed(target, output.Height, syncBlock.Height) { + for i := range unspent { + output := unspent[i] + + details, err := w.TxStore.TxDetails(txmgrNs, &output.Hash) + if err != nil { + return fmt.Errorf("Couldn't get credit details") + } + + // Outputs with fewer confirmations than the minimum or more + // confs than the maximum are excluded. + confs := confirms(output.Height, syncBlock.Height) + if confs < minconf || confs > maxconf { continue } - } - switch details.TxRecord.TxType { - case stake.TxTypeSStx: - // Ticket commitment, only spendable after ticket maturity. - // You can only spent it after TM many blocks has gone past, so - // ticket maturity + 1??? Check this DECRED TODO - if output.Index == 0 { - if !confirmed(int32(w.chainParams.TicketMaturity+1), - details.Height(), syncBlock.Height) { + // Only mature coinbase outputs are included. + if output.FromCoinBase { + target := int32(w.ChainParams().CoinbaseMaturity) + if !confirmed(target, output.Height, syncBlock.Height) { continue } } - // Change outputs. - if (output.Index > 0) && (output.Index%2 == 0) { - if !confirmed(int32(w.chainParams.SStxChangeMaturity), + + switch details.TxRecord.TxType { + case stake.TxTypeSStx: + // Ticket commitment, only spendable after ticket maturity. + // You can only spent it after TM many blocks has gone past, so + // ticket maturity + 1??? Check this DECRED TODO + if output.Index == 0 { + if !confirmed(int32(w.chainParams.TicketMaturity+1), + details.Height(), syncBlock.Height) { + continue + } + } + // Change outputs. + if (output.Index > 0) && (output.Index%2 == 0) { + if !confirmed(int32(w.chainParams.SStxChangeMaturity), + details.Height(), syncBlock.Height) { + continue + } + } + case stake.TxTypeSSGen: + // All non-OP_RETURN outputs for SSGen tx are only spendable + // after coinbase maturity many blocks. + if !confirmed(int32(w.chainParams.CoinbaseMaturity), details.Height(), syncBlock.Height) { continue } + case stake.TxTypeSSRtx: + // All outputs for SSRtx tx are only spendable + // after coinbase maturity many blocks. + if !confirmed(int32(w.chainParams.CoinbaseMaturity), + details.Height(), syncBlock.Height) { + continue + } + } - case stake.TxTypeSSGen: - // All non-OP_RETURN outputs for SSGen tx are only spendable - // after coinbase maturity many blocks. - if !confirmed(int32(w.chainParams.CoinbaseMaturity), - details.Height(), syncBlock.Height) { + + // Exclude locked outputs from the result set. + if w.LockedOutpoint(output.OutPoint) { continue } - case stake.TxTypeSSRtx: - // All outputs for SSRtx tx are only spendable - // after coinbase maturity many blocks. - if !confirmed(int32(w.chainParams.CoinbaseMaturity), - details.Height(), syncBlock.Height) { + + // Lookup the associated account for the output. Use the + // default account name in case there is no associated account + // for some reason, although this should never happen. + // + // This will be unnecessary once transactions and outputs are + // grouped under the associated account in the db. + acctName := defaultAccountName + sc, addrs, _, err := txscript.ExtractPkScriptAddrs( + txscript.DefaultScriptVersion, output.PkScript, w.chainParams) + if err != nil { continue } - - } - - // Exclude locked outputs from the result set. - if w.LockedOutpoint(output.OutPoint) { - continue - } - - // Lookup the associated account for the output. Use the - // default account name in case there is no associated account - // for some reason, although this should never happen. - // - // This will be unnecessary once transactions and outputs are - // grouped under the associated account in the db. - acctName := defaultAccountName - sc, addrs, _, err := txscript.ExtractPkScriptAddrs( - txscript.DefaultScriptVersion, output.PkScript, w.chainParams) - if err != nil { - continue - } - if len(addrs) > 0 { - acct, err := w.Manager.AddrAccount(addrs[0]) - if err == nil { - s, err := w.Manager.AccountName(acct) + if len(addrs) > 0 { + acct, err := w.Manager.AddrAccount( + addrmgrNs, addrs[0]) if err == nil { - acctName = s + s, err := w.Manager.AccountName( + addrmgrNs, acct) + if err == nil { + acctName = s + } } } - } - if filter { - for _, addr := range addrs { - _, ok := addresses[addr.EncodeAddress()] - if ok { - goto include + if filter { + for _, addr := range addrs { + _, ok := addresses[addr.EncodeAddress()] + if ok { + goto include + } } + continue } - continue - } - include: - // At the moment watch-only addresses are not supported, so all - // recorded outputs that are not multisig are "spendable". - // Multisig outputs are only "spendable" if all keys are - // controlled by this wallet. - // - // TODO: Each case will need updates when watch-only addrs - // is added. For P2PK, P2PKH, and P2SH, the address must be - // looked up and not be watching-only. For multisig, all - // pubkeys must belong to the manager with the associated - // private key (currently it only checks whether the pubkey - // exists, since the private key is required at the moment). - var spendable bool - scSwitch: - switch sc { - case txscript.PubKeyHashTy: - spendable = true - case txscript.PubKeyTy: - spendable = true - case txscript.ScriptHashTy: - spendable = true - case txscript.StakeGenTy: - spendable = true - case txscript.StakeRevocationTy: - spendable = true - case txscript.StakeSubChangeTy: - spendable = true - case txscript.MultiSigTy: - for _, a := range addrs { - _, err := w.Manager.Address(a) - if err == nil { - continue - } - if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { - break scSwitch + include: + // At the moment watch-only addresses are not supported, so all + // recorded outputs that are not multisig are "spendable". + // Multisig outputs are only "spendable" if all keys are + // controlled by this wallet. + // + // TODO: Each case will need updates when watch-only addrs + // is added. For P2PK, P2PKH, and P2SH, the address must be + // looked up and not be watching-only. For multisig, all + // pubkeys must belong to the manager with the associated + // private key (currently it only checks whether the pubkey + // exists, since the private key is required at the moment). + var spendable bool + scSwitch: + switch sc { + case txscript.PubKeyHashTy: + spendable = true + case txscript.PubKeyTy: + spendable = true + case txscript.ScriptHashTy: + spendable = true + case txscript.StakeGenTy: + spendable = true + case txscript.StakeRevocationTy: + spendable = true + case txscript.StakeSubChangeTy: + spendable = true + case txscript.MultiSigTy: + for _, a := range addrs { + _, err := w.Manager.Address(addrmgrNs, a) + if err == nil { + continue + } + if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { + break scSwitch + } + return err } - return nil, err + spendable = true } - spendable = true - } - result := &dcrjson.ListUnspentResult{ - TxID: output.OutPoint.Hash.String(), - Vout: output.OutPoint.Index, - Tree: output.OutPoint.Tree, - Account: acctName, - ScriptPubKey: hex.EncodeToString(output.PkScript), - TxType: int(details.TxType), - Amount: output.Amount.ToCoin(), - Confirmations: int64(confs), - Spendable: spendable, - } - - // BUG: this should be a JSON array so that all - // addresses can be included, or removed (and the - // caller extracts addresses from the pkScript). - if len(addrs) > 0 { - result.Address = addrs[0].EncodeAddress() - } + result := &dcrjson.ListUnspentResult{ + TxID: output.OutPoint.Hash.String(), + Vout: output.OutPoint.Index, + Tree: output.OutPoint.Tree, + Account: acctName, + ScriptPubKey: hex.EncodeToString(output.PkScript), + TxType: int(details.TxType), + Amount: output.Amount.ToCoin(), + Confirmations: int64(confs), + Spendable: spendable, + } - results = append(results, result) - } + // BUG: this should be a JSON array so that all + // addresses can be included, or removed (and the + // caller extracts addresses from the pkScript). + if len(addrs) > 0 { + result.Address = addrs[0].EncodeAddress() + } - return results, nil + results = append(results, result) + } + return nil + }) + return results, err } // DumpPrivKeys returns the WIF-encoded private keys for all addresses with // private keys in a wallet. func (w *Wallet) DumpPrivKeys() ([]string, error) { var privkeys []string - // Iterate over each active address, appending the private key to - // privkeys. - err := w.Manager.ForEachActiveAddress(func(addr dcrutil.Address) error { - ma, err := w.Manager.Address(addr) - if err != nil { - return err - } + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + // Iterate over each active address, appending the private key to + // privkeys. + return w.Manager.ForEachActiveAddress(addrmgrNs, func(addr dcrutil.Address) error { + ma, err := w.Manager.Address(addrmgrNs, addr) + if err != nil { + return err + } - // Only those addresses with keys needed. - pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) - if !ok { - return nil - } + // Only those addresses with keys needed. + pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil + } - wif, err := pka.ExportPrivKey() - if err != nil { - // It would be nice to zero out the array here. However, - // since strings in go are immutable, and we have no - // control over the caller I don't think we can. :( - return err - } - privkeys = append(privkeys, wif.String()) - return nil + wif, err := pka.ExportPrivKey() + if err != nil { + // It would be nice to zero out the array here. However, + // since strings in go are immutable, and we have no + // control over the caller I don't think we can. :( + return err + } + privkeys = append(privkeys, wif.String()) + return nil + }) }) return privkeys, err } @@ -2242,13 +2620,19 @@ func (w *Wallet) DumpPrivKeys() ([]string, error) { // DumpWIFPrivateKey returns the WIF encoded private key for a // single wallet address. func (w *Wallet) DumpWIFPrivateKey(addr dcrutil.Address) (string, error) { - // Get private key from wallet if it exists. - address, err := w.Manager.Address(addr) + var maddr waddrmgr.ManagedAddress + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + // Get private key from wallet if it exists. + var err error + maddr, err = w.Manager.Address(waddrmgrNs, addr) + return err + }) if err != nil { return "", err } - pka, ok := address.(waddrmgr.ManagedPubKeyAddress) + pka, ok := maddr.(waddrmgr.ManagedPubKeyAddress) if !ok { return "", fmt.Errorf("address %s is not a key type", addr) } @@ -2295,7 +2679,18 @@ func (w *Wallet) ImportPrivateKey(wif *dcrutil.WIF, bs *waddrmgr.BlockStamp, } // Attempt to import private key into wallet. - addr, err := w.Manager.ImportPrivateKey(wif, bs) + var addr dcrutil.Address + var props *waddrmgr.AccountProperties + err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + maddr, err := w.Manager.ImportPrivateKey(addrmgrNs, wif, bs) + if err == nil { + addr = maddr.Address() + props, err = w.accountProperties( + addrmgrNs, waddrmgr.ImportedAddrAccount) + } + return err + }) if err != nil { return "", err } @@ -2304,7 +2699,7 @@ func (w *Wallet) ImportPrivateKey(wif *dcrutil.WIF, bs *waddrmgr.BlockStamp, // imported address. if rescan { job := &RescanJob{ - Addrs: []dcrutil.Address{addr.Address()}, + Addrs: []dcrutil.Address{addr}, OutPoints: nil, BlockStamp: *bs, } @@ -2315,25 +2710,17 @@ func (w *Wallet) ImportPrivateKey(wif *dcrutil.WIF, bs *waddrmgr.BlockStamp, // required to be read, so discard the return value. _ = w.SubmitRescan(job) } else { - err := w.chainClient.NotifyReceived( - []dcrutil.Address{addr.Address()}) + err := w.chainClient.NotifyReceived([]dcrutil.Address{addr}) if err != nil { return "", fmt.Errorf("Failed to subscribe for address ntfns for "+ - "address %s: %s", addr.Address().EncodeAddress(), - err.Error()) + "address %s: %s", addr.EncodeAddress(), err) } } - addrStr := addr.Address().EncodeAddress() + addrStr := addr.EncodeAddress() log.Infof("Imported payment address %s", addrStr) - props, err := w.AccountProperties(waddrmgr.ImportedAddrAccount) - if err != nil { - log.Errorf("Cannot fetch account properties for imported "+ - "account after importing key: %v", err) - } else { - w.NtfnServer.notifyAccountProperties(props) - } + w.NtfnServer.notifyAccountProperties(props) // Return the payment address string of the imported private key. return addrStr, nil @@ -2347,78 +2734,83 @@ func (w *Wallet) ImportScript(rs []byte, rescan bool, scanFrom int32) error { return fmt.Errorf("invalid scan from height of %v", scanFrom) } - curBlockStamp := w.Manager.SyncedTo() - if scanFrom > curBlockStamp.Height { - return fmt.Errorf("requested height %v to scan from is "+ - "above the tip height of %v", scanFrom, - curBlockStamp.Height) - } + return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) - scanFromHash := w.chainParams.GenesisHash - if scanFrom != 0 { - var err error - scanFromHash, err = w.chainClient.GetBlockHash(int64(scanFrom)) - if err != nil { - return fmt.Errorf("couldn't find block header hash for "+ - "block at height %v: %s", scanFrom, err.Error()) + curBlockStamp := w.Manager.SyncedTo() + if scanFrom > curBlockStamp.Height { + return fmt.Errorf("requested height %v to scan from is "+ + "above the tip height of %v", scanFrom, + curBlockStamp.Height) } - } - err := w.TxStore.InsertTxScript(rs) - if err != nil { - return err - } + scanFromHash := w.chainParams.GenesisHash + if scanFrom != 0 { + var err error + scanFromHash, err = w.chainClient.GetBlockHash(int64(scanFrom)) + if err != nil { + return fmt.Errorf("couldn't find block header hash for "+ + "block at height %v: %s", scanFrom, err.Error()) + } + } - // Get current block's height and hash. - bs := w.Manager.SyncedTo() - mscriptaddr, err := w.Manager.ImportScript(rs, &bs) - if err != nil { - switch { - // Don't care if it's already there. - case waddrmgr.IsError(err, waddrmgr.ErrDuplicateAddress): - return nil - case waddrmgr.IsError(err, waddrmgr.ErrLocked): - log.Debugf("failed to attempt script importation " + - "of incoming tx because addrmgr was locked") - return err - default: + err := w.TxStore.InsertTxScript(txmgrNs, rs) + if err != nil { return err } - } else { - if rescan { - // This is the first time seeing this script address - // belongs to us, so do a rescan and see if there are - // any other outputs to this address. - job := &RescanJob{ - Addrs: []dcrutil.Address{mscriptaddr.Address()}, - OutPoints: nil, - BlockStamp: waddrmgr.BlockStamp{ - Height: int32(scanFrom), - Hash: *scanFromHash, - }, - } - - // Submit rescan job and log when the import has completed. - // Do not block on finishing the rescan. The rescan success - // or failure is logged elsewhere, and the channel is not - // required to be read, so discard the return value. - _ = w.SubmitRescan(job) + + // Get current block's height and hash. + bs := w.Manager.SyncedTo() + mscriptaddr, err := w.Manager.ImportScript(addrmgrNs, rs, &bs) + if err != nil { + switch { + // Don't care if it's already there. + case waddrmgr.IsError(err, waddrmgr.ErrDuplicateAddress): + return nil + case waddrmgr.IsError(err, waddrmgr.ErrLocked): + log.Debugf("failed to attempt script importation " + + "of incoming tx because addrmgr was locked") + return err + default: + return err + } } else { - err := w.chainClient.NotifyReceived( - []dcrutil.Address{mscriptaddr.Address()}) - if err != nil { - return fmt.Errorf("Failed to subscribe for address ntfns for "+ - "address %s: %s", mscriptaddr.Address().EncodeAddress(), - err.Error()) + if rescan { + // This is the first time seeing this script address + // belongs to us, so do a rescan and see if there are + // any other outputs to this address. + job := &RescanJob{ + Addrs: []dcrutil.Address{mscriptaddr.Address()}, + OutPoints: nil, + BlockStamp: waddrmgr.BlockStamp{ + Height: int32(scanFrom), + Hash: *scanFromHash, + }, + } + + // Submit rescan job and log when the import has completed. + // Do not block on finishing the rescan. The rescan success + // or failure is logged elsewhere, and the channel is not + // required to be read, so discard the return value. + _ = w.SubmitRescan(job) + } else { + err := w.chainClient.NotifyReceived( + []dcrutil.Address{mscriptaddr.Address()}) + if err != nil { + return fmt.Errorf("Failed to subscribe for address ntfns for "+ + "address %s: %s", mscriptaddr.Address().EncodeAddress(), + err.Error()) + } } } - } - log.Infof("Redeem script hash %x (address %v) successfully added.", - mscriptaddr.Address().ScriptAddress(), - mscriptaddr.Address().EncodeAddress()) + log.Infof("Redeem script hash %x (address %v) successfully added.", + mscriptaddr.Address().ScriptAddress(), + mscriptaddr.Address().EncodeAddress()) - return nil + return nil + }) } // StakeInfoData is a struct containing the data that would be returned from @@ -2475,190 +2867,196 @@ func (w *Wallet) StakeInfo() (*StakeInfoData, error) { return nil, err } - // Check to ensure both the wallet and the blockchain are synced. - // Return a failure if the wallet is currently processing a new - // block and is not yet synced. - bs := w.Manager.SyncedTo() - chainBest, chainHeight, err := chainClient.GetBestBlock() - if err != nil { - return nil, err - } - if !bs.Hash.IsEqual(chainBest) && (int32(chainHeight) != bs.Height) { - return nil, fmt.Errorf("the wallet is currently syncing to " + - "the best block, please try again later") - } - - // Load all transaction hash data about stake transactions from the - // stake manager. - localTickets, err := w.StakeMgr.DumpSStxHashes() - if err != nil { - return nil, err - } - localVotes, err := w.StakeMgr.DumpSSGenHashes() - if err != nil { - return nil, err - } - revokedTickets, err := w.StakeMgr.DumpSSRtxTickets() - if err != nil { - return nil, err - } + var resp *StakeInfoData + err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { + stakemgrNs := tx.ReadBucket(wstakemgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - // Get the poolsize estimate from the current best block. - // The correct poolsize would be the pool size to be mined - // into the next block, which takes into account maturing - // stake tickets, voters, and expiring tickets. There - // currently isn't a way to get this from the RPC, so - // just use the current block pool size as a "good - // enough" estimate for now. - bestBlock, err := chainClient.GetBlock(&bs.Hash) - if err != nil { - return nil, err - } - poolSize := bestBlock.MsgBlock().Header.PoolSize + // Check to ensure both the wallet and the blockchain are synced. + // Return a failure if the wallet is currently processing a new + // block and is not yet synced. + bs := w.Manager.SyncedTo() + chainBest, chainHeight, err := chainClient.GetBestBlock() + if err != nil { + return err + } + if !bs.Hash.IsEqual(chainBest) && (int32(chainHeight) != bs.Height) { + return fmt.Errorf("the wallet is currently syncing to " + + "the best block, please try again later") + } - // Fetch all transactions from the mempool, and store only the - // the ticket hashes for transactions that are tickets. Then see - // how many of these mempool tickets also belong to the wallet. - allMempoolTickets, err := chainClient.GetRawMempool(dcrjson.GRMTickets) - if err != nil { - return nil, err - } - var localTicketsInMempool []*chainhash.Hash - for i := range localTickets { - if hashInPointerSlice(localTickets[i], allMempoolTickets) { - localTicketsInMempool = append(localTicketsInMempool, - &localTickets[i]) - } - } - - // Access the tickets the wallet owns against the chain server - // and see how many exist in the blockchain and how many are - // immature. The speed this up a little, cheaper ExistsLiveTicket - // calls are first used to determine which tickets are actually - // mature. These tickets are cached. Possibly immature tickets - // are then determined by checking against this list and - // assembling a maybeImmature list. All transactions in the - // maybeImmature list are pulled and their height checked. - // If they aren't in the blockchain, they are skipped, in they - // are in the blockchain and are immature, they are not included - // in the immature number of tickets. - // - // It's not immediately clear why to use this over gettickets. - // GetTickets will only return tickets which are directly held - // by this wallet's public keys and excludes things like P2SH - // scripts that stake pools use. Doing it this way will give - // more accurate results. - var maybeImmature []*chainhash.Hash - liveTicketNum := 0 - immatureTicketNum := 0 - localTicketPtrs := make([]*chainhash.Hash, len(localTickets)) - for i := range localTickets { - localTicketPtrs[i] = &localTickets[i] - } - - // Check the live ticket pool for the presense of tickets. - existsBitSetBStr, err := chainClient.ExistsLiveTickets(localTicketPtrs) - if err != nil { - return nil, fmt.Errorf("Failed to find assess whether tickets "+ - "were in live buckets when generating stake info (err %s)", - err.Error()) - } - existsBitSetB, err := hex.DecodeString(existsBitSetBStr) - if err != nil { - return nil, fmt.Errorf("Failed to decode response for whether tickets "+ - "were in live buckets when generating stake info (err %s)", - err.Error()) - } - existsBitSet := bitset.Bytes(existsBitSetB) - for i := range localTickets { - if existsBitSet.Get(i) { - liveTicketNum++ - } else { - maybeImmature = append(maybeImmature, &localTickets[i]) + // Load all transaction hash data about stake transactions from the + // stake manager. + localTickets, err := w.StakeMgr.DumpSStxHashes() + if err != nil { + return err + } + localVotes, err := w.StakeMgr.DumpSSGenHashes(stakemgrNs) + if err != nil { + return err + } + revokedTickets, err := w.StakeMgr.DumpSSRtxTickets(stakemgrNs) + if err != nil { + return err } - } - curHeight := int64(bs.Height) - ticketMaturity := int64(w.ChainParams().TicketMaturity) - for _, ticketHash := range maybeImmature { - // Skip tickets that aren't in the blockchain. - if hashInPointerSlice(*ticketHash, localTicketsInMempool) { - continue + // Get the poolsize estimate from the current best block. + // The correct poolsize would be the pool size to be mined + // into the next block, which takes into account maturing + // stake tickets, voters, and expiring tickets. There + // currently isn't a way to get this from the RPC, so + // just use the current block pool size as a "good + // enough" estimate for now. + bestBlock, err := chainClient.GetBlock(&bs.Hash) + if err != nil { + return err } + poolSize := bestBlock.MsgBlock().Header.PoolSize - txResult, err := w.TxStore.TxDetails(ticketHash) - if err != nil || txResult == nil { - log.Tracef("Failed to find ticket in blockchain while generating "+ - "stake info (hash %v, err %s)", ticketHash, err) - continue + // Fetch all transactions from the mempool, and store only the + // the ticket hashes for transactions that are tickets. Then see + // how many of these mempool tickets also belong to the wallet. + allMempoolTickets, err := chainClient.GetRawMempool(dcrjson.GRMTickets) + if err != nil { + return err + } + var localTicketsInMempool []*chainhash.Hash + for i := range localTickets { + if hashInPointerSlice(localTickets[i], allMempoolTickets) { + localTicketsInMempool = append(localTicketsInMempool, + &localTickets[i]) + } } - immature := (txResult.Block.Height != -1) && - (curHeight-int64(txResult.Block.Height) < ticketMaturity) - if immature { - immatureTicketNum++ + // Access the tickets the wallet owns against the chain server + // and see how many exist in the blockchain and how many are + // immature. The speed this up a little, cheaper ExistsLiveTicket + // calls are first used to determine which tickets are actually + // mature. These tickets are cached. Possibly immature tickets + // are then determined by checking against this list and + // assembling a maybeImmature list. All transactions in the + // maybeImmature list are pulled and their height checked. + // If they aren't in the blockchain, they are skipped, in they + // are in the blockchain and are immature, they are not included + // in the immature number of tickets. + // + // It's not immediately clear why to use this over gettickets. + // GetTickets will only return tickets which are directly held + // by this wallet's public keys and excludes things like P2SH + // scripts that stake pools use. Doing it this way will give + // more accurate results. + var maybeImmature []*chainhash.Hash + liveTicketNum := 0 + immatureTicketNum := 0 + localTicketPtrs := make([]*chainhash.Hash, len(localTickets)) + for i := range localTickets { + localTicketPtrs[i] = &localTickets[i] + } + + // Check the live ticket pool for the presense of tickets. + existsBitSetBStr, err := chainClient.ExistsLiveTickets(localTicketPtrs) + if err != nil { + return fmt.Errorf("Failed to find assess whether tickets "+ + "were in live buckets when generating stake info (err %s)", + err.Error()) + } + existsBitSetB, err := hex.DecodeString(existsBitSetBStr) + if err != nil { + return fmt.Errorf("Failed to decode response for whether tickets "+ + "were in live buckets when generating stake info (err %s)", + err.Error()) + } + existsBitSet := bitset.Bytes(existsBitSetB) + for i := range localTickets { + if existsBitSet.Get(i) { + liveTicketNum++ + } else { + maybeImmature = append(maybeImmature, &localTickets[i]) + } } - } - // Get all the missed tickets from mainnet and determine how many - // from this wallet are still missed. Add the number of revoked - // tickets to this sum as well. - missedNum := 0 - missedOnChain, err := chainClient.MissedTickets() - if err != nil { - return nil, err - } - // Determine if one of your current localTickets has been missed on Chain - for i := range localTickets { - found := false - if hashInPointerSlice(localTickets[i], missedOnChain) { - // Increment missedNum if the missed ticket doesn't have a - // revoked associated with it - for j := range revokedTickets { - if localTickets[i] == revokedTickets[j] { - found = true - break - } + curHeight := int64(bs.Height) + ticketMaturity := int64(w.ChainParams().TicketMaturity) + for _, ticketHash := range maybeImmature { + // Skip tickets that aren't in the blockchain. + if hashInPointerSlice(*ticketHash, localTicketsInMempool) { + continue + } + + txResult, err := w.TxStore.TxDetails(txmgrNs, ticketHash) + if err != nil || txResult == nil { + log.Tracef("Failed to find ticket in blockchain while generating "+ + "stake info (hash %v, err %s)", ticketHash, err) + continue } - if !found { - missedNum++ + + immature := (txResult.Block.Height != -1) && + (curHeight-int64(txResult.Block.Height) < ticketMaturity) + if immature { + immatureTicketNum++ } } - } - - missedNum += len(revokedTickets) - // Get all the subsidy for votes cast by this wallet so far - // by accessing the votes directly from the daemon blockchain. - votesNum := 0 - totalSubsidy := dcrutil.Amount(0) - for i := range localVotes { - msgTx, err := w.TxStore.Tx(&localVotes[i]) - if err != nil || msgTx == nil { - log.Tracef("Failed to find vote in blockchain while generating "+ - "stake info (hash %v, err %s)", localVotes[i], err) - continue + // Get all the missed tickets from mainnet and determine how many + // from this wallet are still missed. Add the number of revoked + // tickets to this sum as well. + missedNum := 0 + missedOnChain, err := chainClient.MissedTickets() + if err != nil { + return err + } + // Determine if one of your current localTickets has been missed on Chain + for i := range localTickets { + found := false + if hashInPointerSlice(localTickets[i], missedOnChain) { + // Increment missedNum if the missed ticket doesn't have a + // revoked associated with it + for j := range revokedTickets { + if localTickets[i] == revokedTickets[j] { + found = true + break + } + } + if !found { + missedNum++ + } + } } - votesNum++ - totalSubsidy += dcrutil.Amount(msgTx.TxIn[0].ValueIn) - } + missedNum += len(revokedTickets) - // Bring it all together. - resp := &StakeInfoData{ - BlockHeight: int64(bs.Height), - PoolSize: poolSize, - AllMempoolTix: uint32(len(allMempoolTickets)), - OwnMempoolTix: uint32(len(localTicketsInMempool)), - Immature: uint32(immatureTicketNum), - Live: uint32(liveTicketNum), - Voted: uint32(votesNum), - TotalSubsidy: totalSubsidy, - Missed: uint32(missedNum), - Revoked: uint32(len(revokedTickets)), - } + // Get all the subsidy for votes cast by this wallet so far + // by accessing the votes directly from the daemon blockchain. + votesNum := 0 + totalSubsidy := dcrutil.Amount(0) + for i := range localVotes { + msgTx, err := w.TxStore.Tx(txmgrNs, &localVotes[i]) + if err != nil || msgTx == nil { + log.Tracef("Failed to find vote in blockchain while generating "+ + "stake info (hash %v, err %s)", localVotes[i], err) + continue + } + + votesNum++ + totalSubsidy += dcrutil.Amount(msgTx.TxIn[0].ValueIn) + } - return resp, nil + // Bring it all together. + resp = &StakeInfoData{ + BlockHeight: int64(bs.Height), + PoolSize: poolSize, + AllMempoolTix: uint32(len(allMempoolTickets)), + OwnMempoolTix: uint32(len(localTicketsInMempool)), + Immature: uint32(immatureTicketNum), + Live: uint32(liveTicketNum), + Voted: uint32(votesNum), + TotalSubsidy: totalSubsidy, + Missed: uint32(missedNum), + Revoked: uint32(len(revokedTickets)), + } + return nil + }) + return resp, err } // exportBase64 exports a wallet's serialized database as a base64-encoded @@ -2712,21 +3110,28 @@ func (w *Wallet) LockedOutpoints() []dcrjson.TransactionInput { return locked } -// ResendUnminedTxs iterates through all transactions that spend from wallet +// resendUnminedTxs iterates through all transactions that spend from wallet // credits that are not known to have been mined into a block, and attempts // to send each to the chain server for relay. -func (w *Wallet) ResendUnminedTxs() { +func (w *Wallet) resendUnminedTxs() { chainClient, err := w.requireChainClient() if err != nil { log.Errorf("No chain server available to resend unmined transactions") return } - txs, err := w.TxStore.UnminedTxs() + var txs []*wire.MsgTx + err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + txs, err = w.TxStore.UnminedTxs(txmgrNs) + return err + }) if err != nil { log.Errorf("Cannot load unmined transactions for resending: %v", err) return } + for _, tx := range txs { resp, err := chainClient.SendRawTransaction(tx, w.AllowHighFees) if err != nil { @@ -2744,9 +3149,12 @@ func (w *Wallet) ResendUnminedTxs() { // addresses in a wallet. func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { var addrStrs []string - err := w.Manager.ForEachActiveAddress(func(addr dcrutil.Address) error { - addrStrs = append(addrStrs, addr.EncodeAddress()) - return nil + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.ForEachActiveAddress(addrmgrNs, func(addr dcrutil.Address) error { + addrStrs = append(addrStrs, addr.EncodeAddress()) + return nil + }) }) if err != nil { return nil, err @@ -2774,86 +3182,124 @@ func confirms(txHeight, curHeight int32) int32 { } } -// TotalReceivedForAccount iterates through a wallet's transaction history, -// returning the total amount of decred received for a single wallet -// account. -func (w *Wallet) TotalReceivedForAccount(account uint32, minConf int32) (dcrutil.Amount, int32, error) { - syncBlock := w.Manager.SyncedTo() +// AccountTotalReceivedResult is a single result for the +// Wallet.TotalReceivedForAccounts method. +type AccountTotalReceivedResult struct { + AccountNumber uint32 + AccountName string + TotalReceived dcrutil.Amount + LastConfirmation int32 +} - var ( - amount dcrutil.Amount - lastConf int32 // Confs of the last matching transaction. - stopHeight int32 - ) +// TotalReceivedForAccounts iterates through a wallet's transaction history, +// returning the total amount of decred received for all accounts. +func (w *Wallet) TotalReceivedForAccounts(minConf int32) ([]AccountTotalReceivedResult, error) { + var results []AccountTotalReceivedResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - if minConf > 0 { - stopHeight = syncBlock.Height - minConf + 1 - } else { - stopHeight = -1 - } - err := w.TxStore.RangeTransactions(0, stopHeight, func(details []wtxmgr.TxDetails) (bool, error) { - for i := range details { - detail := &details[i] - for _, cred := range detail.Credits { - pkVersion := detail.MsgTx.TxOut[cred.Index].Version - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript - var outputAcct uint32 - _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkVersion, - pkScript, w.chainParams) - if err == nil && len(addrs) > 0 { - outputAcct, err = w.Manager.AddrAccount(addrs[0]) - } - if err == nil && outputAcct == account { - amount += cred.Amount - lastConf = confirms(detail.Block.Height, syncBlock.Height) + syncBlock := w.Manager.SyncedTo() + + err := w.Manager.ForEachAccount(addrmgrNs, func(account uint32) error { + accountName, err := w.Manager.AccountName(addrmgrNs, account) + if err != nil { + return err + } + results = append(results, AccountTotalReceivedResult{ + AccountNumber: account, + AccountName: accountName, + }) + return nil + }) + if err != nil { + return err + } + + var stopHeight int32 + + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkVersion := detail.MsgTx.TxOut[cred.Index].Version + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkVersion, + pkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount( + addrmgrNs, addrs[0]) + } + if err == nil { + acctIndex := int(outputAcct) + if outputAcct == waddrmgr.ImportedAddrAccount { + acctIndex = len(results) - 1 + } + res := &results[acctIndex] + res.TotalReceived += cred.Amount + res.LastConfirmation = confirms( + detail.Block.Height, syncBlock.Height) + } } } + return false, nil } - return false, nil + return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) }) - - return amount, lastConf, err + return results, err } // TotalReceivedForAddr iterates through a wallet's transaction history, // returning the total amount of decred received for a single wallet // address. func (w *Wallet) TotalReceivedForAddr(addr dcrutil.Address, minConf int32) (dcrutil.Amount, error) { - syncBlock := w.Manager.SyncedTo() + var amount dcrutil.Amount + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) - var ( - addrStr = addr.EncodeAddress() - amount dcrutil.Amount - stopHeight int32 - ) + syncBlock := w.Manager.SyncedTo() - if minConf > 0 { - stopHeight = syncBlock.Height - minConf + 1 - } else { - stopHeight = -1 - } - err := w.TxStore.RangeTransactions(0, stopHeight, func(details []wtxmgr.TxDetails) (bool, error) { - for i := range details { - detail := &details[i] - for _, cred := range detail.Credits { - pkVersion := detail.MsgTx.TxOut[cred.Index].Version - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript - _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkVersion, - pkScript, w.chainParams) - // An error creating addresses from the output script only - // indicates a non-standard script, so ignore this credit. - if err != nil { - continue - } - for _, a := range addrs { - if addrStr == a.EncodeAddress() { - amount += cred.Amount - break + var ( + addrStr = addr.EncodeAddress() + stopHeight int32 + ) + + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkVersion := detail.MsgTx.TxOut[cred.Index].Version + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkVersion, + pkScript, w.chainParams) + // An error creating addresses from the output script only + // indicates a non-standard script, so ignore this credit. + if err != nil { + continue + } + for _, a := range addrs { + if addrStr == a.EncodeAddress() { + amount += cred.Amount + break + } } } } + return false, nil } - return false, nil + return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) }) return amount, err } @@ -2906,175 +3352,180 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, p2shRedeemScriptsByAddress map[string][]byte) ([]SignatureError, error) { var signErrors []SignatureError - for i, txIn := range tx.TxIn { - // For an SSGen tx, skip the first input as it is a stake base - // and doesn't need to be signed. - if i == 0 { - isSSGen, err := stake.IsSSGen(dcrutil.NewTx(tx)) - if err == nil && isSSGen { - // Put some garbage in the signature script. - txIn.SignatureScript = []byte{0xDE, 0xAD, 0xBE, 0xEF} - continue - } - } - - prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] - if !ok { - prevHash := &txIn.PreviousOutPoint.Hash - prevIndex := txIn.PreviousOutPoint.Index - txDetails, err := w.TxStore.TxDetails(prevHash) - if err != nil { - return nil, fmt.Errorf("Cannot query previous transaction "+ - "details for %v: %v", txIn.PreviousOutPoint, err) - } - if txDetails == nil { - return nil, fmt.Errorf("%v not found", - txIn.PreviousOutPoint) - } - prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript - } + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) - // Set up our callbacks that we pass to txscript so it can - // look up the appropriate keys and scripts by address. - getKey := txscript.KeyClosure(func(addr dcrutil.Address) ( - chainec.PrivateKey, bool, error) { - if len(additionalKeysByAddress) != 0 { - addrStr := addr.EncodeAddress() - wif, ok := additionalKeysByAddress[addrStr] - if !ok { - return nil, false, - fmt.Errorf("no key for address (needed: %v, have %v)", - addr.EncodeAddress(), additionalKeysByAddress) + for i, txIn := range tx.TxIn { + // For an SSGen tx, skip the first input as it is a stake base + // and doesn't need to be signed. + if i == 0 { + isSSGen, err := stake.IsSSGen(dcrutil.NewTx(tx)) + if err == nil && isSSGen { + // Put some garbage in the signature script. + txIn.SignatureScript = []byte{0xDE, 0xAD, 0xBE, 0xEF} + continue } - return wif.PrivKey, true, nil - } - address, err := w.Manager.Address(addr) - if err != nil { - return nil, false, err } - pka, ok := address.(waddrmgr.ManagedPubKeyAddress) + prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] if !ok { - return nil, false, fmt.Errorf("address %v is not "+ - "a pubkey address", address.Address().EncodeAddress()) + prevHash := &txIn.PreviousOutPoint.Hash + prevIndex := txIn.PreviousOutPoint.Index + txDetails, err := w.TxStore.TxDetails(txmgrNs, prevHash) + if err != nil { + return fmt.Errorf("Cannot query previous transaction "+ + "details for %v: %v", txIn.PreviousOutPoint, err) + } + if txDetails == nil { + return fmt.Errorf("%v not found", + txIn.PreviousOutPoint) + } + prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript } - key, err := pka.PrivKey() - if err != nil { - return nil, false, err - } + // Set up our callbacks that we pass to txscript so it can + // look up the appropriate keys and scripts by address. + getKey := txscript.KeyClosure(func(addr dcrutil.Address) ( + chainec.PrivateKey, bool, error) { + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + wif, ok := additionalKeysByAddress[addrStr] + if !ok { + return nil, false, + fmt.Errorf("no key for address (needed: %v, have %v)", + addr.EncodeAddress(), additionalKeysByAddress) + } + return wif.PrivKey, true, nil + } + address, err := w.Manager.Address(addrmgrNs, addr) + if err != nil { + return nil, false, err + } - return key, pka.Compressed(), nil - }) - getScript := txscript.ScriptClosure(func( - addr dcrutil.Address) ([]byte, error) { - // If keys were provided then we can only use the - // redeem scripts provided with our inputs, too. - if len(additionalKeysByAddress) != 0 { - addrStr := addr.EncodeAddress() - script, ok := p2shRedeemScriptsByAddress[addrStr] + pka, ok := address.(waddrmgr.ManagedPubKeyAddress) if !ok { - return nil, errors.New("no script for " + - "address") + return nil, false, fmt.Errorf("address %v is not "+ + "a pubkey address", address.Address().EncodeAddress()) } - return script, nil - } - // First check tx manager script store. - scrTxStore, err := - w.TxStore.GetTxScript(addr.ScriptAddress()) - if err != nil { - return nil, err - } - if scrTxStore != nil { - return scrTxStore, nil - } + key, err := pka.PrivKey() + if err != nil { + return nil, false, err + } - // Then check the address manager. - address, err := w.Manager.Address(addr) - if err != nil { - return nil, err - } - sa, ok := address.(waddrmgr.ManagedScriptAddress) - if !ok { - return nil, errors.New("address is not a script" + - " address") - } + return key, pka.Compressed(), nil + }) + getScript := txscript.ScriptClosure(func( + addr dcrutil.Address) ([]byte, error) { + // If keys were provided then we can only use the + // redeem scripts provided with our inputs, too. + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + script, ok := p2shRedeemScriptsByAddress[addrStr] + if !ok { + return nil, errors.New("no script for " + + "address") + } + return script, nil + } - return sa.Script() - }) + // First check tx manager script store. + scrTxStore, err := w.TxStore.GetTxScript(txmgrNs, + addr.ScriptAddress()) + if err != nil { + return nil, err + } + if scrTxStore != nil { + return scrTxStore, nil + } - // SigHashSingle inputs can only be signed if there's a - // corresponding output. However this could be already signed, - // so we always verify the output. - if (hashType&txscript.SigHashSingle) != - txscript.SigHashSingle || i < len(tx.TxOut) { - // Check for alternative checksig scripts and - // set the signature suite accordingly. - ecType := chainec.ECTypeSecp256k1 - class := txscript.GetScriptClass(txscript.DefaultScriptVersion, prevOutScript) - if class == txscript.PubkeyAltTy || - class == txscript.PubkeyHashAltTy { - var err error - ecType, err = txscript.ExtractPkScriptAltSigType(prevOutScript) + // Then check the address manager. + address, err := w.Manager.Address(addrmgrNs, addr) if err != nil { - return nil, errors.New("unknown checksigalt signature " + - "suite specified") + return nil, err + } + sa, ok := address.(waddrmgr.ManagedScriptAddress) + if !ok { + return nil, errors.New("address is not a script" + + " address") } - } - script, err := txscript.SignTxOutput(w.ChainParams(), - tx, i, prevOutScript, hashType, getKey, - getScript, txIn.SignatureScript, ecType) - // Failure to sign isn't an error, it just means that - // the tx isn't complete. - if err != nil { - signErrors = append(signErrors, SignatureError{ - InputIndex: uint32(i), - Error: err, - }) - continue - } - txIn.SignatureScript = script - } + return sa.Script() + }) - // Either it was already signed or we just signed it. - // Find out if it is completely satisfied or still needs more. - vm, err := txscript.NewEngine(prevOutScript, tx, i, - txscript.StandardVerifyFlags, txscript.DefaultScriptVersion, nil) - if err == nil { - err = vm.Execute() - } - if err != nil { - multisigNotEnoughSigs := false - class, addr, _, _ := txscript.ExtractPkScriptAddrs( - txscript.DefaultScriptVersion, - additionalPrevScripts[txIn.PreviousOutPoint], - w.ChainParams()) - - if err == txscript.ErrStackUnderflow && - class == txscript.ScriptHashTy { - redeemScript, _ := getScript(addr[0]) - redeemClass := txscript.GetScriptClass( - txscript.DefaultScriptVersion, redeemScript) - if redeemClass == txscript.MultiSigTy { - multisigNotEnoughSigs = true + // SigHashSingle inputs can only be signed if there's a + // corresponding output. However this could be already signed, + // so we always verify the output. + if (hashType&txscript.SigHashSingle) != + txscript.SigHashSingle || i < len(tx.TxOut) { + // Check for alternative checksig scripts and + // set the signature suite accordingly. + ecType := chainec.ECTypeSecp256k1 + class := txscript.GetScriptClass(txscript.DefaultScriptVersion, prevOutScript) + if class == txscript.PubkeyAltTy || + class == txscript.PubkeyHashAltTy { + var err error + ecType, err = txscript.ExtractPkScriptAltSigType(prevOutScript) + if err != nil { + return errors.New("unknown checksigalt signature " + + "suite specified") + } + } + + script, err := txscript.SignTxOutput(w.ChainParams(), + tx, i, prevOutScript, hashType, getKey, + getScript, txIn.SignatureScript, ecType) + // Failure to sign isn't an error, it just means that + // the tx isn't complete. + if err != nil { + signErrors = append(signErrors, SignatureError{ + InputIndex: uint32(i), + Error: err, + }) + continue } + txIn.SignatureScript = script } - // Only report an error for the script engine in the event - // that it's not a multisignature underflow, indicating that - // we didn't have enough signatures in front of the - // redeemScript rather than an actual error. - if !multisigNotEnoughSigs { - signErrors = append(signErrors, SignatureError{ - InputIndex: uint32(i), - Error: err, - }) + + // Either it was already signed or we just signed it. + // Find out if it is completely satisfied or still needs more. + vm, err := txscript.NewEngine(prevOutScript, tx, i, + txscript.StandardVerifyFlags, txscript.DefaultScriptVersion, nil) + if err == nil { + err = vm.Execute() + } + if err != nil { + multisigNotEnoughSigs := false + class, addr, _, _ := txscript.ExtractPkScriptAddrs( + txscript.DefaultScriptVersion, + additionalPrevScripts[txIn.PreviousOutPoint], + w.ChainParams()) + + if err == txscript.ErrStackUnderflow && + class == txscript.ScriptHashTy { + redeemScript, _ := getScript(addr[0]) + redeemClass := txscript.GetScriptClass( + txscript.DefaultScriptVersion, redeemScript) + if redeemClass == txscript.MultiSigTy { + multisigNotEnoughSigs = true + } + } + // Only report an error for the script engine in the event + // that it's not a multisignature underflow, indicating that + // we didn't have enough signatures in front of the + // redeemScript rather than an actual error. + if !multisigNotEnoughSigs { + signErrors = append(signErrors, SignatureError{ + InputIndex: uint32(i), + Error: err, + }) + } } } - } - - return signErrors, nil + return nil + }) + return signErrors, err } // PublishTransaction sends the transaction to the consensus RPC server so it @@ -3117,50 +3568,44 @@ func Create(db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Par return hdkeychain.ErrInvalidSeedLen } - // Create the address manager. - addrMgrNamespace, err := db.Namespace(waddrmgrNamespaceKey) - if err != nil { - return err - } - err = waddrmgr.Create(addrMgrNamespace, seed, pubPass, privPass, - params, nil, unsafeMainNet) - if err != nil { - return err - } + return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + stakemgrNs, err := tx.CreateTopLevelBucket(wstakemgrNamespaceKey) + if err != nil { + return err + } - // Create empty stake manager. - stakeMgrNamespace, err := db.Namespace([]byte("wstakemgr")) - if err != nil { - return err - } - return wstakemgr.Create(stakeMgrNamespace) + err = waddrmgr.Create(addrmgrNs, seed, pubPass, privPass, + params, nil, unsafeMainNet) + if err != nil { + return err + } + return wstakemgr.Create(stakemgrNs) + }) } // CreateWatchOnly creates a watchonly wallet on the provided db. func CreateWatchOnly(db walletdb.DB, extendedPubKey string, pubPass []byte, params *chaincfg.Params) error { - // Create the address manager. - waddrmgrNamespace, err := db.Namespace(waddrmgrNamespaceKey) - if err != nil { - return err - } - - err = waddrmgr.CreateWatchOnly(waddrmgrNamespace, extendedPubKey, - pubPass, params, nil) - if err != nil { - return err - } - - // Create the stake manager/store. - wstakemgrNamespace, err := db.Namespace(wstakemgrNamespaceKey) - if err != nil { - return err - } - err = wstakemgr.Create(wstakemgrNamespace) - if err != nil { - return err - } + return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + stakemgrNs, err := tx.CreateTopLevelBucket(wstakemgrNamespaceKey) + if err != nil { + return err + } - return nil + err = waddrmgr.CreateWatchOnly(addrmgrNs, extendedPubKey, + pubPass, params, nil) + if err != nil { + return err + } + return wstakemgr.Create(stakemgrNs) + }) } // decodeStakePoolColdExtKey decodes the string of stake pool addresses @@ -3228,48 +3673,117 @@ func Open(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, ticketMaxPrice float64, ticketBuyFreq int, poolAddress string, poolFees float64, ticketFee float64, addrIdxScanLen int, stakePoolColdExtKey string, autoRepair, allowHighFees bool, relayFee float64, params *chaincfg.Params) (*Wallet, error) { - addrMgrNS, err := db.Namespace(waddrmgrNamespaceKey) + + missingTxHistory := false + + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + waddrmgrBucket := tx.ReadBucket(waddrmgrNamespaceKey) + if waddrmgrBucket == nil { + return errors.New("missing address manager namespace") + } + wstakemgrBucket := tx.ReadBucket(wstakemgrNamespaceKey) + if wstakemgrBucket == nil { + return errors.New("missing stake manager namespace") + } + missingTxHistory = tx.ReadBucket(wtxmgrNamespaceKey) == nil + return nil + }) if err != nil { return nil, err } - txMgrNS, err := db.Namespace(wtxmgrNamespaceKey) + + // The transaction history namespace is allowed to be missing as this + // means it was manually pruned from the database by other tooling, and + // the wallet should handle this with a full rescan. When this is the + // case, recreate the history. + // + // After all upgrades have been performed, if the transaction history + // was missing, the wallet sync state (saved in the address manager) + // will be unset. + if missingTxHistory { + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + txmgrNs, err := tx.CreateTopLevelBucket(wtxmgrNamespaceKey) + if err != nil { + return err + } + return wtxmgr.Create(txmgrNs) + }) + if err != nil { + return nil, err + } + } + + // Perform upgrades as necessary. Each upgrade is done under its own + // transaction, which is managed by each package itself, so the entire + // DB is passed instead of passing already opened write transaction. + // + // This will need to change later when upgrades in one package depend on + // data in another (such as removing chain synchronization from address + // manager). + err = waddrmgr.DoUpgrades(db, waddrmgrNamespaceKey, pubPass, params, cbs) if err != nil { return nil, err } - stakeMgrNS, err := db.Namespace(wstakemgrNamespaceKey) + err = wtxmgr.DoUpgrades(db, wtxmgrNamespaceKey) if err != nil { return nil, err } - addrMgr, err := waddrmgr.Open(addrMgrNS, pubPass, params, cbs) + err = wstakemgr.DoUpgrades(db, wstakemgrNamespaceKey) if err != nil { return nil, err } - noTxMgr, err := walletdb.NamespaceIsEmpty(txMgrNS) + + // Open database abstraction instances + var ( + addrMgr *waddrmgr.Manager + txMgr *wtxmgr.Store + smgr *wstakemgr.StakeStore + ) + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + stakemgrNs := tx.ReadBucket(wstakemgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var err error + addrMgr, err = waddrmgr.Open(addrmgrNs, pubPass, params) + if err != nil { + return err + } + txMgr, err = wtxmgr.Open(txmgrNs, params, addrMgr.AddrAccount) + if err != nil { + return err + } + smgr, err = wstakemgr.Open(stakemgrNs, addrMgr, params) + return err + }) if err != nil { return nil, err } - if noTxMgr { + + // When transaction history was missing and recreated empty, mark the + // wallet unsynced to trigger a full rescan. + if missingTxHistory { log.Info("No recorded transaction history -- needs full rescan") - err = addrMgr.SetSyncedTo(nil) + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + bucket := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return addrMgr.SetSyncedTo(bucket, nil) + }) if err != nil { return nil, err } - err = wtxmgr.Create(txMgrNS) + } + + // If configured to prune old tickets from transaction history, do so + // now. This step is always skipped on simnet because adjustment times + // are shorter. + if pruneTickets && params != &chaincfg.SimNetParams { + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + bucket := tx.ReadWriteBucket(wtxmgrNamespaceKey) + return txMgr.PruneOldTickets(bucket) + }) if err != nil { return nil, err } } - // Create a callback for account lookup from waddrmgr. - accountCallback := addrMgr.AddrAccount - txMgr, err := wtxmgr.Open(txMgrNS, pruneTickets, params, accountCallback) - if err != nil { - return nil, err - } - - smgr, err := wstakemgr.Open(stakeMgrNS, addrMgr, params) - if err != nil { - return nil, err - } // XXX Should we check error here? Right now error gives default (0). btm, err := dcrutil.NewAmount(balanceToMaintain) diff --git a/walletdb/bdb/db.go b/walletdb/bdb/db.go index 553c83bc2..3f145c36c 100644 --- a/walletdb/bdb/db.go +++ b/walletdb/bdb/db.go @@ -49,34 +49,88 @@ func convertErr(err error) error { return err } +// 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 +} + +func (tx *transaction) ReadBucket(key []byte) walletdb.ReadBucket { + return tx.ReadWriteBucket(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 interface. +// and implements the walletdb Bucket interfaces. type bucket bolt.Bucket -// Enforce bucket implements the walletdb.Bucket interface. -var _ walletdb.Bucket = (*bucket)(nil) +// Enforce bucket implements the walletdb Bucket interfaces. +var _ walletdb.ReadWriteBucket = (*bucket)(nil) -// Bucket retrieves a nested bucket with the given key. Returns nil if -// the bucket does not exist. +// NestedReadWriteBucket 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. +// 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) @@ -89,7 +143,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) @@ -97,12 +151,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)) } @@ -119,13 +173,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. @@ -156,11 +203,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()) } @@ -173,13 +224,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 @@ -226,118 +270,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. @@ -346,51 +278,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) } + return &transaction{boltTx: boltTx}, nil +} - // 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 +func (db *db) BeginReadTx() (walletdb.ReadTx, error) { + return db.beginTx(false) } -// 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) BeginReadWriteTx() (walletdb.ReadWriteTx, error) { + return db.beginTx(true) } // Copy writes a copy of the database to the provided writer. This call will diff --git a/walletdb/bdb/interface_test.go b/walletdb/bdb/interface_test.go index 2950399c4..eee5df878 100644 --- a/walletdb/bdb/interface_test.go +++ b/walletdb/bdb/interface_test.go @@ -46,7 +46,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 != "" { @@ -66,7 +66,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 != "" { @@ -83,7 +83,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) @@ -96,7 +96,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 @@ -115,7 +115,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 @@ -211,7 +211,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 } @@ -221,7 +221,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 @@ -254,7 +254,7 @@ func testBucketInterface(tc *testContext, bucket walletdb.Bucket) bool { tc.t.Errorf("DeleteBucket: unexpected error: %v", err) return false } - if b := bucket.Bucket(testBucketName); b != nil { + if b := bucket.ReadWriteBucket(testBucketName); b != nil { tc.t.Errorf("DeleteBucket: bucket '%s' still exists", testBucketName) return false diff --git a/walletdb/interface.go b/walletdb/interface.go index 05a219a80..177c3c94c 100644 --- a/walletdb/interface.go +++ b/walletdb/interface.go @@ -10,31 +10,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 + + // Rollback closes the transaction, discarding changes (if any) if the + // database was modified by a write transaction. + Rollback() error +} - // 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) +// 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 - // 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) + // 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 - // 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 + // 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 @@ -48,15 +64,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). // @@ -67,6 +74,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. @@ -74,26 +119,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) @@ -116,88 +148,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 - - // Commit commits all changes that have been made through the root - // bucket and all of its sub-buckets to persistent storage. - Commit() error +// 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 - // Rollback undoes all changes that have been made to the root bucket - // and all of its sub-buckets. - Rollback() 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 + // 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 } -// 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 +// 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 } -// 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. @@ -207,6 +185,47 @@ type DB interface { Close() error } +// View opens a database read transaction and executes the function f with the +// transaction passed as a parameter. After f exits, the transaction is rolled +// back. If f errors, its error is returned, not a rollback error (if any +// occur). +func View(db DB, f func(tx ReadTx) error) error { + tx, err := db.BeginReadTx() + if err != nil { + return err + } + err = f(tx) + rollbackErr := tx.Rollback() + if err != nil { + return err + } + if rollbackErr != nil { + return rollbackErr + } + return nil +} + +// Update opens a database read/write transaction and executes the function f +// with the transaction passed as a parameter. After f exits, if f did not +// error, the transaction is committed. Otherwise, if f did error, the +// transaction is rolled back. If the rollback fails, the original error +// returned by f is still returned. If the commit fails, the commit error is +// returned. +func Update(db DB, f func(tx ReadWriteTx) error) error { + tx, err := db.BeginReadWriteTx() + if err != nil { + return err + } + err = f(tx) + if err != nil { + // Want to return the original error, not a rollback error if + // any occur. + _ = tx.Rollback() + return err + } + return tx.Commit() +} + // Driver defines a structure for backend drivers to use when they registered // themselves as a backend which implements the Db interface. type Driver struct { diff --git a/walletsetup.go b/walletsetup.go index 1338f311f..301d93795 100644 --- a/walletsetup.go +++ b/walletsetup.go @@ -14,14 +14,10 @@ import ( "strings" "github.com/decred/dcrd/chaincfg" - "github.com/decred/dcrd/chaincfg/chainec" "github.com/decred/dcrd/wire" - "github.com/decred/dcrutil" "github.com/decred/dcrutil/hdkeychain" - "github.com/decred/dcrwallet/internal/legacy/keystore" "github.com/decred/dcrwallet/internal/prompt" "github.com/decred/dcrwallet/pgpwordlist" - "github.com/decred/dcrwallet/waddrmgr" "github.com/decred/dcrwallet/wallet" "github.com/decred/dcrwallet/walletdb" _ "github.com/decred/dcrwallet/walletdb/bdb" @@ -44,62 +40,6 @@ func networkDir(dataDir string, chainParams *chaincfg.Params) string { return filepath.Join(dataDir, netname) } -// 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 { - netParams := legacyKeyStore.Net() - blockStamp := waddrmgr.BlockStamp{ - Height: 0, - Hash: *netParams.GenesisHash, - } - for _, walletAddr := range legacyKeyStore.ActiveAddresses() { - switch addr := walletAddr.(type) { - case keystore.PubKeyAddress: - privKey, err := addr.PrivKey() - if err != nil { - fmt.Printf("WARN: Failed to obtain private key "+ - "for address %v: %v\n", addr.Address(), - err) - continue - } - - wif, err := dcrutil.NewWIF((chainec.PrivateKey)(privKey), - netParams, chainec.ECTypeSecp256k1) - if err != nil { - fmt.Printf("WARN: Failed to create wallet "+ - "import format for address %v: %v\n", - addr.Address(), err) - continue - } - - _, err = manager.ImportPrivateKey(wif, &blockStamp) - if err != nil { - fmt.Printf("WARN: Failed to import private "+ - "key for address %v: %v\n", - addr.Address(), err) - continue - } - - case keystore.ScriptAddress: - _, err := manager.ImportScript(addr.Script(), &blockStamp) - if err != nil { - fmt.Printf("WARN: Failed to import "+ - "pay-to-script-hash script for "+ - "address %v: %v\n", addr.Address(), err) - continue - } - - default: - fmt.Printf("WARN: Skipping unrecognized legacy "+ - "keystore type: %T\n", addr) - continue - } - } - - return nil -} - // createWallet prompts the user for information needed to generate a new wallet // and generates the wallet accordingly. The new wallet will reside at the // provided path. The bool passed back gives whether or not the wallet was diff --git a/wstakemgr/db.go b/wstakemgr/db.go index 904bd310d..a6a076079 100644 --- a/wstakemgr/db.go +++ b/wstakemgr/db.go @@ -570,24 +570,15 @@ func serializeSSRtxRecords(records []*ssrtxRecord) []byte { // stakeStoreExists returns whether or not the stake store has already // been created in the given database namespace. -func stakeStoreExists(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, stakeStoreError(ErrDatabase, str, err) - } - return exists, nil +func stakeStoreExists(ns walletdb.ReadBucket) bool { + mainBucket := ns.NestedReadBucket(mainBucketName) + return mainBucket != nil } // fetchSStxRecord retrieves a tx record from the sstx records bucket // with the given hash. -func fetchSStxRecord(tx walletdb.Tx, hash *chainhash.Hash) (*sstxRecord, error) { - bucket := tx.RootBucket().Bucket(sstxRecordsBucketName) +func fetchSStxRecord(ns walletdb.ReadBucket, hash *chainhash.Hash) (*sstxRecord, error) { + bucket := ns.NestedReadBucket(sstxRecordsBucketName) key := hash.Bytes() val := bucket.Get(key) @@ -601,9 +592,10 @@ func fetchSStxRecord(tx walletdb.Tx, hash *chainhash.Hash) (*sstxRecord, error) // fetchSStxRecordSStxTicketScriptHash retrieves a ticket 0th output script or // pubkeyhash from the sstx records bucket with the given hash. -func fetchSStxRecordSStxTicketScriptHash(tx walletdb.Tx, +func fetchSStxRecordSStxTicketScriptHash(ns walletdb.ReadBucket, hash *chainhash.Hash) ([]byte, error) { - bucket := tx.RootBucket().Bucket(sstxRecordsBucketName) + + bucket := ns.NestedReadBucket(sstxRecordsBucketName) key := hash.Bytes() val := bucket.Get(key) @@ -617,9 +609,10 @@ func fetchSStxRecordSStxTicketScriptHash(tx walletdb.Tx, // fetchSStxRecordVoteBits fetches an individual ticket's intended voteBits // which are used to override the default voteBits when voting. -func fetchSStxRecordVoteBits(tx walletdb.Tx, hash *chainhash.Hash) (bool, uint16, +func fetchSStxRecordVoteBits(ns walletdb.ReadBucket, hash *chainhash.Hash) (bool, uint16, error) { - bucket := tx.RootBucket().Bucket(sstxRecordsBucketName) + + bucket := ns.NestedReadBucket(sstxRecordsBucketName) key := hash.Bytes() val := bucket.Get(key) @@ -651,9 +644,10 @@ func fetchSStxRecordVoteBits(tx walletdb.Tx, hash *chainhash.Hash) (bool, uint16 // updateSStxRecordVoteBits updates an individual ticket's intended voteBits // which are used to override the default voteBits when voting. -func updateSStxRecordVoteBits(tx walletdb.Tx, hash *chainhash.Hash, +func updateSStxRecordVoteBits(ns walletdb.ReadWriteBucket, hash *chainhash.Hash, voteBits uint16) error { - bucket := tx.RootBucket().Bucket(sstxRecordsBucketName) + + bucket := ns.NestedReadWriteBucket(sstxRecordsBucketName) key := hash.Bytes() val := bucket.Get(key) @@ -687,8 +681,8 @@ func updateSStxRecordVoteBits(tx walletdb.Tx, hash *chainhash.Hash, } // updateSStxRecord updates a sstx record in the sstx records bucket. -func updateSStxRecord(tx walletdb.Tx, record *sstxRecord, voteBits uint16) error { - bucket := tx.RootBucket().Bucket(sstxRecordsBucketName) +func updateSStxRecord(ns walletdb.ReadWriteBucket, record *sstxRecord, voteBits uint16) error { + bucket := ns.NestedReadWriteBucket(sstxRecordsBucketName) // Write the serialized txrecord keyed by the tx hash. serializedSStxRecord, err := serializeSStxRecord(record, voteBits) @@ -705,15 +699,16 @@ func updateSStxRecord(tx walletdb.Tx, record *sstxRecord, voteBits uint16) error } // putSStxRecord inserts a given SStx record to the SStxrecords bucket. -func putSStxRecord(tx walletdb.Tx, record *sstxRecord, voteBits uint16) error { - return updateSStxRecord(tx, record, voteBits) +func putSStxRecord(ns walletdb.ReadWriteBucket, record *sstxRecord, voteBits uint16) error { + return updateSStxRecord(ns, record, voteBits) } // fetchSSGenRecords retrieves SSGen records from the SSGenRecords bucket with // the given hash. -func fetchSSGenRecords(tx walletdb.Tx, hash *chainhash.Hash) ([]*ssgenRecord, +func fetchSSGenRecords(ns walletdb.ReadBucket, hash *chainhash.Hash) ([]*ssgenRecord, error) { - bucket := tx.RootBucket().Bucket(ssgenRecordsBucketName) + + bucket := ns.NestedReadBucket(ssgenRecordsBucketName) key := hash.Bytes() val := bucket.Get(key) @@ -727,32 +722,31 @@ func fetchSSGenRecords(tx walletdb.Tx, hash *chainhash.Hash) ([]*ssgenRecord, // ssgenRecordExistsInRecords checks to see if a record already exists // in a slice of ssgen records. -func ssgenRecordExistsInRecords(record *ssgenRecord, - records []*ssgenRecord) bool { +func ssgenRecordExistsInRecords(record *ssgenRecord, records []*ssgenRecord) bool { for _, r := range records { if r.txHash.IsEqual(&record.txHash) { return true } } - return false } // updateSSGenRecord updates an SSGen record in the SSGen records bucket. -func updateSSGenRecord(tx walletdb.Tx, hash *chainhash.Hash, +func updateSSGenRecord(ns walletdb.ReadWriteBucket, hash *chainhash.Hash, record *ssgenRecord) error { + // Fetch the current content of the key. // Possible buggy behaviour: If deserialization fails, // we won't detect it here. We assume we're throwing // ErrSSGenNotFound. - oldRecords, _ := fetchSSGenRecords(tx, hash) + oldRecords, _ := fetchSSGenRecords(ns, hash) // Don't reinsert records we already have. if ssgenRecordExistsInRecords(record, oldRecords) { return nil } - bucket := tx.RootBucket().Bucket(ssgenRecordsBucketName) + bucket := ns.NestedReadWriteBucket(ssgenRecordsBucketName) var records []*ssgenRecord // Either create a slice if currently nothing exists for this @@ -776,16 +770,18 @@ func updateSSGenRecord(tx walletdb.Tx, hash *chainhash.Hash, } // putSSGenRecord inserts a given SSGen record to the SSGenrecords bucket. -func putSSGenRecord(tx walletdb.Tx, hash *chainhash.Hash, +func putSSGenRecord(ns walletdb.ReadWriteBucket, hash *chainhash.Hash, record *ssgenRecord) error { - return updateSSGenRecord(tx, hash, record) + + return updateSSGenRecord(ns, hash, record) } // fetchSSRtxRecords retrieves SSRtx records from the SSRtxRecords bucket with // the given hash. -func fetchSSRtxRecords(tx walletdb.Tx, hash *chainhash.Hash) ([]*ssrtxRecord, +func fetchSSRtxRecords(ns walletdb.ReadBucket, hash *chainhash.Hash) ([]*ssrtxRecord, error) { - bucket := tx.RootBucket().Bucket(ssrtxRecordsBucketName) + + bucket := ns.NestedReadBucket(ssrtxRecordsBucketName) key := hash.Bytes() val := bucket.Get(key) @@ -799,32 +795,31 @@ func fetchSSRtxRecords(tx walletdb.Tx, hash *chainhash.Hash) ([]*ssrtxRecord, // ssrtxRecordExistsInRecords checks to see if a record already exists // in a slice of ssrtx records. -func ssrtxRecordExistsInRecords(record *ssrtxRecord, - records []*ssrtxRecord) bool { +func ssrtxRecordExistsInRecords(record *ssrtxRecord, records []*ssrtxRecord) bool { for _, r := range records { if r.txHash.IsEqual(&record.txHash) { return true } } - return false } // updateSSRtxRecord updates an SSRtx record in the SSRtx records bucket. -func updateSSRtxRecord(tx walletdb.Tx, hash *chainhash.Hash, +func updateSSRtxRecord(ns walletdb.ReadWriteBucket, hash *chainhash.Hash, record *ssrtxRecord) error { + // Fetch the current content of the key. // Possible buggy behaviour: If deserialization fails, // we won't detect it here. We assume we're throwing // ErrSSRtxsNotFound. - oldRecords, _ := fetchSSRtxRecords(tx, hash) + oldRecords, _ := fetchSSRtxRecords(ns, hash) // Don't reinsert records we already have. if ssrtxRecordExistsInRecords(record, oldRecords) { return nil } - bucket := tx.RootBucket().Bucket(ssrtxRecordsBucketName) + bucket := ns.NestedReadWriteBucket(ssrtxRecordsBucketName) var records []*ssrtxRecord // Either create a slice if currently nothing exists for this @@ -848,16 +843,15 @@ func updateSSRtxRecord(tx walletdb.Tx, hash *chainhash.Hash, } // putSSRtxRecord inserts a given SSRtxs record to the SSRtxs records bucket. -func putSSRtxRecord(tx walletdb.Tx, hash *chainhash.Hash, +func putSSRtxRecord(ns walletdb.ReadWriteBucket, hash *chainhash.Hash, record *ssrtxRecord) error { - return updateSSRtxRecord(tx, hash, record) + + return updateSSRtxRecord(ns, hash, record) } // deserializeUserTicket deserializes the passed serialized user // ticket information. -func deserializeUserTicket(serializedTicket []byte) (*PoolTicket, - error) { - +func deserializeUserTicket(serializedTicket []byte) (*PoolTicket, error) { // Cursory check to make sure that the size of the // ticket makes sense. if len(serializedTicket)%stakePoolUserTicketSize != 0 { @@ -978,9 +972,10 @@ func serializeUserTickets(records []*PoolTicket) []byte { // fetchStakePoolUserTickets retrieves pool user tickets from the meta bucket with // the given hash. -func fetchStakePoolUserTickets(tx walletdb.Tx, +func fetchStakePoolUserTickets(ns walletdb.ReadBucket, scriptHash [20]byte) ([]*PoolTicket, error) { - bucket := tx.RootBucket().Bucket(metaBucketName) + + bucket := ns.NestedReadBucket(metaBucketName) key := make([]byte, stakePoolTicketsPrefixSize+scriptHashSize) copy(key[0:stakePoolTicketsPrefixSize], stakePoolTicketsPrefix) @@ -998,28 +993,24 @@ func fetchStakePoolUserTickets(tx walletdb.Tx, // duplicateExistsInUserTickets checks to see if an exact duplicated of a // record already exists in a slice of user ticket records. -func duplicateExistsInUserTickets(record *PoolTicket, - records []*PoolTicket) bool { +func duplicateExistsInUserTickets(record *PoolTicket, records []*PoolTicket) bool { for _, r := range records { if *r == *record { return true } } - return false } // recordExistsInUserTickets checks to see if a record already exists // in a slice of user ticket records. If it does exist, it returns // the location where it exists in the slice. -func recordExistsInUserTickets(record *PoolTicket, - records []*PoolTicket) (bool, int) { +func recordExistsInUserTickets(record *PoolTicket, records []*PoolTicket) (bool, int) { for i, r := range records { if r.Ticket == record.Ticket { return true, i } } - return false, 0 } @@ -1027,13 +1018,14 @@ func recordExistsInUserTickets(record *PoolTicket, // The function pulls the current entry in the database, checks to see if the // ticket is already there, updates it accordingly, or adds it to the list of // tickets. -func updateStakePoolUserTickets(tx walletdb.Tx, scriptHash [20]byte, +func updateStakePoolUserTickets(ns walletdb.ReadWriteBucket, scriptHash [20]byte, record *PoolTicket) error { + // Fetch the current content of the key. // Possible buggy behaviour: If deserialization fails, // we won't detect it here. We assume we're throwing // ErrPoolUserTicketsNotFound. - oldRecords, _ := fetchStakePoolUserTickets(tx, scriptHash) + oldRecords, _ := fetchStakePoolUserTickets(ns, scriptHash) // Don't reinsert duplicate records we already have. if duplicateExistsInUserTickets(record, oldRecords) { @@ -1059,7 +1051,7 @@ func updateStakePoolUserTickets(tx walletdb.Tx, scriptHash [20]byte, } } - bucket := tx.RootBucket().Bucket(metaBucketName) + bucket := ns.NestedReadWriteBucket(metaBucketName) key := make([]byte, stakePoolTicketsPrefixSize+scriptHashSize) copy(key[0:stakePoolTicketsPrefixSize], stakePoolTicketsPrefix) copy(key[stakePoolTicketsPrefixSize:stakePoolTicketsPrefixSize+scriptHashSize], @@ -1129,9 +1121,10 @@ func serializeUserInvalTickets(records []*chainhash.Hash) []byte { // fetchStakePoolUserInvalTickets retrieves the list of invalid pool user tickets // from the meta bucket with the given hash. -func fetchStakePoolUserInvalTickets(tx walletdb.Tx, +func fetchStakePoolUserInvalTickets(ns walletdb.ReadBucket, scriptHash [20]byte) ([]*chainhash.Hash, error) { - bucket := tx.RootBucket().Bucket(metaBucketName) + + bucket := ns.NestedReadBucket(metaBucketName) key := make([]byte, stakePoolInvalidPrefixSize+scriptHashSize) copy(key[0:stakePoolInvalidPrefixSize], stakePoolInvalidPrefix) @@ -1164,13 +1157,14 @@ func duplicateExistsInInvalTickets(record *chainhash.Hash, // invalid tickets. The function pulls the current entry in the database, // checks to see if the ticket is already there. If it is it returns, otherwise // it adds it to the list of tickets. -func updateStakePoolInvalUserTickets(tx walletdb.Tx, scriptHash [20]byte, +func updateStakePoolInvalUserTickets(ns walletdb.ReadWriteBucket, scriptHash [20]byte, record *chainhash.Hash) error { + // Fetch the current content of the key. // Possible buggy behaviour: If deserialization fails, // we won't detect it here. We assume we're throwing // ErrPoolUserInvalTcktsNotFound. - oldRecords, _ := fetchStakePoolUserInvalTickets(tx, scriptHash) + oldRecords, _ := fetchStakePoolUserInvalTickets(ns, scriptHash) // Don't reinsert duplicate records we already have. if duplicateExistsInInvalTickets(record, oldRecords) { @@ -1187,7 +1181,7 @@ func updateStakePoolInvalUserTickets(tx walletdb.Tx, scriptHash [20]byte, records = append(oldRecords, record) } - bucket := tx.RootBucket().Bucket(metaBucketName) + bucket := ns.NestedReadWriteBucket(metaBucketName) key := make([]byte, stakePoolInvalidPrefixSize+scriptHashSize) copy(key[0:stakePoolInvalidPrefixSize], stakePoolInvalidPrefix) copy(key[stakePoolInvalidPrefixSize:stakePoolInvalidPrefixSize+scriptHashSize], @@ -1206,8 +1200,8 @@ func updateStakePoolInvalUserTickets(tx walletdb.Tx, scriptHash [20]byte, } // putMeta puts a k-v into the meta bucket. -func putMeta(tx walletdb.Tx, key []byte, n int32) error { - bucket := tx.RootBucket().Bucket(metaBucketName) +func putMeta(ns walletdb.ReadWriteBucket, key []byte, n int32) error { + bucket := ns.NestedReadWriteBucket(metaBucketName) err := bucket.Put(key, uint32ToBytes(uint32(n))) if err != nil { str := fmt.Sprintf("failed to store meta key '%s'", key) @@ -1217,8 +1211,8 @@ func putMeta(tx walletdb.Tx, key []byte, n int32) error { } // fetchMeta fetches a v from a k in the meta bucket. -func fetchMeta(tx walletdb.Tx, key []byte) (int32, error) { - bucket := tx.RootBucket().Bucket(metaBucketName) +func fetchMeta(ns walletdb.ReadBucket, key []byte) (int32, error) { + bucket := ns.NestedReadBucket(metaBucketName) val := bucket.Get(key) // Return 0 if the metadata is uninitialized @@ -1235,76 +1229,70 @@ func fetchMeta(tx walletdb.Tx, key []byte) (int32, error) { // initialize creates the DB if it doesn't exist, and otherwise // loads the database. -func initializeEmpty(namespace walletdb.Namespace) error { +func initializeEmpty(ns walletdb.ReadWriteBucket) error { // Initialize the buckets and main db fields as needed. - var version uint32 - var createDate uint64 - err := namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - mainBucket, err := rootBucket.CreateBucketIfNotExists( - mainBucketName) - if err != nil { - str := "failed to create main bucket" - return stakeStoreError(ErrDatabase, str, err) - } + mainBucket, err := ns.CreateBucketIfNotExists(mainBucketName) + if err != nil { + str := "failed to create main bucket" + return stakeStoreError(ErrDatabase, str, err) + } - _, err = rootBucket.CreateBucketIfNotExists(sstxRecordsBucketName) - if err != nil { - str := "failed to create sstx records bucket" - return stakeStoreError(ErrDatabase, str, err) - } + _, err = ns.CreateBucketIfNotExists(sstxRecordsBucketName) + if err != nil { + str := "failed to create sstx records bucket" + return stakeStoreError(ErrDatabase, str, err) + } - _, err = rootBucket.CreateBucketIfNotExists(ssgenRecordsBucketName) - if err != nil { - str := "failed to create ssgen records bucket" - return stakeStoreError(ErrDatabase, str, err) - } + _, err = ns.CreateBucketIfNotExists(ssgenRecordsBucketName) + if err != nil { + str := "failed to create ssgen records bucket" + return stakeStoreError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucketIfNotExists(ssrtxRecordsBucketName) + if err != nil { + str := "failed to create ssrtx records bucket" + return stakeStoreError(ErrDatabase, str, err) + } + + _, err = ns.CreateBucketIfNotExists(metaBucketName) + if err != nil { + str := "failed to create meta bucket" + return stakeStoreError(ErrDatabase, str, err) + } + + // Save the most recent tx store version if it isn't already + // there, otherwise keep track of it for potential upgrades. + var version uint32 + verBytes := mainBucket.Get(stakeStoreVersionName) + if verBytes == nil { + version = LatestStakeMgrVersion - _, err = rootBucket.CreateBucketIfNotExists(ssrtxRecordsBucketName) + var buf [4]byte + byteOrder.PutUint32(buf[:], version) + err := mainBucket.Put(stakeStoreVersionName, buf[:]) if err != nil { - str := "failed to create ssrtx records bucket" + str := "failed to store latest database version" return stakeStoreError(ErrDatabase, str, err) } + } else { + version = byteOrder.Uint32(verBytes) + } - _, err = rootBucket.CreateBucketIfNotExists(metaBucketName) + var createDate uint64 + createBytes := mainBucket.Get(stakeStoreCreateDateName) + if createBytes == nil { + createDate = uint64(time.Now().Unix()) + var buf [8]byte + byteOrder.PutUint64(buf[:], createDate) + err := mainBucket.Put(stakeStoreCreateDateName, buf[:]) if err != nil { - str := "failed to create meta bucket" + str := "failed to store database creation time" return stakeStoreError(ErrDatabase, str, err) } - - // Save the most recent tx store version if it isn't already - // there, otherwise keep track of it for potential upgrades. - verBytes := mainBucket.Get(stakeStoreVersionName) - if verBytes == nil { - version = LatestStakeMgrVersion - - var buf [4]byte - byteOrder.PutUint32(buf[:], version) - err := mainBucket.Put(stakeStoreVersionName, buf[:]) - if err != nil { - str := "failed to store latest database version" - return stakeStoreError(ErrDatabase, str, err) - } - } else { - version = byteOrder.Uint32(verBytes) - } - - createBytes := mainBucket.Get(stakeStoreCreateDateName) - if createBytes == nil { - createDate = uint64(time.Now().Unix()) - var buf [8]byte - byteOrder.PutUint64(buf[:], createDate) - err := mainBucket.Put(stakeStoreCreateDateName, buf[:]) - if err != nil { - str := "failed to store database creation time" - return stakeStoreError(ErrDatabase, str, err) - } - } else { - createDate = byteOrder.Uint64(createBytes) - } - - return nil - }) + } else { + createDate = byteOrder.Uint64(createBytes) + } if err != nil { str := "failed to load database" diff --git a/wstakemgr/stake.go b/wstakemgr/stake.go index 523fcf77a..ca34932d1 100644 --- a/wstakemgr/stake.go +++ b/wstakemgr/stake.go @@ -111,11 +111,10 @@ type StakePoolUser struct { type StakeStore struct { mtx *sync.Mutex - namespace walletdb.Namespace - Params *chaincfg.Params - Manager *waddrmgr.Manager - chainSvr *walletchain.RPCClient - isClosed bool + Params *chaincfg.Params + Manager *waddrmgr.Manager + chainSvr *walletchain.RPCClient + isClosed bool ownedSStxs map[chainhash.Hash]struct{} } @@ -158,7 +157,7 @@ func (s *StakeStore) addHashToStore(hash *chainhash.Hash) { } // insertSStx inserts an SStx into the store. -func (s *StakeStore) insertSStx(sstx *dcrutil.Tx, voteBits uint16) error { +func (s *StakeStore) insertSStx(ns walletdb.ReadWriteBucket, sstx *dcrutil.Tx, voteBits uint16) error { // If we already have the SStx, no need to // try to include twice. exists := s.checkHashInStore(sstx.Sha()) @@ -176,13 +175,7 @@ func (s *StakeStore) insertSStx(sstx *dcrutil.Tx, voteBits uint16) error { } // Add the SStx to the database. - err := s.namespace.Update(func(tx walletdb.Tx) error { - if putErr := putSStxRecord(tx, record, voteBits); putErr != nil { - return putErr - } - - return nil - }) + err := putSStxRecord(ns, record, voteBits) if err != nil { return err } @@ -195,7 +188,7 @@ func (s *StakeStore) insertSStx(sstx *dcrutil.Tx, voteBits uint16) error { // InsertSStx is the exported version of insertSStx that is safe for concurrent // access. -func (s *StakeStore) InsertSStx(sstx *dcrutil.Tx, voteBits uint16) error { +func (s *StakeStore) InsertSStx(ns walletdb.ReadWriteBucket, sstx *dcrutil.Tx, voteBits uint16) error { if s.isClosed { str := "stake store is closed" return stakeStoreError(ErrStoreClosed, str, nil) @@ -204,12 +197,12 @@ func (s *StakeStore) InsertSStx(sstx *dcrutil.Tx, voteBits uint16) error { s.mtx.Lock() defer s.mtx.Unlock() - return s.insertSStx(sstx, voteBits) + return s.insertSStx(ns, sstx, voteBits) } // sstxVoteBits fetches the intended voteBits for a given SStx. This per- // ticket voteBits will override the default voteBits set by the wallet. -func (s *StakeStore) sstxVoteBits(sstx *chainhash.Hash) (bool, uint16, error) { +func (s *StakeStore) sstxVoteBits(ns walletdb.ReadBucket, sstx *chainhash.Hash) (bool, uint16, error) { // If we already have the SStx, no need to // try to include twice. exists := s.checkHashInStore(sstx) @@ -219,17 +212,7 @@ func (s *StakeStore) sstxVoteBits(sstx *chainhash.Hash) (bool, uint16, error) { } // Attempt to update the SStx in the database. - voteBitsSet := false - voteBits := uint16(0) - err := s.namespace.View(func(tx walletdb.Tx) error { - var fetchErr error - if voteBitsSet, voteBits, fetchErr = fetchSStxRecordVoteBits(tx, - sstx); fetchErr != nil { - return fetchErr - } - - return nil - }) + voteBitsSet, voteBits, err := fetchSStxRecordVoteBits(ns, sstx) if err != nil { return voteBitsSet, voteBits, err } @@ -239,7 +222,7 @@ func (s *StakeStore) sstxVoteBits(sstx *chainhash.Hash) (bool, uint16, error) { // SStxVoteBits is the exported version of sstxVoteBits that is // safe for concurrent access. -func (s *StakeStore) SStxVoteBits(sstx *chainhash.Hash) (bool, uint16, error) { +func (s *StakeStore) SStxVoteBits(ns walletdb.ReadBucket, sstx *chainhash.Hash) (bool, uint16, error) { if s.isClosed { str := "stake store is closed" return false, 0, stakeStoreError(ErrStoreClosed, str, nil) @@ -248,12 +231,12 @@ func (s *StakeStore) SStxVoteBits(sstx *chainhash.Hash) (bool, uint16, error) { s.mtx.Lock() defer s.mtx.Unlock() - return s.sstxVoteBits(sstx) + return s.sstxVoteBits(ns, sstx) } // updateSStxVoteBits updates the intended voteBits for a given SStx. This per- // ticket voteBits will override the default voteBits set by the wallet. -func (s *StakeStore) updateSStxVoteBits(sstx *chainhash.Hash, +func (s *StakeStore) updateSStxVoteBits(ns walletdb.ReadWriteBucket, sstx *chainhash.Hash, voteBits uint16) error { // If we already have the SStx, no need to // try to include twice. @@ -264,13 +247,7 @@ func (s *StakeStore) updateSStxVoteBits(sstx *chainhash.Hash, } // Attempt to update the SStx in the database. - err := s.namespace.Update(func(tx walletdb.Tx) error { - if updErr := updateSStxRecordVoteBits(tx, sstx, voteBits); updErr != nil { - return updErr - } - - return nil - }) + err := updateSStxRecordVoteBits(ns, sstx, voteBits) if err != nil { return err } @@ -280,7 +257,7 @@ func (s *StakeStore) updateSStxVoteBits(sstx *chainhash.Hash, // UpdateSStxVoteBits is the exported version of updateSStxVoteBits that is // safe for concurrent access. -func (s *StakeStore) UpdateSStxVoteBits(sstx *chainhash.Hash, +func (s *StakeStore) UpdateSStxVoteBits(ns walletdb.ReadWriteBucket, sstx *chainhash.Hash, voteBits uint16) error { if s.isClosed { str := "stake store is closed" @@ -290,7 +267,7 @@ func (s *StakeStore) UpdateSStxVoteBits(sstx *chainhash.Hash, s.mtx.Lock() defer s.mtx.Unlock() - return s.updateSStxVoteBits(sstx, voteBits) + return s.updateSStxVoteBits(ns, sstx, voteBits) } // dumpSStxHashes dumps the hashes of all owned SStxs. Note @@ -330,7 +307,7 @@ func (s *StakeStore) DumpSStxHashes() ([]chainhash.Hash, error) { } // dumpSStxHashes dumps the hashes of all owned SStxs for some address. -func (s *StakeStore) dumpSStxHashesForAddress(addr dcrutil.Address) ([]chainhash.Hash, error) { +func (s *StakeStore) dumpSStxHashesForAddress(ns walletdb.ReadBucket, addr dcrutil.Address) ([]chainhash.Hash, error) { // Extract the HASH160 script hash; if it's not 20 bytes // long, return an error. scriptHash := addr.ScriptAddress() @@ -339,28 +316,19 @@ func (s *StakeStore) dumpSStxHashesForAddress(addr dcrutil.Address) ([]chainhash return nil, stakeStoreError(ErrInput, str, nil) } - var err error allTickets := s.dumpSStxHashes() var ticketsForAddr []chainhash.Hash // Access the database and store the result locally. - err = s.namespace.View(func(tx walletdb.Tx) error { - var err error - var thisScrHash []byte - for _, h := range allTickets { - thisScrHash, err = fetchSStxRecordSStxTicketScriptHash(tx, &h) - if err != nil { - return err - } - if bytes.Equal(scriptHash, thisScrHash) { - ticketsForAddr = append(ticketsForAddr, h) - } + for _, h := range allTickets { + thisScrHash, err := fetchSStxRecordSStxTicketScriptHash(ns, &h) + if err != nil { + str := "failure getting ticket 0th out script hashes from db" + return nil, stakeStoreError(ErrDatabase, str, err) + } + if bytes.Equal(scriptHash, thisScrHash) { + ticketsForAddr = append(ticketsForAddr, h) } - return nil - }) - if err != nil { - str := "failure getting ticket 0th out script hashes from db" - return nil, stakeStoreError(ErrDatabase, str, err) } return ticketsForAddr, nil @@ -368,7 +336,7 @@ func (s *StakeStore) dumpSStxHashesForAddress(addr dcrutil.Address) ([]chainhash // DumpSStxHashesForAddress is the exported version of dumpSStxHashesForAddress // that is safe for concurrent access. -func (s *StakeStore) DumpSStxHashesForAddress(addr dcrutil.Address) ([]chainhash.Hash, error) { +func (s *StakeStore) DumpSStxHashesForAddress(ns walletdb.ReadBucket, addr dcrutil.Address) ([]chainhash.Hash, error) { if s.isClosed { str := "stake store is closed" return nil, stakeStoreError(ErrStoreClosed, str, nil) @@ -377,27 +345,19 @@ func (s *StakeStore) DumpSStxHashesForAddress(addr dcrutil.Address) ([]chainhash s.mtx.Lock() defer s.mtx.Unlock() - return s.dumpSStxHashesForAddress(addr) + return s.dumpSStxHashesForAddress(ns, addr) } // sstxAddress returns the address for a given ticket. -func (s *StakeStore) sstxAddress(hash *chainhash.Hash) (dcrutil.Address, error) { +func (s *StakeStore) sstxAddress(ns walletdb.ReadBucket, hash *chainhash.Hash) (dcrutil.Address, error) { // Access the database and store the result locally. - var addr dcrutil.Address - err := s.namespace.View(func(tx walletdb.Tx) error { - thisScrHash, errFetch := fetchSStxRecordSStxTicketScriptHash(tx, hash) - if errFetch != nil { - return errFetch - } - var errAddr error - addr, errAddr = dcrutil.NewAddressScriptHashFromHash(thisScrHash, - s.Params) - if errAddr != nil { - return errAddr - } - - return nil - }) + thisScrHash, err := fetchSStxRecordSStxTicketScriptHash(ns, hash) + if err != nil { + str := "failure getting ticket 0th out script hashes from db" + return nil, stakeStoreError(ErrDatabase, str, err) + } + addr, err := dcrutil.NewAddressScriptHashFromHash(thisScrHash, + s.Params) if err != nil { str := "failure getting ticket 0th out script hashes from db" return nil, stakeStoreError(ErrDatabase, str, err) @@ -407,7 +367,7 @@ func (s *StakeStore) sstxAddress(hash *chainhash.Hash) (dcrutil.Address, error) } // SStxAddress is the exported, concurrency safe version of sstxAddress. -func (s *StakeStore) SStxAddress(hash *chainhash.Hash) (dcrutil.Address, error) { +func (s *StakeStore) SStxAddress(ns walletdb.ReadBucket, hash *chainhash.Hash) (dcrutil.Address, error) { if s.isClosed { str := "stake store is closed" return nil, stakeStoreError(ErrStoreClosed, str, nil) @@ -416,46 +376,36 @@ func (s *StakeStore) SStxAddress(hash *chainhash.Hash) (dcrutil.Address, error) s.mtx.Lock() defer s.mtx.Unlock() - return s.sstxAddress(hash) + return s.sstxAddress(ns, hash) } // dumpSSGenHashes fetches and returns the entire list of votes generated by // this wallet, including votes that were produced but were never included in // the blockchain. -func (s *StakeStore) dumpSSGenHashes() ([]chainhash.Hash, error) { +func (s *StakeStore) dumpSSGenHashes(ns walletdb.ReadBucket) ([]chainhash.Hash, error) { var voteList []chainhash.Hash - err := s.namespace.View(func(tx walletdb.Tx) error { - var errForEach error + // Open the vite records database. + bucket := ns.NestedReadBucket(ssgenRecordsBucketName) - // Open the vite records database. - bucket := tx.RootBucket().Bucket(ssgenRecordsBucketName) - - // Store each hash sequentially. - errForEach = bucket.ForEach(func(k []byte, v []byte) error { - recs, errDeser := deserializeSSGenRecords(v) - if errDeser != nil { - return errDeser - } - - for _, rec := range recs { - voteList = append(voteList, rec.txHash) - } - return nil - }) + // Store each hash sequentially. + err := bucket.ForEach(func(k []byte, v []byte) error { + recs, errDeser := deserializeSSGenRecords(v) + if errDeser != nil { + return errDeser + } - return errForEach + for _, rec := range recs { + voteList = append(voteList, rec.txHash) + } + return nil }) - if err != nil { - return nil, err - } - - return voteList, nil + return voteList, err } // DumpSSGenHashes is the exported version of dumpSSGenHashes that is safe // for concurrent access. -func (s *StakeStore) DumpSSGenHashes() ([]chainhash.Hash, error) { +func (s *StakeStore) DumpSSGenHashes(ns walletdb.ReadBucket) ([]chainhash.Hash, error) { if s.isClosed { str := "stake store is closed" return nil, stakeStoreError(ErrStoreClosed, str, nil) @@ -464,43 +414,33 @@ func (s *StakeStore) DumpSSGenHashes() ([]chainhash.Hash, error) { s.mtx.Lock() defer s.mtx.Unlock() - return s.dumpSSGenHashes() + return s.dumpSSGenHashes(ns) } // dumpSSRtxHashes fetches the entire list of revocations generated by this // wallet. -func (s *StakeStore) dumpSSRtxHashes() ([]chainhash.Hash, error) { +func (s *StakeStore) dumpSSRtxHashes(ns walletdb.ReadBucket) ([]chainhash.Hash, error) { var revocationList []chainhash.Hash - err := s.namespace.View(func(tx walletdb.Tx) error { - var errForEach error - - // Open the revocation records database. - bucket := tx.RootBucket().Bucket(ssrtxRecordsBucketName) + // Open the revocation records database. + bucket := ns.NestedReadBucket(ssrtxRecordsBucketName) - // Store each hash sequentially. - errForEach = bucket.ForEach(func(k []byte, v []byte) error { - rec, errDeser := deserializeSSRtxRecord(v) - if errDeser != nil { - return errDeser - } - - revocationList = append(revocationList, rec.txHash) - return nil - }) + // Store each hash sequentially. + err := bucket.ForEach(func(k []byte, v []byte) error { + rec, errDeser := deserializeSSRtxRecord(v) + if errDeser != nil { + return errDeser + } - return errForEach + revocationList = append(revocationList, rec.txHash) + return nil }) - if err != nil { - return nil, err - } - - return revocationList, nil + return revocationList, err } // DumpSSRtxHashes is the exported version of dumpSSRtxHashes that is safe // for concurrent access. -func (s *StakeStore) DumpSSRtxHashes() ([]chainhash.Hash, error) { +func (s *StakeStore) DumpSSRtxHashes(ns walletdb.ReadBucket) ([]chainhash.Hash, error) { if s.isClosed { str := "stake store is closed" return nil, stakeStoreError(ErrStoreClosed, str, nil) @@ -509,43 +449,33 @@ func (s *StakeStore) DumpSSRtxHashes() ([]chainhash.Hash, error) { s.mtx.Lock() defer s.mtx.Unlock() - return s.dumpSSRtxHashes() + return s.dumpSSRtxHashes(ns) } // dumpSSRtxTickets fetches the entire list of tickets spent as revocations // byt this wallet. -func (s *StakeStore) dumpSSRtxTickets() ([]chainhash.Hash, error) { +func (s *StakeStore) dumpSSRtxTickets(ns walletdb.ReadBucket) ([]chainhash.Hash, error) { var ticketList []chainhash.Hash - err := s.namespace.View(func(tx walletdb.Tx) error { - var errForEach error + // Open the revocation records database. + bucket := ns.NestedReadBucket(ssrtxRecordsBucketName) - // Open the revocation records database. - bucket := tx.RootBucket().Bucket(ssrtxRecordsBucketName) - - // Store each hash sequentially. - errForEach = bucket.ForEach(func(k []byte, v []byte) error { - ticket, errDeser := chainhash.NewHash(k) - if errDeser != nil { - return errDeser - } - - ticketList = append(ticketList, *ticket) - return nil - }) + // Store each hash sequentially. + err := bucket.ForEach(func(k []byte, v []byte) error { + ticket, errDeser := chainhash.NewHash(k) + if errDeser != nil { + return errDeser + } - return errForEach + ticketList = append(ticketList, *ticket) + return nil }) - if err != nil { - return nil, err - } - - return ticketList, nil + return ticketList, err } // DumpSSRtxTickets is the exported version of dumpSSRtxTickets that is safe // for concurrent access. -func (s *StakeStore) DumpSSRtxTickets() ([]chainhash.Hash, error) { +func (s *StakeStore) DumpSSRtxTickets(ns walletdb.ReadBucket) ([]chainhash.Hash, error) { if s.isClosed { str := "stake store is closed" return nil, stakeStoreError(ErrStoreClosed, str, nil) @@ -554,30 +484,17 @@ func (s *StakeStore) DumpSSRtxTickets() ([]chainhash.Hash, error) { s.mtx.Lock() defer s.mtx.Unlock() - return s.dumpSSRtxTickets() + return s.dumpSSRtxTickets(ns) } // A function to get a single owned SStx. -func (s *StakeStore) getSStx(hash *chainhash.Hash) (*sstxRecord, error) { - var record *sstxRecord - - // Access the database and store the result locally. - err := s.namespace.View(func(tx walletdb.Tx) error { - var err error - record, err = fetchSStxRecord(tx, hash) - - return err - }) - if err != nil { - return nil, err - } - - return record, nil +func (s *StakeStore) getSStx(ns walletdb.ReadBucket, hash *chainhash.Hash) (*sstxRecord, error) { + return fetchSStxRecord(ns, hash) } // insertSSGen inserts an SSGen record into the DB (keyed to the SStx it // spends. -func (s *StakeStore) insertSSGen(blockHash *chainhash.Hash, blockHeight int64, +func (s *StakeStore) insertSSGen(ns walletdb.ReadWriteBucket, blockHash *chainhash.Hash, blockHeight int64, ssgenHash *chainhash.Hash, voteBits uint16, sstxHash *chainhash.Hash) error { if blockHeight <= 0 { @@ -593,23 +510,12 @@ func (s *StakeStore) insertSSGen(blockHash *chainhash.Hash, blockHeight int64, } // Add the SSGen to the database. - err := s.namespace.Update(func(tx walletdb.Tx) error { - if putErr := putSSGenRecord(tx, sstxHash, record); putErr != nil { - return putErr - } - - return nil - }) - if err != nil { - return err - } - - return nil + return putSSGenRecord(ns, sstxHash, record) } // InsertSSGen is the exported version of insertSSGen that is safe for // concurrent access. -func (s *StakeStore) InsertSSGen(blockHash *chainhash.Hash, blockHeight int64, +func (s *StakeStore) InsertSSGen(ns walletdb.ReadWriteBucket, blockHash *chainhash.Hash, blockHeight int64, ssgenHash *chainhash.Hash, voteBits uint16, sstxHash *chainhash.Hash) error { if s.isClosed { str := "stake store is closed" @@ -619,33 +525,21 @@ func (s *StakeStore) InsertSSGen(blockHash *chainhash.Hash, blockHeight int64, s.mtx.Lock() defer s.mtx.Unlock() - return s.insertSSGen(blockHash, blockHeight, ssgenHash, voteBits, sstxHash) + return s.insertSSGen(ns, blockHash, blockHeight, ssgenHash, voteBits, sstxHash) } // getSSGens gets a list of SSGens that have been generated for some stake // ticket. -func (s *StakeStore) getSSGens(sstxHash *chainhash.Hash) ([]*ssgenRecord, error) { - var records []*ssgenRecord - - // Access the database and store the result locally. - err := s.namespace.View(func(tx walletdb.Tx) error { - var err error - records, err = fetchSSGenRecords(tx, sstxHash) - - return err - }) - if err != nil { - return nil, err - } - - return records, nil +func (s *StakeStore) getSSGens(ns walletdb.ReadBucket, sstxHash *chainhash.Hash) ([]*ssgenRecord, error) { + return fetchSSGenRecords(ns, sstxHash) } // SignVRTransaction signs a vote (SSGen) or revocation (SSRtx) // transaction. isSSGen indicates if it is an SSGen; if it's not, // it's an SSRtx. -func (s *StakeStore) SignVRTransaction(msgTx *wire.MsgTx, sstx *dcrutil.Tx, - isSSGen bool) error { +func (s *StakeStore) SignVRTransaction(waddrmgrNs walletdb.ReadBucket, msgTx *wire.MsgTx, + sstx *dcrutil.Tx, isSSGen bool) error { + if s.isClosed { str := "stake store is closed" return stakeStoreError(ErrStoreClosed, str, nil) @@ -669,7 +563,7 @@ func (s *StakeStore) SignVRTransaction(msgTx *wire.MsgTx, sstx *dcrutil.Tx, // look up the appropriate keys and scripts by address. getKey := txscript.KeyClosure(func(addr dcrutil.Address) ( chainec.PrivateKey, bool, error) { - address, err := s.Manager.Address(addr) + address, err := s.Manager.Address(waddrmgrNs, addr) if err != nil { return nil, false, err } @@ -690,7 +584,7 @@ func (s *StakeStore) SignVRTransaction(msgTx *wire.MsgTx, sstx *dcrutil.Tx, getScript := txscript.ScriptClosure(func( addr dcrutil.Address) ([]byte, error) { - address, err := s.Manager.Address(addr) + address, err := s.Manager.Address(waddrmgrNs, addr) if err != nil { return nil, err } @@ -748,11 +642,13 @@ var initSudsidyCacheOnce sync.Once // GenerateVote creates a new SSGen given a header hash, height, sstx // tx hash, and votebits. -func (s *StakeStore) generateVote(blockHash *chainhash.Hash, height int64, - sstxHash *chainhash.Hash, defaultVoteBits uint16, allowHighFees bool) (*StakeNotification, error) { +func (s *StakeStore) generateVote(ns walletdb.ReadWriteBucket, waddrmgrNs walletdb.ReadBucket, + blockHash *chainhash.Hash, height int64, sstxHash *chainhash.Hash, defaultVoteBits uint16, + allowHighFees bool) (*StakeNotification, error) { + // 1. Fetch the SStx, then calculate all the values we'll need later for // the generation of the SSGen tx outputs. - sstxRecord, err := s.getSStx(sstxHash) + sstxRecord, err := s.getSStx(ns, sstxHash) if err != nil { return nil, err } @@ -863,13 +759,13 @@ func (s *StakeStore) generateVote(blockHash *chainhash.Hash, height int64, } // Sign the transaction. - err = s.SignVRTransaction(msgTx, sstx, true) + err = s.SignVRTransaction(waddrmgrNs, msgTx, sstx, true) if err != nil { return nil, err } // Store the information about the SSGen. - err = s.insertSSGen(blockHash, + err = s.insertSSGen(ns, blockHash, height, ssgenTx.Sha(), voteBits, @@ -904,8 +800,9 @@ func (s *StakeStore) generateVote(blockHash *chainhash.Hash, height int64, // insertSSRtx inserts an SSRtx record into the DB (keyed to the SStx it // spends. -func (s *StakeStore) insertSSRtx(blockHash *chainhash.Hash, blockHeight int64, +func (s *StakeStore) insertSSRtx(ns walletdb.ReadWriteBucket, blockHash *chainhash.Hash, blockHeight int64, ssrtxHash *chainhash.Hash, sstxHash *chainhash.Hash) error { + if blockHeight <= 0 { return fmt.Errorf("invalid SSRtx block height") } @@ -918,24 +815,14 @@ func (s *StakeStore) insertSSRtx(blockHash *chainhash.Hash, blockHeight int64, } // Add the SSRtx to the database. - err := s.namespace.Update(func(tx walletdb.Tx) error { - if putErr := putSSRtxRecord(tx, sstxHash, record); putErr != nil { - return putErr - } - - return nil - }) - if err != nil { - return err - } - - return nil + return putSSRtxRecord(ns, sstxHash, record) } // InsertSSRtx is the exported version of insertSSRtx that is safe for // concurrent access. -func (s *StakeStore) InsertSSRtx(blockHash *chainhash.Hash, blockHeight int64, +func (s *StakeStore) InsertSSRtx(ns walletdb.ReadWriteBucket, blockHash *chainhash.Hash, blockHeight int64, ssrtxHash *chainhash.Hash, sstxHash *chainhash.Hash) error { + if s.isClosed { str := "stake store is closed" return stakeStoreError(ErrStoreClosed, str, nil) @@ -944,36 +831,25 @@ func (s *StakeStore) InsertSSRtx(blockHash *chainhash.Hash, blockHeight int64, s.mtx.Lock() defer s.mtx.Unlock() - return s.insertSSRtx(blockHash, blockHeight, ssrtxHash, sstxHash) + return s.insertSSRtx(ns, blockHash, blockHeight, ssrtxHash, sstxHash) } // GetSSRtxs gets a list of SSRtxs that have been generated for some stake // ticket. -func (s *StakeStore) getSSRtxs(sstxHash *chainhash.Hash) ([]*ssrtxRecord, error) { - var records []*ssrtxRecord - - // Access the database and store the result locally. - err := s.namespace.View(func(tx walletdb.Tx) error { - var err error - records, err = fetchSSRtxRecords(tx, sstxHash) - - return err - }) - if err != nil { - return nil, err - } - - return records, nil +func (s *StakeStore) getSSRtxs(ns walletdb.ReadBucket, sstxHash *chainhash.Hash) ([]*ssrtxRecord, error) { + return fetchSSRtxRecords(ns, sstxHash) } // GenerateRevocation generates a revocation (SSRtx), signs it, and // submits it by SendRawTransaction. It also stores a record of it // in the local database. -func (s *StakeStore) generateRevocation(blockHash *chainhash.Hash, height int64, - sstxHash *chainhash.Hash, allowHighFees bool) (*StakeNotification, error) { +func (s *StakeStore) generateRevocation(ns walletdb.ReadWriteBucket, waddrmgrNs walletdb.ReadBucket, + blockHash *chainhash.Hash, height int64, sstxHash *chainhash.Hash, + allowHighFees bool) (*StakeNotification, error) { + // 1. Fetch the SStx, then calculate all the values we'll need later for // the generation of the SSRtx tx outputs. - sstxRecord, err := s.getSStx(sstxHash) + sstxRecord, err := s.getSStx(ns, sstxHash) if err != nil { return nil, err } @@ -1048,13 +924,13 @@ func (s *StakeStore) generateRevocation(blockHash *chainhash.Hash, height int64, } // Sign the transaction. - err = s.SignVRTransaction(msgTx, sstx, false) + err = s.SignVRTransaction(waddrmgrNs, msgTx, sstx, false) if err != nil { return nil, err } // Store the information about the SSRtx. - err = s.insertSSRtx(blockHash, + err = s.insertSSRtx(ns, blockHash, height, ssrtxTx.Sha(), sstx.Sha()) @@ -1088,11 +964,13 @@ func (s *StakeStore) generateRevocation(blockHash *chainhash.Hash, height int64, // HandleWinningTicketsNtfn scans the list of eligible tickets and, if any // of these tickets in the sstx store match these tickets, spends them as // votes. -func (s StakeStore) HandleWinningTicketsNtfn(blockHash *chainhash.Hash, +func (s StakeStore) HandleWinningTicketsNtfn(ns walletdb.ReadWriteBucket, waddrmgrNs walletdb.ReadBucket, + blockHash *chainhash.Hash, blockHeight int64, tickets []*chainhash.Hash, defaultVoteBits uint16, allowHighFees bool) ([]*StakeNotification, error) { + if s.isClosed { str := "stake store is closed" return nil, stakeStoreError(ErrStoreClosed, str, nil) @@ -1122,7 +1000,7 @@ func (s StakeStore) HandleWinningTicketsNtfn(blockHash *chainhash.Hash, voteErrors := make([]error, len(ticketsToPull), len(ticketsToPull)) // Matching tickets (yay!), generate some SSGen. for i, ticket := range ticketsToPull { - ntfns[i], voteErrors[i] = s.generateVote(blockHash, blockHeight, ticket, + ntfns[i], voteErrors[i] = s.generateVote(ns, waddrmgrNs, blockHash, blockHeight, ticket, defaultVoteBits, allowHighFees) } @@ -1146,7 +1024,8 @@ func (s StakeStore) HandleWinningTicketsNtfn(blockHash *chainhash.Hash, // HandleMissedTicketsNtfn scans the list of missed tickets and, if any // of these tickets in the sstx store match these tickets, spends them as // SSRtx. -func (s StakeStore) HandleMissedTicketsNtfn(blockHash *chainhash.Hash, +func (s StakeStore) HandleMissedTicketsNtfn(ns walletdb.ReadWriteBucket, waddrmgrNs walletdb.ReadBucket, + blockHash *chainhash.Hash, blockHeight int64, tickets []*chainhash.Hash, allowHighFees bool) ([]*StakeNotification, error) { @@ -1179,8 +1058,8 @@ func (s StakeStore) HandleMissedTicketsNtfn(blockHash *chainhash.Hash, revocationErrors := make([]error, len(ticketsToPull), len(ticketsToPull)) // Matching tickets, generate some SSRtx. for i, ticket := range ticketsToPull { - ntfns[i], revocationErrors[i] = s.generateRevocation(blockHash, - blockHeight, ticket, allowHighFees) + ntfns[i], revocationErrors[i] = s.generateRevocation(ns, waddrmgrNs, + blockHash, blockHeight, ticket, allowHighFees) } errStr := "" @@ -1203,8 +1082,9 @@ func (s StakeStore) HandleMissedTicketsNtfn(blockHash *chainhash.Hash, // updateStakePoolUserTickets updates a stake pool ticket for a given user. // If the ticket does not currently exist in the database, it adds it. If it // does exist (the ticket hash exists), it replaces the old record. -func (s *StakeStore) updateStakePoolUserTickets(user dcrutil.Address, - ticket *PoolTicket) error { +func (s *StakeStore) updateStakePoolUserTickets(ns walletdb.ReadWriteBucket, waddrmgrNs walletdb.ReadBucket, + user dcrutil.Address, ticket *PoolTicket) error { + _, isScriptHash := user.(*dcrutil.AddressScriptHash) _, isP2PKH := user.(*dcrutil.AddressPubKeyHash) if !(isScriptHash || isP2PKH) { @@ -1215,36 +1095,26 @@ func (s *StakeStore) updateStakePoolUserTickets(user dcrutil.Address, scriptHash := new([20]byte) copy(scriptHash[:], scriptHashB) - err := s.namespace.Update(func(tx walletdb.Tx) error { - if updErr := updateStakePoolUserTickets(tx, *scriptHash, - ticket); updErr != nil { - return updErr - } - - return nil - }) - if err != nil { - return err - } - - return nil + return updateStakePoolUserTickets(ns, *scriptHash, ticket) } // UpdateStakePoolUserTickets is the exported and concurrency safe form of // updateStakePoolUserTickets. -func (s *StakeStore) UpdateStakePoolUserTickets(user dcrutil.Address, - ticket *PoolTicket) error { +func (s *StakeStore) UpdateStakePoolUserTickets(ns walletdb.ReadWriteBucket, waddrmgrNs walletdb.ReadBucket, + user dcrutil.Address, ticket *PoolTicket) error { + s.mtx.Lock() defer s.mtx.Unlock() - return s.updateStakePoolUserTickets(user, ticket) + return s.updateStakePoolUserTickets(ns, waddrmgrNs, user, ticket) } // updateStakePoolUserInvalTickets updates the list of invalid stake pool // tickets for a given user. If the ticket does not currently exist in the // database, it adds it. -func (s *StakeStore) updateStakePoolUserInvalTickets(user dcrutil.Address, +func (s *StakeStore) updateStakePoolUserInvalTickets(ns walletdb.ReadWriteBucket, user dcrutil.Address, ticket *chainhash.Hash) error { + _, isScriptHash := user.(*dcrutil.AddressScriptHash) _, isP2PKH := user.(*dcrutil.AddressPubKeyHash) if !(isScriptHash || isP2PKH) { @@ -1255,35 +1125,23 @@ func (s *StakeStore) updateStakePoolUserInvalTickets(user dcrutil.Address, scriptHash := new([20]byte) copy(scriptHash[:], scriptHashB) - err := s.namespace.Update(func(tx walletdb.Tx) error { - if updErr := updateStakePoolInvalUserTickets(tx, *scriptHash, - ticket); updErr != nil { - return updErr - } - - return nil - }) - if err != nil { - return err - } - - return nil + return updateStakePoolInvalUserTickets(ns, *scriptHash, ticket) } // UpdateStakePoolUserInvalTickets is the exported and concurrency safe form of // updateStakePoolUserInvalTickets. -func (s *StakeStore) UpdateStakePoolUserInvalTickets(user dcrutil.Address, +func (s *StakeStore) UpdateStakePoolUserInvalTickets(ns walletdb.ReadWriteBucket, user dcrutil.Address, ticket *chainhash.Hash) error { + s.mtx.Lock() defer s.mtx.Unlock() - return s.updateStakePoolUserInvalTickets(user, ticket) + return s.updateStakePoolUserInvalTickets(ns, user, ticket) } // stakePoolUserInfo returns the stake pool user information for a given stake // pool user, keyed to their P2SH voting address. -func (s *StakeStore) stakePoolUserInfo(user dcrutil.Address) (*StakePoolUser, - error) { +func (s *StakeStore) stakePoolUserInfo(ns walletdb.ReadBucket, user dcrutil.Address) (*StakePoolUser, error) { _, isScriptHash := user.(*dcrutil.AddressScriptHash) _, isP2PKH := user.(*dcrutil.AddressPubKeyHash) if !(isScriptHash || isP2PKH) { @@ -1296,91 +1154,77 @@ func (s *StakeStore) stakePoolUserInfo(user dcrutil.Address) (*StakePoolUser, stakePoolUser := new(StakePoolUser) - err := s.namespace.View(func(tx walletdb.Tx) error { - // Catch missing user errors below and blank out the stake - // pool user information for the section if the user has - // no entries. - missingValidTickets, missingInvalidTickets := false, false - - userTickets, fetchErrVal := fetchStakePoolUserTickets(tx, *scriptHash) - if fetchErrVal != nil { - stakeMgrErr, is := fetchErrVal.(StakeStoreError) - if is { - missingValidTickets = stakeMgrErr.ErrorCode == - ErrPoolUserTicketsNotFound - } else { - return fetchErrVal - } - } - if missingValidTickets { - userTickets = make([]*PoolTicket, 0) + // Catch missing user errors below and blank out the stake + // pool user information for the section if the user has + // no entries. + missingValidTickets, missingInvalidTickets := false, false + + userTickets, fetchErrVal := fetchStakePoolUserTickets(ns, *scriptHash) + if fetchErrVal != nil { + stakeMgrErr, is := fetchErrVal.(StakeStoreError) + if is { + missingValidTickets = stakeMgrErr.ErrorCode == + ErrPoolUserTicketsNotFound + } else { + return nil, fetchErrVal } + } + if missingValidTickets { + userTickets = make([]*PoolTicket, 0) + } - invalTickets, fetchErrInval := fetchStakePoolUserInvalTickets(tx, - *scriptHash) - if fetchErrInval != nil { - stakeMgrErr, is := fetchErrInval.(StakeStoreError) - if is { - missingInvalidTickets = stakeMgrErr.ErrorCode == - ErrPoolUserInvalTcktsNotFound - } else { - return fetchErrInval - } - } - if missingInvalidTickets { - invalTickets = make([]*chainhash.Hash, 0) + invalTickets, fetchErrInval := fetchStakePoolUserInvalTickets(ns, + *scriptHash) + if fetchErrInval != nil { + stakeMgrErr, is := fetchErrInval.(StakeStoreError) + if is { + missingInvalidTickets = stakeMgrErr.ErrorCode == + ErrPoolUserInvalTcktsNotFound + } else { + return nil, fetchErrInval } - - stakePoolUser.Tickets = userTickets - stakePoolUser.InvalidTickets = invalTickets - - return nil - }) - if err != nil { - return nil, err + } + if missingInvalidTickets { + invalTickets = make([]*chainhash.Hash, 0) } + stakePoolUser.Tickets = userTickets + stakePoolUser.InvalidTickets = invalTickets + return stakePoolUser, nil } // StakePoolUserInfo is the exported and concurrency safe form of // stakePoolUserInfo. -func (s *StakeStore) StakePoolUserInfo(user dcrutil.Address) (*StakePoolUser, - error) { +func (s *StakeStore) StakePoolUserInfo(ns walletdb.ReadBucket, user dcrutil.Address) (*StakePoolUser, error) { s.mtx.Lock() defer s.mtx.Unlock() - return s.stakePoolUserInfo(user) + return s.stakePoolUserInfo(ns, user) } // loadManager returns a new stake manager that results from loading it from // the passed opened database. The public passphrase is required to decrypt the // public keys. -func (s *StakeStore) loadOwnedSStxs(namespace walletdb.Namespace) error { +func (s *StakeStore) loadOwnedSStxs(ns walletdb.ReadBucket) error { // Regenerate the list of tickets. // Perform all database lookups in a read-only view. ticketList := make(map[chainhash.Hash]struct{}) - err := namespace.View(func(tx walletdb.Tx) error { - var errForEach error - - // Open the sstx records database. - bucket := tx.RootBucket().Bucket(sstxRecordsBucketName) + // Open the sstx records database. + bucket := ns.NestedReadBucket(sstxRecordsBucketName) - // Store each key sequentially. - errForEach = bucket.ForEach(func(k []byte, v []byte) error { - var errNewHash error - var hash *chainhash.Hash + // Store each key sequentially. + err := bucket.ForEach(func(k []byte, v []byte) error { + var errNewHash error + var hash *chainhash.Hash - hash, errNewHash = chainhash.NewHash(k) - if errNewHash != nil { - return errNewHash - } - ticketList[*hash] = struct{}{} - return nil - }) - - return errForEach + hash, errNewHash = chainhash.NewHash(k) + if errNewHash != nil { + return errNewHash + } + ticketList[*hash] = struct{}{} + return nil }) if err != nil { return err @@ -1397,13 +1241,11 @@ func (s *StakeStore) SetChainSvr(chainSvr *walletchain.RPCClient) { } // newStakeStore initializes a new stake store with the given parameters. -func newStakeStore(namespace walletdb.Namespace, params *chaincfg.Params, - manager *waddrmgr.Manager) *StakeStore { +func newStakeStore(params *chaincfg.Params, manager *waddrmgr.Manager) *StakeStore { var mtx = &sync.Mutex{} return &StakeStore{ mtx: mtx, - namespace: namespace, Params: params, Manager: manager, chainSvr: nil, @@ -1412,27 +1254,30 @@ func newStakeStore(namespace walletdb.Namespace, params *chaincfg.Params, } } +// DoUpgrades performs any necessary upgrades to the stake manager 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 loads an existing stake manager from the given namespace, waddrmgr, and // network parameters. // // A ManagerError with an error code of ErrNoExist will be returned if the // passed manager does not exist in the specified namespace. -func Open(namespace walletdb.Namespace, manager *waddrmgr.Manager, - params *chaincfg.Params) (*StakeStore, error) { +func Open(ns walletdb.ReadBucket, manager *waddrmgr.Manager, params *chaincfg.Params) (*StakeStore, error) { // Return an error if the manager has NOT already been created in the // given database namespace. - exists, err := stakeStoreExists(namespace) - if err != nil { - return nil, err - } + exists := stakeStoreExists(ns) if !exists { str := "the specified stake store/manager does not exist in db" return nil, stakeStoreError(ErrNoExist, str, nil) } - ss := newStakeStore(namespace, params, manager) + ss := newStakeStore(params, manager) - err = ss.loadOwnedSStxs(namespace) + err := ss.loadOwnedSStxs(ns) if err != nil { return nil, err } @@ -1443,20 +1288,17 @@ func Open(namespace walletdb.Namespace, manager *waddrmgr.Manager, // Create creates a new persistent stake manager in the passed database namespace. // A ManagerError with an error code of ErrAlreadyExists will be returned the // address manager already exists in the specified namespace. -func Create(namespace walletdb.Namespace) error { +func Create(ns walletdb.ReadWriteBucket) error { // Return an error if the manager has already been created in the given // database namespace. - exists, err := stakeStoreExists(namespace) - if err != nil { - return err - } + exists := stakeStoreExists(ns) if exists { str := "error, stake store exists already" return stakeStoreError(ErrAlreadyExists, str, nil) } // Initialize the database for first use. - return initializeEmpty(namespace) + return initializeEmpty(ns) } // Close cleanly shuts down the stake store. diff --git a/wtxmgr/common.go b/wtxmgr/common.go index 84e1bd5d2..731ced5ec 100644 --- a/wtxmgr/common.go +++ b/wtxmgr/common.go @@ -75,23 +75,17 @@ func writeUnspentDebugDataToBuf(buf *bytes.Buffer, udd *unspentDebugData) { // DebugBucketUnspentString is the exported version of debugBuckedUnspentString // It returns string versions of unspent outpoints for debug ussage (with a flag // for including unmined txes or not). -func (s *Store) DebugBucketUnspentString(inclUnmined bool) (string, error) { - var str string - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - str, err = s.debugBucketUnspentString(ns, inclUnmined) - return err - }) - return str, err +func (s *Store) DebugBucketUnspentString(ns walletdb.ReadBucket, inclUnmined bool) (string, error) { + return s.debugBucketUnspentString(ns, inclUnmined) } -func (s *Store) debugBucketUnspentString(ns walletdb.Bucket, +func (s *Store) debugBucketUnspentString(ns walletdb.ReadBucket, inclUnmined bool) (string, error) { var unspent []*unspentDebugData 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 diff --git a/wtxmgr/db.go b/wtxmgr/db.go index b3f69a989..3dc356dc5 100644 --- a/wtxmgr/db.go +++ b/wtxmgr/db.go @@ -175,7 +175,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) (dcrutil.Amount, error) { +func fetchMinedBalance(ns walletdb.ReadBucket) (dcrutil.Amount, error) { v := ns.Get(rootMinedBalance) if len(v) != 8 { str := fmt.Sprintf("mined balance: short read (expected 8 bytes, "+ @@ -185,7 +185,7 @@ func fetchMinedBalance(ns walletdb.Bucket) (dcrutil.Amount, error) { return dcrutil.Amount(byteOrder.Uint64(v)), nil } -func putMinedBalance(ns walletdb.Bucket, amt dcrutil.Amount) error { +func putMinedBalance(ns walletdb.ReadWriteBucket, amt dcrutil.Amount) error { v := make([]byte, 8) byteOrder.PutUint64(v, uint64(amt)) err := ns.Put(rootMinedBalance, v) @@ -315,8 +315,8 @@ func removeRawBlockRecord(v []byte, txHash *chainhash.Hash) ([]byte, error) { return newValue, 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) @@ -324,16 +324,16 @@ func putRawBlockRecord(ns walletdb.Bucket, k, v []byte) error { return nil } -func putBlockRecord(ns walletdb.Bucket, block *BlockMeta, +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) < 46 { str := fmt.Sprintf("%s: short read for fetchBlockTime (expected "+ "%d bytes, read %d)", bucketBlocks, 46, len(v)) @@ -342,16 +342,16 @@ func fetchBlockTime(ns walletdb.Bucket, height int32) (time.Time, error) { return time.Unix(int64(byteOrder.Uint64(v[32:40])), 0), nil } -func fetchBlockRecord(ns walletdb.Bucket, height int32) (*blockRecord, error) { +func fetchBlockRecord(ns walletdb.ReadBucket, height int32) (*blockRecord, error) { br := &blockRecord{} k := keyBlockRecord(height) - v := ns.Bucket(bucketBlocks).Get(k) + v := ns.NestedReadBucket(bucketBlocks).Get(k) err := readRawBlockRecord(k, v, br) return br, err } -func fetchChainHeight(ns walletdb.Bucket, startHeight int32) (int32, error) { +func fetchChainHeight(ns walletdb.ReadBucket, startHeight int32) (int32, error) { lastValidHeight := int32(0) for i := startHeight; ; i++ { _, v := existsBlockRecord(ns, i) @@ -369,9 +369,9 @@ func fetchChainHeight(ns walletdb.Bucket, startHeight int32) (int32, error) { return lastValidHeight, 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 } @@ -410,7 +410,7 @@ func readRawBlockRecord(k, v []byte, block *blockRecord) error { } type blockIterator struct { - c walletdb.Cursor + c walletdb.ReadWriteCursor seek []byte ck []byte cv []byte @@ -418,22 +418,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 @@ -505,9 +519,9 @@ func (it *blockIterator) delete() error { return nil } -func deleteBlockRecord(ns walletdb.Bucket, height int32) error { +func deleteBlockRecord(ns walletdb.ReadWriteBucket, height int32) error { k := keyBlockRecord(height) - return ns.Bucket(bucketBlocks).Delete(k) + return ns.NestedReadWriteBucket(bucketBlocks).Delete(k) } // Transaction records are keyed as such: @@ -552,13 +566,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) @@ -566,8 +580,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) @@ -661,36 +675,36 @@ func fetchRawTxRecordReceived(v []byte) time.Time { return time.Unix(int64(byteOrder.Uint64(v)), 0) } -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) return rec, err } -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) { @@ -782,8 +796,8 @@ func valueUnspentCredit(cred *credit, scrType scriptType, scrLoc uint32, 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) @@ -794,7 +808,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, scrType scriptType, +func putUnspentCredit(ns walletdb.ReadWriteBucket, cred *credit, scrType scriptType, scrLoc uint32, scrLen uint32, account uint32) error { k := keyCredit(&cred.outPoint.Hash, cred.outPoint.Index, &cred.block) v := valueUnspentCredit(cred, scrType, scrLoc, scrLen, account) @@ -926,8 +940,8 @@ func fetchRawCreditAccount(v []byte) (uint32, 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) (dcrutil.Amount, error) { - v := ns.Bucket(bucketCredits).Get(k) +func spendCredit(ns walletdb.ReadWriteBucket, k []byte, spender *indexedIncidence) (dcrutil.Amount, error) { + v := ns.NestedReadWriteBucket(bucketCredits).Get(k) newv := make([]byte, creditValueSize) copy(newv, v) v = newv @@ -943,8 +957,8 @@ func spendCredit(ns walletdb.Bucket, k []byte, spender *indexedIncidence) (dcrut // 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) (dcrutil.Amount, error) { - b := ns.Bucket(bucketCredits) +func unspendRawCredit(ns walletdb.ReadWriteBucket, k []byte) (dcrutil.Amount, error) { + b := ns.NestedReadWriteBucket(bucketCredits) v := b.Get(k) if v == nil { return 0, nil @@ -961,18 +975,18 @@ func unspendRawCredit(ns walletdb.Bucket, k []byte) (dcrutil.Amount, error) { return dcrutil.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) @@ -1001,7 +1015,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 @@ -1009,11 +1023,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 for credit iterator key "+ @@ -1079,10 +1098,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) @@ -1090,8 +1109,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) @@ -1112,7 +1131,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 @@ -1120,11 +1139,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 } @@ -1135,8 +1154,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) @@ -1172,14 +1191,14 @@ func keyDebit(txHash *chainhash.Hash, index uint32, block *Block) []byte { return k } -func putDebit(ns walletdb.Bucket, txHash *chainhash.Hash, index uint32, amount dcrutil.Amount, block *Block, credKey []byte) error { +func putDebit(ns walletdb.ReadWriteBucket, txHash *chainhash.Hash, index uint32, amount dcrutil.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) @@ -1195,10 +1214,10 @@ 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, +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 } @@ -1210,8 +1229,8 @@ func existsDebit(ns walletdb.Bucket, txHash *chainhash.Hash, index uint32, 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) @@ -1234,7 +1253,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 @@ -1242,11 +1261,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 for debit iterator key "+ @@ -1292,8 +1316,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) @@ -1310,12 +1334,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) @@ -1383,8 +1407,8 @@ func valueUnminedCredit(amount dcrutil.Amount, change bool, opCode uint8, 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) @@ -1462,12 +1486,12 @@ func fetchRawUnminedCreditAccount(v []byte) (uint32, error) { return byteOrder.Uint32(v[18:22]), 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) @@ -1495,7 +1519,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 @@ -1503,11 +1527,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 { @@ -1571,8 +1609,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) @@ -1580,12 +1618,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) @@ -1599,8 +1637,8 @@ func keyTxScript(script []byte) []byte { return dcrutil.Hash160(script) } -func deleteRawTxScript(ns walletdb.Bucket, hash []byte) error { - err := ns.Bucket(bucketScripts).Delete(hash) +func deleteRawTxScript(ns walletdb.ReadWriteBucket, hash []byte) error { + err := ns.NestedReadWriteBucket(bucketScripts).Delete(hash) if err != nil { str := "failed to delete tx script" return storeError(ErrDatabase, str, err) @@ -1608,9 +1646,9 @@ func deleteRawTxScript(ns walletdb.Bucket, hash []byte) error { return nil } -func putTxScript(ns walletdb.Bucket, script []byte) error { +func putTxScript(ns walletdb.ReadWriteBucket, script []byte) error { k := keyTxScript(script) - err := ns.Bucket(bucketScripts).Put(k, script) + err := ns.NestedReadWriteBucket(bucketScripts).Put(k, script) if err != nil { str := "failed to put tx script" return storeError(ErrDatabase, str, err) @@ -1618,8 +1656,8 @@ func putTxScript(ns walletdb.Bucket, script []byte) error { return nil } -func existsTxScript(ns walletdb.Bucket, hash []byte) []byte { - vOrig := ns.Bucket(bucketScripts).Get(hash) +func existsTxScript(ns walletdb.ReadBucket, hash []byte) []byte { + vOrig := ns.NestedReadBucket(bucketScripts).Get(hash) if vOrig == nil { return nil } @@ -1804,8 +1842,8 @@ func setMultisigOutUnmined(v []byte) { byteOrder.PutUint32(v[55:59], 0) } -func deleteMultisigOut(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketMultisig).Delete(k) +func deleteMultisigOut(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketMultisig).Delete(k) if err != nil { str := "failed to delete multisig output" return storeError(ErrDatabase, str, err) @@ -1813,7 +1851,7 @@ func deleteMultisigOut(ns walletdb.Bucket, k []byte) error { return nil } -func putMultisigOut(ns walletdb.Bucket, mso *MultisigOut) error { +func putMultisigOut(ns walletdb.ReadWriteBucket, mso *MultisigOut) error { msok := keyMultisigOut(mso.OutPoint.Hash, mso.OutPoint.Index) msov := valueMultisigOut(mso.ScriptHash, @@ -1827,7 +1865,7 @@ func putMultisigOut(ns walletdb.Bucket, mso *MultisigOut) error { mso.SpentBy, mso.SpentByIndex, mso.TxHash) - err := ns.Bucket(bucketMultisig).Put(msok, msov) + err := ns.NestedReadWriteBucket(bucketMultisig).Put(msok, msov) if err != nil { str := "failed to put multisig output" return storeError(ErrDatabase, str, err) @@ -1835,8 +1873,8 @@ func putMultisigOut(ns walletdb.Bucket, mso *MultisigOut) error { return nil } -func putMultisigOutRawValues(ns walletdb.Bucket, k []byte, v []byte) error { - err := ns.Bucket(bucketMultisig).Put(k, v) +func putMultisigOutRawValues(ns walletdb.ReadWriteBucket, k []byte, v []byte) error { + err := ns.NestedReadWriteBucket(bucketMultisig).Put(k, v) if err != nil { str := "failed to put multisig output" return storeError(ErrDatabase, str, err) @@ -1844,8 +1882,8 @@ func putMultisigOutRawValues(ns walletdb.Bucket, k []byte, v []byte) error { return nil } -func existsMultisigOut(ns walletdb.Bucket, k []byte) []byte { - vOrig := ns.Bucket(bucketMultisig).Get(k) +func existsMultisigOut(ns walletdb.ReadBucket, k []byte) []byte { + vOrig := ns.NestedReadBucket(bucketMultisig).Get(k) if vOrig == nil { return nil } @@ -1860,9 +1898,9 @@ func keyMultisigOutUS(hash chainhash.Hash, index uint32) []byte { return canonicalOutPoint(&hash, index) } -func putMultisigOutUS(ns walletdb.Bucket, k []byte) error { +func putMultisigOutUS(ns walletdb.ReadWriteBucket, k []byte) error { blank := []byte{0x00} - err := ns.Bucket(bucketMultisigUsp).Put(k, blank) + err := ns.NestedReadWriteBucket(bucketMultisigUsp).Put(k, blank) if err != nil { str := "failed to put unspent multisig output" return storeError(ErrDatabase, str, err) @@ -1870,11 +1908,11 @@ func putMultisigOutUS(ns walletdb.Bucket, k []byte) error { return nil } -func putMultisigOutOutpointUS(ns walletdb.Bucket, op *wire.OutPoint) error { +func putMultisigOutOutpointUS(ns walletdb.ReadWriteBucket, op *wire.OutPoint) error { msok := keyMultisigOut(op.Hash, op.Index) blank := []byte{0x00} - err := ns.Bucket(bucketMultisigUsp).Put(msok, blank) + err := ns.NestedReadWriteBucket(bucketMultisigUsp).Put(msok, blank) if err != nil { str := "failed to put unspent multisig output" return storeError(ErrDatabase, str, err) @@ -1882,8 +1920,8 @@ func putMultisigOutOutpointUS(ns walletdb.Bucket, op *wire.OutPoint) error { return nil } -func deleteMultisigOutUS(ns walletdb.Bucket, k []byte) error { - err := ns.Bucket(bucketMultisigUsp).Delete(k) +func deleteMultisigOutUS(ns walletdb.ReadWriteBucket, k []byte) error { + err := ns.NestedReadWriteBucket(bucketMultisigUsp).Delete(k) if err != nil { str := "failed to delete multisig output" return storeError(ErrDatabase, str, err) @@ -1891,44 +1929,29 @@ func deleteMultisigOutUS(ns walletdb.Bucket, k []byte) error { return nil } -func existsMultisigOutUS(ns walletdb.Bucket, k []byte) bool { - v := ns.Bucket(bucketMultisigUsp).Get(k) +func existsMultisigOutUS(ns walletdb.ReadBucket, k []byte) bool { + v := ns.NestedReadBucket(bucketMultisigUsp).Get(k) return v != 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) } @@ -1938,137 +1961,124 @@ 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) - } - - // 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) - } +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) + } - // 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) - } + // 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) + } - _, err = ns.CreateBucket(bucketBlocks) - if err != nil { - str := "failed to create blocks bucket" - 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(bucketTxRecords) - if err != nil { - str := "failed to create tx records bucket" - 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(bucketCredits) - if err != nil { - str := "failed to create credits 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(bucketDebits) - if err != nil { - str := "failed to create debits 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(bucketUnspent) - if err != nil { - str := "failed to create unspent 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(bucketUnmined) - if err != nil { - str := "failed to create unmined 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(bucketUnminedCredits) - if err != nil { - str := "failed to create unmined credits 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(bucketUnminedInputs) - if err != nil { - str := "failed to create unmined inputs 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(bucketScripts) - if err != nil { - str := "failed to create scripts 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) + } - _, err = ns.CreateBucket(bucketMultisig) - if err != nil { - str := "failed to create multisig tx bucket" - return storeError(ErrDatabase, str, err) - } + _, err = ns.CreateBucket(bucketScripts) + if err != nil { + str := "failed to create scripts bucket" + return storeError(ErrDatabase, str, err) + } - _, err = ns.CreateBucket(bucketMultisigUsp) - if err != nil { - str := "failed to create multisig unspent tx bucket" - return storeError(ErrDatabase, str, err) - } + _, err = ns.CreateBucket(bucketMultisig) + if err != nil { + str := "failed to create multisig tx bucket" + return storeError(ErrDatabase, str, err) + } - return nil - }) + _, err = ns.CreateBucket(bucketMultisigUsp) 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 create multisig unspent tx 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 @@ -2083,20 +2093,20 @@ func scopedUpdate(ns walletdb.Namespace, f func(walletdb.Bucket) error) error { return nil } -func scopedView(ns walletdb.Namespace, f func(walletdb.Bucket) error) error { - tx, err := ns.Begin(false) +func scopedView(db walletdb.DB, namespaceKey []byte, f func(walletdb.ReadBucket) error) error { + tx, err := db.BeginReadTx() if err != nil { str := "cannot begin view" return storeError(ErrDatabase, str, err) } - err = f(tx.RootBucket()) - rbErr := tx.Rollback() + err = f(tx.ReadBucket(namespaceKey)) + rollbackErr := tx.Rollback() if err != nil { return err } - if rbErr != nil { + if rollbackErr != nil { str := "cannot close view" - return storeError(ErrDatabase, str, rbErr) + return storeError(ErrDatabase, str, rollbackErr) } return nil } diff --git a/wtxmgr/error.go b/wtxmgr/error.go index 8f13031c4..4da7ab214 100644 --- a/wtxmgr/error.go +++ b/wtxmgr/error.go @@ -48,6 +48,10 @@ const ( // twice. ErrDoubleSpend + // 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. diff --git a/wtxmgr/query.go b/wtxmgr/query.go index 2ab139f7b..56bd3b7e8 100644 --- a/wtxmgr/query.go +++ b/wtxmgr/query.go @@ -51,7 +51,7 @@ func (t *TxDetails) Height() int32 { // 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 @@ -69,7 +69,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" @@ -89,7 +89,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" @@ -103,7 +103,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}}, } @@ -112,7 +112,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" @@ -175,30 +175,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 - - // 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 - } +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) + } - // 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) } // parseTx deserializes a transaction into a MsgTx using the readRawTxRecord @@ -221,59 +213,41 @@ func (s *Store) parseTx(txHash chainhash.Hash, v []byte) (*wire.MsgTx, error) { // // Not finding a transaction with this hash is not an error. In this case, // a nil TxDetails is returned. -func (s *Store) Tx(txHash *chainhash.Hash) (*wire.MsgTx, error) { - var msgTx *wire.MsgTx - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - - // First, check whether there exists an unmined transaction with this - // hash. Use it if found. - v := existsRawUnmined(ns, txHash[:]) - if v != nil { - msgTx, err = s.parseTx(*txHash, v) - return err - } +func (s *Store) Tx(ns walletdb.ReadBucket, txHash *chainhash.Hash) (*wire.MsgTx, 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.parseTx(*txHash, v) + } - // Otherwise, if there exists a mined transaction with this matching - // hash, skip over to the newest and begin fetching the msgTx. - _, v = latestTxRecord(ns, txHash) - if v == nil { - // not found - return nil - } - msgTx, err = s.parseTx(*txHash, v) - return err - }) - return msgTx, err + // Otherwise, if there exists a mined transaction with this matching + // hash, skip over to the newest and begin fetching the msgTx. + _, v = latestTxRecord(ns, txHash) + if v == nil { + // not found + return nil, nil + } + return s.parseTx(*txHash, v) } // ExistsTx checks to see if a transaction exists in the database. -func (s *Store) ExistsTx(txHash *chainhash.Hash) (bool, error) { - exists := false - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - // First, check whether there exists an unmined transaction with this - // hash. Use it if found. - v := existsRawUnmined(ns, txHash[:]) - if v != nil { - exists = true - return nil - } - - // Otherwise, if there exists a mined transaction with this matching - // hash, skip over to the newest and begin fetching the msgTx. - _, v = latestTxRecord(ns, txHash) - if v != nil { - exists = true - return nil - } +func (s *Store) ExistsTx(ns walletdb.ReadBucket, txHash *chainhash.Hash) bool { + // First, check whether there exists an unmined transaction with this + // hash. Use it if found. + v := existsRawUnmined(ns, txHash[:]) + if v != nil { + return true + } - return nil - }) - if err != nil { - return false, err + // Otherwise, if there exists a mined transaction with this matching + // hash, skip over to the newest and begin fetching the msgTx. + _, v = latestTxRecord(ns, txHash) + if v != nil { + return true } - return exists, nil + return false } // UniqueTxDetails looks up all recorded details for a transaction recorded @@ -281,27 +255,22 @@ func (s *Store) ExistsTx(txHash *chainhash.Hash) (bool, 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 @@ -309,9 +278,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)) @@ -341,7 +310,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) @@ -354,7 +325,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 @@ -363,7 +334,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 @@ -401,7 +372,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" @@ -422,7 +393,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" @@ -467,121 +438,122 @@ 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. - - 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) - vUC := existsRawUnminedCredit(ns, k) - if vUC == nil { - continue - } - - // If we encounter an error here, it likely means - // we have a legacy outpoint. Ignore the error and - // just let the scrPos be 0, which will trigger - // whole transaction deserialization to retrieve - // the script. - scrPos := fetchRawUnminedCreditScriptOffset(vUC) - scrLen := fetchRawUnminedCreditScriptLength(vUC) - - pkScript, err := fetchRawTxRecordPkScript( - prevOut.Hash[:], v, prevOut.Index, scrPos, scrLen) - if err != nil { - return err - } - pkScripts = append(pkScripts, pkScript) + + 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. + + 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) + vUC := existsRawUnminedCredit(ns, k) + if vUC == nil { continue } - _, credKey := existsUnspent(ns, prevOut) - if credKey != nil { - credVal := existsRawCredit(ns, credKey) - if credVal == nil { - return storeError(ErrDatabase, fmt.Sprintf("credit "+ - "val for %x missing", credVal), - fmt.Errorf("no exists")) - } - - // Legacy outputs in the credit bucket may be of the - // wrong size. - scrPos := fetchRawCreditScriptOffset(credVal) - scrLen := fetchRawCreditScriptLength(credVal) - - k := extractRawCreditTxRecordKey(credKey) - v = existsRawTxRecord(ns, k) - pkScript, err := fetchRawTxRecordPkScript(k, v, - prevOut.Index, scrPos, scrLen) - if err != nil { - return err - } - pkScripts = append(pkScripts, pkScript) + // If we encounter an error here, it likely means + // we have a legacy outpoint. Ignore the error and + // just let the scrPos be 0, which will trigger + // whole transaction deserialization to retrieve + // the script. + scrPos := fetchRawUnminedCreditScriptOffset(vUC) + scrLen := fetchRawUnminedCreditScriptLength(vUC) + + pkScript, err := fetchRawTxRecordPkScript( + prevOut.Hash[:], v, prevOut.Index, scrPos, scrLen) + 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) - - credVal := existsRawCredit(ns, credKey) - if credVal == nil { - return storeError(ErrDatabase, fmt.Sprintf("credit "+ - "val for %x missing in debit itr", credVal), - fmt.Errorf("no exists")) + _, credKey := existsUnspent(ns, prevOut) + if credKey != nil { + credVal := existsRawCredit(ns, credKey) + if credVal == nil { + return nil, storeError(ErrDatabase, fmt.Sprintf("credit "+ + "val for %x missing", credVal), + fmt.Errorf("no exists")) + } + + // Legacy outputs in the credit bucket may be of the + // wrong size. + scrPos := fetchRawCreditScriptOffset(credVal) + scrLen := fetchRawCreditScriptLength(credVal) + + k := extractRawCreditTxRecordKey(credKey) + v = existsRawTxRecord(ns, k) + pkScript, err := fetchRawTxRecordPkScript(k, v, + prevOut.Index, scrPos, scrLen) + if err != nil { + return nil, err + } + pkScripts = append(pkScripts, pkScript) } + } + } + + recKey := keyTxRecord(&rec.Hash, block) + it := makeReadDebitIterator(ns, recKey) + for it.next() { + credKey := extractRawDebitCreditKey(it.cv) + index := extractRawCreditIndex(credKey) + + credVal := existsRawCredit(ns, credKey) + if credVal == nil { + return nil, storeError(ErrDatabase, fmt.Sprintf("credit "+ + "val for %x missing in debit itr", credVal), + fmt.Errorf("no exists")) + } - // Legacy credit output values may be of the wrong - // size. - scrPos := fetchRawCreditScriptOffset(credVal) - scrLen := fetchRawCreditScriptLength(credVal) + // Legacy credit output values may be of the wrong + // size. + scrPos := fetchRawCreditScriptOffset(credVal) + scrLen := fetchRawCreditScriptLength(credVal) - k := extractRawCreditTxRecordKey(credKey) - v := existsRawTxRecord(ns, k) - pkScript, err := fetchRawTxRecordPkScript(k, v, index, - scrPos, scrLen) - if err != nil { - return err - } - pkScripts = append(pkScripts, pkScript) + k := extractRawCreditTxRecordKey(credKey) + v := existsRawTxRecord(ns, k) + pkScript, err := fetchRawTxRecordPkScript(k, v, index, + scrPos, scrLen) + if err != nil { + return nil, err } - return it.err - }) - return pkScripts, err + pkScripts = append(pkScripts, pkScript) + } + if it.err != nil { + return nil, it.err + } + + return pkScripts, nil } diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go index fa5bf1da7..2bd74d061 100644 --- a/wtxmgr/tx.go +++ b/wtxmgr/tx.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "sort" - "sync" "time" "github.com/btcsuite/golangcrypto/ripemd160" @@ -186,11 +185,12 @@ func (p SortableTxRecords) Less(i, j int) bool { } func (p SortableTxRecords) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -// pruneOldTickets prunes old stake tickets from before ticketCutoff from the -// database. Maybe corrupt the database if the user Ctrl+C's during this short -// function. -func (s *Store) pruneOldTickets(ns walletdb.Bucket, - ticketCutoff time.Duration) error { +// PruneOldTickets prunes old stake tickets from before ticketCutoff from the +// database. +func (s *Store) PruneOldTickets(ns walletdb.ReadWriteBucket) error { + ticketCutoff := s.chainParams.TimePerBlock * + time.Duration(s.chainParams.WorkDiffWindowSize) + current := time.Now() log.Infof("Pruning old tickets from before from the transaction " + "database, please do not attempt to close your wallet.") @@ -203,7 +203,7 @@ func (s *Store) pruneOldTickets(ns walletdb.Bucket, // Cache for records, so we can sort them. var savedSStxs SortableTxRecords - err = ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error { + err = ns.NestedReadWriteBucket(bucketUnmined).ForEach(func(k, v []byte) error { // TODO: Parsing transactions from the db may be a little // expensive. It's possible the caller only wants the // serialized transactions. @@ -256,7 +256,7 @@ func (s *Store) pruneOldTickets(ns walletdb.Bucket, var valCredit []byte // Look up and see if it spends a mined credit. spendsMinedCredit := true - err := ns.Bucket(bucketCredits).ForEach( + err := ns.NestedReadWriteBucket(bucketCredits).ForEach( func(lkc, lvc []byte) error { lcHash := extractRawCreditTxHash(lkc) lcIndex := extractRawCreditIndex(lkc) @@ -364,82 +364,50 @@ func (s *Store) pruneOldTickets(ns walletdb.Bucket, // Store implements a transaction store for storing and managing wallet // transactions. type Store struct { - mutex *sync.Mutex - isClosed bool - - namespace walletdb.Namespace chainParams *chaincfg.Params - acctLookupFunc func(dcrutil.Address) (uint32, error) + acctLookupFunc func(walletdb.ReadBucket, dcrutil.Address) (uint32, error) // Event callbacks. These execute in the same goroutine as the wtxmgr // caller. 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, pruneTickets bool, - chainParams *chaincfg.Params, - acctLookupFunc func(dcrutil.Address) (uint32, error)) (*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, + acctLookupFunc func(walletdb.ReadBucket, dcrutil.Address) (uint32, error)) (*Store, error) { + + // Open the store. + err := openStore(ns) if err != nil { return nil, err } - s := &Store{new(sync.Mutex), false, namespace, chainParams, acctLookupFunc, nil} // TODO: set callbacks - - // Skip pruning on simnet, because the adjustment times are - // so short. - if pruneTickets && chainParams.Name != "simnet" { - ticketCutoff := chainParams.TimePerBlock * - time.Duration(chainParams.WorkDiffWindowSize) - err = scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - var err error - err = s.pruneOldTickets(ns, ticketCutoff) - return err - }) - if err != nil { - return nil, err - } - } - + s := &Store{chainParams, acctLookupFunc, 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) -} - -// Close safely closes the transaction manager by waiting for the mutex to -// unlock and then preventing any new calls to the transaction manager. -func (s *Store) Close() { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.isClosed = true +func Create(ns walletdb.ReadWriteBucket) error { + return createStore(ns) } // InsertBlock inserts a block into the block database if it doesn't already // exist. -func (s *Store) InsertBlock(bm *BlockMeta) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } - - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - return s.insertBlock(ns, bm) - }) +func (s *Store) InsertBlock(ns walletdb.ReadWriteBucket, bm *BlockMeta) error { + return s.insertBlock(ns, bm) } -func (s *Store) insertBlock(ns walletdb.Bucket, bm *BlockMeta) error { +func (s *Store) insertBlock(ns walletdb.ReadWriteBucket, bm *BlockMeta) error { // Check to see if the block already exists; nothing to add in this case. blockKey, blockVal := existsBlockRecord(ns, bm.Height) if blockVal != nil { @@ -453,26 +421,11 @@ func (s *Store) insertBlock(ns walletdb.Bucket, bm *BlockMeta) error { // GetBlockHash fetches the block hash for the block at the given height, // and returns an error if it's missing. -func (s *Store) GetBlockHash(height int32) (chainhash.Hash, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return chainhash.Hash{}, storeError(ErrIsClosed, str, nil) - } - - var blockHash chainhash.Hash - var err error - err = scopedView(s.namespace, func(ns walletdb.Bucket) error { - blockHash, err = s.getBlockHash(ns, height) - return err - }) - - return blockHash, err +func (s *Store) GetBlockHash(ns walletdb.ReadBucket, height int32) (chainhash.Hash, error) { + return s.getBlockHash(ns, height) } -func (s *Store) getBlockHash(ns walletdb.Bucket, height int32) (chainhash.Hash, +func (s *Store) getBlockHash(ns walletdb.ReadBucket, height int32) (chainhash.Hash, error) { br, err := fetchBlockRecord(ns, height) if err != nil { @@ -484,86 +437,72 @@ func (s *Store) getBlockHash(ns walletdb.Bucket, height int32) (chainhash.Hash, // PruneUnconfirmed prunes old stake tickets that are below the current stake // difficulty or any unconfirmed transaction which is expired. -func (s *Store) PruneUnconfirmed(height int32, stakeDiff int64) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } - +func (s *Store) PruneUnconfirmed(ns walletdb.ReadWriteBucket, height int32, stakeDiff int64) error { var unconfTxRs []*TxRecord var uTxRstoRemove []*TxRecord // Open an update transaction in the database and // remove all of the tagged transactions. - err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - errDb := ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error { - // TODO: Parsing transactions from the db may be a little - // expensive. It's possible the caller only wants the - // serialized transactions. - var txHash chainhash.Hash - errLocal := readRawUnminedHash(k, &txHash) - if errLocal != nil { - return errLocal - } - - var rec TxRecord - errLocal = readRawTxRecord(&txHash, v, &rec) - if errLocal != nil { - return errLocal - } - - unconfTxRs = append(unconfTxRs, &rec) + errDb := ns.NestedReadWriteBucket(bucketUnmined).ForEach(func(k, v []byte) error { + // TODO: Parsing transactions from the db may be a little + // expensive. It's possible the caller only wants the + // serialized transactions. + var txHash chainhash.Hash + errLocal := readRawUnminedHash(k, &txHash) + if errLocal != nil { + return errLocal + } - return nil - }) - if errDb != nil { - return errDb + var rec TxRecord + errLocal = readRawTxRecord(&txHash, v, &rec) + if errLocal != nil { + return errLocal } - for _, uTxR := range unconfTxRs { - // Tag all transactions that are expired. - if uTxR.MsgTx.Expiry <= uint32(height) && - uTxR.MsgTx.Expiry != wire.NoExpiryValue { - log.Debugf("Tagging expired tx %v for removal (expiry %v, "+ - "height %v)", uTxR.Hash, uTxR.MsgTx.Expiry, height) - uTxRstoRemove = append(uTxRstoRemove, uTxR) - } + unconfTxRs = append(unconfTxRs, &rec) - // Tag all stake tickets which are below - // network difficulty. - if uTxR.TxType == stake.TxTypeSStx { - if uTxR.MsgTx.TxOut[0].Value < stakeDiff { - log.Debugf("Tagging low diff ticket %v for removal "+ - "(stake %v, target %v)", uTxR.Hash, - uTxR.MsgTx.TxOut[0].Value, stakeDiff) - uTxRstoRemove = append(uTxRstoRemove, uTxR) - } + return nil + }) + if errDb != nil { + return errDb + } + + for _, uTxR := range unconfTxRs { + // Tag all transactions that are expired. + if uTxR.MsgTx.Expiry <= uint32(height) && + uTxR.MsgTx.Expiry != wire.NoExpiryValue { + log.Debugf("Tagging expired tx %v for removal (expiry %v, "+ + "height %v)", uTxR.Hash, uTxR.MsgTx.Expiry, height) + uTxRstoRemove = append(uTxRstoRemove, uTxR) + } + + // Tag all stake tickets which are below + // network difficulty. + if uTxR.TxType == stake.TxTypeSStx { + if uTxR.MsgTx.TxOut[0].Value < stakeDiff { + log.Debugf("Tagging low diff ticket %v for removal "+ + "(stake %v, target %v)", uTxR.Hash, + uTxR.MsgTx.TxOut[0].Value, stakeDiff) + uTxRstoRemove = append(uTxRstoRemove, uTxR) } } + } - for _, uTxR := range uTxRstoRemove { - errLocal := s.removeUnconfirmed(ns, uTxR) - if errLocal != nil { - return errLocal - } + for _, uTxR := range uTxRstoRemove { + errLocal := s.removeUnconfirmed(ns, uTxR) + if errLocal != nil { + return errLocal } - return nil - }) - if err != nil { - return err } - return nil } // fetchAccountForPkScript fetches an account for a given pkScript given a // credit value, the script, and an account lookup function. It does this // to maintain compatibility with older versions of the database. -func (s *Store) fetchAccountForPkScript(credVal []byte, unminedCredVal []byte, - pkScript []byte) (uint32, error) { +func (s *Store) fetchAccountForPkScript(addrmgrNs walletdb.ReadBucket, + credVal []byte, unminedCredVal []byte, pkScript []byte) (uint32, error) { + // Attempt to get the account from the mined credit. If the // account was never stored, we can ignore the error and // fall through to do the lookup with the acctLookupFunc. @@ -613,7 +552,7 @@ func (s *Store) fetchAccountForPkScript(credVal []byte, unminedCredVal []byte, // Only look at the first address returned. This does not // handle multisignature or other custom pkScripts in the // correct way, which requires multiple account tracking. - acct, err := s.acctLookupFunc(addrs[0]) + acct, err := s.acctLookupFunc(addrmgrNs, addrs[0]) if err != nil { return 0, err } @@ -623,7 +562,7 @@ func (s *Store) fetchAccountForPkScript(credVal []byte, unminedCredVal []byte, // moveMinedTx moves a transaction record from the unmined buckets to block // buckets. -func (s *Store) moveMinedTx(ns walletdb.Bucket, rec *TxRecord, recKey, +func (s *Store) moveMinedTx(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, rec *TxRecord, recKey, recVal []byte, block *BlockMeta) error { log.Debugf("Marking unconfirmed transaction %v mined in block %d", &rec.Hash, block.Height) @@ -741,7 +680,7 @@ func (s *Store) moveMinedTx(ns walletdb.Bucket, rec *TxRecord, recKey, return err } - acct, err := s.fetchAccountForPkScript(nil, it.cv, pkScript) + acct, err := s.fetchAccountForPkScript(addrmgrNs, nil, it.cv, pkScript) if err != nil { return err } @@ -779,28 +718,18 @@ func (s *Store) moveMinedTx(ns walletdb.Bucket, rec *TxRecord, recKey, // 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 { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) +func (s *Store) InsertTx(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, rec *TxRecord, block *BlockMeta) error { + if block == nil { + return s.insertMemPoolTx(ns, rec) } - - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - if block == nil { - return s.insertMemPoolTx(ns, rec) - } - return s.insertMinedTx(ns, rec, block) - }) + return s.insertMinedTx(ns, addrmgrNs, 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, +func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, rec *TxRecord, block *BlockMeta) error { // Fetch the mined balance in case we need to update it. minedBalance, err := fetchMinedBalance(ns) @@ -881,7 +810,7 @@ func (s *Store) insertMinedTx(ns walletdb.Bucket, rec *TxRecord, // unconfirmed, move it to a block. v = existsRawUnmined(ns, rec.Hash[:]) if v != nil { - return s.moveMinedTx(ns, rec, k, v, block) + return s.moveMinedTx(ns, addrmgrNs, rec, k, v, block) } // As there may be unconfirmed transactions that are invalidated by this @@ -926,27 +855,15 @@ func (s *Store) insertMinedTx(ns walletdb.Bucket, rec *TxRecord, // 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, account uint32) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } +func (s *Store) AddCredit(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, + index uint32, change bool, account uint32) 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, account) - return err - }) + isNew, err := s.addCredit(ns, rec, block, index, change, account) if err == nil && isNew && s.NotifyUnspent != nil { // This causes a lockup because wtxmgr is non-reentrant. // TODO: move this call outside of wtxmgr and do not use @@ -1011,7 +928,7 @@ func pkScriptType(pkScript []byte) scriptType { return scriptTypeUnspecified } -func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, +func (s *Store) addCredit(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32, change bool, account uint32) (bool, error) { opCode := getP2PKHOpCode(rec.MsgTx.TxOut[index].PkScript) isCoinbase := blockchain.IsCoinBaseTx(&rec.MsgTx) @@ -1085,27 +1002,18 @@ func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, // was not mined, the output is updated so its value reflects the block // it was included in. // -func (s *Store) AddMultisigOut(rec *TxRecord, block *BlockMeta, +func (s *Store) AddMultisigOut(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } if int(index) >= len(rec.MsgTx.TxOut) { str := "transaction output does not exist" return storeError(ErrInput, str, nil) } - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - return s.addMultisigOut(ns, rec, block, index) - }) + return s.addMultisigOut(ns, rec, block, index) } -func (s *Store) addMultisigOut(ns walletdb.Bucket, rec *TxRecord, +func (s *Store) addMultisigOut(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32) error { empty := &chainhash.Hash{} @@ -1201,22 +1109,13 @@ func (s *Store) addMultisigOut(ns walletdb.Bucket, rec *TxRecord, // SpendMultisigOut spends a multisignature output by making it spent in // the general bucket and removing it from the unspent bucket. -func (s *Store) SpendMultisigOut(op *wire.OutPoint, spendHash chainhash.Hash, - spendIndex uint32) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } +func (s *Store) SpendMultisigOut(ns walletdb.ReadWriteBucket, op *wire.OutPoint, + spendHash chainhash.Hash, spendIndex uint32) error { - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - return s.spendMultisigOut(ns, op, spendHash, spendIndex) - }) + return s.spendMultisigOut(ns, op, spendHash, spendIndex) } -func (s *Store) spendMultisigOut(ns walletdb.Bucket, op *wire.OutPoint, +func (s *Store) spendMultisigOut(ns walletdb.ReadWriteBucket, op *wire.OutPoint, spendHash chainhash.Hash, spendIndex uint32) error { // Mark the output spent. key := keyMultisigOut(op.Hash, op.Index) @@ -1262,25 +1161,15 @@ func (s *Store) spendMultisigOut(ns walletdb.Bucket, op *wire.OutPoint, // Rollback removes all blocks at height onwards, moving any transactions within // each block to the unconfirmed pool. -func (s *Store) Rollback(height int32) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } - - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - return s.rollback(ns, height) - }) +func (s *Store) Rollback(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, height int32) error { + return s.rollback(ns, addrmgrNs, height) } // rollbackTransaction removes a transaction that was previously contained // in a block during reorganization handling. func (s *Store) rollbackTransaction(hash chainhash.Hash, b *blockRecord, coinBaseCredits *[]wire.OutPoint, minedBalance *dcrutil.Amount, - ns walletdb.Bucket, isParent bool) error { + ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, isParent bool) error { txHash := &hash recKey := keyTxRecord(txHash, &b.Block) @@ -1491,7 +1380,7 @@ func (s *Store) rollbackTransaction(hash chainhash.Hash, b *blockRecord, scrLoc := rec.MsgTx.PkScriptLocs()[i] scrLen := len(rec.MsgTx.TxOut[i].PkScript) - acct, err := s.fetchAccountForPkScript(v, nil, output.PkScript) + acct, err := s.fetchAccountForPkScript(addrmgrNs, v, nil, output.PkScript) if err != nil { return err } @@ -1541,7 +1430,7 @@ func (s *Store) rollbackTransaction(hash chainhash.Hash, b *blockRecord, return nil } -func (s *Store) rollback(ns walletdb.Bucket, height int32) error { +func (s *Store) rollback(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, height int32) error { minedBalanceWallet, err := fetchMinedBalance(ns) if err != nil { return err @@ -1627,12 +1516,12 @@ func (s *Store) rollback(ns walletdb.Bucket, height int32) error { // removed backwards. for j := len(stakeTxFromBlock) - 1; j >= 0; j-- { s.rollbackTransaction(stakeTxFromBlock[j], b, coinBaseCredits, - minedBalance, ns, false) + minedBalance, ns, addrmgrNs, false) } if parentIsValid { for j := len(regularTxFromParent) - 1; j >= 0; j-- { s.rollbackTransaction(regularTxFromParent[j], pb, - coinBaseCredits, minedBalance, ns, true) + coinBaseCredits, minedBalance, ns, addrmgrNs, true) } } @@ -1668,27 +1557,13 @@ 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) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - 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.ReadBucket) ([]*Credit, error) { + return s.unspentOutputs(ns) } // outputCreditInfo fetches information about a credit from the database, // fills out a credit struct, and returns it. -func (s *Store) outputCreditInfo(ns walletdb.Bucket, op wire.OutPoint, +func (s *Store) outputCreditInfo(ns walletdb.ReadBucket, op wire.OutPoint, block *Block) (*Credit, error) { // It has to exists as a credit or an unmined credit. // Look both of these up. If it doesn't, throw an @@ -1813,13 +1688,13 @@ func (s *Store) outputCreditInfo(ns walletdb.Bucket, op wire.OutPoint, return c, nil } -func (s *Store) unspentOutputs(ns walletdb.Bucket) ([]*Credit, error) { +func (s *Store) unspentOutputs(ns walletdb.ReadBucket) ([]*Credit, error) { var unspent []*Credit numUtxos := 0 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 @@ -1853,7 +1728,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. @@ -1890,29 +1765,15 @@ func (s *Store) unspentOutputs(ns walletdb.Bucket) ([]*Credit, error) { // UnspentOutpoints returns all unspent received transaction outpoints. // The order is undefined. -func (s *Store) UnspentOutpoints() ([]*wire.OutPoint, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var credits []*wire.OutPoint - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - credits, err = s.unspentOutpoints(ns) - return err - }) - return credits, err +func (s *Store) UnspentOutpoints(ns walletdb.ReadBucket) ([]*wire.OutPoint, error) { + return s.unspentOutpoints(ns) } -func (s *Store) unspentOutpoints(ns walletdb.Bucket) ([]*wire.OutPoint, error) { +func (s *Store) unspentOutpoints(ns walletdb.ReadBucket) ([]*wire.OutPoint, error) { var unspent []*wire.OutPoint numUtxos := 0 - err := ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error { var op wire.OutPoint err := readCanonicalOutPoint(k, &op) if err != nil { @@ -1950,7 +1811,7 @@ func (s *Store) unspentOutpoints(ns walletdb.Bucket) ([]*wire.OutPoint, error) { } var unspentZC []*wire.OutPoint - 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. @@ -1989,33 +1850,21 @@ func (s *Store) unspentOutpoints(ns walletdb.Bucket) ([]*wire.OutPoint, error) { // UnspentTickets returns all unspent tickets that are known for this wallet. // The order is undefined. -func (s *Store) UnspentTickets(syncHeight int32, +func (s *Store) UnspentTickets(ns walletdb.ReadBucket, syncHeight int32, includeImmature bool) ([]chainhash.Hash, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - var tickets []chainhash.Hash - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - tickets, err = s.unspentTickets(ns, syncHeight, includeImmature) - return err - }) - return tickets, err + return s.unspentTickets(ns, syncHeight, includeImmature) } -func (s *Store) unspentTickets(ns walletdb.Bucket, syncHeight int32, +func (s *Store) unspentTickets(ns walletdb.ReadBucket, syncHeight int32, includeImmature bool) ([]chainhash.Hash, error) { + var tickets []chainhash.Hash numTickets := 0 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 @@ -2054,7 +1903,7 @@ func (s *Store) unspentTickets(ns walletdb.Bucket, syncHeight int32, } if includeImmature { - 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. @@ -2098,25 +1947,11 @@ type MultisigCredit struct { // GetMultisigCredit takes an outpoint and returns multisignature // credit data stored about it. -func (s *Store) GetMultisigCredit(op *wire.OutPoint) (*MultisigCredit, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var credit *MultisigCredit - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - credit, err = s.getMultisigCredit(ns, op) - return err - }) - return credit, err +func (s *Store) GetMultisigCredit(ns walletdb.ReadBucket, op *wire.OutPoint) (*MultisigCredit, error) { + return s.getMultisigCredit(ns, op) } -func (s *Store) getMultisigCredit(ns walletdb.Bucket, +func (s *Store) getMultisigCredit(ns walletdb.ReadBucket, op *wire.OutPoint) (*MultisigCredit, error) { if op == nil { str := fmt.Sprintf("missing input outpoint") @@ -2166,26 +2001,11 @@ func (s *Store) getMultisigCredit(ns walletdb.Bucket, // GetMultisigOutput takes an outpoint and returns multisignature // credit data stored about it. -func (s *Store) GetMultisigOutput(op *wire.OutPoint) (*MultisigOut, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var credit *MultisigOut - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - credit, err = s.getMultisigOutput(ns, op) - return err - }) - return credit, err +func (s *Store) GetMultisigOutput(ns walletdb.ReadBucket, op *wire.OutPoint) (*MultisigOut, error) { + return s.getMultisigOutput(ns, op) } -func (s *Store) getMultisigOutput(ns walletdb.Bucket, - op *wire.OutPoint) (*MultisigOut, error) { +func (s *Store) getMultisigOutput(ns walletdb.ReadBucket, op *wire.OutPoint) (*MultisigOut, error) { if op == nil { str := fmt.Sprintf("missing input outpoint") return nil, storeError(ErrInput, str, nil) @@ -2211,29 +2031,15 @@ func (s *Store) getMultisigOutput(ns walletdb.Bucket, // UnspentMultisigCredits returns all unspent multisignature P2SH credits in // the wallet. -func (s *Store) UnspentMultisigCredits() ([]*MultisigCredit, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var credits []*MultisigCredit - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - credits, err = s.unspentMultisigCredits(ns) - return err - }) - return credits, err +func (s *Store) UnspentMultisigCredits(ns walletdb.ReadBucket) ([]*MultisigCredit, error) { + return s.unspentMultisigCredits(ns) } -func (s *Store) unspentMultisigCredits(ns walletdb.Bucket) ([]*MultisigCredit, +func (s *Store) unspentMultisigCredits(ns walletdb.ReadBucket) ([]*MultisigCredit, error) { var unspentKeys [][]byte - err := ns.Bucket(bucketMultisigUsp).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketMultisigUsp).ForEach(func(k, v []byte) error { unspentKeys = append(unspentKeys, k) return nil }) @@ -2280,26 +2086,13 @@ func (s *Store) unspentMultisigCredits(ns walletdb.Bucket) ([]*MultisigCredit, // UnspentMultisigCreditsForAddress returns all unspent multisignature P2SH // credits in the wallet for some specified address. -func (s *Store) UnspentMultisigCreditsForAddress( +func (s *Store) UnspentMultisigCreditsForAddress(ns walletdb.ReadBucket, addr dcrutil.Address) ([]*MultisigCredit, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var credits []*MultisigCredit - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - credits, err = s.unspentMultisigCreditsForAddress(ns, addr) - return err - }) - return credits, err + return s.unspentMultisigCreditsForAddress(ns, addr) } -func (s *Store) unspentMultisigCreditsForAddress(ns walletdb.Bucket, +func (s *Store) unspentMultisigCreditsForAddress(ns walletdb.ReadBucket, addr dcrutil.Address) ([]*MultisigCredit, error) { // Make sure the address is P2SH, then get the // Hash160 for the script from the address. @@ -2312,7 +2105,7 @@ func (s *Store) unspentMultisigCreditsForAddress(ns walletdb.Bucket, } var unspentKeys [][]byte - err := ns.Bucket(bucketMultisigUsp).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketMultisigUsp).ForEach(func(k, v []byte) error { unspentKeys = append(unspentKeys, k) return nil }) @@ -2367,24 +2160,11 @@ func (s *Store) unspentMultisigCreditsForAddress(ns walletdb.Bucket, // UnspentOutputsForAmount returns all non-stake outputs that sum up to the // amount passed. If not enough funds are found, a nil pointer is returned // without error. -func (s *Store) UnspentOutputsForAmount(amt dcrutil.Amount, height int32, - minConf int32, all bool, account uint32) ([]*Credit, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } +func (s *Store) UnspentOutputsForAmount(ns, addrmgrNs walletdb.ReadBucket, + amt dcrutil.Amount, height int32, minConf int32, all bool, + account uint32) ([]*Credit, error) { - var credits []*Credit - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - credits, err = s.unspentOutputsForAmount(ns, amt, height, minConf, - all, account) - return err - }) - return credits, err + return s.unspentOutputsForAmount(ns, addrmgrNs, amt, height, minConf, all, account) } type minimalCredit struct { @@ -2423,7 +2203,7 @@ func confirms(txHeight, curHeight int32) int32 { // outputCreditInfo fetches information about a credit from the database, // fills out a credit struct, and returns it. -func (s *Store) fastCreditPkScriptLookup(ns walletdb.Bucket, credKey []byte, +func (s *Store) fastCreditPkScriptLookup(ns walletdb.ReadBucket, credKey []byte, unminedCredKey []byte) ([]byte, error) { // It has to exists as a credit or an unmined credit. // Look both of these up. If it doesn't, throw an @@ -2491,7 +2271,7 @@ func (s *Store) fastCreditPkScriptLookup(ns walletdb.Bucket, credKey []byte, // minimalCreditToCredit looks up a minimal credit's data and prepares a Credit // from this data. -func (s *Store) minimalCreditToCredit(ns walletdb.Bucket, +func (s *Store) minimalCreditToCredit(ns walletdb.ReadBucket, mc *minimalCredit) (*Credit, error) { var cred *Credit @@ -2539,14 +2319,14 @@ func (s *Store) minimalCreditToCredit(ns walletdb.Bucket, // errForEachBreakout is used to break out of a a wallet db ForEach loop. var errForEachBreakout = errors.New("forEachBreakout") -func (s *Store) unspentOutputsForAmount(ns walletdb.Bucket, needed dcrutil.Amount, +func (s *Store) unspentOutputsForAmount(ns, addrmgrNs walletdb.ReadBucket, needed dcrutil.Amount, syncHeight int32, minConf int32, all bool, account uint32) ([]*Credit, error) { var eligible []*minimalCredit var toUse []*minimalCredit var unspent []*Credit found := dcrutil.Amount(0) - err := ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error { if found >= needed { return errForEachBreakout } @@ -2574,7 +2354,7 @@ func (s *Store) unspentOutputsForAmount(ns walletdb.Bucket, needed dcrutil.Amoun if err != nil { return err } - thisAcct, err := s.fetchAccountForPkScript(cVal, nil, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, cVal, nil, pkScript) if err != nil { return err } @@ -2659,7 +2439,7 @@ func (s *Store) unspentOutputsForAmount(ns walletdb.Bucket, needed dcrutil.Amoun // Unconfirmed transaction output handling. if minConf == 0 { - err = ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { + err = ns.NestedReadBucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { if found >= needed { return errForEachBreakout } @@ -2676,7 +2456,7 @@ func (s *Store) unspentOutputsForAmount(ns walletdb.Bucket, needed dcrutil.Amoun if err != nil { return err } - thisAcct, err := s.fetchAccountForPkScript(nil, v, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, nil, v, pkScript) if err != nil { return err } @@ -2774,12 +2554,9 @@ func (s *Store) unspentOutputsForAmount(ns walletdb.Bucket, needed dcrutil.Amoun } // InputSource provides a method (SelectInputs) to incrementally select unspent -// outputs to use as transaction inputs. It represents an open database -// transaction view, and must be closed with CloseTransaction when input -// selection is finished. +// outputs to use as transaction inputs. type InputSource struct { source func(dcrutil.Amount) (dcrutil.Amount, []*wire.TxIn, [][]byte, error) - tx walletdb.Tx } // SelectInputs selects transaction inputs to redeem unspent outputs stored in @@ -2792,44 +2569,10 @@ func (s *InputSource) SelectInputs(target dcrutil.Amount) (dcrutil.Amount, []*wi return s.source(target) } -// CloseTransaction closes any transaction opened when creating the InputSource. -// Calling SelectInputs will panic after CloseTransaction has been called. -func (s *InputSource) CloseTransaction() error { - s.source = nil - if s.tx != nil { - return s.tx.Rollback() - } - return nil -} - // MakeInputSource creates an InputSource to redeem unspent outputs from an // account. The minConf and syncHeight parameters are used to filter outputs // based on some spendable policy. -func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) InputSource { - defer s.mutex.Unlock() - s.mutex.Lock() - - sourceError := func(err error) InputSource { - f := func(dcrutil.Amount) (dcrutil.Amount, []*wire.TxIn, [][]byte, error) { - return 0, nil, nil, err - } - return InputSource{source: f, tx: nil} - } - - if s.isClosed { - str := "tx manager is closed" - err := storeError(ErrIsClosed, str, nil) - return sourceError(err) - } - - tx, err := s.namespace.Begin(false) - if err != nil { - str := "cannot begin read transaction" - err := storeError(ErrDatabase, str, err) - return sourceError(err) - } - ns := tx.RootBucket() - +func (s *Store) MakeInputSource(ns, addrmgrNs walletdb.ReadBucket, account uint32, minConf, syncHeight int32) InputSource { // Cursors to iterate over the (mined) unspent and unmined credit // buckets. These are closed over by the returned input source and // reused across multiple calls. @@ -2840,7 +2583,7 @@ func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) Input // The simplest way to handle this is to branch to either cursor.First // or cursor.Next depending on whether the cursor has already been // created or not. - var bucketUnspentCursor, bucketUnminedCreditsCursor walletdb.Cursor + var bucketUnspentCursor, bucketUnminedCreditsCursor walletdb.ReadCursor // Current inputs and their total value. These are closed over by the // returned input source and reused across multiple calls. @@ -2854,8 +2597,8 @@ func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) Input for currentTotal < target { var k, v []byte if bucketUnspentCursor == nil { - b := ns.Bucket(bucketUnspent) - bucketUnspentCursor = b.Cursor() + b := ns.NestedReadBucket(bucketUnspent) + bucketUnspentCursor = b.ReadCursor() k, v = bucketUnspentCursor.First() } else { k, v = bucketUnspentCursor.Next() @@ -2882,7 +2625,7 @@ func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) Input if err != nil { return 0, nil, nil, err } - thisAcct, err := s.fetchAccountForPkScript(cVal, nil, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, cVal, nil, pkScript) if err != nil { return 0, nil, nil, err } @@ -2972,8 +2715,8 @@ func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) Input for currentTotal < target { var k, v []byte if bucketUnminedCreditsCursor == nil { - b := ns.Bucket(bucketUnminedCredits) - bucketUnminedCreditsCursor = b.Cursor() + b := ns.NestedReadBucket(bucketUnminedCredits) + bucketUnminedCreditsCursor = b.ReadCursor() k, v = bucketUnminedCreditsCursor.First() } else { k, v = bucketUnminedCreditsCursor.Next() @@ -2993,7 +2736,7 @@ func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) Input if err != nil { return 0, nil, nil, err } - thisAcct, err := s.fetchAccountForPkScript(nil, v, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, nil, v, pkScript) if err != nil { return 0, nil, nil, err } @@ -3043,7 +2786,7 @@ func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) Input return currentTotal, currentInputs, currentScripts, nil } - return InputSource{source: f, tx: tx} + return InputSource{source: f} } // Balance returns the spendable wallet balance (total value of all unspent @@ -3053,36 +2796,23 @@ func (s *Store) MakeInputSource(account uint32, minConf, syncHeight int32) Input // // 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, balanceType BehaviorFlags, - all bool, account uint32) (dcrutil.Amount, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return 0, storeError(ErrIsClosed, str, nil) - } +func (s *Store) Balance(ns, addrmgrNs walletdb.ReadBucket, minConf, syncHeight int32, + balanceType BehaviorFlags, all bool, account uint32) (dcrutil.Amount, error) { - var amt dcrutil.Amount - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - var err error - amt, err = s.balance(ns, minConf, syncHeight, balanceType, all, account) - return err - }) - return amt, err + return s.balance(ns, addrmgrNs, minConf, syncHeight, balanceType, all, account) } -func (s *Store) balance(ns walletdb.Bucket, minConf int32, syncHeight int32, +func (s *Store) balance(ns, addrmgrNs walletdb.ReadBucket, minConf int32, syncHeight int32, balanceType BehaviorFlags, all bool, account uint32) (dcrutil.Amount, error) { switch balanceType { case BFBalanceFullScan: - return s.balanceFullScan(ns, minConf, syncHeight, all, account) + return s.balanceFullScan(ns, addrmgrNs, minConf, syncHeight, all, account) case BFBalanceSpendable: return s.balanceSpendable(ns, minConf, syncHeight) case BFBalanceLockedStake: - return s.balanceLockedStake(ns, minConf, syncHeight, all, account) + return s.balanceLockedStake(ns, addrmgrNs, minConf, syncHeight, all, account) case BFBalanceAll: - return s.balanceAll(ns, minConf, syncHeight, all, account) + return s.balanceAll(ns, addrmgrNs, minConf, syncHeight, all, account) default: return 0, fmt.Errorf("unknown balance type flag") } @@ -3091,11 +2821,11 @@ func (s *Store) balance(ns walletdb.Bucket, minConf int32, syncHeight int32, // balanceFullScan does a fullscan of the UTXO set to get the current balance. // It is less efficient than the other balance functions, but works fine for // accounts. -func (s *Store) balanceFullScan(ns walletdb.Bucket, minConf int32, +func (s *Store) balanceFullScan(ns, addrmgrNs walletdb.ReadBucket, minConf int32, syncHeight int32, all bool, account uint32) (dcrutil.Amount, error) { var amt dcrutil.Amount - err := ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error { if existsRawUnminedInput(ns, k) != nil { // Output is spent by an unmined transaction. // Skip to next unmined credit. @@ -3119,7 +2849,7 @@ func (s *Store) balanceFullScan(ns walletdb.Bucket, minConf int32, if err != nil { return err } - thisAcct, err := s.fetchAccountForPkScript(cVal, nil, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, cVal, nil, pkScript) if err != nil { return err } @@ -3184,7 +2914,7 @@ func (s *Store) balanceFullScan(ns walletdb.Bucket, minConf int32, // Unconfirmed transaction output handling. if minConf == 0 { - err = ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { + err = ns.NestedReadBucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { // Make sure this output was not spent by an unmined transaction. // If it was, skip this credit. if existsRawUnminedInput(ns, k) != nil { @@ -3197,7 +2927,7 @@ func (s *Store) balanceFullScan(ns walletdb.Bucket, minConf int32, if err != nil { return err } - thisAcct, err := s.fetchAccountForPkScript(nil, v, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, nil, v, pkScript) if err != nil { return err } @@ -3244,7 +2974,7 @@ func (s *Store) balanceFullScan(ns walletdb.Bucket, minConf int32, // Use only with minconf>0. // It is only to be used for simulation testing of wallet database // integrity. -func (s *Store) balanceFullScanSimulated(ns walletdb.Bucket, minConf int32, +func (s *Store) balanceFullScanSimulated(ns walletdb.ReadBucket, minConf int32, syncHeight int32, unminedInputs map[string][]byte) (dcrutil.Amount, error) { if minConf <= 0 { return 0, storeError(ErrInput, "0 or negative minconf given "+ @@ -3253,7 +2983,7 @@ func (s *Store) balanceFullScanSimulated(ns walletdb.Bucket, minConf int32, var amt dcrutil.Amount - err := ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) _, ok := unminedInputs[strK] if ok { @@ -3326,7 +3056,7 @@ func (s *Store) balanceFullScanSimulated(ns walletdb.Bucket, minConf int32, // balanceSpendable is the current spendable balance of all accounts in the // wallet. -func (s *Store) balanceSpendable(ns walletdb.Bucket, minConf int32, +func (s *Store) balanceSpendable(ns walletdb.ReadBucket, minConf int32, syncHeight int32) (dcrutil.Amount, error) { bal, err := fetchMinedBalance(ns) if err != nil { @@ -3337,7 +3067,7 @@ func (s *Store) balanceSpendable(ns walletdb.Bucket, minConf int32, // transaction, except for those spending tickets. 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 @@ -3374,7 +3104,7 @@ func (s *Store) balanceSpendable(ns walletdb.Bucket, minConf int32, stopConf = int32(s.chainParams.CoinbaseMaturity) } lastHeight := syncHeight - stopConf - blockIt := makeReverseBlockIterator(ns) + blockIt := makeReadReverseBlockIterator(ns) for blockIt.prev() { blockIter := &blockIt.elem @@ -3455,7 +3185,7 @@ func (s *Store) balanceSpendable(ns walletdb.Bucket, minConf int32, // 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. @@ -3487,7 +3217,7 @@ func (s *Store) balanceSpendable(ns walletdb.Bucket, minConf int32, // function that allows you to verify the integrity of a balance after // performing a rollback by using an old bucket of unmined inputs. // Use only with minconf>0. -func (s *Store) balanceSpendableSimulated(ns walletdb.Bucket, minConf int32, +func (s *Store) balanceSpendableSimulated(ns walletdb.ReadBucket, minConf int32, syncHeight int32, unminedInputs map[string][]byte) (dcrutil.Amount, error) { bal, err := fetchMinedBalance(ns) if err != nil { @@ -3498,7 +3228,7 @@ func (s *Store) balanceSpendableSimulated(ns walletdb.Bucket, minConf int32, // transaction, except for those spending tickets. 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 @@ -3538,7 +3268,7 @@ func (s *Store) balanceSpendableSimulated(ns walletdb.Bucket, minConf int32, stopConf = int32(s.chainParams.CoinbaseMaturity) } lastHeight := syncHeight - stopConf - blockIt := makeReverseBlockIterator(ns) + blockIt := makeReadReverseBlockIterator(ns) for blockIt.prev() { blockIter := &blockIt.elem @@ -3623,12 +3353,12 @@ func (s *Store) balanceSpendableSimulated(ns walletdb.Bucket, minConf int32, // balanceLockedStake returns the current balance of the wallet that is locked // in tickets. -func (s *Store) balanceLockedStake(ns walletdb.Bucket, minConf int32, +func (s *Store) balanceLockedStake(ns, addrmgrNs walletdb.ReadBucket, minConf int32, syncHeight int32, all bool, account uint32) (dcrutil.Amount, error) { var amt dcrutil.Amount var op wire.OutPoint - 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 @@ -3657,7 +3387,7 @@ func (s *Store) balanceLockedStake(ns walletdb.Bucket, minConf int32, if err != nil { return err } - thisAcct, err := s.fetchAccountForPkScript(cVal, nil, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, cVal, nil, pkScript) if err != nil { return err } @@ -3689,12 +3419,12 @@ func (s *Store) balanceLockedStake(ns walletdb.Bucket, minConf int32, } // balanceAll returns the balance of all unspent outputs. -func (s *Store) balanceAll(ns walletdb.Bucket, minConf int32, +func (s *Store) balanceAll(ns, addrmgrNs walletdb.ReadBucket, minConf int32, syncHeight int32, all bool, account uint32) (dcrutil.Amount, error) { var amt dcrutil.Amount var op wire.OutPoint - 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 @@ -3722,7 +3452,7 @@ func (s *Store) balanceAll(ns walletdb.Bucket, minConf int32, if err != nil { return err } - thisAcct, err := s.fetchAccountForPkScript(cVal, nil, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, cVal, nil, pkScript) if err != nil { return err } @@ -3746,7 +3476,7 @@ func (s *Store) balanceAll(ns walletdb.Bucket, minConf int32, // Unconfirmed transaction output handling. if minConf == 0 { - err = ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { + err = ns.NestedReadBucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { // Make sure this output was not spent by an unmined transaction. // If it was, skip this credit. if existsRawUnminedInput(ns, k) != nil { @@ -3759,7 +3489,7 @@ func (s *Store) balanceAll(ns walletdb.Bucket, minConf int32, if err != nil { return err } - thisAcct, err := s.fetchAccountForPkScript(nil, v, pkScript) + thisAcct, err := s.fetchAccountForPkScript(addrmgrNs, nil, v, pkScript) if err != nil { return err } @@ -3796,123 +3526,72 @@ type Balances struct { // AccountBalances returns a Balances struct for some given account at // syncHeight block height with all UTXOs that have minConf many confirms. -func (s *Store) AccountBalances(syncHeight int32, minConf int32, - account uint32) (Balances, error) { - s.mutex.Lock() - defer s.mutex.Unlock() +func (s *Store) AccountBalances(ns, addrmgrNs walletdb.ReadBucket, syncHeight int32, + minConf int32, account uint32) (Balances, error) { - if s.isClosed { - str := "tx manager is closed" - return Balances{}, storeError(ErrIsClosed, str, nil) + bal, err := s.balanceFullScan(ns, addrmgrNs, minConf, syncHeight, + false, account) + if err != nil { + return Balances{}, err } - var bals Balances - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - bal, err := s.balanceFullScan(ns, minConf, syncHeight, - false, account) - if err != nil { - return err - } - - bal0Conf, err := s.balanceFullScan(ns, 0, syncHeight, - false, account) - if err != nil { - return err - } - - balTotal, err := s.balanceAll(ns, minConf, syncHeight, - false, account) - if err != nil { - return err - } - - balAll, err := s.balanceAll(ns, 0, syncHeight, - false, account) - if err != nil { - return err - } + bal0Conf, err := s.balanceFullScan(ns, addrmgrNs, 0, syncHeight, + false, account) + if err != nil { + return Balances{}, err + } - bals.Total = balTotal - bals.Spendable = bal - bals.ImmatureReward = balAll - bal0Conf + balTotal, err := s.balanceAll(ns, addrmgrNs, minConf, syncHeight, + false, account) + if err != nil { + return Balances{}, err + } - return nil - }) + balAll, err := s.balanceAll(ns, addrmgrNs, 0, syncHeight, + false, account) + if err != nil { + return Balances{}, err + } - return bals, err + bals := Balances{ + Total: balTotal, + Spendable: bal, + ImmatureReward: balAll - bal0Conf, + } + return bals, nil } // InsertTxScript is the exported version of insertTxScript. -func (s *Store) InsertTxScript(script []byte) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } - - err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - var err error - err = s.insertTxScript(ns, script) - return err - }) - return err +func (s *Store) InsertTxScript(ns walletdb.ReadWriteBucket, script []byte) error { + return s.insertTxScript(ns, script) } // insertTxScript inserts a transaction script into the database. -func (s *Store) insertTxScript(ns walletdb.Bucket, script []byte) error { +func (s *Store) insertTxScript(ns walletdb.ReadWriteBucket, script []byte) error { return putTxScript(ns, script) } // GetTxScript is the exported version of getTxScript. -func (s *Store) GetTxScript(hash []byte) ([]byte, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var script []byte - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - script = s.getTxScript(ns, hash) - return nil - }) - return script, err +func (s *Store) GetTxScript(ns walletdb.ReadBucket, hash []byte) ([]byte, error) { + return s.getTxScript(ns, hash), nil } // getTxScript fetches a transaction script from the database using // the RIPEMD160 hash as a key. -func (s *Store) getTxScript(ns walletdb.Bucket, hash []byte) []byte { +func (s *Store) getTxScript(ns walletdb.ReadBucket, hash []byte) []byte { return existsTxScript(ns, hash) } // StoredTxScripts is the exported version of storedTxScripts. -func (s *Store) StoredTxScripts() ([][]byte, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var scripts [][]byte - err := scopedView(s.namespace, func(ns walletdb.Bucket) error { - localScripts, localErr := s.storedTxScripts(ns) - scripts = localScripts - return localErr - }) - return scripts, err +func (s *Store) StoredTxScripts(ns walletdb.ReadBucket) ([][]byte, error) { + return s.storedTxScripts(ns) } // storedTxScripts returns a slice of byte slices containing all the transaction // scripts currently stored in wallet. -func (s *Store) storedTxScripts(ns walletdb.Bucket) ([][]byte, error) { +func (s *Store) storedTxScripts(ns walletdb.ReadBucket) ([][]byte, error) { var scripts [][]byte - err := ns.Bucket(bucketScripts).ForEach(func(k, v []byte) error { + err := ns.NestedReadBucket(bucketScripts).ForEach(func(k, v []byte) error { scripts = append(scripts, v) return nil }) @@ -3932,22 +3611,8 @@ func (s *Store) storedTxScripts(ns walletdb.Bucket) ([][]byte, error) { // UTXOs so wallet can further investigate whether or not they exist in daemon // and, if they don't, can trigger their deletion. // -func (s *Store) RepairInconsistencies() ([]*wire.OutPoint, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - - var utxos []*wire.OutPoint - err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - ut, localErr := s.repairInconsistencies(ns) - utxos = ut - return localErr - }) - return utxos, err +func (s *Store) RepairInconsistencies(ns walletdb.ReadWriteBucket) ([]*wire.OutPoint, error) { + return s.repairInconsistencies(ns) } type dbCredit struct { @@ -3955,7 +3620,7 @@ type dbCredit struct { bl *Block } -func (s *Store) repairInconsistencies(ns walletdb.Bucket) ([]*wire.OutPoint, +func (s *Store) repairInconsistencies(ns walletdb.ReadWriteBucket) ([]*wire.OutPoint, error) { var unspent []*wire.OutPoint var badUnspent []*wire.OutPoint @@ -3964,7 +3629,7 @@ func (s *Store) repairInconsistencies(ns walletdb.Bucket) ([]*wire.OutPoint, // Unspent should map 1:1 with credits. If the credit can't be found, the utxo // should be deleted. If the credit can be found but the transaction or block // doesn't exist, the credit and unspent should be deleted. - err := ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + err := ns.NestedReadWriteBucket(bucketUnspent).ForEach(func(k, v []byte) error { op := new(wire.OutPoint) readCanonicalOutPoint(k, op) bl := new(Block) @@ -4028,23 +3693,11 @@ func (s *Store) repairInconsistencies(ns walletdb.Bucket) ([]*wire.OutPoint, // DeleteUnspent allows an external caller to delete unspent transaction outputs // of its choosing, e.g. if those unspent outpoint transactions are found to not // exist in the daemon. -func (s *Store) DeleteUnspent(utxos []*wire.OutPoint) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } - - err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - localErr := s.deleteUnspent(ns, utxos) - return localErr - }) - return err +func (s *Store) DeleteUnspent(ns walletdb.ReadWriteBucket, utxos []*wire.OutPoint) error { + return s.deleteUnspent(ns, utxos) } -func (s *Store) deleteUnspent(ns walletdb.Bucket, utxos []*wire.OutPoint) error { +func (s *Store) deleteUnspent(ns walletdb.ReadWriteBucket, utxos []*wire.OutPoint) error { // Look up the credit and see if it exists; if it does, we want to // get rid of that too. for _, bad := range utxos { @@ -4074,24 +3727,12 @@ func (s *Store) deleteUnspent(ns walletdb.Bucket, utxos []*wire.OutPoint) error // RepairMinedBalance allows an external caller to attempt to fix the mined // balance with a full scan balance call. -func (s *Store) RepairMinedBalance(curHeight int32) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return storeError(ErrIsClosed, str, nil) - } - - err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - localErr := s.repairMinedBalance(ns, curHeight) - return localErr - }) - return err +func (s *Store) RepairMinedBalance(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, curHeight int32) error { + return s.repairMinedBalance(ns, addrmgrNs, curHeight) } -func (s *Store) repairMinedBalance(ns walletdb.Bucket, curHeight int32) error { - bal, err := s.balanceFullScan(ns, 1, curHeight, true, 0) +func (s *Store) repairMinedBalance(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, curHeight int32) error { + bal, err := s.balanceFullScan(ns, addrmgrNs, 1, curHeight, true, 0) if err != nil { return err } @@ -4277,29 +3918,15 @@ func (d1 *DatabaseContents) Equals(d2 *DatabaseContents, skipUnmined bool) (bool // DatabaseDump is a testing function for wallet that exports the contents of // all databases as a -func (s *Store) DatabaseDump(height int32, +func (s *Store) DatabaseDump(ns, addrmgrNs walletdb.ReadBucket, height int32, oldUnminedInputs map[string][]byte) (*DatabaseContents, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.isClosed { - str := "tx manager is closed" - return nil, storeError(ErrIsClosed, str, nil) - } - var dbDump *DatabaseContents - var err error - err = scopedView(s.namespace, func(ns walletdb.Bucket) error { - var localErr error - dbDump, localErr = s.generateDatabaseDump(ns, height, oldUnminedInputs) - return localErr - }) - return dbDump, err + return s.generateDatabaseDump(ns, addrmgrNs, height, oldUnminedInputs) } // storedTxScripts returns a slice of byte slices containing all the transaction // scripts currently stored in wallet. -func (s *Store) generateDatabaseDump(ns walletdb.Bucket, +func (s *Store) generateDatabaseDump(ns, addrmgrNs walletdb.ReadBucket, height int32, oldUnminedInputs map[string][]byte) (*DatabaseContents, error) { dbDump := new(DatabaseContents) @@ -4335,7 +3962,7 @@ func (s *Store) generateDatabaseDump(ns walletdb.Bucket, } if oldUnminedInputs == nil { - dbDump.OneConfCalcBalance, err = s.balanceFullScan(ns, 1, height, true, + dbDump.OneConfCalcBalance, err = s.balanceFullScan(ns, addrmgrNs, 1, height, true, 0) if err != nil { return nil, err @@ -4348,59 +3975,59 @@ func (s *Store) generateDatabaseDump(ns walletdb.Bucket, } } - ns.Bucket(bucketBlocks).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketBlocks).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) vCopy := make([]byte, len(v), len(v)) copy(vCopy, v) dbDump.BucketBlocks[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketTxRecords).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketTxRecords).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketTxRecords[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketCredits).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketCredits).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketCredits[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketUnspent[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketDebits).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketDebits).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketDebits[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketUnmined).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketUnmined[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketUnminedCredits).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketUnminedCredits[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketUnminedInputs).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketUnminedInputs).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketUnminedInputs[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketScripts).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketScripts).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketScripts[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketMultisig).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketMultisig).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketMultisig[strK] = append([]byte(nil), v...) return nil }) - ns.Bucket(bucketMultisigUsp).ForEach(func(k, v []byte) error { + ns.NestedReadBucket(bucketMultisigUsp).ForEach(func(k, v []byte) error { strK := hex.EncodeToString(k) dbDump.BucketMultisigUsp[strK] = append([]byte(nil), v...) return nil diff --git a/wtxmgr/unconfirmed.go b/wtxmgr/unconfirmed.go index caddcdb75..7878f61bf 100644 --- a/wtxmgr/unconfirmed.go +++ b/wtxmgr/unconfirmed.go @@ -13,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? @@ -49,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) @@ -81,7 +81,7 @@ func (s *Store) removeDoubleSpends(ns walletdb.Bucket, rec *TxRecord) error { // and to remove transactions that spend coinbase transactions on reorgs. It // can also be used to remove old tickets that do not meet the network difficulty // and expired transactions. -func (s *Store) removeUnconfirmed(ns walletdb.Bucket, rec *TxRecord) error { +func (s *Store) removeUnconfirmed(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. @@ -129,13 +129,8 @@ func (s *Store) removeUnconfirmed(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 } @@ -148,9 +143,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 { @@ -171,19 +166,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 {