From: Chengcheng Zhang <943420582@qq.com> Date: Thu, 11 Jul 2019 06:02:58 +0000 (+0800) Subject: Wallet store interface (#217) X-Git-Tag: v1.0.5~163 X-Git-Url: http://git.osdn.net/view?p=bytom%2Fvapor.git;a=commitdiff_plain;h=51100c2a5afb320a9b16674f8c66b067fe760eb3;ds=sidebyside Wallet store interface (#217) * update * sort wallet store functions * update * update * update * remove DeleteAccountByAlias * remove DeleteAccountUTXOs * update * update * update * remove deleteAccountControlPrograms * remove DeleteBip44ContractIndex DeleteContractIndex DeleteControlProgram * rename GetAccountByID to GetAccount * update GetAsset * move TestReserve to account * move TestReserveParticular to account * move TestFindUtxos to account * update * delete utxo_keeper_test * update * update * move TestReserveBtmUtxoChain to account * update * update * update * remove mock/UTXO.go * update ListTransactions * update * update * update AccountStorer * update SetAccountIndex * update * update * update * update DeleteAccountUTXOs * update DeleteAccount * update * update ListUTXOs * update * update * update getAccountFromACP * update Restore * update Restore * update Restore * update * update * update GetCoinbaseCtrlProgram * update Create * update Create * update SaveAccount * update UpdateAccountAlias * update SetAccountIndex * update FindByAlias * update WalletStore * update * remove GetAccount * update * udpate * update * add SetStandardUTXO * remove SetStandardUTXO in walletstore * update * remove DeleteStandardUTXO * update * update * update * update * update * add mockAccountStore in wallet_test * update CalcGlobalTxIndex in wallet_test * add mockWalletStore in wallet_test * update TestWalletVersion * move TestEncodeDecodeGlobalTxIndex * add test/wallet_test.go * update * update * update TestXPubsRecoveryLock * update * update TestExtendScanAddresses * update TestRecoveryFromXPubs * add mock wallet store * update * add mock account store * update TestRecoveryByRescanAccount * update * update recovery_test.go * update TestWalletUnconfirmedTxs * update TestGetAccountUtxos * update * update * update * update * update * update * update id * update * update * update * update * update TestGetAccountUtxos * update * update * update * update * update * update * update * update * update * add MockAccountStore * remove mock * remove mock * update * remove test mock * comment some test functions * update SetWalletInfo * update * update * update loadWalletInfo * update * update * rename RecoveryState * update * update commitStatusInfo * update * update * update * remove recoveryKey * update * update * update * rename calcGlobalTxIndexKey to CalcGlobalTxIndexKey * update * update * update TestWalletUpdate * remove comments * update wallet_test * update LoadWalletInfo * rename w.status * update TestRescanWallet * add TestMemPoolTxQueryLoop * update TestMemPoolTxQueryLoop * update * update * fix TestFilterAccountUtxo * update * update * refine code * remove TestRescanWallet * add account store * comment test * update TestFilterAccountUtxo * fix w.AccountMgr.GetControlProgram * remove GetControlProgram * remove comment * update * update * update * update * rename accountDB to db * rename walletDB to db * update * update * update * update * update * update * update * update * update deleteAccountUTXOs * update * add deleteAccountControlPrograms * update ListAccountUTXOs * update GetAccountByProgram * update * update * update * update * update * rm key * update utxo_keeper * update DeleteTransactions * update Bip44ContractIndexKey * update dbm * update TestFilterAccountUtxo * update * add accountstore * update TestFilterAccountUtxo * updata TestFilterAccountUtxo * move db account store prefix * move db wallet store prefix * add InitStore * update * rename CommitStore * add InitStore * rename CommitStore * fix bug * update * update * update * update * update * update * update * update * move accountAliasKey * update * rename InitStore to InitBatch * rename CommitStore to CommitBatch * update * update * update * update * update * update * update * fix TestWalletUpdate * fix bugs * remove loop * fix bug * update * update * update Restore * update Restore * update saveExternalAssetDefinition * move ErrAccntTxIDNotFound * update GetCoinbaseCtrlProgram * update getExternalDefinition --- diff --git a/account/accounts.go b/account/accounts.go index e26833b0..ed86cc7c 100644 --- a/account/accounts.go +++ b/account/accounts.go @@ -2,9 +2,7 @@ package account import ( - "encoding/json" "reflect" - "sort" "strings" "sync" @@ -20,7 +18,6 @@ import ( "github.com/vapor/crypto" "github.com/vapor/crypto/ed25519/chainkd" "github.com/vapor/crypto/sha3pool" - dbm "github.com/vapor/database/leveldb" "github.com/vapor/errors" "github.com/vapor/protocol" "github.com/vapor/protocol/bc" @@ -36,57 +33,22 @@ const ( logModule = "account" ) -var ( - accountIndexPrefix = []byte("AccountIndex:") - accountPrefix = []byte("Account:") - aliasPrefix = []byte("AccountAlias:") - contractIndexPrefix = []byte("ContractIndex") - contractPrefix = []byte("Contract:") - miningAddressKey = []byte("MiningAddress") - CoinbaseAbKey = []byte("CoinbaseArbitrary") -) - // pre-define errors for supporting bytom errorFormatter var ( - ErrDuplicateAlias = errors.New("Duplicate account alias") - ErrDuplicateIndex = errors.New("Duplicate account with same xPubs and index") - ErrFindAccount = errors.New("Failed to find account") - ErrMarshalAccount = errors.New("Failed to marshal account") - ErrInvalidAddress = errors.New("Invalid address") - ErrFindCtrlProgram = errors.New("Failed to find account control program") - ErrDeriveRule = errors.New("Invalid key derivation rule") - ErrContractIndex = errors.New("Exceeded maximum addresses per account") - ErrAccountIndex = errors.New("Exceeded maximum accounts per xpub") - ErrFindTransaction = errors.New("No transaction") - ErrAccountIDEmpty = errors.New("account_id is empty") + ErrDuplicateAlias = errors.New("Duplicate account alias") + ErrDuplicateIndex = errors.New("Duplicate account with same xPubs and index") + ErrFindAccount = errors.New("Failed to find account") + ErrMarshalAccount = errors.New("Failed to marshal account") + ErrInvalidAddress = errors.New("Invalid address") + ErrFindCtrlProgram = errors.New("Failed to find account control program") + ErrDeriveRule = errors.New("Invalid key derivation rule") + ErrContractIndex = errors.New("Exceeded maximum addresses per account") + ErrAccountIndex = errors.New("Exceeded maximum accounts per xpub") + ErrFindTransaction = errors.New("No transaction") + ErrFindMiningAddress = errors.New("Failed to find mining address") + ErrAccountIDEmpty = errors.New("account_id is empty") ) -// ContractKey account control promgram store prefix -func ContractKey(hash common.Hash) []byte { - return append(contractPrefix, hash[:]...) -} - -// Key account store prefix -func Key(name string) []byte { - return append(accountPrefix, []byte(name)...) -} - -func aliasKey(name string) []byte { - return append(aliasPrefix, []byte(name)...) -} - -func bip44ContractIndexKey(accountID string, change bool) []byte { - key := append(contractIndexPrefix, accountID...) - if change { - return append(key, []byte{1}...) - } - return append(key, []byte{0}...) -} - -func contractIndexKey(accountID string) []byte { - return append(contractIndexPrefix, []byte(accountID)...) -} - // Account is structure of Bytom account type Account struct { *signers.Signer @@ -105,7 +67,7 @@ type CtrlProgram struct { // Manager stores accounts and their associated control programs. type Manager struct { - db dbm.DB + store AccountStore chain *protocol.Chain utxoKeeper *utxoKeeper @@ -121,11 +83,11 @@ type Manager struct { } // NewManager creates a new account manager -func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager { +func NewManager(store AccountStore, chain *protocol.Chain) *Manager { return &Manager{ - db: walletDB, + store: store, chain: chain, - utxoKeeper: newUtxoKeeper(chain.BestBlockHeight, walletDB), + utxoKeeper: newUtxoKeeper(chain.BestBlockHeight, store), cache: lru.New(maxAccountCache), aliasCache: lru.New(maxAccountCache), delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram), @@ -152,19 +114,19 @@ func CreateAccount(xpubs []chainkd.XPub, quorum int, alias string, acctIndex uin return &Account{Signer: signer, ID: id, Alias: strings.ToLower(strings.TrimSpace(alias))}, nil } -func (m *Manager) saveAccount(account *Account, updateIndex bool) error { - rawAccount, err := json.Marshal(account) - if err != nil { - return ErrMarshalAccount +func (m *Manager) saveAccount(account *Account) error { + newStore := m.store.InitBatch() + + // update account index + newStore.SetAccountIndex(account) + if err := newStore.SetAccount(account); err != nil { + return err } - storeBatch := m.db.NewBatch() - storeBatch.Set(Key(account.ID), rawAccount) - storeBatch.Set(aliasKey(account.Alias), []byte(account.ID)) - if updateIndex { - storeBatch.Set(GetAccountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex)) + if err := newStore.CommitBatch(); err != nil { + return err } - storeBatch.Write() + return nil } @@ -173,24 +135,23 @@ func (m *Manager) SaveAccount(account *Account) error { m.accountMu.Lock() defer m.accountMu.Unlock() - if existed := m.db.Get(aliasKey(account.Alias)); existed != nil { + _, err := m.store.GetAccountByAlias(account.Alias) + if err == nil { return ErrDuplicateAlias } - acct, err := m.GetAccountByXPubsIndex(account.XPubs, account.KeyIndex) - if err != nil { + if err != ErrFindAccount { return err } - if acct != nil { + acct, err := m.GetAccountByXPubsIndex(account.XPubs, account.KeyIndex) + if err != nil && err != ErrFindAccount { + return err + } else if acct != nil { return ErrDuplicateIndex } - currentIndex := uint64(0) - if rawIndexBytes := m.db.Get(GetAccountIndexKey(account.XPubs)); rawIndexBytes != nil { - currentIndex = common.BytesToUnit64(rawIndexBytes) - } - return m.saveAccount(account, account.KeyIndex > currentIndex) + return m.saveAccount(account) } // Create creates and save a new Account. @@ -198,27 +159,31 @@ func (m *Manager) Create(xpubs []chainkd.XPub, quorum int, alias string, deriveR m.accountMu.Lock() defer m.accountMu.Unlock() - if existed := m.db.Get(aliasKey(alias)); existed != nil { + _, err := m.store.GetAccountByAlias(alias) + if err == nil { return nil, ErrDuplicateAlias + } else if err != ErrFindAccount { + return nil, err } acctIndex := uint64(1) - if rawIndexBytes := m.db.Get(GetAccountIndexKey(xpubs)); rawIndexBytes != nil { - acctIndex = common.BytesToUnit64(rawIndexBytes) + 1 + if currentIndex := m.store.GetAccountIndex(xpubs); currentIndex != 0 { + acctIndex = currentIndex + 1 } + account, err := CreateAccount(xpubs, quorum, alias, acctIndex, deriveRule) if err != nil { return nil, err } - if err := m.saveAccount(account, true); err != nil { + if err := m.saveAccount(account); err != nil { return nil, err } return account, nil } -func (m *Manager) UpdateAccountAlias(accountID string, newAlias string) (err error) { +func (m *Manager) UpdateAccountAlias(accountID string, newAlias string) error { m.accountMu.Lock() defer m.accountMu.Unlock() @@ -226,28 +191,37 @@ func (m *Manager) UpdateAccountAlias(accountID string, newAlias string) (err err if err != nil { return err } - oldAlias := account.Alias + oldAccount := *account normalizedAlias := strings.ToLower(strings.TrimSpace(newAlias)) - if existed := m.db.Get(aliasKey(normalizedAlias)); existed != nil { + _, err = m.store.GetAccountByAlias(normalizedAlias) + if err == nil { return ErrDuplicateAlias } + if err != ErrFindAccount { + return err + } m.cacheMu.Lock() - m.aliasCache.Remove(oldAlias) + m.aliasCache.Remove(oldAccount.Alias) m.cacheMu.Unlock() account.Alias = normalizedAlias - rawAccount, err := json.Marshal(account) - if err != nil { - return ErrMarshalAccount + + newStore := m.store.InitBatch() + + if err := newStore.DeleteAccount(&oldAccount); err != nil { + return err + } + + if err := newStore.SetAccount(account); err != nil { + return err + } + + if err := newStore.CommitBatch(); err != nil { + return err } - storeBatch := m.db.NewBatch() - storeBatch.Delete(aliasKey(oldAlias)) - storeBatch.Set(Key(accountID), rawAccount) - storeBatch.Set(aliasKey(normalizedAlias), []byte(accountID)) - storeBatch.Write() return nil } @@ -303,45 +277,8 @@ func (m *Manager) CreateBatchAddresses(accountID string, change bool, stopIndex return nil } -// deleteAccountControlPrograms deletes control program matching accountID -func (m *Manager) deleteAccountControlPrograms(accountID string) error { - cps, err := m.ListControlProgram() - if err != nil { - return err - } - - var hash common.Hash - for _, cp := range cps { - if cp.AccountID == accountID { - sha3pool.Sum256(hash[:], cp.ControlProgram) - m.db.Delete(ContractKey(hash)) - } - } - m.db.Delete(bip44ContractIndexKey(accountID, false)) - m.db.Delete(bip44ContractIndexKey(accountID, true)) - m.db.Delete(contractIndexKey(accountID)) - return nil -} - -// deleteAccountUtxos deletes utxos matching accountID -func (m *Manager) deleteAccountUtxos(accountID string) error { - accountUtxoIter := m.db.IteratorPrefix([]byte(UTXOPreFix)) - defer accountUtxoIter.Release() - for accountUtxoIter.Next() { - accountUtxo := &UTXO{} - if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil { - return err - } - - if accountID == accountUtxo.AccountID { - m.db.Delete(StandardUTXOKey(accountUtxo.OutputID)) - } - } - return nil -} - // DeleteAccount deletes the account's ID or alias matching account ID. -func (m *Manager) DeleteAccount(accountID string) (err error) { +func (m *Manager) DeleteAccount(accountID string) error { m.accountMu.Lock() defer m.accountMu.Unlock() @@ -350,22 +287,11 @@ func (m *Manager) DeleteAccount(accountID string) (err error) { return err } - if err := m.deleteAccountControlPrograms(accountID); err != nil { - return err - } - if err := m.deleteAccountUtxos(accountID); err != nil { - return err - } - m.cacheMu.Lock() m.aliasCache.Remove(account.Alias) m.cacheMu.Unlock() - storeBatch := m.db.NewBatch() - storeBatch.Delete(aliasKey(account.Alias)) - storeBatch.Delete(Key(account.ID)) - storeBatch.Write() - return nil + return m.store.DeleteAccount(account) } // FindByAlias retrieves an account's Signer record by its alias @@ -377,16 +303,7 @@ func (m *Manager) FindByAlias(alias string) (*Account, error) { return m.FindByID(cachedID.(string)) } - rawID := m.db.Get(aliasKey(alias)) - if rawID == nil { - return nil, ErrFindAccount - } - - accountID := string(rawID) - m.cacheMu.Lock() - m.aliasCache.Add(alias, accountID) - m.cacheMu.Unlock() - return m.FindByID(accountID) + return m.store.GetAccountByAlias(alias) } // FindByID returns an account's Signer record by its ID. @@ -398,13 +315,8 @@ func (m *Manager) FindByID(id string) (*Account, error) { return cachedAccount.(*Account), nil } - rawAccount := m.db.Get(Key(id)) - if rawAccount == nil { - return nil, ErrFindAccount - } - - account := &Account{} - if err := json.Unmarshal(rawAccount, account); err != nil { + account, err := m.store.GetAccountByID(id) + if err != nil { return nil, err } @@ -416,13 +328,7 @@ func (m *Manager) FindByID(id string) (*Account, error) { // GetAccountByProgram return Account by given CtrlProgram func (m *Manager) GetAccountByProgram(program *CtrlProgram) (*Account, error) { - rawAccount := m.db.Get(Key(program.AccountID)) - if rawAccount == nil { - return nil, ErrFindAccount - } - - account := &Account{} - return account, json.Unmarshal(rawAccount, account) + return m.store.GetAccountByID(program.AccountID) } // GetAccountByXPubsIndex get account by xPubs and index @@ -437,26 +343,21 @@ func (m *Manager) GetAccountByXPubsIndex(xPubs []chainkd.XPub, index uint64) (*A return account, nil } } - return nil, nil + return nil, ErrFindAccount } // GetAliasByID return the account alias by given ID func (m *Manager) GetAliasByID(id string) string { - rawAccount := m.db.Get(Key(id)) - if rawAccount == nil { + account, err := m.store.GetAccountByID(id) + if err != nil { log.Warn("GetAliasByID fail to find account") return "" } - - account := &Account{} - if err := json.Unmarshal(rawAccount, account); err != nil { - log.Warn(err) - } return account.Alias } func (m *Manager) GetCoinbaseArbitrary() []byte { - if arbitrary := m.db.Get(CoinbaseAbKey); arbitrary != nil { + if arbitrary := m.store.GetCoinbaseArbitrary(); arbitrary != nil { return arbitrary } return []byte{} @@ -469,28 +370,32 @@ func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) { log.Warningf("GetCoinbaseControlProgram: can't find any account in db") return vmutil.DefaultCoinbaseProgram() } + if err != nil { return nil, err } + return cp.ControlProgram, nil } // GetCoinbaseCtrlProgram will return the coinbase CtrlProgram func (m *Manager) GetCoinbaseCtrlProgram() (*CtrlProgram, error) { - if data := m.db.Get(miningAddressKey); data != nil { - cp := &CtrlProgram{} - return cp, json.Unmarshal(data, cp) + if cp, err := m.store.GetMiningAddress(); err == nil { + return cp, nil + } else if err != ErrFindMiningAddress { + return nil, err } - accountIter := m.db.IteratorPrefix([]byte(accountPrefix)) - defer accountIter.Release() - if !accountIter.Next() { - return nil, ErrFindAccount + account := new(Account) + accounts, err := m.store.ListAccounts("") + if err != nil { + return nil, err } - account := &Account{} - if err := json.Unmarshal(accountIter.Value(), account); err != nil { - return nil, err + if len(accounts) > 0 { + account = accounts[0] + } else { + return nil, ErrFindAccount } program, err := m.CreateAddress(account.ID, false) @@ -498,31 +403,16 @@ func (m *Manager) GetCoinbaseCtrlProgram() (*CtrlProgram, error) { return nil, err } - rawCP, err := json.Marshal(program) - if err != nil { + if err := m.store.SetMiningAddress(program); err != nil { return nil, err } - m.db.Set(miningAddressKey, rawCP) return program, nil } -// GetContractIndex return the current index -func (m *Manager) GetContractIndex(accountID string) uint64 { - index := uint64(0) - if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil { - index = common.BytesToUnit64(rawIndexBytes) - } - return index -} - // GetBip44ContractIndex return the current bip44 contract index func (m *Manager) GetBip44ContractIndex(accountID string, change bool) uint64 { - index := uint64(0) - if rawIndexBytes := m.db.Get(bip44ContractIndexKey(accountID, change)); rawIndexBytes != nil { - index = common.BytesToUnit64(rawIndexBytes) - } - return index + return m.store.GetBip44ContractIndex(accountID, change) } // GetLocalCtrlProgramByAddress return CtrlProgram by given address @@ -534,13 +424,13 @@ func (m *Manager) GetLocalCtrlProgramByAddress(address string) (*CtrlProgram, er var hash [32]byte sha3pool.Sum256(hash[:], program) - rawProgram := m.db.Get(ContractKey(hash)) - if rawProgram == nil { - return nil, ErrFindCtrlProgram + + cp, err := m.store.GetControlProgram(bc.NewHash(hash)) + if err != nil { + return nil, err } - cp := &CtrlProgram{} - return cp, json.Unmarshal(rawProgram, cp) + return cp, nil } // GetMiningAddress will return the mining address @@ -549,47 +439,29 @@ func (m *Manager) GetMiningAddress() (string, error) { if err != nil { return "", err } + return cp.Address, nil } // IsLocalControlProgram check is the input control program belong to local func (m *Manager) IsLocalControlProgram(prog []byte) bool { - var hash common.Hash + var hash [32]byte sha3pool.Sum256(hash[:], prog) - bytes := m.db.Get(ContractKey(hash)) - return bytes != nil + cp, err := m.store.GetControlProgram(bc.NewHash(hash)) + if err != nil || cp == nil { + return false + } + return true } // ListAccounts will return the accounts in the db func (m *Manager) ListAccounts(id string) ([]*Account, error) { - accounts := []*Account{} - accountIter := m.db.IteratorPrefix(Key(strings.TrimSpace(id))) - defer accountIter.Release() - - for accountIter.Next() { - account := &Account{} - if err := json.Unmarshal(accountIter.Value(), &account); err != nil { - return nil, err - } - accounts = append(accounts, account) - } - return accounts, nil + return m.store.ListAccounts(id) } // ListControlProgram return all the local control program func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) { - cps := []*CtrlProgram{} - cpIter := m.db.IteratorPrefix(contractPrefix) - defer cpIter.Release() - - for cpIter.Next() { - cp := &CtrlProgram{} - if err := json.Unmarshal(cpIter.Value(), cp); err != nil { - return nil, err - } - cps = append(cps, cp) - } - return cps, nil + return m.store.ListControlPrograms() } func (m *Manager) ListUnconfirmedUtxo(accountID string, isSmartContract bool) []*UTXO { @@ -619,17 +491,15 @@ func (m *Manager) SetMiningAddress(miningAddress string) (string, error) { Address: miningAddress, ControlProgram: program, } - rawCP, err := json.Marshal(cp) - if err != nil { - return "", err + if err := m.store.SetMiningAddress(cp); err != nil { + return cp.Address, err } - m.db.Set(miningAddressKey, rawCP) return m.GetMiningAddress() } func (m *Manager) SetCoinbaseArbitrary(arbitrary []byte) { - m.db.Set(CoinbaseAbKey, arbitrary) + m.store.SetCoinbaseArbitrary(arbitrary) } // CreateCtrlProgram generate an address for the select account @@ -644,9 +514,11 @@ func CreateCtrlProgram(account *Account, addrIdx uint64, change bool) (cp *CtrlP } else { cp, err = createP2SH(account, path) } + if err != nil { return nil, err } + cp.KeyIndex, cp.Change = addrIdx, change return cp, nil } @@ -680,8 +552,8 @@ func createP2SH(account *Account, path [][]byte) (*CtrlProgram, error) { if err != nil { return nil, err } - scriptHash := crypto.Sha256(signScript) + scriptHash := crypto.Sha256(signScript) address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams) if err != nil { return nil, err @@ -699,24 +571,16 @@ func createP2SH(account *Account, path [][]byte) (*CtrlProgram, error) { }, nil } -func GetAccountIndexKey(xpubs []chainkd.XPub) []byte { - var hash [32]byte - var xPubs []byte - cpy := append([]chainkd.XPub{}, xpubs[:]...) - sort.Sort(signers.SortKeys(cpy)) - for _, xpub := range cpy { - xPubs = append(xPubs, xpub[:]...) - } - sha3pool.Sum256(hash[:], xPubs) - return append(accountIndexPrefix, hash[:]...) +func (m *Manager) GetContractIndex(accountID string) uint64 { + return m.store.GetContractIndex(accountID) } func (m *Manager) getCurrentContractIndex(account *Account, change bool) (uint64, error) { switch account.DeriveRule { case signers.BIP0032: - return m.GetContractIndex(account.ID), nil + return m.store.GetContractIndex(account.ID), nil case signers.BIP0044: - return m.GetBip44ContractIndex(account.ID, change), nil + return m.store.GetBip44ContractIndex(account.ID, change), nil } return 0, ErrDeriveRule } @@ -726,6 +590,7 @@ func (m *Manager) getProgramByAddress(address string) ([]byte, error) { if err != nil { return nil, err } + redeemContract := addr.ScriptAddress() program := []byte{} switch addr.(type) { @@ -739,11 +604,12 @@ func (m *Manager) getProgramByAddress(address string) ([]byte, error) { if err != nil { return nil, err } + return program, nil } func (m *Manager) saveControlProgram(prog *CtrlProgram, updateIndex bool) error { - var hash common.Hash + var hash [32]byte sha3pool.Sum256(hash[:], prog.ControlProgram) acct, err := m.GetAccountByProgram(prog) @@ -751,24 +617,22 @@ func (m *Manager) saveControlProgram(prog *CtrlProgram, updateIndex bool) error return err } - accountCP, err := json.Marshal(prog) - if err != nil { - return err + newStore := m.store.InitBatch() + + if err := newStore.SetControlProgram(bc.NewHash(hash), prog); err != nil { + return nil } - storeBatch := m.db.NewBatch() - storeBatch.Set(ContractKey(hash), accountCP) if updateIndex { switch acct.DeriveRule { case signers.BIP0032: - storeBatch.Set(contractIndexKey(acct.ID), common.Unit64ToBytes(prog.KeyIndex)) + newStore.SetContractIndex(acct.ID, prog.KeyIndex) case signers.BIP0044: - storeBatch.Set(bip44ContractIndexKey(acct.ID, prog.Change), common.Unit64ToBytes(prog.KeyIndex)) + newStore.SetBip44ContractIndex(acct.ID, prog.Change, prog.KeyIndex) } } - storeBatch.Write() - return nil + return newStore.CommitBatch() } // SaveControlPrograms save account control programs @@ -791,3 +655,15 @@ func (m *Manager) SaveControlPrograms(progs ...*CtrlProgram) error { } return nil } + +func (m *Manager) SetStandardUTXO(outputID bc.Hash, utxo *UTXO) error { + return m.store.SetStandardUTXO(outputID, utxo) +} + +func (m *Manager) DeleteStandardUTXO(outputID bc.Hash) { + m.store.DeleteStandardUTXO(outputID) +} + +func (m *Manager) GetControlProgram(hash bc.Hash) (*CtrlProgram, error) { + return m.store.GetControlProgram(hash) +} diff --git a/account/builder.go b/account/builder.go index 5f2448a7..143ad24f 100644 --- a/account/builder.go +++ b/account/builder.go @@ -90,7 +90,7 @@ func (m *Manager) reserveBtmUtxoChain(builder *txbuilder.TemplateBuilder, accoun return utxos, nil } -func (m *Manager) buildBtmTxChain(utxos []*UTXO, signer *signers.Signer) ([]*txbuilder.Template, *UTXO, error) { +func (m *Manager) BuildBtmTxChain(utxos []*UTXO, signer *signers.Signer) ([]*txbuilder.Template, *UTXO, error) { if len(utxos) == 0 { return nil, nil, errors.New("mergeSpendActionUTXO utxos num 0") } @@ -166,6 +166,7 @@ func SpendAccountChain(ctx context.Context, builder *txbuilder.TemplateBuilder, if !ok { return nil, errors.New("fail to convert the spend action") } + if *act.AssetId != *consensus.BTMAssetID { return nil, errors.New("spend chain action only support BTM") } @@ -180,7 +181,7 @@ func SpendAccountChain(ctx context.Context, builder *txbuilder.TemplateBuilder, return nil, err } - tpls, utxo, err := act.accounts.buildBtmTxChain(utxos, acct.Signer) + tpls, utxo, err := act.accounts.BuildBtmTxChain(utxos, acct.Signer) if err != nil { return nil, err } @@ -324,6 +325,7 @@ func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.S if err != nil { return nil, nil, err } + if u.Address == "" { sigInst.AddWitnessKeys(signer.XPubs, path, signer.Quorum) return txInput, sigInst, nil @@ -382,6 +384,7 @@ func (m *Manager) insertControlProgramDelayed(b *txbuilder.TemplateBuilder, acp if len(acps) == 0 { return nil } + return m.SaveControlPrograms(acps...) }) } diff --git a/account/builder_test.go b/account/builder_test.go index dbc26555..c5f2cbd8 100644 --- a/account/builder_test.go +++ b/account/builder_test.go @@ -1,200 +1,15 @@ package account import ( - "encoding/json" "testing" "time" - "github.com/vapor/blockchain/signers" "github.com/vapor/blockchain/txbuilder" "github.com/vapor/consensus" - "github.com/vapor/crypto/ed25519/chainkd" "github.com/vapor/protocol/bc" "github.com/vapor/testutil" ) -func TestReserveBtmUtxoChain(t *testing.T) { - chainTxUtxoNum = 3 - utxos := []*UTXO{} - m := mockAccountManager(t) - for i := uint64(1); i <= 20; i++ { - utxo := &UTXO{ - OutputID: bc.Hash{V0: i}, - AccountID: "TestAccountID", - AssetID: *consensus.BTMAssetID, - Amount: i * chainTxMergeGas, - } - utxos = append(utxos, utxo) - - data, err := json.Marshal(utxo) - if err != nil { - t.Fatal(err) - } - - m.db.Set(StandardUTXOKey(utxo.OutputID), data) - } - - cases := []struct { - amount uint64 - want []uint64 - err bool - }{ - { - amount: 1 * chainTxMergeGas, - want: []uint64{1}, - }, - { - amount: 888888 * chainTxMergeGas, - want: []uint64{}, - err: true, - }, - { - amount: 7 * chainTxMergeGas, - want: []uint64{4, 3, 1}, - }, - { - amount: 15 * chainTxMergeGas, - want: []uint64{5, 4, 3, 2, 1, 6}, - }, - { - amount: 163 * chainTxMergeGas, - want: []uint64{20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 2, 1, 3}, - }, - } - - for i, c := range cases { - m.utxoKeeper.expireReservation(time.Unix(999999999, 0)) - utxos, err := m.reserveBtmUtxoChain(&txbuilder.TemplateBuilder{}, "TestAccountID", c.amount, false) - - if err != nil != c.err { - t.Fatalf("case %d got err %v want err = %v", i, err, c.err) - } - - got := []uint64{} - for _, utxo := range utxos { - got = append(got, utxo.Amount/chainTxMergeGas) - } - - if !testutil.DeepEqual(got, c.want) { - t.Fatalf("case %d got %d want %d", i, got, c.want) - } - } - -} - -func TestBuildBtmTxChain(t *testing.T) { - chainTxUtxoNum = 3 - m := mockAccountManager(t) - cases := []struct { - inputUtxo []uint64 - wantInput [][]uint64 - wantOutput [][]uint64 - wantUtxo uint64 - }{ - { - inputUtxo: []uint64{5}, - wantInput: [][]uint64{}, - wantOutput: [][]uint64{}, - wantUtxo: 5 * chainTxMergeGas, - }, - { - inputUtxo: []uint64{5, 4}, - wantInput: [][]uint64{ - []uint64{5, 4}, - }, - wantOutput: [][]uint64{ - []uint64{8}, - }, - wantUtxo: 8 * chainTxMergeGas, - }, - { - inputUtxo: []uint64{5, 4, 1, 1}, - wantInput: [][]uint64{ - []uint64{5, 4, 1}, - []uint64{1, 9}, - }, - wantOutput: [][]uint64{ - []uint64{9}, - []uint64{9}, - }, - wantUtxo: 9 * chainTxMergeGas, - }, - { - inputUtxo: []uint64{22, 123, 53, 234, 23, 4, 2423, 24, 23, 43, 34, 234, 234, 24}, - wantInput: [][]uint64{ - []uint64{22, 123, 53}, - []uint64{234, 23, 4}, - []uint64{2423, 24, 23}, - []uint64{43, 34, 234}, - []uint64{234, 24, 197}, - []uint64{260, 2469, 310}, - []uint64{454, 3038}, - }, - wantOutput: [][]uint64{ - []uint64{197}, - []uint64{260}, - []uint64{2469}, - []uint64{310}, - []uint64{454}, - []uint64{3038}, - []uint64{3491}, - }, - wantUtxo: 3491 * chainTxMergeGas, - }, - } - - acct, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "testAccount", signers.BIP0044) - if err != nil { - t.Fatal(err) - } - - acp, err := m.CreateAddress(acct.ID, false) - if err != nil { - t.Fatal(err) - } - - for caseIndex, c := range cases { - utxos := []*UTXO{} - for _, amount := range c.inputUtxo { - utxos = append(utxos, &UTXO{ - Amount: amount * chainTxMergeGas, - AssetID: *consensus.BTMAssetID, - Address: acp.Address, - ControlProgram: acp.ControlProgram, - }) - } - - tpls, gotUtxo, err := m.buildBtmTxChain(utxos, acct.Signer) - if err != nil { - t.Fatal(err) - } - - for i, tpl := range tpls { - gotInput := []uint64{} - for _, input := range tpl.Transaction.Inputs { - gotInput = append(gotInput, input.Amount()/chainTxMergeGas) - } - - gotOutput := []uint64{} - for _, output := range tpl.Transaction.Outputs { - gotOutput = append(gotOutput, output.AssetAmount().Amount/chainTxMergeGas) - } - - if !testutil.DeepEqual(c.wantInput[i], gotInput) { - t.Fatalf("case %d tx %d input got %d want %d", caseIndex, i, gotInput, c.wantInput[i]) - } - if !testutil.DeepEqual(c.wantOutput[i], gotOutput) { - t.Fatalf("case %d tx %d output got %d want %d", caseIndex, i, gotOutput, c.wantOutput[i]) - } - } - - if c.wantUtxo != gotUtxo.Amount { - t.Fatalf("case %d got utxo=%d want utxo=%d", caseIndex, gotUtxo.Amount, c.wantUtxo) - } - } - -} - func TestMergeSpendAction(t *testing.T) { testBTM := &bc.AssetID{} if err := testBTM.UnmarshalText([]byte("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); err != nil { @@ -586,3 +401,66 @@ func TestCalcMergeGas(t *testing.T) { } } } + +func TestReserveBtmUtxoChain(t *testing.T) { + chainTxUtxoNum = 3 + utxos := []*UTXO{} + m := mockAccountManager(t) + for i := uint64(1); i <= 20; i++ { + utxo := &UTXO{ + OutputID: bc.Hash{V0: i}, + AccountID: "TestAccountID", + AssetID: *consensus.BTMAssetID, + Amount: i * chainTxMergeGas, + } + utxos = append(utxos, utxo) + + m.store.SetStandardUTXO(utxo.OutputID, utxo) + } + + cases := []struct { + amount uint64 + want []uint64 + err bool + }{ + { + amount: 1 * chainTxMergeGas, + want: []uint64{1}, + }, + { + amount: 888888 * chainTxMergeGas, + want: []uint64{}, + err: true, + }, + { + amount: 7 * chainTxMergeGas, + want: []uint64{4, 3, 1}, + }, + { + amount: 15 * chainTxMergeGas, + want: []uint64{5, 4, 3, 2, 1, 6}, + }, + { + amount: 163 * chainTxMergeGas, + want: []uint64{20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 2, 1, 3}, + }, + } + + for i, c := range cases { + m.utxoKeeper.expireReservation(time.Unix(999999999, 0)) + utxos, err := m.reserveBtmUtxoChain(&txbuilder.TemplateBuilder{}, "TestAccountID", c.amount, false) + + if err != nil != c.err { + t.Fatalf("case %d got err %v want err = %v", i, err, c.err) + } + + got := []uint64{} + for _, utxo := range utxos { + got = append(got, utxo.Amount/chainTxMergeGas) + } + + if !testutil.DeepEqual(got, c.want) { + t.Fatalf("case %d got %d want %d", i, got, c.want) + } + } +} diff --git a/account/image.go b/account/image.go index 3562c5e9..b3ce30e1 100644 --- a/account/image.go +++ b/account/image.go @@ -2,8 +2,6 @@ package account import ( - "encoding/json" - log "github.com/sirupsen/logrus" ) @@ -27,17 +25,15 @@ func (m *Manager) Backup() (*Image, error) { Slice: []*ImageSlice{}, } - accountIter := m.db.IteratorPrefix(accountPrefix) - defer accountIter.Release() - for accountIter.Next() { - a := &Account{} - if err := json.Unmarshal(accountIter.Value(), a); err != nil { - return nil, err - } + accounts, err := m.store.ListAccounts("") + if err != nil { + return nil, err + } + for _, account := range accounts { image.Slice = append(image.Slice, &ImageSlice{ - Account: a, - ContractIndex: m.GetContractIndex(a.ID), + Account: account, + ContractIndex: m.store.GetContractIndex(account.ID), }) } return image, nil @@ -48,29 +44,30 @@ func (m *Manager) Restore(image *Image) error { m.accountMu.Lock() defer m.accountMu.Unlock() - storeBatch := m.db.NewBatch() + newStore := m.store.InitBatch() + for _, slice := range image.Slice { - if existed := m.db.Get(Key(slice.Account.ID)); existed != nil { + if _, err := newStore.GetAccountByID(slice.Account.ID); err == nil { log.WithFields(log.Fields{ "module": logModule, "alias": slice.Account.Alias, "id": slice.Account.ID, }).Warning("skip restore account due to already existed") continue + } else if err != ErrFindAccount { + return err } - if existed := m.db.Get(aliasKey(slice.Account.Alias)); existed != nil { + + if _, err := newStore.GetAccountByAlias(slice.Account.Alias); err == nil { return ErrDuplicateAlias + } else if err != ErrFindAccount { + return err } - rawAccount, err := json.Marshal(slice.Account) - if err != nil { - return ErrMarshalAccount + if err := newStore.SetAccount(slice.Account); err != nil { + return err } - - storeBatch.Set(Key(slice.Account.ID), rawAccount) - storeBatch.Set(aliasKey(slice.Account.Alias), []byte(slice.Account.ID)) } - storeBatch.Write() - return nil + return newStore.CommitBatch() } diff --git a/account/indexer.go b/account/indexer.go index dd96a7eb..7e8b3de6 100644 --- a/account/indexer.go +++ b/account/indexer.go @@ -2,28 +2,8 @@ package account import ( "github.com/vapor/blockchain/query" - "github.com/vapor/protocol/bc" ) -const ( - //UTXOPreFix is StandardUTXOKey prefix - UTXOPreFix = "ACU:" - //SUTXOPrefix is ContractUTXOKey prefix - SUTXOPrefix = "SCU:" -) - -// StandardUTXOKey makes an account unspent outputs key to store -func StandardUTXOKey(id bc.Hash) []byte { - name := id.String() - return []byte(UTXOPreFix + name) -} - -// ContractUTXOKey makes a smart contract unspent outputs key to store -func ContractUTXOKey(id bc.Hash) []byte { - name := id.String() - return []byte(SUTXOPrefix + name) -} - //Annotated init an annotated account object func Annotated(a *Account) *query.AnnotatedAccount { return &query.AnnotatedAccount{ diff --git a/account/store.go b/account/store.go new file mode 100644 index 00000000..b502980c --- /dev/null +++ b/account/store.go @@ -0,0 +1,34 @@ +package account + +import ( + "github.com/vapor/crypto/ed25519/chainkd" + "github.com/vapor/protocol/bc" +) + +// AccountStore interface contains account storage functions. +type AccountStore interface { + InitBatch() AccountStore + CommitBatch() error + DeleteAccount(*Account) error + DeleteStandardUTXO(bc.Hash) + GetAccountByAlias(string) (*Account, error) + GetAccountByID(string) (*Account, error) + GetAccountIndex([]chainkd.XPub) uint64 + GetBip44ContractIndex(string, bool) uint64 + GetCoinbaseArbitrary() []byte + GetContractIndex(string) uint64 + GetControlProgram(bc.Hash) (*CtrlProgram, error) + GetMiningAddress() (*CtrlProgram, error) + GetUTXO(bc.Hash) (*UTXO, error) + ListAccounts(string) ([]*Account, error) + ListControlPrograms() ([]*CtrlProgram, error) + ListUTXOs() ([]*UTXO, error) + SetAccount(*Account) error + SetAccountIndex(*Account) + SetBip44ContractIndex(string, bool, uint64) + SetCoinbaseArbitrary([]byte) + SetContractIndex(string, uint64) + SetControlProgram(bc.Hash, *CtrlProgram) error + SetMiningAddress(*CtrlProgram) error + SetStandardUTXO(bc.Hash, *UTXO) error +} diff --git a/account/utxo_keeper.go b/account/utxo_keeper.go index ed4d685c..4af1a5db 100644 --- a/account/utxo_keeper.go +++ b/account/utxo_keeper.go @@ -3,15 +3,12 @@ package account import ( "bytes" "container/list" - "encoding/json" "sort" "sync" "sync/atomic" "time" log "github.com/sirupsen/logrus" - - dbm "github.com/vapor/database/leveldb" "github.com/vapor/errors" "github.com/vapor/protocol/bc" ) @@ -56,7 +53,7 @@ type utxoKeeper struct { // `sync/atomic` expects the first word in an allocated struct to be 64-bit // aligned on both ARM and x86-32. See https://goo.gl/zW7dgq for more details. nextIndex uint64 - db dbm.DB + store AccountStore mtx sync.RWMutex currentHeight func() uint64 @@ -65,9 +62,9 @@ type utxoKeeper struct { reservations map[uint64]*reservation } -func newUtxoKeeper(f func() uint64, walletdb dbm.DB) *utxoKeeper { +func newUtxoKeeper(f func() uint64, store AccountStore) *utxoKeeper { uk := &utxoKeeper{ - db: walletdb, + store: store, currentHeight: f, unconfirmed: make(map[bc.Hash]*UTXO), reserved: make(map[bc.Hash]uint64), @@ -216,6 +213,7 @@ func (uk *utxoKeeper) findUtxos(accountID string, assetID *bc.AssetID, useUnconf if u.AccountID != accountID || u.AssetID != *assetID || !bytes.Equal(u.Vote, vote) { return } + if u.ValidHeight > currentHeight { immatureAmount += u.Amount } else { @@ -223,16 +221,15 @@ func (uk *utxoKeeper) findUtxos(accountID string, assetID *bc.AssetID, useUnconf } } - utxoIter := uk.db.IteratorPrefix([]byte(UTXOPreFix)) - defer utxoIter.Release() - for utxoIter.Next() { - u := &UTXO{} - if err := json.Unmarshal(utxoIter.Value(), u); err != nil { - log.WithFields(log.Fields{"module": logModule, "err": err}).Error("utxoKeeper findUtxos fail on unmarshal utxo") - continue - } - appendUtxo(u) + UTXOs, err := uk.store.ListUTXOs() + if err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err}).Error("utxoKeeper findUtxos fail on unmarshal utxo") } + + for _, UTXO := range UTXOs { + appendUtxo(UTXO) + } + if !useUnconfirmed { return utxos, immatureAmount } @@ -247,15 +244,7 @@ func (uk *utxoKeeper) findUtxo(outHash bc.Hash, useUnconfirmed bool) (*UTXO, err if u, ok := uk.unconfirmed[outHash]; useUnconfirmed && ok { return u, nil } - - u := &UTXO{} - if data := uk.db.Get(StandardUTXOKey(outHash)); data != nil { - return u, json.Unmarshal(data, u) - } - if data := uk.db.Get(ContractUTXOKey(outHash)); data != nil { - return u, json.Unmarshal(data, u) - } - return nil, ErrMatchUTXO + return uk.store.GetUTXO(outHash) } func (uk *utxoKeeper) optUTXOs(utxos []*UTXO, amount uint64) ([]*UTXO, uint64, uint64) { diff --git a/account/utxo_keeper_test.go b/account/utxo_keeper_test.go index 6cd48791..50b2d77e 100644 --- a/account/utxo_keeper_test.go +++ b/account/utxo_keeper_test.go @@ -2,10 +2,14 @@ package account import ( "encoding/json" + "io/ioutil" "os" "testing" "time" + "github.com/golang/groupcache/lru" + "github.com/vapor/blockchain/txbuilder" + "github.com/vapor/crypto/ed25519/chainkd" dbm "github.com/vapor/database/leveldb" "github.com/vapor/protocol/bc" "github.com/vapor/testutil" @@ -278,6 +282,8 @@ func TestReserve(t *testing.T) { os.RemoveAll("temp") }() + accountStore := newMockAccountStore(testDB) + cases := []struct { before utxoKeeper after utxoKeeper @@ -288,13 +294,13 @@ func TestReserve(t *testing.T) { }{ { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, reserved: map[bc.Hash]uint64{}, reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, reserved: map[bc.Hash]uint64{}, reservations: map[uint64]*reservation{}, @@ -304,7 +310,7 @@ func TestReserve(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -317,7 +323,7 @@ func TestReserve(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -334,7 +340,7 @@ func TestReserve(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -348,7 +354,7 @@ func TestReserve(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -366,7 +372,7 @@ func TestReserve(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -381,7 +387,7 @@ func TestReserve(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -400,7 +406,7 @@ func TestReserve(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -413,7 +419,7 @@ func TestReserve(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -446,7 +452,7 @@ func TestReserve(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, nextIndex: 1, unconfirmed: map[bc.Hash]*UTXO{ @@ -472,7 +478,7 @@ func TestReserve(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -522,7 +528,7 @@ func TestReserve(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -536,7 +542,7 @@ func TestReserve(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -585,6 +591,8 @@ func TestReserveParticular(t *testing.T) { testDB := dbm.NewDB("testdb", "leveldb", "temp") defer os.RemoveAll("temp") + accountStore := newMockAccountStore(testDB) + cases := []struct { before utxoKeeper after utxoKeeper @@ -594,7 +602,7 @@ func TestReserveParticular(t *testing.T) { }{ { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -609,7 +617,7 @@ func TestReserveParticular(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -628,7 +636,7 @@ func TestReserveParticular(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -642,7 +650,7 @@ func TestReserveParticular(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -660,7 +668,7 @@ func TestReserveParticular(t *testing.T) { }, { before: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -673,7 +681,7 @@ func TestReserveParticular(t *testing.T) { reservations: map[uint64]*reservation{}, }, after: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -743,6 +751,8 @@ func TestFindUtxos(t *testing.T) { os.RemoveAll("temp") }() + accountStore := newMockAccountStore(testDB) + cases := []struct { uk utxoKeeper dbUtxos []*UTXO @@ -753,7 +763,7 @@ func TestFindUtxos(t *testing.T) { }{ { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{}, }, @@ -764,7 +774,7 @@ func TestFindUtxos(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{}, }, @@ -793,7 +803,7 @@ func TestFindUtxos(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{}, }, @@ -811,7 +821,7 @@ func TestFindUtxos(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -840,7 +850,7 @@ func TestFindUtxos(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x11}): &UTXO{ @@ -874,7 +884,7 @@ func TestFindUtxos(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{ @@ -918,7 +928,7 @@ func TestFindUtxos(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{}, }, @@ -946,11 +956,9 @@ func TestFindUtxos(t *testing.T) { for i, c := range cases { for _, u := range c.dbUtxos { - data, err := json.Marshal(u) - if err != nil { + if err := c.uk.store.SetStandardUTXO(u.OutputID, u); err != nil { t.Error(err) } - testDB.Set(StandardUTXOKey(u.OutputID), data) } gotUtxos, immatureAmount := c.uk.findUtxos("testAccount", &bc.AssetID{}, c.useUnconfirmed, c.vote) @@ -962,16 +970,18 @@ func TestFindUtxos(t *testing.T) { } for _, u := range c.dbUtxos { - testDB.Delete(StandardUTXOKey(u.OutputID)) + c.uk.store.DeleteStandardUTXO(u.OutputID) } } } -func TestFindUtxo(t *testing.T) { +func TestFindUTXO(t *testing.T) { currentHeight := func() uint64 { return 9527 } testDB := dbm.NewDB("testdb", "leveldb", "temp") defer os.RemoveAll("temp") + accountStore := newMockAccountStore(testDB) + cases := []struct { uk utxoKeeper dbUtxos map[string]*UTXO @@ -982,7 +992,7 @@ func TestFindUtxo(t *testing.T) { }{ { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{}, }, @@ -993,7 +1003,7 @@ func TestFindUtxo(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})}, @@ -1007,7 +1017,7 @@ func TestFindUtxo(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})}, @@ -1021,7 +1031,7 @@ func TestFindUtxo(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{}, }, @@ -1035,7 +1045,7 @@ func TestFindUtxo(t *testing.T) { }, { uk: utxoKeeper{ - db: testDB, + store: accountStore, currentHeight: currentHeight, unconfirmed: map[bc.Hash]*UTXO{}, }, @@ -1067,7 +1077,7 @@ func TestFindUtxo(t *testing.T) { } for _, u := range c.dbUtxos { - testDB.Delete(StandardUTXOKey(u.OutputID)) + c.uk.store.DeleteStandardUTXO(u.OutputID) } } } @@ -1300,3 +1310,165 @@ func checkUtxoKeeperEqual(t *testing.T, i int, a, b *utxoKeeper) { t.Errorf("case %d: reservations got %v want %v", i, a.reservations, b.reservations) } } + +const ( + utxoPrefix byte = iota //UTXOPrefix is StandardUTXOKey prefix + contractPrefix + contractIndexPrefix + accountPrefix // AccountPrefix is account ID prefix + accountIndexPrefix +) + +// leveldb key prefix +var ( + colon byte = 0x3a + accountStore = []byte("AS:") + UTXOPrefix = append(accountStore, utxoPrefix, colon) + ContractPrefix = append(accountStore, contractPrefix, colon) + ContractIndexPrefix = append(accountStore, contractIndexPrefix, colon) + AccountPrefix = append(accountStore, accountPrefix, colon) // AccountPrefix is account ID prefix + AccountIndexPrefix = append(accountStore, accountIndexPrefix, colon) +) + +const ( + sutxoPrefix byte = iota //SUTXOPrefix is ContractUTXOKey prefix + accountAliasPrefix + txPrefix //TxPrefix is wallet database transactions prefix + txIndexPrefix //TxIndexPrefix is wallet database tx index prefix + unconfirmedTxPrefix //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix + globalTxIndexPrefix //GlobalTxIndexPrefix is wallet database global tx index prefix + walletKey + miningAddressKey + coinbaseAbKey + recoveryKey //recoveryKey key for db store recovery info. +) + +var ( + walletStore = []byte("WS:") + SUTXOPrefix = append(walletStore, sutxoPrefix, colon) + AccountAliasPrefix = append(walletStore, accountAliasPrefix, colon) + TxPrefix = append(walletStore, txPrefix, colon) //TxPrefix is wallet database transactions prefix + TxIndexPrefix = append(walletStore, txIndexPrefix, colon) //TxIndexPrefix is wallet database tx index prefix + UnconfirmedTxPrefix = append(walletStore, unconfirmedTxPrefix, colon) //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix + GlobalTxIndexPrefix = append(walletStore, globalTxIndexPrefix, colon) //GlobalTxIndexPrefix is wallet database global tx index prefix + WalletKey = append(walletStore, walletKey) + MiningAddressKey = append(walletStore, miningAddressKey) + CoinbaseAbKey = append(walletStore, coinbaseAbKey) + RecoveryKey = append(walletStore, recoveryKey) +) + +type mockAccountStore struct { + db dbm.DB + batch dbm.Batch +} + +// NewAccountStore create new AccountStore. +func newMockAccountStore(db dbm.DB) *mockAccountStore { + return &mockAccountStore{ + db: db, + batch: nil, + } +} + +// StandardUTXOKey makes an account unspent outputs key to store +func StandardUTXOKey(id bc.Hash) []byte { + return append(UTXOPrefix, id.Bytes()...) +} + +// ContractUTXOKey makes a smart contract unspent outputs key to store +func ContractUTXOKey(id bc.Hash) []byte { + return append(SUTXOPrefix, id.Bytes()...) +} + +func (store *mockAccountStore) InitBatch() AccountStore { return nil } +func (store *mockAccountStore) CommitBatch() error { return nil } +func (store *mockAccountStore) DeleteAccount(*Account) error { return nil } +func (store *mockAccountStore) GetAccountByAlias(string) (*Account, error) { return nil, nil } +func (store *mockAccountStore) GetAccountByID(string) (*Account, error) { return nil, nil } +func (store *mockAccountStore) GetAccountIndex([]chainkd.XPub) uint64 { return 0 } +func (store *mockAccountStore) GetBip44ContractIndex(string, bool) uint64 { return 0 } +func (store *mockAccountStore) GetCoinbaseArbitrary() []byte { return nil } +func (store *mockAccountStore) GetContractIndex(string) uint64 { return 0 } +func (store *mockAccountStore) GetControlProgram(bc.Hash) (*CtrlProgram, error) { return nil, nil } +func (store *mockAccountStore) GetMiningAddress() (*CtrlProgram, error) { return nil, nil } +func (store *mockAccountStore) ListAccounts(string) ([]*Account, error) { return nil, nil } +func (store *mockAccountStore) ListControlPrograms() ([]*CtrlProgram, error) { return nil, nil } +func (store *mockAccountStore) SetAccount(*Account) error { return nil } +func (store *mockAccountStore) SetAccountIndex(*Account) { return } +func (store *mockAccountStore) SetBip44ContractIndex(string, bool, uint64) { return } +func (store *mockAccountStore) SetCoinbaseArbitrary([]byte) { return } +func (store *mockAccountStore) SetContractIndex(string, uint64) { return } +func (store *mockAccountStore) SetControlProgram(bc.Hash, *CtrlProgram) error { return nil } +func (store *mockAccountStore) SetMiningAddress(*CtrlProgram) error { return nil } + +// DeleteStandardUTXO delete utxo by outpu id +func (store *mockAccountStore) DeleteStandardUTXO(outputID bc.Hash) { + if store.batch == nil { + store.db.Delete(StandardUTXOKey(outputID)) + } else { + store.batch.Delete(StandardUTXOKey(outputID)) + } +} + +// GetUTXO get standard utxo by id +func (store *mockAccountStore) GetUTXO(outid bc.Hash) (*UTXO, error) { + u := new(UTXO) + if data := store.db.Get(StandardUTXOKey(outid)); data != nil { + return u, json.Unmarshal(data, u) + } + if data := store.db.Get(ContractUTXOKey(outid)); data != nil { + return u, json.Unmarshal(data, u) + } + return nil, ErrMatchUTXO +} + +// ListUTXOs get utxos by accountID +func (store *mockAccountStore) ListUTXOs() ([]*UTXO, error) { + utxoIter := store.db.IteratorPrefix([]byte(UTXOPrefix)) + defer utxoIter.Release() + + utxos := []*UTXO{} + for utxoIter.Next() { + utxo := new(UTXO) + if err := json.Unmarshal(utxoIter.Value(), utxo); err != nil { + return nil, err + } + utxos = append(utxos, utxo) + } + return utxos, nil +} + +// SetStandardUTXO set standard utxo +func (store *mockAccountStore) SetStandardUTXO(outputID bc.Hash, utxo *UTXO) error { + data, err := json.Marshal(utxo) + if err != nil { + return err + } + if store.batch == nil { + store.db.Set(StandardUTXOKey(outputID), data) + } else { + store.batch.Set(StandardUTXOKey(outputID), data) + } + return nil +} + +func mockAccountManager(t *testing.T) *Manager { + dirPath, err := ioutil.TempDir(".", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dirPath) + + testDB := dbm.NewDB("testdb", "memdb", dirPath) + accountStore := newMockAccountStore(testDB) + bestBlockHeight := func() uint64 { return 9527 } + + return &Manager{ + store: accountStore, + chain: nil, + utxoKeeper: newUtxoKeeper(bestBlockHeight, accountStore), + cache: lru.New(maxAccountCache), + aliasCache: lru.New(maxAccountCache), + delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram), + } +} diff --git a/database/account_store.go b/database/account_store.go new file mode 100644 index 00000000..619dd8d4 --- /dev/null +++ b/database/account_store.go @@ -0,0 +1,436 @@ +package database + +import ( + "encoding/json" + "sort" + "strings" + + acc "github.com/vapor/account" + "github.com/vapor/blockchain/signers" + "github.com/vapor/common" + "github.com/vapor/crypto/ed25519/chainkd" + "github.com/vapor/crypto/sha3pool" + dbm "github.com/vapor/database/leveldb" + "github.com/vapor/errors" + "github.com/vapor/protocol/bc" +) + +const ( + utxoPrefix byte = iota //UTXOPrefix is StandardUTXOKey prefix + contractPrefix + contractIndexPrefix + accountPrefix // AccountPrefix is account ID prefix + accountIndexPrefix +) + +// leveldb key prefix +var ( + accountStore = []byte("AS:") + UTXOPrefix = append(accountStore, utxoPrefix, colon) + ContractPrefix = append(accountStore, contractPrefix, colon) + ContractIndexPrefix = append(accountStore, contractIndexPrefix, colon) + AccountPrefix = append(accountStore, accountPrefix, colon) // AccountPrefix is account ID prefix + AccountIndexPrefix = append(accountStore, accountIndexPrefix, colon) +) + +func accountIndexKey(xpubs []chainkd.XPub) []byte { + var hash [32]byte + var xPubs []byte + cpy := append([]chainkd.XPub{}, xpubs[:]...) + sort.Sort(signers.SortKeys(cpy)) + for _, xpub := range cpy { + xPubs = append(xPubs, xpub[:]...) + } + sha3pool.Sum256(hash[:], xPubs) + return append(AccountIndexPrefix, hash[:]...) +} + +func Bip44ContractIndexKey(accountID string, change bool) []byte { + key := append(ContractIndexPrefix, []byte(accountID)...) + if change { + return append(key, 0x01) + } + return append(key, 0x00) +} + +// ContractKey account control promgram store prefix +func ContractKey(hash bc.Hash) []byte { + return append(ContractPrefix, hash.Bytes()...) +} + +// AccountIDKey account id store prefix +func AccountIDKey(accountID string) []byte { + return append(AccountPrefix, []byte(accountID)...) +} + +// StandardUTXOKey makes an account unspent outputs key to store +func StandardUTXOKey(id bc.Hash) []byte { + return append(UTXOPrefix, id.Bytes()...) +} + +func accountAliasKey(name string) []byte { + return append(AccountAliasPrefix, []byte(name)...) +} + +// AccountStore satisfies AccountStore interface. +type AccountStore struct { + db dbm.DB + batch dbm.Batch +} + +// NewAccountStore create new AccountStore. +func NewAccountStore(db dbm.DB) *AccountStore { + return &AccountStore{ + db: db, + batch: nil, + } +} + +// InitBatch initial new account store +func (store *AccountStore) InitBatch() acc.AccountStore { + newStore := NewAccountStore(store.db) + newStore.batch = newStore.db.NewBatch() + return newStore +} + +// CommitBatch commit batch +func (store *AccountStore) CommitBatch() error { + if store.batch == nil { + return errors.New("AccountStore commit fail, store batch is nil.") + } + store.batch.Write() + store.batch = nil + return nil +} + +// DeleteAccount set account account ID, account alias and raw account. +func (store *AccountStore) DeleteAccount(account *acc.Account) error { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + + // delete account utxos + store.deleteAccountUTXOs(account.ID, batch) + + // delete account control program + if err := store.deleteAccountControlPrograms(account.ID, batch); err != nil { + return err + } + + // delete bip44 contract index + batch.Delete(Bip44ContractIndexKey(account.ID, false)) + batch.Delete(Bip44ContractIndexKey(account.ID, true)) + + // delete contract index + batch.Delete(contractIndexKey(account.ID)) + + // delete account id + batch.Delete(AccountIDKey(account.ID)) + batch.Delete(accountAliasKey(account.Alias)) + if store.batch == nil { + batch.Write() + } + return nil +} + +// deleteAccountUTXOs delete account utxos by accountID +func (store *AccountStore) deleteAccountUTXOs(accountID string, batch dbm.Batch) error { + accountUtxoIter := store.db.IteratorPrefix(UTXOPrefix) + defer accountUtxoIter.Release() + + for accountUtxoIter.Next() { + accountUtxo := new(acc.UTXO) + if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil { + return err + } + + if accountID == accountUtxo.AccountID { + batch.Delete(StandardUTXOKey(accountUtxo.OutputID)) + } + } + + return nil +} + +// deleteAccountControlPrograms deletes account control program +func (store *AccountStore) deleteAccountControlPrograms(accountID string, batch dbm.Batch) error { + cps, err := store.ListControlPrograms() + if err != nil { + return err + } + + var hash [32]byte + for _, cp := range cps { + if cp.AccountID == accountID { + sha3pool.Sum256(hash[:], cp.ControlProgram) + batch.Delete(ContractKey(bc.NewHash(hash))) + } + } + return nil +} + +// DeleteStandardUTXO delete utxo by outpu id +func (store *AccountStore) DeleteStandardUTXO(outputID bc.Hash) { + if store.batch == nil { + store.db.Delete(StandardUTXOKey(outputID)) + } else { + store.batch.Delete(StandardUTXOKey(outputID)) + } +} + +// GetAccountByAlias get account by account alias +func (store *AccountStore) GetAccountByAlias(accountAlias string) (*acc.Account, error) { + accountID := store.db.Get(accountAliasKey(accountAlias)) + if accountID == nil { + return nil, acc.ErrFindAccount + } + + return store.GetAccountByID(string(accountID)) +} + +// GetAccountByID get account by accountID +func (store *AccountStore) GetAccountByID(accountID string) (*acc.Account, error) { + rawAccount := store.db.Get(AccountIDKey(accountID)) + if rawAccount == nil { + return nil, acc.ErrFindAccount + } + + account := new(acc.Account) + if err := json.Unmarshal(rawAccount, account); err != nil { + return nil, err + } + + return account, nil +} + +// GetAccountIndex get account index by account xpubs +func (store *AccountStore) GetAccountIndex(xpubs []chainkd.XPub) uint64 { + currentIndex := uint64(0) + if rawIndexBytes := store.db.Get(accountIndexKey(xpubs)); rawIndexBytes != nil { + currentIndex = common.BytesToUnit64(rawIndexBytes) + } + return currentIndex +} + +// GetBip44ContractIndex get bip44 contract index +func (store *AccountStore) GetBip44ContractIndex(accountID string, change bool) uint64 { + index := uint64(0) + if rawIndexBytes := store.db.Get(Bip44ContractIndexKey(accountID, change)); rawIndexBytes != nil { + index = common.BytesToUnit64(rawIndexBytes) + } + return index +} + +// GetCoinbaseArbitrary get coinbase arbitrary +func (store *AccountStore) GetCoinbaseArbitrary() []byte { + return store.db.Get(CoinbaseAbKey) +} + +// GetContractIndex get contract index +func (store *AccountStore) GetContractIndex(accountID string) uint64 { + index := uint64(0) + if rawIndexBytes := store.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil { + index = common.BytesToUnit64(rawIndexBytes) + } + return index +} + +// GetControlProgram get control program +func (store *AccountStore) GetControlProgram(hash bc.Hash) (*acc.CtrlProgram, error) { + rawProgram := store.db.Get(ContractKey(hash)) + if rawProgram == nil { + return nil, acc.ErrFindCtrlProgram + } + + cp := new(acc.CtrlProgram) + if err := json.Unmarshal(rawProgram, cp); err != nil { + return nil, err + } + + return cp, nil +} + +// GetMiningAddress get mining address +func (store *AccountStore) GetMiningAddress() (*acc.CtrlProgram, error) { + rawCP := store.db.Get(MiningAddressKey) + if rawCP == nil { + return nil, acc.ErrFindMiningAddress + } + + cp := new(acc.CtrlProgram) + if err := json.Unmarshal(rawCP, cp); err != nil { + return nil, err + } + + return cp, nil +} + +// GetUTXO get standard utxo by id +func (store *AccountStore) GetUTXO(outid bc.Hash) (*acc.UTXO, error) { + u := new(acc.UTXO) + if data := store.db.Get(StandardUTXOKey(outid)); data != nil { + return u, json.Unmarshal(data, u) + } + + if data := store.db.Get(ContractUTXOKey(outid)); data != nil { + return u, json.Unmarshal(data, u) + } + + return nil, acc.ErrMatchUTXO +} + +// ListAccounts get all accounts which name prfix is id. +func (store *AccountStore) ListAccounts(id string) ([]*acc.Account, error) { + accounts := []*acc.Account{} + accountIter := store.db.IteratorPrefix(AccountIDKey(strings.TrimSpace(id))) + defer accountIter.Release() + + for accountIter.Next() { + account := new(acc.Account) + if err := json.Unmarshal(accountIter.Value(), account); err != nil { + return nil, err + } + + accounts = append(accounts, account) + } + return accounts, nil +} + +// ListControlPrograms get all local control programs +func (store *AccountStore) ListControlPrograms() ([]*acc.CtrlProgram, error) { + cps := []*acc.CtrlProgram{} + cpIter := store.db.IteratorPrefix(ContractPrefix) + defer cpIter.Release() + + for cpIter.Next() { + cp := new(acc.CtrlProgram) + if err := json.Unmarshal(cpIter.Value(), cp); err != nil { + return nil, err + } + + cps = append(cps, cp) + } + return cps, nil +} + +// ListUTXOs get utxos by accountID +func (store *AccountStore) ListUTXOs() ([]*acc.UTXO, error) { + utxoIter := store.db.IteratorPrefix(UTXOPrefix) + defer utxoIter.Release() + + utxos := []*acc.UTXO{} + for utxoIter.Next() { + utxo := new(acc.UTXO) + if err := json.Unmarshal(utxoIter.Value(), utxo); err != nil { + return nil, err + } + + utxos = append(utxos, utxo) + } + return utxos, nil +} + +// SetAccount set account account ID, account alias and raw account. +func (store *AccountStore) SetAccount(account *acc.Account) error { + rawAccount, err := json.Marshal(account) + if err != nil { + return acc.ErrMarshalAccount + } + + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + + batch.Set(AccountIDKey(account.ID), rawAccount) + batch.Set(accountAliasKey(account.Alias), []byte(account.ID)) + + if store.batch == nil { + batch.Write() + } + return nil +} + +// SetAccountIndex update account index +func (store *AccountStore) SetAccountIndex(account *acc.Account) { + currentIndex := store.GetAccountIndex(account.XPubs) + if account.KeyIndex > currentIndex { + if store.batch == nil { + store.db.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex)) + } else { + store.batch.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex)) + } + } +} + +// SetBip44ContractIndex set contract index +func (store *AccountStore) SetBip44ContractIndex(accountID string, change bool, index uint64) { + if store.batch == nil { + store.db.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index)) + } else { + store.batch.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index)) + } +} + +// SetCoinbaseArbitrary set coinbase arbitrary +func (store *AccountStore) SetCoinbaseArbitrary(arbitrary []byte) { + if store.batch == nil { + store.db.Set(CoinbaseAbKey, arbitrary) + } else { + store.batch.Set(CoinbaseAbKey, arbitrary) + } +} + +// SetContractIndex set contract index +func (store *AccountStore) SetContractIndex(accountID string, index uint64) { + if store.batch == nil { + store.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(index)) + } else { + store.batch.Set(contractIndexKey(accountID), common.Unit64ToBytes(index)) + } +} + +// SetControlProgram set raw program +func (store *AccountStore) SetControlProgram(hash bc.Hash, program *acc.CtrlProgram) error { + accountCP, err := json.Marshal(program) + if err != nil { + return err + } + if store.batch == nil { + store.db.Set(ContractKey(hash), accountCP) + } else { + store.batch.Set(ContractKey(hash), accountCP) + } + return nil +} + +// SetMiningAddress set mining address +func (store *AccountStore) SetMiningAddress(program *acc.CtrlProgram) error { + rawProgram, err := json.Marshal(program) + if err != nil { + return err + } + + if store.batch == nil { + store.db.Set(MiningAddressKey, rawProgram) + } else { + store.batch.Set(MiningAddressKey, rawProgram) + } + return nil +} + +// SetStandardUTXO set standard utxo +func (store *AccountStore) SetStandardUTXO(outputID bc.Hash, utxo *acc.UTXO) error { + data, err := json.Marshal(utxo) + if err != nil { + return err + } + + if store.batch == nil { + store.db.Set(StandardUTXOKey(outputID), data) + } else { + store.batch.Set(StandardUTXOKey(outputID), data) + } + return nil +} diff --git a/database/wallet_store.go b/database/wallet_store.go new file mode 100644 index 00000000..85eb2e16 --- /dev/null +++ b/database/wallet_store.go @@ -0,0 +1,500 @@ +package database + +import ( + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + + log "github.com/sirupsen/logrus" + + acc "github.com/vapor/account" + "github.com/vapor/asset" + "github.com/vapor/blockchain/query" + dbm "github.com/vapor/database/leveldb" + "github.com/vapor/errors" + "github.com/vapor/protocol/bc" + "github.com/vapor/wallet" +) + +const ( + sutxoPrefix byte = iota //SUTXOPrefix is ContractUTXOKey prefix + accountAliasPrefix + txPrefix //TxPrefix is wallet database transactions prefix + txIndexPrefix //TxIndexPrefix is wallet database tx index prefix + unconfirmedTxPrefix //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix + globalTxIndexPrefix //GlobalTxIndexPrefix is wallet database global tx index prefix + walletKey + miningAddressKey + coinbaseAbKey + recoveryKey //recoveryKey key for db store recovery info. +) + +var ( + walletStore = []byte("WS:") + SUTXOPrefix = append(walletStore, sutxoPrefix, colon) + AccountAliasPrefix = append(walletStore, accountAliasPrefix, colon) + TxPrefix = append(walletStore, txPrefix, colon) //TxPrefix is wallet database transactions prefix + TxIndexPrefix = append(walletStore, txIndexPrefix, colon) //TxIndexPrefix is wallet database tx index prefix + UnconfirmedTxPrefix = append(walletStore, unconfirmedTxPrefix, colon) //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix + GlobalTxIndexPrefix = append(walletStore, globalTxIndexPrefix, colon) //GlobalTxIndexPrefix is wallet database global tx index prefix + WalletKey = append(walletStore, walletKey) + MiningAddressKey = append(walletStore, miningAddressKey) + CoinbaseAbKey = append(walletStore, coinbaseAbKey) + RecoveryKey = append(walletStore, recoveryKey) +) + +// ContractUTXOKey makes a smart contract unspent outputs key to store +func ContractUTXOKey(id bc.Hash) []byte { + return append(SUTXOPrefix, id.Bytes()...) +} + +func calcDeleteKey(blockHeight uint64) []byte { + return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight)) +} + +func calcTxIndexKey(txID string) []byte { + return append(TxIndexPrefix, []byte(txID)...) +} + +func calcAnnotatedKey(formatKey string) []byte { + return append(TxPrefix, []byte(formatKey)...) +} + +func calcUnconfirmedTxKey(formatKey string) []byte { + return append(UnconfirmedTxPrefix, []byte(formatKey)...) +} + +func CalcGlobalTxIndexKey(txID string) []byte { + return append(GlobalTxIndexPrefix, []byte(txID)...) +} + +func CalcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte { + txIdx := make([]byte, 40) + copy(txIdx[:32], blockHash.Bytes()) + binary.BigEndian.PutUint64(txIdx[32:], position) + return txIdx +} + +func formatKey(blockHeight uint64, position uint32) string { + return fmt.Sprintf("%016x%08x", blockHeight, position) +} + +func contractIndexKey(accountID string) []byte { + return append(ContractIndexPrefix, []byte(accountID)...) +} + +// WalletStore store wallet using leveldb +type WalletStore struct { + db dbm.DB + batch dbm.Batch +} + +// NewWalletStore create new WalletStore struct +func NewWalletStore(db dbm.DB) *WalletStore { + return &WalletStore{ + db: db, + batch: nil, + } +} + +// InitBatch initial new wallet store +func (store *WalletStore) InitBatch() wallet.WalletStore { + newStore := NewWalletStore(store.db) + newStore.batch = newStore.db.NewBatch() + return newStore +} + +// CommitBatch commit batch +func (store *WalletStore) CommitBatch() error { + if store.batch == nil { + return errors.New("WalletStore commit fail, store batch is nil.") + } + + store.batch.Write() + store.batch = nil + return nil +} + +// DeleteContractUTXO delete contract utxo by outputID +func (store *WalletStore) DeleteContractUTXO(outputID bc.Hash) { + if store.batch == nil { + store.db.Delete(ContractUTXOKey(outputID)) + } else { + store.batch.Delete(ContractUTXOKey(outputID)) + } +} + +// DeleteRecoveryStatus delete recovery status +func (store *WalletStore) DeleteRecoveryStatus() { + if store.batch == nil { + store.db.Delete(RecoveryKey) + } else { + store.batch.Delete(RecoveryKey) + } +} + +// DeleteTransactions delete transactions when orphan block rollback +func (store *WalletStore) DeleteTransactions(height uint64) { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + txIter := store.db.IteratorPrefix(calcDeleteKey(height)) + defer txIter.Release() + + for txIter.Next() { + tmpTx := new(query.AnnotatedTx) + if err := json.Unmarshal(txIter.Value(), tmpTx); err == nil { + batch.Delete(calcTxIndexKey(tmpTx.ID.String())) + } else { + log.WithFields(log.Fields{"module": logModule, "err": err}).Warning("fail on DeleteTransactions.") + } + batch.Delete(txIter.Key()) + } + if store.batch == nil { + batch.Write() + } +} + +// DeleteUnconfirmedTransaction delete unconfirmed tx by txID +func (store *WalletStore) DeleteUnconfirmedTransaction(txID string) { + if store.batch == nil { + store.db.Delete(calcUnconfirmedTxKey(txID)) + } else { + store.batch.Delete(calcUnconfirmedTxKey(txID)) + } +} + +// DeleteWalletTransactions delete all txs in wallet +func (store *WalletStore) DeleteWalletTransactions() { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + txIter := store.db.IteratorPrefix(TxPrefix) + defer txIter.Release() + + for txIter.Next() { + batch.Delete(txIter.Key()) + } + + txIndexIter := store.db.IteratorPrefix(TxIndexPrefix) + defer txIndexIter.Release() + + for txIndexIter.Next() { + batch.Delete(txIndexIter.Key()) + } + if store.batch == nil { + batch.Write() + } +} + +// DeleteWalletUTXOs delete all txs in wallet +func (store *WalletStore) DeleteWalletUTXOs() { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + + ruIter := store.db.IteratorPrefix(UTXOPrefix) + defer ruIter.Release() + + for ruIter.Next() { + batch.Delete(ruIter.Key()) + } + + suIter := store.db.IteratorPrefix(SUTXOPrefix) + defer suIter.Release() + + for suIter.Next() { + batch.Delete(suIter.Key()) + } + if store.batch == nil { + batch.Write() + } +} + +// GetAsset get asset by assetID +func (store *WalletStore) GetAsset(assetID *bc.AssetID) (*asset.Asset, error) { + definitionByte := store.db.Get(asset.ExtAssetKey(assetID)) + if definitionByte == nil { + return nil, wallet.ErrGetAsset + } + + definitionMap := make(map[string]interface{}) + if err := json.Unmarshal(definitionByte, &definitionMap); err != nil { + return nil, err + } + + alias := assetID.String() + externalAsset := &asset.Asset{ + AssetID: *assetID, + Alias: &alias, + DefinitionMap: definitionMap, + RawDefinitionByte: definitionByte, + } + return externalAsset, nil +} + +// GetGlobalTransactionIndex get global tx by txID +func (store *WalletStore) GetGlobalTransactionIndex(txID string) []byte { + return store.db.Get(CalcGlobalTxIndexKey(txID)) +} + +// GetStandardUTXO get standard utxo by id +func (store *WalletStore) GetStandardUTXO(outid bc.Hash) (*acc.UTXO, error) { + rawUTXO := store.db.Get(StandardUTXOKey(outid)) + if rawUTXO == nil { + return nil, fmt.Errorf("failed get standard UTXO, outputID: %s ", outid.String()) + } + + UTXO := new(acc.UTXO) + if err := json.Unmarshal(rawUTXO, UTXO); err != nil { + return nil, err + } + + return UTXO, nil +} + +// GetTransaction get tx by txid +func (store *WalletStore) GetTransaction(txID string) (*query.AnnotatedTx, error) { + formatKey := store.db.Get(calcTxIndexKey(txID)) + if formatKey == nil { + return nil, wallet.ErrAccntTxIDNotFound + } + + rawTx := store.db.Get(calcAnnotatedKey(string(formatKey))) + tx := new(query.AnnotatedTx) + if err := json.Unmarshal(rawTx, tx); err != nil { + return nil, err + } + + return tx, nil +} + +// GetUnconfirmedTransaction get unconfirmed tx by txID +func (store *WalletStore) GetUnconfirmedTransaction(txID string) (*query.AnnotatedTx, error) { + rawUnconfirmedTx := store.db.Get(calcUnconfirmedTxKey(txID)) + if rawUnconfirmedTx == nil { + return nil, fmt.Errorf("failed get unconfirmed tx, txID: %s ", txID) + } + + tx := new(query.AnnotatedTx) + if err := json.Unmarshal(rawUnconfirmedTx, tx); err != nil { + return nil, err + } + + return tx, nil +} + +// GetRecoveryStatus delete recovery status +func (store *WalletStore) GetRecoveryStatus() (*wallet.RecoveryState, error) { + rawStatus := store.db.Get(RecoveryKey) + if rawStatus == nil { + return nil, wallet.ErrGetRecoveryStatus + } + + state := new(wallet.RecoveryState) + if err := json.Unmarshal(rawStatus, state); err != nil { + return nil, err + } + + return state, nil +} + +// GetWalletInfo get wallet information +func (store *WalletStore) GetWalletInfo() (*wallet.StatusInfo, error) { + rawStatus := store.db.Get(WalletKey) + if rawStatus == nil { + return nil, wallet.ErrGetWalletStatusInfo + } + + status := new(wallet.StatusInfo) + if err := json.Unmarshal(rawStatus, status); err != nil { + return nil, err + } + + return status, nil +} + +// ListAccountUTXOs get all account unspent outputs +func (store *WalletStore) ListAccountUTXOs(id string, isSmartContract bool) ([]*acc.UTXO, error) { + prefix := UTXOPrefix + if isSmartContract { + prefix = SUTXOPrefix + } + + idBytes, err := hex.DecodeString(id) + if err != nil { + return nil, err + } + + accountUtxoIter := store.db.IteratorPrefix(append(prefix, idBytes...)) + defer accountUtxoIter.Release() + + confirmedUTXOs := []*acc.UTXO{} + for accountUtxoIter.Next() { + utxo := new(acc.UTXO) + if err := json.Unmarshal(accountUtxoIter.Value(), utxo); err != nil { + return nil, err + } + + confirmedUTXOs = append(confirmedUTXOs, utxo) + } + return confirmedUTXOs, nil +} + +func (store *WalletStore) ListTransactions(accountID string, StartTxID string, count uint, unconfirmed bool) ([]*query.AnnotatedTx, error) { + annotatedTxs := []*query.AnnotatedTx{} + var startKey []byte + preFix := TxPrefix + + if StartTxID != "" { + if unconfirmed { + startKey = calcUnconfirmedTxKey(StartTxID) + } else { + formatKey := store.db.Get(calcTxIndexKey(StartTxID)) + if formatKey == nil { + return nil, wallet.ErrAccntTxIDNotFound + } + + startKey = calcAnnotatedKey(string(formatKey)) + } + } + + if unconfirmed { + preFix = UnconfirmedTxPrefix + } + + itr := store.db.IteratorPrefixWithStart(preFix, startKey, true) + defer itr.Release() + + for txNum := count; itr.Next() && txNum > 0; { + annotatedTx := new(query.AnnotatedTx) + if err := json.Unmarshal(itr.Value(), annotatedTx); err != nil { + return nil, err + } + + annotatedTxs = append(annotatedTxs, annotatedTx) + txNum-- + } + + return annotatedTxs, nil +} + +// ListUnconfirmedTransactions get all unconfirmed txs +func (store *WalletStore) ListUnconfirmedTransactions() ([]*query.AnnotatedTx, error) { + annotatedTxs := []*query.AnnotatedTx{} + txIter := store.db.IteratorPrefix(UnconfirmedTxPrefix) + defer txIter.Release() + + for txIter.Next() { + annotatedTx := new(query.AnnotatedTx) + if err := json.Unmarshal(txIter.Value(), annotatedTx); err != nil { + return nil, err + } + + annotatedTxs = append(annotatedTxs, annotatedTx) + } + return annotatedTxs, nil +} + +// SetAssetDefinition set assetID and definition +func (store *WalletStore) SetAssetDefinition(assetID *bc.AssetID, definition []byte) { + if store.batch == nil { + store.db.Set(asset.ExtAssetKey(assetID), definition) + } else { + store.batch.Set(asset.ExtAssetKey(assetID), definition) + } +} + +// SetContractUTXO set standard utxo +func (store *WalletStore) SetContractUTXO(outputID bc.Hash, utxo *acc.UTXO) error { + data, err := json.Marshal(utxo) + if err != nil { + return err + } + + if store.batch == nil { + store.db.Set(ContractUTXOKey(outputID), data) + } else { + store.batch.Set(ContractUTXOKey(outputID), data) + } + return nil +} + +// SetGlobalTransactionIndex set global tx index by blockhash and position +func (store *WalletStore) SetGlobalTransactionIndex(globalTxID string, blockHash *bc.Hash, position uint64) { + if store.batch == nil { + store.db.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position)) + } else { + store.batch.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position)) + } +} + +// SetRecoveryStatus set recovery status +func (store *WalletStore) SetRecoveryStatus(recoveryState *wallet.RecoveryState) error { + rawStatus, err := json.Marshal(recoveryState) + if err != nil { + return err + } + + if store.batch == nil { + store.db.Set(RecoveryKey, rawStatus) + } else { + store.batch.Set(RecoveryKey, rawStatus) + } + return nil +} + +// SetTransaction set raw transaction by block height and tx position +func (store *WalletStore) SetTransaction(height uint64, tx *query.AnnotatedTx) error { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + + rawTx, err := json.Marshal(tx) + if err != nil { + return err + } + + batch.Set(calcAnnotatedKey(formatKey(height, tx.Position)), rawTx) + batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(height, tx.Position))) + + if store.batch == nil { + batch.Write() + } + return nil +} + +// SetUnconfirmedTransaction set unconfirmed tx by txID +func (store *WalletStore) SetUnconfirmedTransaction(txID string, tx *query.AnnotatedTx) error { + rawTx, err := json.Marshal(tx) + if err != nil { + return err + } + + if store.batch == nil { + store.db.Set(calcUnconfirmedTxKey(txID), rawTx) + } else { + store.batch.Set(calcUnconfirmedTxKey(txID), rawTx) + } + return nil +} + +// SetWalletInfo get wallet information +func (store *WalletStore) SetWalletInfo(status *wallet.StatusInfo) error { + rawWallet, err := json.Marshal(status) + if err != nil { + return err + } + + if store.batch == nil { + store.db.Set(WalletKey, rawWallet) + } else { + store.batch.Set(WalletKey, rawWallet) + } + return nil +} diff --git a/database/wallet_store_test.go b/database/wallet_store_test.go new file mode 100644 index 00000000..f2aa9434 --- /dev/null +++ b/database/wallet_store_test.go @@ -0,0 +1,44 @@ +package database + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/vapor/blockchain/pseudohsm" + "github.com/vapor/crypto/ed25519/chainkd" +) + +func TestAccountIndexKey(t *testing.T) { + dirPath, err := ioutil.TempDir(".", "TestAccount") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dirPath) + + hsm, err := pseudohsm.New(dirPath) + if err != nil { + t.Fatal(err) + } + + xpub1, _, err := hsm.XCreate("TestAccountIndex1", "password", "en") + if err != nil { + t.Fatal(err) + } + + xpub2, _, err := hsm.XCreate("TestAccountIndex2", "password", "en") + if err != nil { + t.Fatal(err) + } + + xpubs1 := []chainkd.XPub{xpub1.XPub, xpub2.XPub} + xpubs2 := []chainkd.XPub{xpub2.XPub, xpub1.XPub} + if !reflect.DeepEqual(accountIndexKey(xpubs1), accountIndexKey(xpubs2)) { + t.Fatal("accountIndexKey test err") + } + + if reflect.DeepEqual(xpubs1, xpubs2) { + t.Fatal("accountIndexKey test err") + } +} diff --git a/node/node.go b/node/node.go index d82c1558..764edf4d 100644 --- a/node/node.go +++ b/node/node.go @@ -110,9 +110,11 @@ func NewNode(config *cfg.Config) *Node { if !config.Wallet.Disable { walletDB := dbm.NewDB("wallet", config.DBBackend, config.DBDir()) - accounts = account.NewManager(walletDB, chain) + walletStore := database.NewWalletStore(walletDB) + accountStore := database.NewAccountStore(walletDB) + accounts = account.NewManager(accountStore, chain) assets = asset.NewRegistry(walletDB, chain) - wallet, err = w.NewWallet(walletDB, accounts, assets, hsm, chain, dispatcher, config.Wallet.TxIndex) + wallet, err = w.NewWallet(walletStore, accounts, assets, hsm, chain, dispatcher, config.Wallet.TxIndex) if err != nil { log.WithFields(log.Fields{"module": logModule, "error": err}).Error("init NewWallet") } diff --git a/account/accounts_test.go b/test/accounts_test.go similarity index 52% rename from account/accounts_test.go rename to test/accounts_test.go index d2722bb5..ba95631f 100644 --- a/account/accounts_test.go +++ b/test/accounts_test.go @@ -1,13 +1,12 @@ -package account +package test import ( "io/ioutil" "os" - "reflect" "strings" "testing" - "github.com/vapor/blockchain/pseudohsm" + acc "github.com/vapor/account" "github.com/vapor/blockchain/signers" "github.com/vapor/config" "github.com/vapor/crypto/ed25519/chainkd" @@ -22,7 +21,7 @@ import ( func TestCreateAccountWithUppercase(t *testing.T) { m := mockAccountManager(t) alias := "UPPER" - account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044) + account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044) if err != nil { t.Fatal(err) @@ -36,7 +35,7 @@ func TestCreateAccountWithUppercase(t *testing.T) { func TestCreateAccountWithSpaceTrimed(t *testing.T) { m := mockAccountManager(t) alias := " with space " - account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044) + account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044) if err != nil { t.Fatal(err) @@ -46,12 +45,12 @@ func TestCreateAccountWithSpaceTrimed(t *testing.T) { t.Fatal("created account alias should be lowercase") } - nilAccount, err := m.FindByAlias(alias) + nilAccount, err := m.Manager.FindByAlias(alias) if nilAccount != nil { t.Fatal("expected nil") } - target, err := m.FindByAlias(strings.ToLower(strings.TrimSpace(alias))) + target, err := m.Manager.FindByAlias(strings.ToLower(strings.TrimSpace(alias))) if target == nil { t.Fatal("expected Account, but got nil") } @@ -59,15 +58,16 @@ func TestCreateAccountWithSpaceTrimed(t *testing.T) { func TestCreateAccount(t *testing.T) { m := mockAccountManager(t) - account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044) + account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044) if err != nil { t.Fatal(err) } - found, err := m.FindByID(account.ID) + found, err := m.Manager.FindByID(account.ID) if err != nil { t.Errorf("unexpected error %v", err) } + if !testutil.DeepEqual(account, found) { t.Errorf("expected account %v to be recorded as %v", account, found) } @@ -77,9 +77,9 @@ func TestCreateAccountReusedAlias(t *testing.T) { m := mockAccountManager(t) m.createTestAccount(t, "test-alias", nil) - _, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044) - if errors.Root(err) != ErrDuplicateAlias { - t.Errorf("expected %s when reusing an alias, got %v", ErrDuplicateAlias, err) + _, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044) + if errors.Root(err) != acc.ErrDuplicateAlias { + t.Errorf("expected %s when reusing an alias, got %v", acc.ErrDuplicateAlias, err) } } @@ -89,56 +89,58 @@ func TestUpdateAccountAlias(t *testing.T) { m := mockAccountManager(t) account := m.createTestAccount(t, oldAlias, nil) - if err := m.UpdateAccountAlias("testID", newAlias); err == nil { - t.Fatal("expected error when using an invalid account id") + err := m.Manager.UpdateAccountAlias("testID", newAlias) + if err == nil { + t.Errorf("expected error when using an invalid account id") } - err := m.UpdateAccountAlias(account.ID, oldAlias) - if errors.Root(err) != ErrDuplicateAlias { - t.Errorf("expected %s when using a duplicate alias, got %v", ErrDuplicateAlias, err) + err = m.Manager.UpdateAccountAlias(account.ID, oldAlias) + if errors.Root(err) != acc.ErrDuplicateAlias { + t.Errorf("expected %s when using a duplicate alias, got %v", acc.ErrDuplicateAlias, err) } - if err := m.UpdateAccountAlias(account.ID, newAlias); err != nil { + err = m.Manager.UpdateAccountAlias(account.ID, newAlias) + if err != nil { t.Errorf("expected account %v alias should be update", account) } - updatedAccount, err := m.FindByID(account.ID) + updatedAccount, err := m.Manager.FindByID(account.ID) if err != nil { t.Errorf("unexpected error %v", err) } if updatedAccount.Alias != newAlias { - t.Fatalf("alias:\ngot: %v\nwant: %v", updatedAccount.Alias, newAlias) + t.Errorf("alias:\ngot: %v\nwant: %v", updatedAccount.Alias, newAlias) } - if _, err = m.FindByAlias(oldAlias); errors.Root(err) != ErrFindAccount { - t.Errorf("expected %s when using a old alias, got %v", ErrFindAccount, err) + if _, err = m.Manager.FindByAlias(oldAlias); errors.Root(err) != acc.ErrFindAccount { + t.Errorf("expected %s when using a old alias, got %v", acc.ErrFindAccount, err) } } func TestDeleteAccount(t *testing.T) { m := mockAccountManager(t) - account1, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias1", signers.BIP0044) + account1, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias1", signers.BIP0044) if err != nil { t.Fatal(err) } - account2, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias2", signers.BIP0044) + account2, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias2", signers.BIP0044) if err != nil { t.Fatal(err) } - found, err := m.FindByID(account1.ID) + found, err := m.Manager.FindByID(account1.ID) if err != nil { t.Errorf("expected account %v should be deleted", found) } - if err = m.DeleteAccount(account2.ID); err != nil { + if err = m.Manager.DeleteAccount(account2.ID); err != nil { t.Fatal(err) } - found, err = m.FindByID(account2.ID) + found, err = m.Manager.FindByID(account2.ID) if err != nil { t.Errorf("expected account %v should be deleted", found) } @@ -148,7 +150,7 @@ func TestFindByID(t *testing.T) { m := mockAccountManager(t) account := m.createTestAccount(t, "", nil) - found, err := m.FindByID(account.ID) + found, err := m.Manager.FindByID(account.ID) if err != nil { t.Fatal(err) } @@ -162,7 +164,7 @@ func TestFindByAlias(t *testing.T) { m := mockAccountManager(t) account := m.createTestAccount(t, "some-alias", nil) - found, err := m.FindByAlias("some-alias") + found, err := m.Manager.FindByAlias("some-alias") if err != nil { t.Fatal(err) } @@ -172,40 +174,11 @@ func TestFindByAlias(t *testing.T) { } } -func TestGetAccountIndexKey(t *testing.T) { - dirPath, err := ioutil.TempDir(".", "TestAccount") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dirPath) - - hsm, err := pseudohsm.New(dirPath) - if err != nil { - t.Fatal(err) - } - - xpub1, _, err := hsm.XCreate("TestAccountIndex1", "password", "en") - if err != nil { - t.Fatal(err) - } - - xpub2, _, err := hsm.XCreate("TestAccountIndex2", "password", "en") - if err != nil { - t.Fatal(err) - } - - xpubs1 := []chainkd.XPub{xpub1.XPub, xpub2.XPub} - xpubs2 := []chainkd.XPub{xpub2.XPub, xpub1.XPub} - if !reflect.DeepEqual(GetAccountIndexKey(xpubs1), GetAccountIndexKey(xpubs2)) { - t.Fatal("GetAccountIndexKey test err") - } - - if reflect.DeepEqual(xpubs1, xpubs2) { - t.Fatal("GetAccountIndexKey test err") - } +type mockAccManager struct { + Manager *acc.Manager } -func mockAccountManager(t *testing.T) *Manager { +func mockAccountManager(t *testing.T) *mockAccManager { dirPath, err := ioutil.TempDir(".", "") if err != nil { t.Fatal(err) @@ -215,6 +188,7 @@ func mockAccountManager(t *testing.T) *Manager { testDB := dbm.NewDB("testdb", "memdb", dirPath) dispatcher := event.NewDispatcher() store := database.NewStore(testDB) + accountStore := database.NewAccountStore(testDB) txPool := protocol.NewTxPool(store, dispatcher) config.CommonConfig = config.DefaultConfig() chain, err := protocol.NewChain(store, txPool, dispatcher) @@ -222,15 +196,14 @@ func mockAccountManager(t *testing.T) *Manager { t.Fatal(err) } - return NewManager(testDB, chain) + return &mockAccManager{acc.NewManager(accountStore, chain)} } -func (m *Manager) createTestAccount(t testing.TB, alias string, tags map[string]interface{}) *Account { - account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044) +func (m *mockAccManager) createTestAccount(t testing.TB, alias string, tags map[string]interface{}) *acc.Account { + account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044) if err != nil { t.Fatal(err) } return account - } diff --git a/test/bench_blockchain_test.go b/test/bench_blockchain_test.go index 0d548feb..092811d5 100644 --- a/test/bench_blockchain_test.go +++ b/test/bench_blockchain_test.go @@ -159,7 +159,7 @@ func InsertChain(chain *protocol.Chain, txPool *protocol.TxPool, txs []*types.Tx } } - block, err := proposal.NewBlockTemplate(chain, txPool, nil, uint64(time.Now().UnixNano() / 1e6)) + block, err := proposal.NewBlockTemplate(chain, txPool, nil, uint64(time.Now().UnixNano()/1e6)) if err != nil { return err } @@ -368,7 +368,8 @@ func SetUtxoView(db dbm.DB, view *state.UtxoViewpoint) error { //-------------------------Mock actual transaction---------------------------------- func MockTxsP2PKH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) { - accountManager := account.NewManager(testDB, nil) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, nil) hsm, err := pseudohsm.New(keyDirPath) if err != nil { return nil, err @@ -410,7 +411,8 @@ func MockTxsP2PKH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) } func MockTxsP2SH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) { - accountManager := account.NewManager(testDB, nil) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, nil) hsm, err := pseudohsm.New(keyDirPath) if err != nil { return nil, err @@ -457,7 +459,8 @@ func MockTxsP2SH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) } func MockTxsMultiSign(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) { - accountManager := account.NewManager(testDB, nil) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, nil) hsm, err := pseudohsm.New(keyDirPath) if err != nil { return nil, err diff --git a/test/builder_test.go b/test/builder_test.go new file mode 100644 index 00000000..4328dbc2 --- /dev/null +++ b/test/builder_test.go @@ -0,0 +1,130 @@ +package test + +import ( + "testing" + + acc "github.com/vapor/account" + "github.com/vapor/blockchain/signers" + "github.com/vapor/consensus" + "github.com/vapor/crypto/ed25519/chainkd" + "github.com/vapor/testutil" +) + +var ( + //chainTxUtxoNum maximum utxo quantity in a tx + chainTxUtxoNum = 5 + //chainTxMergeGas chain tx gas + chainTxMergeGas = uint64(10000000) +) + +func TestBuildBtmTxChain(t *testing.T) { + chainTxUtxoNum = 3 + m := mockAccountManager(t) + cases := []struct { + inputUtxo []uint64 + wantInput [][]uint64 + wantOutput [][]uint64 + wantUtxo uint64 + }{ + { + inputUtxo: []uint64{5}, + wantInput: [][]uint64{}, + wantOutput: [][]uint64{}, + wantUtxo: 5 * chainTxMergeGas, + }, + { + inputUtxo: []uint64{5, 4}, + wantInput: [][]uint64{ + []uint64{5, 4}, + }, + wantOutput: [][]uint64{ + []uint64{8}, + }, + wantUtxo: 8 * chainTxMergeGas, + }, + { + inputUtxo: []uint64{5, 4, 1, 1}, + wantInput: [][]uint64{ + []uint64{5, 4, 1, 1}, + []uint64{1, 9}, + }, + wantOutput: [][]uint64{ + []uint64{10}, + []uint64{9}, + }, + wantUtxo: 10 * chainTxMergeGas, + }, + { + inputUtxo: []uint64{22, 123, 53, 234, 23, 4, 2423, 24, 23, 43, 34, 234, 234, 24}, + wantInput: [][]uint64{ + []uint64{22, 123, 53, 234, 23}, + []uint64{4, 2423, 24, 23, 43}, + []uint64{34, 234, 234, 24, 454}, + []uint64{2516, 979}, + []uint64{234, 24, 197}, + []uint64{260, 2469, 310}, + []uint64{454, 3038}, + }, + wantOutput: [][]uint64{ + []uint64{454}, + []uint64{2516}, + []uint64{979}, + []uint64{3494}, + []uint64{454}, + []uint64{3038}, + []uint64{3491}, + }, + wantUtxo: 3494 * chainTxMergeGas, + }, + } + + acct, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "testAccount", signers.BIP0044) + if err != nil { + t.Fatal(err) + } + + acp, err := m.Manager.CreateAddress(acct.ID, false) + if err != nil { + t.Fatal(err) + } + + for caseIndex, c := range cases { + utxos := []*acc.UTXO{} + for _, amount := range c.inputUtxo { + utxos = append(utxos, &acc.UTXO{ + Amount: amount * chainTxMergeGas, + AssetID: *consensus.BTMAssetID, + Address: acp.Address, + ControlProgram: acp.ControlProgram, + }) + } + + tpls, gotUtxo, err := m.Manager.BuildBtmTxChain(utxos, acct.Signer) + if err != nil { + t.Fatal(err) + } + + for i, tpl := range tpls { + gotInput := []uint64{} + for _, input := range tpl.Transaction.Inputs { + gotInput = append(gotInput, input.Amount()/chainTxMergeGas) + } + + gotOutput := []uint64{} + for _, output := range tpl.Transaction.Outputs { + gotOutput = append(gotOutput, output.AssetAmount().Amount/chainTxMergeGas) + } + + if !testutil.DeepEqual(c.wantInput[i], gotInput) { + t.Errorf("case %d tx %d input got %d want %d", caseIndex, i, gotInput, c.wantInput[i]) + } + if !testutil.DeepEqual(c.wantOutput[i], gotOutput) { + t.Errorf("case %d tx %d output got %d want %d", caseIndex, i, gotOutput, c.wantOutput[i]) + } + } + + if c.wantUtxo != gotUtxo.Amount { + t.Errorf("case %d got utxo=%d want utxo=%d", caseIndex, gotUtxo.Amount, c.wantUtxo) + } + } +} diff --git a/test/chain_test_util.go b/test/chain_test_util.go index 9181053f..2f5fdb02 100644 --- a/test/chain_test_util.go +++ b/test/chain_test_util.go @@ -110,12 +110,12 @@ func (ctx *chainTestContext) getUtxoEntries() map[string]*storage.UtxoEntry { defer iter.Release() for iter.Next() { - utxoEntry := storage.UtxoEntry{} - if err := proto.Unmarshal(iter.Value(), &utxoEntry); err != nil { + utxoEntry := new(storage.UtxoEntry) + if err := proto.Unmarshal(iter.Value(), utxoEntry); err != nil { return nil } key := string(iter.Key()) - utxoEntries[key] = &utxoEntry + utxoEntries[key] = utxoEntry } return utxoEntries } diff --git a/test/integration/standard_transaction_test.go b/test/integration/standard_transaction_test.go index fee4e211..a82961db 100644 --- a/test/integration/standard_transaction_test.go +++ b/test/integration/standard_transaction_test.go @@ -10,6 +10,7 @@ import ( "github.com/vapor/blockchain/pseudohsm" "github.com/vapor/blockchain/signers" "github.com/vapor/crypto/ed25519/chainkd" + "github.com/vapor/database" dbm "github.com/vapor/database/leveldb" "github.com/vapor/protocol/bc/types" "github.com/vapor/protocol/validation" @@ -31,7 +32,8 @@ func TestP2PKH(t *testing.T) { t.Fatal(err) } - accountManager := account.NewManager(testDB, chain) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -82,7 +84,8 @@ func TestBip0032P2PKH(t *testing.T) { t.Fatal(err) } - accountManager := account.NewManager(testDB, chain) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -109,7 +112,7 @@ func TestBip0032P2PKH(t *testing.T) { t.Fatal(err) } - testDB.Set(account.Key(testAccount.ID), rawAccount) + testDB.Set(database.AccountIDKey(testAccount.ID), rawAccount) controlProg, err := accountManager.CreateAddress(testAccount.ID, false) if err != nil { t.Fatal(err) @@ -146,7 +149,8 @@ func TestP2SH(t *testing.T) { t.Fatal(err) } - accountManager := account.NewManager(testDB, chain) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -207,7 +211,8 @@ func TestBip0032P2SH(t *testing.T) { t.Fatal(err) } - accountManager := account.NewManager(testDB, chain) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -238,7 +243,7 @@ func TestBip0032P2SH(t *testing.T) { t.Fatal(err) } - testDB.Set(account.Key(testAccount.ID), rawAccount) + testDB.Set(database.AccountIDKey(testAccount.ID), rawAccount) controlProg, err := accountManager.CreateAddress(testAccount.ID, false) if err != nil { @@ -280,7 +285,8 @@ func TestMutilNodeSign(t *testing.T) { t.Fatal(err) } - accountManager := account.NewManager(testDB, chain) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -351,7 +357,8 @@ func TestBip0032MutilNodeSign(t *testing.T) { t.Fatal(err) } - accountManager := account.NewManager(testDB, chain) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -383,7 +390,7 @@ func TestBip0032MutilNodeSign(t *testing.T) { t.Fatal(err) } - testDB.Set(account.Key(testAccount.ID), rawAccount) + testDB.Set(database.AccountIDKey(testAccount.ID), rawAccount) controlProg, err := accountManager.CreateAddress(testAccount.ID, false) if err != nil { diff --git a/test/performance/mining_test.go b/test/performance/mining_test.go index 87390082..0f3f0d66 100644 --- a/test/performance/mining_test.go +++ b/test/performance/mining_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/vapor/account" + "github.com/vapor/database" dbm "github.com/vapor/database/leveldb" "github.com/vapor/proposal" "github.com/vapor/test" @@ -20,10 +21,11 @@ func BenchmarkNewBlockTpl(b *testing.B) { if err != nil { b.Fatal(err) } - accountManager := account.NewManager(testDB, chain) + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) b.ResetTimer() for i := 0; i < b.N; i++ { - proposal.NewBlockTemplate(chain, txPool, accountManager, uint64(time.Now().UnixNano() / 1e6)) + proposal.NewBlockTemplate(chain, txPool, accountManager, uint64(time.Now().UnixNano()/1e6)) } } diff --git a/test/tx_test_util.go b/test/tx_test_util.go index 11c63136..f8ce45d9 100644 --- a/test/tx_test_util.go +++ b/test/tx_test_util.go @@ -14,6 +14,7 @@ import ( "github.com/vapor/consensus" "github.com/vapor/crypto/ed25519/chainkd" "github.com/vapor/crypto/sha3pool" + "github.com/vapor/database" dbm "github.com/vapor/database/leveldb" "github.com/vapor/errors" "github.com/vapor/protocol/bc" @@ -267,7 +268,7 @@ func SignInstructionFor(input *types.SpendInput, db dbm.DB, signer *signers.Sign cp := account.CtrlProgram{} var hash [32]byte sha3pool.Sum256(hash[:], input.ControlProgram) - bytes := db.Get(account.ContractKey(hash)) + bytes := db.Get(database.ContractKey(bc.NewHash(hash))) if bytes == nil { return nil, fmt.Errorf("can't find CtrlProgram for the SpendInput") } diff --git a/test/wallet_test.go b/test/wallet_test.go new file mode 100644 index 00000000..fe4509c4 --- /dev/null +++ b/test/wallet_test.go @@ -0,0 +1,326 @@ +package test + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + "time" + + "github.com/vapor/account" + "github.com/vapor/asset" + "github.com/vapor/blockchain/pseudohsm" + "github.com/vapor/blockchain/signers" + "github.com/vapor/blockchain/txbuilder" + "github.com/vapor/config" + "github.com/vapor/consensus" + "github.com/vapor/crypto/ed25519/chainkd" + "github.com/vapor/database" + dbm "github.com/vapor/database/leveldb" + "github.com/vapor/event" + "github.com/vapor/protocol" + "github.com/vapor/protocol/bc" + "github.com/vapor/protocol/bc/types" + wt "github.com/vapor/wallet" +) + +func TestWalletUpdate(t *testing.T) { + dirPath, err := ioutil.TempDir(".", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dirPath) + + config.CommonConfig = config.DefaultConfig() + testDB := dbm.NewDB("testdb", "leveldb", "temp") + defer func() { + testDB.Close() + os.RemoveAll("temp") + }() + + store := database.NewStore(testDB) + walletStore := database.NewWalletStore(testDB) + dispatcher := event.NewDispatcher() + txPool := protocol.NewTxPool(store, dispatcher) + + chain, err := protocol.NewChain(store, txPool, dispatcher) + if err != nil { + t.Fatal(err) + } + + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) + hsm, err := pseudohsm.New(dirPath) + if err != nil { + t.Fatal(err) + } + + xpub1, _, err := hsm.XCreate("test_pub1", "password", "en") + if err != nil { + t.Fatal(err) + } + + testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044) + if err != nil { + t.Fatal(err) + } + + controlProg, err := accountManager.CreateAddress(testAccount.ID, false) + if err != nil { + t.Fatal(err) + } + + controlProg.KeyIndex = 1 + + reg := asset.NewRegistry(testDB, chain) + asset := bc.AssetID{V0: 5} + + utxos := []*account.UTXO{} + btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID) + utxos = append(utxos, btmUtxo) + OtherUtxo := mockUTXO(controlProg, &asset) + utxos = append(utxos, OtherUtxo) + + _, txData, err := mockTxData(utxos, testAccount) + if err != nil { + t.Fatal(err) + } + + tx := types.NewTx(*txData) + block := mockSingleBlock(tx) + txStatus := bc.NewTransactionStatus() + txStatus.SetStatus(0, false) + txStatus.SetStatus(1, false) + store.SaveBlock(block, txStatus) + + w := newMockWallet(walletStore, accountManager, reg, chain, dispatcher, true) + err = w.AttachBlock(block) + if err != nil { + t.Fatal(err) + } + + if _, err := w.GetTransactionByTxID(tx.ID.String()); err != nil { + t.Fatal(err) + } + + wants, err := w.GetTransactions(testAccount.ID, "", 1, false) + if len(wants) != 1 { + t.Fatal(err) + } + + if wants[0].ID != tx.ID { + t.Fatal("account txID mismatch") + } + + for position, tx := range block.Transactions { + get := testDB.Get(database.CalcGlobalTxIndexKey(tx.ID.String())) + bh := block.BlockHeader.Hash() + expect := database.CalcGlobalTxIndex(&bh, uint64(position)) + if !reflect.DeepEqual(get, expect) { + t.Fatalf("position#%d: compare retrieved globalTxIdx err", position) + } + } +} + +func TestRescanWallet(t *testing.T) { + // prepare wallet & db. + dirPath, err := ioutil.TempDir(".", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dirPath) + + config.CommonConfig = config.DefaultConfig() + testDB := dbm.NewDB("testdb", "leveldb", "temp") + walletStore := database.NewWalletStore(testDB) + defer func() { + testDB.Close() + os.RemoveAll("temp") + }() + + store := database.NewStore(testDB) + dispatcher := event.NewDispatcher() + txPool := protocol.NewTxPool(store, dispatcher) + chain, err := protocol.NewChain(store, txPool, dispatcher) + if err != nil { + t.Fatal(err) + } + + statusInfo := wt.StatusInfo{ + Version: uint(1), + WorkHash: bc.Hash{V0: 0xff}, + } + if err := walletStore.SetWalletInfo(&statusInfo); err != nil { + t.Fatal(err) + } + walletInfo, err := walletStore.GetWalletInfo() + if err != nil { + t.Fatal(err) + } + + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) + w := newMockWallet(walletStore, accountManager, nil, chain, dispatcher, false) + if err != nil { + t.Fatal(err) + } + w.Status = *walletInfo + + // rescan wallet. + if err := w.LoadWalletInfo(); err != nil { + t.Fatal(err) + } + + block := config.GenesisBlock() + if w.Status.WorkHash != block.Hash() { + t.Fatal("reattach from genesis block") + } +} + +func TestMemPoolTxQueryLoop(t *testing.T) { + dirPath, err := ioutil.TempDir(".", "") + if err != nil { + t.Fatal(err) + } + config.CommonConfig = config.DefaultConfig() + testDB := dbm.NewDB("testdb", "leveldb", dirPath) + defer func() { + testDB.Close() + os.RemoveAll(dirPath) + }() + + store := database.NewStore(testDB) + dispatcher := event.NewDispatcher() + txPool := protocol.NewTxPool(store, dispatcher) + + chain, err := protocol.NewChain(store, txPool, dispatcher) + if err != nil { + t.Fatal(err) + } + + accountStore := database.NewAccountStore(testDB) + accountManager := account.NewManager(accountStore, chain) + hsm, err := pseudohsm.New(dirPath) + if err != nil { + t.Fatal(err) + } + + xpub1, _, err := hsm.XCreate("test_pub1", "password", "en") + if err != nil { + t.Fatal(err) + } + + testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044) + if err != nil { + t.Fatal(err) + } + + controlProg, err := accountManager.CreateAddress(testAccount.ID, false) + if err != nil { + t.Fatal(err) + } + + controlProg.KeyIndex = 1 + + reg := asset.NewRegistry(testDB, chain) + asset := bc.AssetID{V0: 5} + + utxos := []*account.UTXO{} + btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID) + utxos = append(utxos, btmUtxo) + OtherUtxo := mockUTXO(controlProg, &asset) + utxos = append(utxos, OtherUtxo) + + _, txData, err := mockTxData(utxos, testAccount) + if err != nil { + t.Fatal(err) + } + + tx := types.NewTx(*txData) + txStatus := bc.NewTransactionStatus() + txStatus.SetStatus(0, false) + walletStore := database.NewWalletStore(testDB) + w := newMockWallet(walletStore, accountManager, reg, chain, dispatcher, false) + go w.MemPoolTxQueryLoop() + w.EventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgNewTx}}) + time.Sleep(time.Millisecond * 10) + if _, err := w.GetUnconfirmedTxByTxID(tx.ID.String()); err != nil { + t.Fatal("dispatch new tx msg error:", err) + } + w.EventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgRemoveTx}}) + time.Sleep(time.Millisecond * 10) + txs, err := w.GetUnconfirmedTxs(testAccount.ID) + if err != nil { + t.Fatal("get unconfirmed tx error:", err) + } + + if len(txs) != 0 { + t.Fatal("dispatch remove tx msg error") + } + + w.EventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: 2}}) +} + +func mockUTXO(controlProg *account.CtrlProgram, assetID *bc.AssetID) *account.UTXO { + utxo := &account.UTXO{} + utxo.OutputID = bc.Hash{V0: 1} + utxo.SourceID = bc.Hash{V0: 2} + utxo.AssetID = *assetID + utxo.Amount = 1000000000 + utxo.SourcePos = 0 + utxo.ControlProgram = controlProg.ControlProgram + utxo.AccountID = controlProg.AccountID + utxo.Address = controlProg.Address + utxo.ControlProgramIndex = controlProg.KeyIndex + return utxo +} + +func mockTxData(utxos []*account.UTXO, testAccount *account.Account) (*txbuilder.Template, *types.TxData, error) { + tplBuilder := txbuilder.NewBuilder(time.Now()) + + for _, utxo := range utxos { + txInput, sigInst, err := account.UtxoToInputs(testAccount.Signer, utxo) + if err != nil { + return nil, nil, err + } + tplBuilder.AddInput(txInput, sigInst) + + out := &types.TxOutput{} + if utxo.AssetID == *consensus.BTMAssetID { + out = types.NewIntraChainOutput(utxo.AssetID, 100, utxo.ControlProgram) + } else { + out = types.NewIntraChainOutput(utxo.AssetID, utxo.Amount, utxo.ControlProgram) + } + tplBuilder.AddOutput(out) + } + + return tplBuilder.Build() +} + +type mockWallet struct { + Wallet *wt.Wallet +} + +func newMockWallet(store wt.WalletStore, account *account.Manager, asset *asset.Registry, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) *wt.Wallet { + wallet := &wt.Wallet{ + Store: store, + AccountMgr: account, + AssetReg: asset, + Chain: chain, + RecoveryMgr: wt.NewRecoveryManager(store, account), + EventDispatcher: dispatcher, + TxIndexFlag: txIndexFlag, + } + wallet.TxMsgSub, _ = wallet.EventDispatcher.Subscribe(protocol.TxMsgEvent{}) + return wallet +} + +func mockSingleBlock(tx *types.Tx) *types.Block { + return &types.Block{ + BlockHeader: types.BlockHeader{ + Version: 1, + Height: 1, + }, + Transactions: []*types.Tx{config.GenesisTx(), tx}, + } +} diff --git a/test/wallet_test_util.go b/test/wallet_test_util.go index b03b430b..570aeccc 100644 --- a/test/wallet_test_util.go +++ b/test/wallet_test_util.go @@ -12,6 +12,7 @@ import ( "github.com/vapor/blockchain/pseudohsm" "github.com/vapor/blockchain/signers" "github.com/vapor/crypto/ed25519/chainkd" + "github.com/vapor/database" dbm "github.com/vapor/database/leveldb" "github.com/vapor/event" "github.com/vapor/protocol" @@ -241,10 +242,12 @@ func (cfg *walletTestConfig) Run() error { return err } walletDB := dbm.NewDB("wallet", "leveldb", path.Join(dirPath, "wallet_db")) - accountManager := account.NewManager(walletDB, chain) + walletStore := database.NewWalletStore(walletDB) + accountStore := database.NewAccountStore(walletDB) + accountManager := account.NewManager(accountStore, chain) assets := asset.NewRegistry(walletDB, chain) dispatcher := event.NewDispatcher() - wallet, err := w.NewWallet(walletDB, accountManager, assets, hsm, chain, dispatcher, false) + wallet, err := w.NewWallet(walletStore, accountManager, assets, hsm, chain, dispatcher, false) if err != nil { return err } diff --git a/wallet/annotated.go b/wallet/annotated.go index db305e57..fe05f7ba 100644 --- a/wallet/annotated.go +++ b/wallet/annotated.go @@ -2,7 +2,6 @@ package wallet import ( "encoding/json" - "fmt" log "github.com/sirupsen/logrus" @@ -13,7 +12,6 @@ import ( "github.com/vapor/consensus" "github.com/vapor/consensus/segwit" "github.com/vapor/crypto/sha3pool" - dbm "github.com/vapor/database/leveldb" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/bc/types" ) @@ -33,29 +31,18 @@ func annotateTxsAsset(w *Wallet, txs []*query.AnnotatedTx) { } func (w *Wallet) getExternalDefinition(assetID *bc.AssetID) json.RawMessage { - // definitionByte := w.DB.Get(asset.ExtAssetKey(assetID)) - definitionByte := w.DB.Get(asset.ExtAssetKey(assetID)) - if definitionByte == nil { - return nil + externalAsset, err := w.Store.GetAsset(assetID) + if err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err, "assetID": assetID.String()}).Info("fail on get asset definition.") } - - definitionMap := make(map[string]interface{}) - if err := json.Unmarshal(definitionByte, &definitionMap); err != nil { + if externalAsset == nil { return nil } - alias := assetID.String() - externalAsset := &asset.Asset{ - AssetID: *assetID, - Alias: &alias, - DefinitionMap: definitionMap, - RawDefinitionByte: definitionByte, + if err := w.AssetReg.SaveAsset(externalAsset, *externalAsset.Alias); err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err, "assetAlias": *externalAsset.Alias}).Info("fail on save external asset to internal asset DB") } - - if err := w.AssetReg.SaveAsset(externalAsset, alias); err != nil { - log.WithFields(log.Fields{"module": logModule, "err": err, "assetID": alias}).Warning("fail on save external asset to internal asset DB") - } - return definitionByte + return json.RawMessage(externalAsset.RawDefinitionByte) } func (w *Wallet) getAliasDefinition(assetID bc.AssetID) (string, json.RawMessage) { @@ -83,14 +70,14 @@ func (w *Wallet) getAliasDefinition(assetID bc.AssetID) (string, json.RawMessage } // annotateTxs adds account data to transactions -func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB dbm.DB) { +func (w *Wallet) annotateTxsAccount(txs []*query.AnnotatedTx) { for i, tx := range txs { for j, input := range tx.Inputs { //issue asset tx input SpentOutputID is nil if input.SpentOutputID == nil { continue } - localAccount, err := getAccountFromACP(input.ControlProgram, walletDB) + localAccount, err := w.getAccountFromACP(input.ControlProgram) if localAccount == nil || err != nil { continue } @@ -98,7 +85,7 @@ func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB dbm.DB) { txs[i].Inputs[j].AccountID = localAccount.ID } for j, output := range tx.Outputs { - localAccount, err := getAccountFromACP(output.ControlProgram, walletDB) + localAccount, err := w.getAccountFromACP(output.ControlProgram) if localAccount == nil || err != nil { continue } @@ -108,32 +95,20 @@ func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB dbm.DB) { } } -func getAccountFromACP(program []byte, walletDB dbm.DB) (*account.Account, error) { - var hash common.Hash - accountCP := account.CtrlProgram{} - localAccount := account.Account{} - +func (w *Wallet) getAccountFromACP(program []byte) (*account.Account, error) { + var hash [32]byte sha3pool.Sum256(hash[:], program) - - rawProgram := walletDB.Get(account.ContractKey(hash)) - if rawProgram == nil { - return nil, fmt.Errorf("failed get account control program:%x ", hash) - } - - if err := json.Unmarshal(rawProgram, &accountCP); err != nil { + accountCP, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash)) + if err != nil { return nil, err } - accountValue := walletDB.Get(account.Key(accountCP.AccountID)) - if accountValue == nil { - return nil, fmt.Errorf("failed get account:%s ", accountCP.AccountID) - } - - if err := json.Unmarshal(accountValue, &localAccount); err != nil { + account, err := w.AccountMgr.FindByID(accountCP.AccountID) + if err != nil { return nil, err } - return &localAccount, nil + return account, nil } var emptyJSONObject = json.RawMessage(`{}`) @@ -169,6 +144,8 @@ func (w *Wallet) BuildAnnotatedInput(tx *types.Tx, i uint32) *query.AnnotatedInp if orig.InputType() != types.CoinbaseInputType { in.AssetID = orig.AssetID() in.Amount = orig.Amount() + } else { + in.AssetID = *consensus.BTMAssetID } id := tx.Tx.InputIDs[i] diff --git a/wallet/indexer.go b/wallet/indexer.go index 73d6b794..317ee0ed 100644 --- a/wallet/indexer.go +++ b/wallet/indexer.go @@ -3,62 +3,20 @@ package wallet import ( "encoding/binary" "encoding/hex" - "encoding/json" "fmt" "sort" log "github.com/sirupsen/logrus" "github.com/vapor/account" - "github.com/vapor/asset" "github.com/vapor/blockchain/query" "github.com/vapor/consensus" "github.com/vapor/crypto/sha3pool" - dbm "github.com/vapor/database/leveldb" chainjson "github.com/vapor/encoding/json" - "github.com/vapor/errors" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/bc/types" ) -const ( - //TxPrefix is wallet database transactions prefix - TxPrefix = "TXS:" - //TxIndexPrefix is wallet database tx index prefix - TxIndexPrefix = "TID:" - //TxIndexPrefix is wallet database global tx index prefix - GlobalTxIndexPrefix = "GTID:" -) - -var ErrAccntTxIDNotFound = errors.New("account TXID not found") - -func formatKey(blockHeight uint64, position uint32) string { - return fmt.Sprintf("%016x%08x", blockHeight, position) -} - -func calcAnnotatedKey(formatKey string) []byte { - return []byte(TxPrefix + formatKey) -} - -func calcDeleteKey(blockHeight uint64) []byte { - return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight)) -} - -func calcTxIndexKey(txID string) []byte { - return []byte(TxIndexPrefix + txID) -} - -func calcGlobalTxIndexKey(txID string) []byte { - return []byte(GlobalTxIndexPrefix + txID) -} - -func calcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte { - txIdx := make([]byte, 40) - copy(txIdx[:32], blockHash.Bytes()) - binary.BigEndian.PutUint64(txIdx[32:], position) - return txIdx -} - func parseGlobalTxIdx(globalTxIdx []byte) (*bc.Hash, uint64) { var hashBytes [32]byte copy(hashBytes[:], globalTxIdx[:32]) @@ -67,37 +25,31 @@ func parseGlobalTxIdx(globalTxIdx []byte) (*bc.Hash, uint64) { return &hash, position } -// deleteTransaction delete transactions when orphan block rollback -func (w *Wallet) deleteTransactions(batch dbm.Batch, height uint64) { - tmpTx := query.AnnotatedTx{} - txIter := w.DB.IteratorPrefix(calcDeleteKey(height)) - defer txIter.Release() - - for txIter.Next() { - if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil { - batch.Delete(calcTxIndexKey(tmpTx.ID.String())) - } - batch.Delete(txIter.Key()) - } -} - // saveExternalAssetDefinition save external and local assets definition, // when query ,query local first and if have no then query external // details see getAliasDefinition -func saveExternalAssetDefinition(b *types.Block, walletDB dbm.DB) { - storeBatch := walletDB.NewBatch() - defer storeBatch.Write() +func saveExternalAssetDefinition(b *types.Block, store WalletStore) error { + newStore := store.InitBatch() for _, tx := range b.Transactions { for _, orig := range tx.Inputs { if cci, ok := orig.TypedInput.(*types.CrossChainInput); ok { assetID := cci.AssetId - if assetExist := walletDB.Get(asset.ExtAssetKey(assetID)); assetExist == nil { - storeBatch.Set(asset.ExtAssetKey(assetID), cci.AssetDefinition) + if _, err := newStore.GetAsset(assetID); err == nil { + continue + } else if err != ErrGetAsset { + return err } + + newStore.SetAssetDefinition(assetID, cci.AssetDefinition) } } } + if err := newStore.CommitBatch(); err != nil { + return err + } + + return nil } // Summary is the struct of transaction's input and output summary @@ -120,23 +72,13 @@ type TxSummary struct { } // indexTransactions saves all annotated transactions to the database. -func (w *Wallet) indexTransactions(batch dbm.Batch, b *types.Block, txStatus *bc.TransactionStatus) error { - annotatedTxs := w.filterAccountTxs(b, txStatus) - saveExternalAssetDefinition(b, w.DB) - annotateTxsAccount(annotatedTxs, w.DB) - +func (w *Wallet) indexTransactions(b *types.Block, txStatus *bc.TransactionStatus, annotatedTxs []*query.AnnotatedTx, store WalletStore) error { for _, tx := range annotatedTxs { - rawTx, err := json.Marshal(tx) - if err != nil { - log.WithFields(log.Fields{"module": logModule, "err": err}).Error("inserting annotated_txs to db") + if err := w.Store.SetTransaction(b.Height, tx); err != nil { return err } - batch.Set(calcAnnotatedKey(formatKey(b.Height, uint32(tx.Position))), rawTx) - batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(b.Height, uint32(tx.Position)))) - - // delete unconfirmed transaction - batch.Delete(calcUnconfirmedTxKey(tx.ID.String())) + store.DeleteUnconfirmedTransaction(tx.ID.String()) } if !w.TxIndexFlag { @@ -145,7 +87,7 @@ func (w *Wallet) indexTransactions(batch dbm.Batch, b *types.Block, txStatus *bc for position, globalTx := range b.Transactions { blockHash := b.BlockHeader.Hash() - batch.Set(calcGlobalTxIndexKey(globalTx.ID.String()), calcGlobalTxIndex(&blockHash, uint64(position))) + store.SetGlobalTransactionIndex(globalTx.ID.String(), &blockHash, uint64(position)) } return nil @@ -161,21 +103,25 @@ transactionLoop: for _, v := range tx.Outputs { var hash [32]byte sha3pool.Sum256(hash[:], v.ControlProgram()) - - if bytes := w.DB.Get(account.ContractKey(hash)); bytes != nil { + if _, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash)); err == nil { annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos)) continue transactionLoop + } else { + log.WithFields(log.Fields{"module": logModule, "err": err, "hash": hex.EncodeToString(hash[:])}).Info("filterAccountTxs fail.") } } for _, v := range tx.Inputs { outid, err := v.SpentOutputID() if err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err, "outputID": hex.EncodeToString(outid.Bytes())}).Info("filterAccountTxs fail.") continue } - if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil { + if _, err = w.Store.GetStandardUTXO(outid); err == nil { annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos)) continue transactionLoop + } else { + log.WithFields(log.Fields{"module": logModule, "err": err, "outputID": hex.EncodeToString(outid.Bytes())}).Info("filterAccountTxs fail.") } } } @@ -195,14 +141,8 @@ func (w *Wallet) GetTransactionByTxID(txID string) (*query.AnnotatedTx, error) { } func (w *Wallet) getAccountTxByTxID(txID string) (*query.AnnotatedTx, error) { - annotatedTx := &query.AnnotatedTx{} - formatKey := w.DB.Get(calcTxIndexKey(txID)) - if formatKey == nil { - return nil, ErrAccntTxIDNotFound - } - - txInfo := w.DB.Get(calcAnnotatedKey(string(formatKey))) - if err := json.Unmarshal(txInfo, annotatedTx); err != nil { + annotatedTx, err := w.Store.GetTransaction(txID) + if err != nil { return nil, err } @@ -211,18 +151,18 @@ func (w *Wallet) getAccountTxByTxID(txID string) (*query.AnnotatedTx, error) { } func (w *Wallet) getGlobalTxByTxID(txID string) (*query.AnnotatedTx, error) { - globalTxIdx := w.DB.Get(calcGlobalTxIndexKey(txID)) + globalTxIdx := w.Store.GetGlobalTransactionIndex(txID) if globalTxIdx == nil { return nil, fmt.Errorf("No transaction(tx_id=%s) ", txID) } blockHash, pos := parseGlobalTxIdx(globalTxIdx) - block, err := w.chain.GetBlockByHash(blockHash) + block, err := w.Chain.GetBlockByHash(blockHash) if err != nil { return nil, err } - txStatus, err := w.chain.GetTransactionStatus(blockHash) + txStatus, err := w.Chain.GetTransactionStatus(blockHash) if err != nil { return nil, err } @@ -291,38 +231,16 @@ func findTransactionsByAccount(annotatedTx *query.AnnotatedTx, accountID string) // GetTransactions get all walletDB transactions or unconfirmed transactions, and filter transactions by accountID and StartTxID optional func (w *Wallet) GetTransactions(accountID string, StartTxID string, count uint, unconfirmed bool) ([]*query.AnnotatedTx, error) { annotatedTxs := []*query.AnnotatedTx{} - var startKey []byte - preFix := TxPrefix - - if StartTxID != "" { - if unconfirmed { - startKey = calcUnconfirmedTxKey(StartTxID) - } else { - formatKey := w.DB.Get(calcTxIndexKey(StartTxID)) - if formatKey == nil { - return nil, ErrAccntTxIDNotFound - } - startKey = calcAnnotatedKey(string(formatKey)) - } - } - - if unconfirmed { - preFix = UnconfirmedTxPrefix + annotatedTxs, err := w.Store.ListTransactions(accountID, StartTxID, count, unconfirmed) + if err != nil { + return nil, err } - itr := w.DB.IteratorPrefixWithStart([]byte(preFix), startKey, true) - defer itr.Release() - - for txNum := count; itr.Next() && txNum > 0; { - annotatedTx := &query.AnnotatedTx{} - if err := json.Unmarshal(itr.Value(), &annotatedTx); err != nil { - return nil, err - } - + newAnnotatedTxs := []*query.AnnotatedTx{} + for _, annotatedTx := range annotatedTxs { if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) { annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx}) - annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...) - txNum-- + newAnnotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, newAnnotatedTxs...) } } @@ -332,7 +250,7 @@ func (w *Wallet) GetTransactions(accountID string, StartTxID string, count uint, sort.Sort(SortByHeight(annotatedTxs)) } - return annotatedTxs, nil + return newAnnotatedTxs, nil } // GetAccountBalances return all account balances diff --git a/wallet/recovery.go b/wallet/recovery.go index 2933ad6f..a2aae5ac 100644 --- a/wallet/recovery.go +++ b/wallet/recovery.go @@ -1,7 +1,6 @@ package wallet import ( - "encoding/json" "fmt" "reflect" "sync" @@ -12,7 +11,6 @@ import ( "github.com/vapor/blockchain/signers" "github.com/vapor/crypto/ed25519/chainkd" "github.com/vapor/crypto/sha3pool" - dbm "github.com/vapor/database/leveldb" "github.com/vapor/errors" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/bc/types" @@ -28,15 +26,14 @@ const ( addrRecoveryWindow = uint64(128) ) -//recoveryKey key for db store recovery info. var ( - recoveryKey = []byte("RecoveryInfo") - // ErrRecoveryBusy another recovery in progress, can not get recovery manager lock ErrRecoveryBusy = errors.New("another recovery in progress") // ErrInvalidAcctID can not find account by account id - ErrInvalidAcctID = errors.New("invalid account id") + ErrInvalidAcctID = errors.New("invalid account id") + ErrGetRecoveryStatus = errors.New("failed to get recovery status.") + ErrRecoveryStatus = errors.New("recovery status is nil.") ) // branchRecoveryState maintains the required state in-order to properly @@ -127,8 +124,8 @@ func newAddressRecoveryState(recoveryWindow uint64, account *account.Account) *a } } -// recoveryState used to record the status of a recovery process. -type recoveryState struct { +// RecoveryState used to record the status of a recovery process. +type RecoveryState struct { // XPubs recovery account xPubs XPubs []chainkd.XPub @@ -145,8 +142,8 @@ type recoveryState struct { AccountsStatus map[string]*addressRecoveryState } -func newRecoveryState() *recoveryState { - return &recoveryState{ +func newRecoveryState() *RecoveryState { + return &RecoveryState{ AccountsStatus: make(map[string]*addressRecoveryState), StartTime: time.Now(), } @@ -155,7 +152,7 @@ func newRecoveryState() *recoveryState { // stateForScope returns a ScopeRecoveryState for the provided key scope. If one // does not already exist, a new one will be generated with the RecoveryState's // recoveryWindow. -func (rs *recoveryState) stateForScope(account *account.Account) { +func (rs *RecoveryState) stateForScope(account *account.Account) { // If the account recovery state already exists, return it. if _, ok := rs.AccountsStatus[account.ID]; ok { return @@ -170,7 +167,7 @@ func (rs *recoveryState) stateForScope(account *account.Account) { type recoveryManager struct { mu sync.Mutex - db dbm.DB + store WalletStore accountMgr *account.Manager locked int32 @@ -179,17 +176,17 @@ type recoveryManager struct { // state encapsulates and allocates the necessary recovery state for all // key scopes and subsidiary derivation paths. - state *recoveryState + state *RecoveryState //addresses all addresses derivation lookahead used when // attempting to recover the set of used addresses. addresses map[bc.Hash]*account.CtrlProgram } -// newRecoveryManager create recovery manger. -func newRecoveryManager(db dbm.DB, accountMgr *account.Manager) *recoveryManager { +// NewRecoveryManager create recovery manger. +func NewRecoveryManager(store WalletStore, accountMgr *account.Manager) *recoveryManager { return &recoveryManager{ - db: db, + store: store, accountMgr: accountMgr, addresses: make(map[bc.Hash]*account.CtrlProgram), state: newRecoveryState(), @@ -249,13 +246,7 @@ func (m *recoveryManager) AcctResurrect(xPubs []chainkd.XPub) error { } func (m *recoveryManager) commitStatusInfo() error { - rawStatus, err := json.Marshal(m.state) - if err != nil { - return err - } - - m.db.Set(recoveryKey, rawStatus) - return nil + return m.store.SetRecoveryStatus(m.state) } func genAcctAlias(xPubs []chainkd.XPub, index uint64) string { @@ -364,7 +355,7 @@ func (m *recoveryManager) FilterRecoveryTxs(b *types.Block) error { } func (m *recoveryManager) finished() { - m.db.Delete(recoveryKey) + m.store.DeleteRecoveryStatus() m.started = false m.addresses = make(map[bc.Hash]*account.CtrlProgram) m.state = newRecoveryState() @@ -375,14 +366,17 @@ func (m *recoveryManager) LoadStatusInfo() error { m.mu.Lock() defer m.mu.Unlock() - rawStatus := m.db.Get(recoveryKey) - if rawStatus == nil { + if m.state == nil { + return ErrRecoveryStatus + } + status, err := m.store.GetRecoveryStatus() + if err == ErrGetRecoveryStatus { return nil } - - if err := json.Unmarshal(rawStatus, m.state); err != nil { + if err != nil { return err } + m.state = status if m.state.XPubs != nil && !m.tryStartXPubsRec() { return ErrRecoveryBusy diff --git a/wallet/recovery_test.go b/wallet/recovery_test.go index 0b6fb56d..c4ec390e 100644 --- a/wallet/recovery_test.go +++ b/wallet/recovery_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/vapor/account" + acc "github.com/vapor/account" "github.com/vapor/blockchain/pseudohsm" "github.com/vapor/blockchain/signers" "github.com/vapor/blockchain/txbuilder" @@ -141,6 +142,7 @@ func TestXPubsRecoveryLock(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", dirPath) + walletStore := NewMockWalletStore(testDB) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -151,8 +153,9 @@ func TestXPubsRecoveryLock(t *testing.T) { t.Fatal(err) } - acctMgr := account.NewManager(testDB, nil) - recoveryMgr := newRecoveryManager(testDB, acctMgr) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) + recoveryMgr := NewRecoveryManager(walletStore, acctMgr) recoveryMgr.state = newRecoveryState() recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub} recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow) @@ -190,6 +193,7 @@ func TestExtendScanAddresses(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", dirPath) + walletStore := NewMockWalletStore(testDB) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -200,8 +204,9 @@ func TestExtendScanAddresses(t *testing.T) { t.Fatal(err) } - acctMgr := account.NewManager(testDB, nil) - recoveryMgr := newRecoveryManager(testDB, acctMgr) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) + recoveryMgr := NewRecoveryManager(walletStore, acctMgr) acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}} acc2 := &account.Account{ID: "testB", Alias: "test2"} acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 2, DeriveRule: 3}} @@ -246,6 +251,7 @@ func TestRecoveryFromXPubs(t *testing.T) { testDB := dbm.NewDB("testdb", "leveldb", dirPath) recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath) + recoveryStore := NewMockWalletStore(recoveryDB) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -256,10 +262,12 @@ func TestRecoveryFromXPubs(t *testing.T) { t.Fatal(err) } - acctMgr := account.NewManager(testDB, nil) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, false) - recAcctMgr := account.NewManager(recoveryDB, nil) - recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr) + recActStore := NewMockAccountStore(recoveryDB) + recAcctMgr := account.NewManager(recActStore, nil) + recoveryMgr := NewRecoveryManager(recoveryStore, recAcctMgr) cases := []struct { xPubs []chainkd.XPub @@ -290,7 +298,7 @@ func TestRecoveryFromXPubs(t *testing.T) { for _, acct := range Accounts { tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex) - if err != nil { + if err != nil && err != acc.ErrFindAccount { t.Fatal("recovery from XPubs err:", err) } @@ -320,6 +328,7 @@ func TestRecoveryByRescanAccount(t *testing.T) { testDB := dbm.NewDB("testdb", "leveldb", dirPath) recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath) + recoveryStore := NewMockWalletStore(recoveryDB) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -330,7 +339,8 @@ func TestRecoveryByRescanAccount(t *testing.T) { t.Fatal(err) } - acctMgr := account.NewManager(testDB, nil) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, true) if err != nil { t.Fatal("recovery by rescan account err:", err) @@ -341,14 +351,15 @@ func TestRecoveryByRescanAccount(t *testing.T) { t.Fatal("recovery by rescan account err:", err) } - recAcctMgr := account.NewManager(recoveryDB, nil) + recActStore := NewMockAccountStore(recoveryDB) + recAcctMgr := account.NewManager(recActStore, nil) for _, acct := range allAccounts { if err := recAcctMgr.SaveAccount(acct); err != nil { t.Fatal("recovery by rescan account err:", err) } } - recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr) + recoveryMgr := NewRecoveryManager(recoveryStore, recAcctMgr) acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}} @@ -376,7 +387,7 @@ func TestRecoveryByRescanAccount(t *testing.T) { for _, acct := range accounts { tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex) - if err != nil { + if err != nil && err != acc.ErrFindAccount { t.Fatal("recovery from XPubs err:", err) } @@ -408,6 +419,7 @@ func TestReportFound(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", dirPath) + testStore := NewMockWalletStore(testDB) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -423,8 +435,9 @@ func TestReportFound(t *testing.T) { t.Fatal(err) } - acctMgr := account.NewManager(testDB, nil) - recoveryMgr := newRecoveryManager(testDB, acctMgr) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) + recoveryMgr := NewRecoveryManager(testStore, acctMgr) acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}} acc2 := &account.Account{ID: "testB", Alias: "test2", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0032}} acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 2, DeriveRule: signers.BIP0044}} @@ -495,6 +508,7 @@ func TestLoadStatusInfo(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", "temp") + testStore := NewMockWalletStore(testDB) defer os.RemoveAll("temp") hsm, err := pseudohsm.New(dirPath) @@ -507,8 +521,9 @@ func TestLoadStatusInfo(t *testing.T) { t.Fatal(err) } - acctMgr := account.NewManager(testDB, nil) - recoveryMgr := newRecoveryManager(testDB, acctMgr) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) + recoveryMgr := NewRecoveryManager(testStore, acctMgr) // StatusInit init recovery status manager. recoveryMgr.state = newRecoveryState() recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub} @@ -521,7 +536,7 @@ func TestLoadStatusInfo(t *testing.T) { recoveryMgr.commitStatusInfo() - recoveryMgrRestore := newRecoveryManager(testDB, acctMgr) + recoveryMgrRestore := NewRecoveryManager(testStore, acctMgr) if err := recoveryMgrRestore.LoadStatusInfo(); err != nil { t.Fatal("TestLoadStatusInfo err:", err) } @@ -569,10 +584,12 @@ func TestLock(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", "temp") + testStore := NewMockWalletStore(testDB) defer os.RemoveAll("temp") - acctMgr := account.NewManager(testDB, nil) - recoveryMgr := newRecoveryManager(testDB, acctMgr) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) + recoveryMgr := NewRecoveryManager(testStore, acctMgr) if !recoveryMgr.tryStartXPubsRec() { t.Fatal("recovery manager try lock test err") } @@ -610,15 +627,6 @@ func TestStateForScope(t *testing.T) { } } -func bip44ContractIndexKey(accountID string, change bool) []byte { - contractIndexPrefix := []byte("ContractIndex") - key := append(contractIndexPrefix, accountID...) - if change { - return append(key, []byte{1}...) - } - return append(key, []byte{0}...) -} - func TestContractIndexResidue(t *testing.T) { dirPath, err := ioutil.TempDir(".", "") if err != nil { @@ -627,6 +635,7 @@ func TestContractIndexResidue(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", dirPath) + testStore := NewMockWalletStore(testDB) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -638,14 +647,15 @@ func TestContractIndexResidue(t *testing.T) { } contractIndexResidue := uint64(5) - acctMgr := account.NewManager(testDB, nil) - recoveryMgr := newRecoveryManager(testDB, acctMgr) + acctStore := NewMockAccountStore(testDB) + acctMgr := account.NewManager(acctStore, nil) + recoveryMgr := NewRecoveryManager(testStore, acctMgr) acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}} cp1 := &account.CtrlProgram{AccountID: acct.ID, Address: "address1", KeyIndex: 10, Change: false} setContractIndexKey := func(acctMgr *account.Manager, accountID string, change bool) { - testDB.Set(bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(contractIndexResidue)) + testDB.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(contractIndexResidue)) } delAccount := func(acctMgr *account.Manager, accountID string, change bool) { diff --git a/wallet/store.go b/wallet/store.go new file mode 100644 index 00000000..90b6050c --- /dev/null +++ b/wallet/store.go @@ -0,0 +1,37 @@ +package wallet + +import ( + acc "github.com/vapor/account" + "github.com/vapor/asset" + "github.com/vapor/blockchain/query" + "github.com/vapor/protocol/bc" +) + +// WalletStore interface contains wallet storage functions. +type WalletStore interface { + InitBatch() WalletStore + CommitBatch() error + DeleteContractUTXO(bc.Hash) + DeleteRecoveryStatus() + DeleteTransactions(uint64) + DeleteUnconfirmedTransaction(string) + DeleteWalletTransactions() + DeleteWalletUTXOs() + GetAsset(*bc.AssetID) (*asset.Asset, error) + GetGlobalTransactionIndex(string) []byte + GetStandardUTXO(bc.Hash) (*acc.UTXO, error) + GetTransaction(string) (*query.AnnotatedTx, error) + GetUnconfirmedTransaction(string) (*query.AnnotatedTx, error) + GetRecoveryStatus() (*RecoveryState, error) + GetWalletInfo() (*StatusInfo, error) + ListAccountUTXOs(string, bool) ([]*acc.UTXO, error) + ListTransactions(string, string, uint, bool) ([]*query.AnnotatedTx, error) + ListUnconfirmedTransactions() ([]*query.AnnotatedTx, error) + SetAssetDefinition(*bc.AssetID, []byte) + SetContractUTXO(bc.Hash, *acc.UTXO) error + SetGlobalTransactionIndex(string, *bc.Hash, uint64) + SetRecoveryStatus(*RecoveryState) error + SetTransaction(uint64, *query.AnnotatedTx) error + SetUnconfirmedTransaction(string, *query.AnnotatedTx) error + SetWalletInfo(*StatusInfo) error +} diff --git a/wallet/unconfirmed.go b/wallet/unconfirmed.go index 843c289c..8effda0a 100644 --- a/wallet/unconfirmed.go +++ b/wallet/unconfirmed.go @@ -1,14 +1,14 @@ package wallet import ( - "encoding/json" "fmt" "sort" "time" + "github.com/vapor/protocol/bc" + log "github.com/sirupsen/logrus" - "github.com/vapor/account" "github.com/vapor/blockchain/query" "github.com/vapor/crypto/sha3pool" "github.com/vapor/protocol" @@ -16,16 +16,10 @@ import ( ) const ( - //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix - UnconfirmedTxPrefix = "UTXS:" UnconfirmedTxCheckPeriod = 30 * time.Minute MaxUnconfirmedTxDuration = 24 * time.Hour ) -func calcUnconfirmedTxKey(formatKey string) []byte { - return []byte(UnconfirmedTxPrefix + formatKey) -} - // SortByTimestamp implements sort.Interface for AnnotatedTx slices type SortByTimestamp []*query.AnnotatedTx @@ -54,37 +48,33 @@ func (w *Wallet) AddUnconfirmedTx(txD *protocol.TxDesc) { // GetUnconfirmedTxs get account unconfirmed transactions, filter transactions by accountID when accountID is not empty func (w *Wallet) GetUnconfirmedTxs(accountID string) ([]*query.AnnotatedTx, error) { annotatedTxs := []*query.AnnotatedTx{} - txIter := w.DB.IteratorPrefix([]byte(UnconfirmedTxPrefix)) - defer txIter.Release() - - for txIter.Next() { - annotatedTx := &query.AnnotatedTx{} - if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil { - return nil, err - } + annotatedTxs, err := w.Store.ListUnconfirmedTransactions() + if err != nil { + return nil, err + } + newAnnotatedTxs := []*query.AnnotatedTx{} + for _, annotatedTx := range annotatedTxs { if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) { annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx}) - annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...) + newAnnotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, newAnnotatedTxs...) } } - sort.Sort(SortByTimestamp(annotatedTxs)) - return annotatedTxs, nil + sort.Sort(SortByTimestamp(newAnnotatedTxs)) + return newAnnotatedTxs, nil } // GetUnconfirmedTxByTxID get unconfirmed transaction by txID func (w *Wallet) GetUnconfirmedTxByTxID(txID string) (*query.AnnotatedTx, error) { - annotatedTx := &query.AnnotatedTx{} - txInfo := w.DB.Get(calcUnconfirmedTxKey(txID)) - if txInfo == nil { - return nil, fmt.Errorf("No transaction(tx_id=%s) from txpool", txID) - } - - if err := json.Unmarshal(txInfo, annotatedTx); err != nil { + annotatedTx, err := w.Store.GetUnconfirmedTransaction(txID) + if err != nil { return nil, err } + if annotatedTx == nil { + return nil, fmt.Errorf("No transaction(tx_id=%s) from txpool", txID) + } annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx}) return annotatedTx, nil } @@ -94,7 +84,8 @@ func (w *Wallet) RemoveUnconfirmedTx(txD *protocol.TxDesc) { if !w.checkRelatedTransaction(txD.Tx) { return } - w.DB.Delete(calcUnconfirmedTxKey(txD.Tx.ID.String())) + + w.Store.DeleteUnconfirmedTransaction(txD.Tx.ID.String()) w.AccountMgr.RemoveUnconfirmedUtxo(txD.Tx.ResultIds) } @@ -121,7 +112,12 @@ func (w *Wallet) checkRelatedTransaction(tx *types.Tx) bool { for _, v := range tx.Outputs { var hash [32]byte sha3pool.Sum256(hash[:], v.ControlProgram()) - if bytes := w.DB.Get(account.ContractKey(hash)); bytes != nil { + cp, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash)) + if err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err, "hash": string(hash[:])}).Error("checkRelatedTransaction fail.") + continue + } + if cp != nil { return true } } @@ -131,7 +127,12 @@ func (w *Wallet) checkRelatedTransaction(tx *types.Tx) bool { if err != nil { continue } - if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil { + utxo, err := w.Store.GetStandardUTXO(outid) + if err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err, "outputID": outid.String()}).Error("checkRelatedTransaction fail.") + continue + } + if utxo != nil { return true } } @@ -148,14 +149,11 @@ func (w *Wallet) saveUnconfirmedTx(tx *types.Tx) error { annotatedTx := w.buildAnnotatedUnconfirmedTx(tx) annotatedTxs := []*query.AnnotatedTx{} annotatedTxs = append(annotatedTxs, annotatedTx) - annotateTxsAccount(annotatedTxs, w.DB) + w.annotateTxsAccount(annotatedTxs) - rawTx, err := json.Marshal(annotatedTxs[0]) - if err != nil { + if err := w.Store.SetUnconfirmedTransaction(tx.ID.String(), annotatedTxs[0]); err != nil { return err } - - w.DB.Set(calcUnconfirmedTxKey(tx.ID.String()), rawTx) return nil } @@ -164,9 +162,10 @@ func (w *Wallet) delExpiredTxs() error { if err != nil { return err } + for _, tx := range AnnotatedTx { if time.Now().After(time.Unix(int64(tx.Timestamp), 0).Add(MaxUnconfirmedTxDuration)) { - w.DB.Delete(calcUnconfirmedTxKey(tx.ID.String())) + w.Store.DeleteUnconfirmedTransaction(tx.ID.String()) } } return nil @@ -178,8 +177,10 @@ func (w *Wallet) delUnconfirmedTx() { log.WithFields(log.Fields{"module": logModule, "err": err}).Error("wallet fail on delUnconfirmedTx") return } + ticker := time.NewTicker(UnconfirmedTxCheckPeriod) defer ticker.Stop() + for { <-ticker.C if err := w.delExpiredTxs(); err != nil { diff --git a/wallet/unconfirmed_test.go b/wallet/unconfirmed_test.go index 60ece604..59048455 100644 --- a/wallet/unconfirmed_test.go +++ b/wallet/unconfirmed_test.go @@ -27,9 +27,11 @@ func TestWalletUnconfirmedTxs(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", "temp") + testStore := NewMockWalletStore(testDB) defer os.RemoveAll("temp") - accountManager := account.NewManager(testDB, nil) + accountStore := NewMockAccountStore(testDB) + accountManager := account.NewManager(accountStore, nil) hsm, err := pseudohsm.New(dirPath) if err != nil { t.Fatal(err) @@ -56,7 +58,7 @@ func TestWalletUnconfirmedTxs(t *testing.T) { asset := bc.AssetID{V0: 5} dispatcher := event.NewDispatcher() - w := mockWallet(testDB, accountManager, reg, nil, dispatcher, false) + w := mockWallet(testStore, accountManager, reg, nil, dispatcher, false) utxos := []*account.UTXO{} btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID) utxos = append(utxos, btmUtxo) @@ -126,7 +128,7 @@ func AnnotatedTxs(txs []*types.Tx, w *Wallet) []*query.AnnotatedTx { annotatedTxs = append(annotatedTxs, annotatedTx) } - annotateTxsAccount(annotatedTxs, w.DB) + w.annotateTxsAccount(annotatedTxs) annotateTxsAsset(w, annotatedTxs) return annotatedTxs diff --git a/wallet/utxo.go b/wallet/utxo.go index 1eec505f..0419dafa 100644 --- a/wallet/utxo.go +++ b/wallet/utxo.go @@ -1,54 +1,42 @@ package wallet import ( - "encoding/json" - log "github.com/sirupsen/logrus" "github.com/vapor/account" "github.com/vapor/consensus" "github.com/vapor/consensus/segwit" "github.com/vapor/crypto/sha3pool" - dbm "github.com/vapor/database/leveldb" - "github.com/vapor/errors" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/bc/types" ) // GetAccountUtxos return all account unspent outputs func (w *Wallet) GetAccountUtxos(accountID string, id string, unconfirmed, isSmartContract bool, vote bool) []*account.UTXO { - prefix := account.UTXOPreFix - if isSmartContract { - prefix = account.SUTXOPrefix - } - accountUtxos := []*account.UTXO{} if unconfirmed { accountUtxos = w.AccountMgr.ListUnconfirmedUtxo(accountID, isSmartContract) } - accountUtxoIter := w.DB.IteratorPrefix([]byte(prefix + id)) - defer accountUtxoIter.Release() - - for accountUtxoIter.Next() { - accountUtxo := &account.UTXO{} - if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil { - log.WithFields(log.Fields{"module": logModule, "err": err}).Warn("GetAccountUtxos fail on unmarshal utxo") - continue - } + confirmedUTXOs, err := w.Store.ListAccountUTXOs(id, isSmartContract) + if err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err}).Error("GetAccountUtxos fail.") + } + accountUtxos = append(accountUtxos, confirmedUTXOs...) + newAccountUtxos := []*account.UTXO{} + for _, accountUtxo := range accountUtxos { if vote && accountUtxo.Vote == nil { continue } - if accountID == accountUtxo.AccountID || accountID == "" { - accountUtxos = append(accountUtxos, accountUtxo) + newAccountUtxos = append(newAccountUtxos, accountUtxo) } } - return accountUtxos + return newAccountUtxos } -func (w *Wallet) attachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.TransactionStatus) { +func (w *Wallet) attachUtxos(b *types.Block, txStatus *bc.TransactionStatus, store WalletStore) { for txIndex, tx := range b.Transactions { statusFail, err := txStatus.GetStatus(txIndex) if err != nil { @@ -60,22 +48,22 @@ func (w *Wallet) attachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.Trans inputUtxos := txInToUtxos(tx, statusFail) for _, inputUtxo := range inputUtxos { if segwit.IsP2WScript(inputUtxo.ControlProgram) { - batch.Delete(account.StandardUTXOKey(inputUtxo.OutputID)) + w.AccountMgr.DeleteStandardUTXO(inputUtxo.OutputID) } else { - batch.Delete(account.ContractUTXOKey(inputUtxo.OutputID)) + store.DeleteContractUTXO(inputUtxo.OutputID) } } //hand update the transaction output utxos outputUtxos := txOutToUtxos(tx, statusFail, b.Height) utxos := w.filterAccountUtxo(outputUtxos) - if err := batchSaveUtxos(utxos, batch); err != nil { - log.WithFields(log.Fields{"module": logModule, "err": err}).Error("attachUtxos fail on batchSaveUtxos") + if err := w.saveUtxos(utxos, store); err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err}).Error("attachUtxos fail on saveUtxos") } } } -func (w *Wallet) detachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.TransactionStatus) { +func (w *Wallet) detachUtxos(b *types.Block, txStatus *bc.TransactionStatus, store WalletStore) { for txIndex := len(b.Transactions) - 1; txIndex >= 0; txIndex-- { tx := b.Transactions[txIndex] for j := range tx.Outputs { @@ -93,9 +81,9 @@ func (w *Wallet) detachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.Trans } if segwit.IsP2WScript(code) { - batch.Delete(account.StandardUTXOKey(*tx.ResultIds[j])) + w.AccountMgr.DeleteStandardUTXO(*tx.ResultIds[j]) } else { - batch.Delete(account.ContractUTXOKey(*tx.ResultIds[j])) + store.DeleteContractUTXO(*tx.ResultIds[j]) } } @@ -107,7 +95,7 @@ func (w *Wallet) detachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.Trans inputUtxos := txInToUtxos(tx, statusFail) utxos := w.filterAccountUtxo(inputUtxos) - if err := batchSaveUtxos(utxos, batch); err != nil { + if err := w.saveUtxos(utxos, store); err != nil { log.WithFields(log.Fields{"module": logModule, "err": err}).Error("detachUtxos fail on batchSaveUtxos") return } @@ -132,14 +120,12 @@ func (w *Wallet) filterAccountUtxo(utxos []*account.UTXO) []*account.UTXO { var hash [32]byte sha3pool.Sum256(hash[:], []byte(s)) - data := w.DB.Get(account.ContractKey(hash)) - if data == nil { + cp, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash)) + if err != nil { + log.WithFields(log.Fields{"module": logModule, "err": err}).Error("filterAccountUtxo fail.") continue } - - cp := &account.CtrlProgram{} - if err := json.Unmarshal(data, cp); err != nil { - log.WithFields(log.Fields{"module": logModule, "err": err}).Error("filterAccountUtxo fail on unmarshal control program") + if cp == nil { continue } @@ -154,17 +140,16 @@ func (w *Wallet) filterAccountUtxo(utxos []*account.UTXO) []*account.UTXO { return result } -func batchSaveUtxos(utxos []*account.UTXO, batch dbm.Batch) error { +func (w *Wallet) saveUtxos(utxos []*account.UTXO, store WalletStore) error { for _, utxo := range utxos { - data, err := json.Marshal(utxo) - if err != nil { - return errors.Wrap(err, "failed marshal accountutxo") - } - if segwit.IsP2WScript(utxo.ControlProgram) { - batch.Set(account.StandardUTXOKey(utxo.OutputID), data) + if err := w.AccountMgr.SetStandardUTXO(utxo.OutputID, utxo); err != nil { + return err + } } else { - batch.Set(account.ContractUTXOKey(utxo.OutputID), data) + if err := store.SetContractUTXO(utxo.OutputID, utxo); err != nil { + return err + } } } return nil diff --git a/wallet/utxo_test.go b/wallet/utxo_test.go index 57443920..2df108c3 100644 --- a/wallet/utxo_test.go +++ b/wallet/utxo_test.go @@ -18,6 +18,7 @@ import ( func TestGetAccountUtxos(t *testing.T) { testDB := dbm.NewDB("testdb", "leveldb", "temp") + testStore := NewMockWalletStore(testDB) defer func() { testDB.Close() os.RemoveAll("temp") @@ -40,16 +41,16 @@ func TestGetAccountUtxos(t *testing.T) { }, { dbUtxos: map[string]*account.UTXO{ - string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{ OutputID: bc.Hash{V0: 1}, }, - string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{ OutputID: bc.Hash{V0: 2}, }, - string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ OutputID: bc.Hash{V0: 3}, }, - string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ + string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ OutputID: bc.Hash{V0: 4}, }, }, @@ -64,16 +65,16 @@ func TestGetAccountUtxos(t *testing.T) { }, { dbUtxos: map[string]*account.UTXO{ - string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{ OutputID: bc.Hash{V0: 1}, }, - string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{ OutputID: bc.Hash{V0: 2}, }, - string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ OutputID: bc.Hash{V0: 3}, }, - string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ + string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ OutputID: bc.Hash{V0: 4}, }, }, @@ -92,16 +93,16 @@ func TestGetAccountUtxos(t *testing.T) { }, { dbUtxos: map[string]*account.UTXO{ - string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{ OutputID: bc.Hash{V0: 1}, }, - string(account.StandardUTXOKey(bc.Hash{V0: 1, V1: 2})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 1, V1: 2})): &account.UTXO{ OutputID: bc.Hash{V0: 1, V1: 2}, }, - string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{ OutputID: bc.Hash{V0: 2}, }, - string(account.StandardUTXOKey(bc.Hash{V0: 2, V1: 2})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 2, V1: 2})): &account.UTXO{ OutputID: bc.Hash{V0: 2, V1: 2}, }, }, @@ -121,10 +122,10 @@ func TestGetAccountUtxos(t *testing.T) { }, { dbUtxos: map[string]*account.UTXO{ - string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ OutputID: bc.Hash{V0: 3}, }, - string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ + string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ OutputID: bc.Hash{V0: 4}, }, }, @@ -153,10 +154,10 @@ func TestGetAccountUtxos(t *testing.T) { }, { dbUtxos: map[string]*account.UTXO{ - string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ + string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{ OutputID: bc.Hash{V0: 3}, }, - string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ + string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{ OutputID: bc.Hash{V0: 4}, }, }, @@ -185,7 +186,7 @@ func TestGetAccountUtxos(t *testing.T) { }, } - w := &Wallet{DB: testDB} + w := &Wallet{Store: testStore} for i, c := range cases { for k, u := range c.dbUtxos { data, err := json.Marshal(u) @@ -195,7 +196,8 @@ func TestGetAccountUtxos(t *testing.T) { testDB.Set([]byte(k), data) } - w.AccountMgr = account.NewManager(testDB, nil) + acccountStore := NewMockAccountStore(testDB) + w.AccountMgr = account.NewManager(acccountStore, nil) w.AccountMgr.AddUnconfirmedUtxo(c.unconfirmedUtxos) gotUtxos := w.GetAccountUtxos("", c.id, c.unconfirmed, c.isSmartContract, false) if !testutil.DeepEqual(gotUtxos, c.wantUtxos) { @@ -210,6 +212,7 @@ func TestGetAccountUtxos(t *testing.T) { func TestFilterAccountUtxo(t *testing.T) { testDB := dbm.NewDB("testdb", "leveldb", "temp") + testStore := NewMockWalletStore(testDB) defer func() { testDB.Close() os.RemoveAll("temp") @@ -227,7 +230,7 @@ func TestFilterAccountUtxo(t *testing.T) { }, { dbPrograms: map[string]*account.CtrlProgram{ - "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{ + "41533a013a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{ AccountID: "testAccount", Address: "testAddress", KeyIndex: 53, @@ -287,13 +290,13 @@ func TestFilterAccountUtxo(t *testing.T) { }, { dbPrograms: map[string]*account.CtrlProgram{ - "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{ + "41533a013a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{ AccountID: "testAccount", Address: "testAddress", KeyIndex: 53, Change: true, }, - "436f6e74726163743adb4d86262c12ba70d50b3ca3ae102d5682436243bd1e8c79569603f75675036a": &account.CtrlProgram{ + "41533a013adb4d86262c12ba70d50b3ca3ae102d5682436243bd1e8c79569603f75675036a": &account.CtrlProgram{ AccountID: "testAccount2", Address: "testAddress2", KeyIndex: 72, @@ -349,7 +352,12 @@ func TestFilterAccountUtxo(t *testing.T) { }, } - w := &Wallet{DB: testDB} + accountStore := NewMockAccountStore(testDB) + accountManager := account.NewManager(accountStore, nil) + w := &Wallet{ + Store: testStore, + AccountMgr: accountManager, + } for i, c := range cases { for s, p := range c.dbPrograms { data, err := json.Marshal(p) diff --git a/wallet/wallet.go b/wallet/wallet.go index 92a24b96..ab865bbd 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1,7 +1,6 @@ package wallet import ( - "encoding/json" "sync" log "github.com/sirupsen/logrus" @@ -9,7 +8,6 @@ import ( "github.com/vapor/account" "github.com/vapor/asset" "github.com/vapor/blockchain/pseudohsm" - dbm "github.com/vapor/database/leveldb" "github.com/vapor/errors" "github.com/vapor/event" "github.com/vapor/protocol" @@ -25,10 +23,12 @@ const ( var ( currentVersion = uint(1) - walletKey = []byte("walletInfo") errBestBlockNotFoundInCore = errors.New("best block not found in core") errWalletVersionMismatch = errors.New("wallet version mismatch") + ErrGetWalletStatusInfo = errors.New("failed get wallet info") + ErrGetAsset = errors.New("Failed to find asset definition") + ErrAccntTxIDNotFound = errors.New("account TXID not found") ) //StatusInfo is base valid block info to handle orphan block rollback @@ -42,36 +42,36 @@ type StatusInfo struct { //Wallet is related to storing account unspent outputs type Wallet struct { - DB dbm.DB + Store WalletStore rw sync.RWMutex - status StatusInfo + Status StatusInfo TxIndexFlag bool AccountMgr *account.Manager AssetReg *asset.Registry Hsm *pseudohsm.HSM - chain *protocol.Chain + Chain *protocol.Chain RecoveryMgr *recoveryManager - eventDispatcher *event.Dispatcher - txMsgSub *event.Subscription + EventDispatcher *event.Dispatcher + TxMsgSub *event.Subscription rescanCh chan struct{} } //NewWallet return a new wallet instance -func NewWallet(walletDB dbm.DB, account *account.Manager, asset *asset.Registry, hsm *pseudohsm.HSM, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) (*Wallet, error) { +func NewWallet(store WalletStore, account *account.Manager, asset *asset.Registry, hsm *pseudohsm.HSM, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) (*Wallet, error) { w := &Wallet{ - DB: walletDB, + Store: store, AccountMgr: account, AssetReg: asset, - chain: chain, + Chain: chain, Hsm: hsm, - RecoveryMgr: newRecoveryManager(walletDB, account), - eventDispatcher: dispatcher, + RecoveryMgr: NewRecoveryManager(store, account), + EventDispatcher: dispatcher, rescanCh: make(chan struct{}, 1), TxIndexFlag: txIndexFlag, } - if err := w.loadWalletInfo(); err != nil { + if err := w.LoadWalletInfo(); err != nil { return nil, err } @@ -80,22 +80,22 @@ func NewWallet(walletDB dbm.DB, account *account.Manager, asset *asset.Registry, } var err error - w.txMsgSub, err = w.eventDispatcher.Subscribe(protocol.TxMsgEvent{}) + w.TxMsgSub, err = w.EventDispatcher.Subscribe(protocol.TxMsgEvent{}) if err != nil { return nil, err } go w.walletUpdater() go w.delUnconfirmedTx() - go w.memPoolTxQueryLoop() + go w.MemPoolTxQueryLoop() return w, nil } -// memPoolTxQueryLoop constantly pass a transaction accepted by mempool to the wallet. -func (w *Wallet) memPoolTxQueryLoop() { +// MemPoolTxQueryLoop constantly pass a transaction accepted by mempool to the wallet. +func (w *Wallet) MemPoolTxQueryLoop() { for { select { - case obj, ok := <-w.txMsgSub.Chan(): + case obj, ok := <-w.TxMsgSub.Chan(): if !ok { log.WithFields(log.Fields{"module": logModule}).Warning("tx pool tx msg subscription channel closed") return @@ -120,51 +120,50 @@ func (w *Wallet) memPoolTxQueryLoop() { } func (w *Wallet) checkWalletInfo() error { - if w.status.Version != currentVersion { + if w.Status.Version != currentVersion { return errWalletVersionMismatch - } else if !w.chain.BlockExist(&w.status.BestHash) { + } else if !w.Chain.BlockExist(&w.Status.BestHash) { return errBestBlockNotFoundInCore } return nil } -//loadWalletInfo return stored wallet info and nil, +//LoadWalletInfo return stored wallet info and nil, //if error, return initial wallet info and err -func (w *Wallet) loadWalletInfo() error { - if rawWallet := w.DB.Get(walletKey); rawWallet != nil { - if err := json.Unmarshal(rawWallet, &w.status); err != nil { - return err - } +func (w *Wallet) LoadWalletInfo() error { + walletStatus, err := w.Store.GetWalletInfo() + if walletStatus == nil && err != ErrGetWalletStatusInfo { + return err + } - err := w.checkWalletInfo() + if walletStatus != nil { + w.Status = *walletStatus + err = w.checkWalletInfo() if err == nil { return nil } log.WithFields(log.Fields{"module": logModule}).Warn(err.Error()) - w.deleteAccountTxs() - w.deleteUtxos() + w.Store.DeleteWalletTransactions() + w.Store.DeleteWalletUTXOs() } - w.status.Version = currentVersion - w.status.WorkHash = bc.Hash{} - block, err := w.chain.GetBlockByHeight(0) + w.Status.Version = currentVersion + w.Status.WorkHash = bc.Hash{} + block, err := w.Chain.GetBlockByHeight(0) if err != nil { return err } + return w.AttachBlock(block) } -func (w *Wallet) commitWalletInfo(batch dbm.Batch) error { - rawWallet, err := json.Marshal(w.status) - if err != nil { +func (w *Wallet) commitWalletInfo(store WalletStore) error { + if err := store.SetWalletInfo(&w.Status); err != nil { log.WithFields(log.Fields{"module": logModule, "err": err}).Error("save wallet info") return err } - - batch.Set(walletKey, rawWallet) - batch.Write() return nil } @@ -173,13 +172,13 @@ func (w *Wallet) AttachBlock(block *types.Block) error { w.rw.Lock() defer w.rw.Unlock() - if block.PreviousBlockHash != w.status.WorkHash { + if block.PreviousBlockHash != w.Status.WorkHash { log.Warn("wallet skip attachBlock due to status hash not equal to previous hash") return nil } blockHash := block.Hash() - txStatus, err := w.chain.GetTransactionStatus(&blockHash) + txStatus, err := w.Chain.GetTransactionStatus(&blockHash) if err != nil { return err } @@ -189,19 +188,35 @@ func (w *Wallet) AttachBlock(block *types.Block) error { w.RecoveryMgr.finished() } - storeBatch := w.DB.NewBatch() - if err := w.indexTransactions(storeBatch, block, txStatus); err != nil { + annotatedTxs := w.filterAccountTxs(block, txStatus) + if err := saveExternalAssetDefinition(block, w.Store); err != nil { + return err + } + + w.annotateTxsAccount(annotatedTxs) + + newStore := w.Store.InitBatch() + if err := w.indexTransactions(block, txStatus, annotatedTxs, newStore); err != nil { + return err + } + + w.attachUtxos(block, txStatus, newStore) + w.Status.WorkHeight = block.Height + w.Status.WorkHash = block.Hash() + if w.Status.WorkHeight >= w.Status.BestHeight { + w.Status.BestHeight = w.Status.WorkHeight + w.Status.BestHash = w.Status.WorkHash + } + + if err := w.commitWalletInfo(newStore); err != nil { return err } - w.attachUtxos(storeBatch, block, txStatus) - w.status.WorkHeight = block.Height - w.status.WorkHash = block.Hash() - if w.status.WorkHeight >= w.status.BestHeight { - w.status.BestHeight = w.status.WorkHeight - w.status.BestHash = w.status.WorkHash + if err := newStore.CommitBatch(); err != nil { + return err } - return w.commitWalletInfo(storeBatch) + + return nil } // DetachBlock detach a block and rollback state @@ -210,32 +225,40 @@ func (w *Wallet) DetachBlock(block *types.Block) error { defer w.rw.Unlock() blockHash := block.Hash() - txStatus, err := w.chain.GetTransactionStatus(&blockHash) + txStatus, err := w.Chain.GetTransactionStatus(&blockHash) if err != nil { return err } - storeBatch := w.DB.NewBatch() - w.detachUtxos(storeBatch, block, txStatus) - w.deleteTransactions(storeBatch, w.status.BestHeight) + newStore := w.Store.InitBatch() - w.status.BestHeight = block.Height - 1 - w.status.BestHash = block.PreviousBlockHash + w.detachUtxos(block, txStatus, newStore) + newStore.DeleteTransactions(w.Status.BestHeight) - if w.status.WorkHeight > w.status.BestHeight { - w.status.WorkHeight = w.status.BestHeight - w.status.WorkHash = w.status.BestHash + w.Status.BestHeight = block.Height - 1 + w.Status.BestHash = block.PreviousBlockHash + + if w.Status.WorkHeight > w.Status.BestHeight { + w.Status.WorkHeight = w.Status.BestHeight + w.Status.WorkHash = w.Status.BestHash + } + if err := w.commitWalletInfo(newStore); err != nil { + return err } - return w.commitWalletInfo(storeBatch) + if err := newStore.CommitBatch(); err != nil { + return err + } + + return nil } //WalletUpdate process every valid block and reverse every invalid block which need to rollback func (w *Wallet) walletUpdater() { for { w.getRescanNotification() - for !w.chain.InMainChain(w.status.BestHash) { - block, err := w.chain.GetBlockByHash(&w.status.BestHash) + for !w.Chain.InMainChain(w.Status.BestHash) { + block, err := w.Chain.GetBlockByHash(&w.Status.BestHash) if err != nil { log.WithFields(log.Fields{"module": logModule, "err": err}).Error("walletUpdater GetBlockByHash") return @@ -247,7 +270,7 @@ func (w *Wallet) walletUpdater() { } } - block, _ := w.chain.GetBlockByHeight(w.status.WorkHeight + 1) + block, _ := w.Chain.GetBlockByHeight(w.Status.WorkHeight + 1) if block == nil { w.walletBlockWaiter() continue @@ -269,43 +292,6 @@ func (w *Wallet) RescanBlocks() { } } -// deleteAccountTxs deletes all txs in wallet -func (w *Wallet) deleteAccountTxs() { - storeBatch := w.DB.NewBatch() - - txIter := w.DB.IteratorPrefix([]byte(TxPrefix)) - defer txIter.Release() - - for txIter.Next() { - storeBatch.Delete(txIter.Key()) - } - - txIndexIter := w.DB.IteratorPrefix([]byte(TxIndexPrefix)) - defer txIndexIter.Release() - - for txIndexIter.Next() { - storeBatch.Delete(txIndexIter.Key()) - } - - storeBatch.Write() -} - -func (w *Wallet) deleteUtxos() { - storeBatch := w.DB.NewBatch() - ruIter := w.DB.IteratorPrefix([]byte(account.UTXOPreFix)) - defer ruIter.Release() - for ruIter.Next() { - storeBatch.Delete(ruIter.Key()) - } - - suIter := w.DB.IteratorPrefix([]byte(account.SUTXOPrefix)) - defer suIter.Release() - for suIter.Next() { - storeBatch.Delete(suIter.Key()) - } - storeBatch.Write() -} - // DeleteAccount deletes account matching accountID, then rescan wallet func (w *Wallet) DeleteAccount(accountID string) (err error) { w.rw.Lock() @@ -315,7 +301,7 @@ func (w *Wallet) DeleteAccount(accountID string) (err error) { return err } - w.deleteAccountTxs() + w.Store.DeleteWalletTransactions() w.RescanBlocks() return nil } @@ -328,7 +314,7 @@ func (w *Wallet) UpdateAccountAlias(accountID string, newAlias string) (err erro return err } - w.deleteAccountTxs() + w.Store.DeleteWalletTransactions() w.RescanBlocks() return nil } @@ -343,14 +329,14 @@ func (w *Wallet) getRescanNotification() { } func (w *Wallet) setRescanStatus() { - block, _ := w.chain.GetBlockByHeight(0) - w.status.WorkHash = bc.Hash{} + block, _ := w.Chain.GetBlockByHeight(0) + w.Status.WorkHash = bc.Hash{} w.AttachBlock(block) } func (w *Wallet) walletBlockWaiter() { select { - case <-w.chain.BlockWaiter(w.status.WorkHeight + 1): + case <-w.Chain.BlockWaiter(w.Status.WorkHeight + 1): case <-w.rescanCh: w.setRescanStatus() } @@ -361,5 +347,5 @@ func (w *Wallet) GetWalletStatusInfo() StatusInfo { w.rw.RLock() defer w.rw.RUnlock() - return w.status + return w.Status } diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index ac966f48..49ad6939 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -1,23 +1,30 @@ package wallet import ( + "encoding/binary" + "encoding/hex" "encoding/json" + "fmt" "io/ioutil" "os" - "reflect" + "sort" + "strings" "testing" "time" "github.com/vapor/account" + acc "github.com/vapor/account" "github.com/vapor/asset" - "github.com/vapor/blockchain/pseudohsm" + "github.com/vapor/blockchain/query" "github.com/vapor/blockchain/signers" "github.com/vapor/blockchain/txbuilder" + "github.com/vapor/common" "github.com/vapor/config" "github.com/vapor/consensus" "github.com/vapor/crypto/ed25519/chainkd" - "github.com/vapor/database" + "github.com/vapor/crypto/sha3pool" dbm "github.com/vapor/database/leveldb" + "github.com/vapor/errors" "github.com/vapor/event" "github.com/vapor/protocol" "github.com/vapor/protocol/bc" @@ -33,7 +40,7 @@ func TestEncodeDecodeGlobalTxIndex(t *testing.T) { Position: 1, } - globalTxIdx := calcGlobalTxIndex(&want.BlockHash, want.Position) + globalTxIdx := CalcGlobalTxIndex(&want.BlockHash, want.Position) blockHashGot, positionGot := parseGlobalTxIdx(globalTxIdx) if *blockHashGot != want.BlockHash { t.Errorf("blockHash mismatch. Get: %v. Expect: %v", *blockHashGot, want.BlockHash) @@ -53,35 +60,25 @@ func TestWalletVersion(t *testing.T) { defer os.RemoveAll(dirPath) testDB := dbm.NewDB("testdb", "leveldb", "temp") + walletStore := NewMockWalletStore(testDB) defer func() { testDB.Close() os.RemoveAll("temp") }() dispatcher := event.NewDispatcher() - w := mockWallet(testDB, nil, nil, nil, dispatcher, false) + w := mockWallet(walletStore, nil, nil, nil, dispatcher, false) - // legacy status test case - type legacyStatusInfo struct { - WorkHeight uint64 - WorkHash bc.Hash - BestHeight uint64 - BestHash bc.Hash - } - rawWallet, err := json.Marshal(legacyStatusInfo{}) - if err != nil { - t.Fatal("Marshal legacyStatusInfo") - } - - w.DB.Set(walletKey, rawWallet) - rawWallet = w.DB.Get(walletKey) - if rawWallet == nil { - t.Fatal("fail to load wallet StatusInfo") + walletStatus := new(StatusInfo) + if err := w.Store.SetWalletInfo(walletStatus); err != nil { + t.Fatal(err) } - if err := json.Unmarshal(rawWallet, &w.status); err != nil { + status, err := w.Store.GetWalletInfo() + if err != nil { t.Fatal(err) } + w.Status = *status if err := w.checkWalletInfo(); err != errWalletVersionMismatch { t.Fatal("fail to detect legacy wallet version") @@ -89,315 +86,946 @@ func TestWalletVersion(t *testing.T) { // lower wallet version test case lowerVersion := StatusInfo{Version: currentVersion - 1} - rawWallet, err = json.Marshal(lowerVersion) - if err != nil { - t.Fatal("save wallet info") - } - - w.DB.Set(walletKey, rawWallet) - rawWallet = w.DB.Get(walletKey) - if rawWallet == nil { - t.Fatal("fail to load wallet StatusInfo") + if err := w.Store.SetWalletInfo(&lowerVersion); err != nil { + t.Fatal(err) } - if err := json.Unmarshal(rawWallet, &w.status); err != nil { + status, err = w.Store.GetWalletInfo() + if err != nil { t.Fatal(err) } + w.Status = *status if err := w.checkWalletInfo(); err != errWalletVersionMismatch { t.Fatal("fail to detect expired wallet version") } } -func TestWalletUpdate(t *testing.T) { - dirPath, err := ioutil.TempDir(".", "") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dirPath) +func mockUTXO(controlProg *account.CtrlProgram, assetID *bc.AssetID) *account.UTXO { + utxo := &account.UTXO{} + utxo.OutputID = bc.Hash{V0: 1} + utxo.SourceID = bc.Hash{V0: 2} + utxo.AssetID = *assetID + utxo.Amount = 1000000000 + utxo.SourcePos = 0 + utxo.ControlProgram = controlProg.ControlProgram + utxo.AccountID = controlProg.AccountID + utxo.Address = controlProg.Address + utxo.ControlProgramIndex = controlProg.KeyIndex + return utxo +} - config.CommonConfig = config.DefaultConfig() - testDB := dbm.NewDB("testdb", "leveldb", "temp") - defer func() { - testDB.Close() - os.RemoveAll("temp") - }() +func mockTxData(utxos []*account.UTXO, testAccount *account.Account) (*txbuilder.Template, *types.TxData, error) { + tplBuilder := txbuilder.NewBuilder(time.Now()) - store := database.NewStore(testDB) - dispatcher := event.NewDispatcher() - txPool := protocol.NewTxPool(store, dispatcher) + for _, utxo := range utxos { + txInput, sigInst, err := account.UtxoToInputs(testAccount.Signer, utxo) + if err != nil { + return nil, nil, err + } + tplBuilder.AddInput(txInput, sigInst) - chain, err := protocol.NewChain(store, txPool, dispatcher) - if err != nil { - t.Fatal(err) + out := &types.TxOutput{} + if utxo.AssetID == *consensus.BTMAssetID { + out = types.NewIntraChainOutput(utxo.AssetID, 100, utxo.ControlProgram) + } else { + out = types.NewIntraChainOutput(utxo.AssetID, utxo.Amount, utxo.ControlProgram) + } + tplBuilder.AddOutput(out) } - accountManager := account.NewManager(testDB, chain) - hsm, err := pseudohsm.New(dirPath) - if err != nil { - t.Fatal(err) + return tplBuilder.Build() +} + +func mockWallet(store WalletStore, account *account.Manager, asset *asset.Registry, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) *Wallet { + wallet := &Wallet{ + Store: store, + AccountMgr: account, + AssetReg: asset, + Chain: chain, + RecoveryMgr: NewRecoveryManager(store, account), + EventDispatcher: dispatcher, + TxIndexFlag: txIndexFlag, } + wallet.TxMsgSub, _ = wallet.EventDispatcher.Subscribe(protocol.TxMsgEvent{}) + return wallet +} - xpub1, _, err := hsm.XCreate("test_pub1", "password", "en") - if err != nil { - t.Fatal(err) +func mockSingleBlock(tx *types.Tx) *types.Block { + return &types.Block{ + BlockHeader: types.BlockHeader{ + Version: 1, + Height: 1, + }, + Transactions: []*types.Tx{config.GenesisTx(), tx}, } +} - testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044) - if err != nil { - t.Fatal(err) +// errors +var ( + errAccntTxIDNotFound = errors.New("account TXID not found") + errGetAsset = errors.New("Failed to find asset definition") +) + +const ( + utxoPrefix byte = iota //UTXOPrefix is StandardUTXOKey prefix + contractPrefix + contractIndexPrefix + accountPrefix // AccountPrefix is account ID prefix + accountIndexPrefix +) + +// leveldb key prefix +var ( + colon byte = 0x3a + accountStore = []byte("AS:") + UTXOPrefix = append(accountStore, utxoPrefix, colon) + ContractPrefix = append(accountStore, contractPrefix, colon) + ContractIndexPrefix = append(accountStore, contractIndexPrefix, colon) + AccountPrefix = append(accountStore, accountPrefix, colon) // AccountPrefix is account ID prefix + AccountIndexPrefix = append(accountStore, accountIndexPrefix, colon) +) + +const ( + sutxoPrefix byte = iota //SUTXOPrefix is ContractUTXOKey prefix + accountAliasPrefix + txPrefix //TxPrefix is wallet database transactions prefix + txIndexPrefix //TxIndexPrefix is wallet database tx index prefix + unconfirmedTxPrefix //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix + globalTxIndexPrefix //GlobalTxIndexPrefix is wallet database global tx index prefix + walletKey + miningAddressKey + coinbaseAbKey + recoveryKey //recoveryKey key for db store recovery info. +) + +var ( + walletStore = []byte("WS:") + SUTXOPrefix = append(walletStore, sutxoPrefix, colon) + AccountAliasPrefix = append(walletStore, accountAliasPrefix, colon) + TxPrefix = append(walletStore, txPrefix, colon) //TxPrefix is wallet database transactions prefix + TxIndexPrefix = append(walletStore, txIndexPrefix, colon) //TxIndexPrefix is wallet database tx index prefix + UnconfirmedTxPrefix = append(walletStore, unconfirmedTxPrefix, colon) //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix + GlobalTxIndexPrefix = append(walletStore, globalTxIndexPrefix, colon) //GlobalTxIndexPrefix is wallet database global tx index prefix + WalletKey = append(walletStore, walletKey) + MiningAddressKey = append(walletStore, miningAddressKey) + CoinbaseAbKey = append(walletStore, coinbaseAbKey) + RecoveryKey = append(walletStore, recoveryKey) +) + +func accountIndexKey(xpubs []chainkd.XPub) []byte { + var hash [32]byte + var xPubs []byte + cpy := append([]chainkd.XPub{}, xpubs[:]...) + sort.Sort(signers.SortKeys(cpy)) + for _, xpub := range cpy { + xPubs = append(xPubs, xpub[:]...) } + sha3pool.Sum256(hash[:], xPubs) + return append([]byte(AccountIndexPrefix), hash[:]...) +} - controlProg, err := accountManager.CreateAddress(testAccount.ID, false) - if err != nil { - t.Fatal(err) +func Bip44ContractIndexKey(accountID string, change bool) []byte { + key := append([]byte(ContractIndexPrefix), accountID...) + if change { + return append(key, []byte{1}...) } + return append(key, []byte{0}...) +} - controlProg.KeyIndex = 1 +// ContractKey account control promgram store prefix +func ContractKey(hash bc.Hash) []byte { + return append([]byte(ContractPrefix), hash.Bytes()...) +} - reg := asset.NewRegistry(testDB, chain) - asset := bc.AssetID{V0: 5} +// AccountIDKey account id store prefix +func AccountIDKey(accountID string) []byte { + return append([]byte(AccountPrefix), []byte(accountID)...) +} - utxos := []*account.UTXO{} - btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID) - utxos = append(utxos, btmUtxo) - OtherUtxo := mockUTXO(controlProg, &asset) - utxos = append(utxos, OtherUtxo) +// StandardUTXOKey makes an account unspent outputs key to store +func StandardUTXOKey(id bc.Hash) []byte { + return append(UTXOPrefix, id.Bytes()...) +} - _, txData, err := mockTxData(utxos, testAccount) - if err != nil { - t.Fatal(err) +// ContractUTXOKey makes a smart contract unspent outputs key to store +func ContractUTXOKey(id bc.Hash) []byte { + return append(SUTXOPrefix, id.Bytes()...) +} + +func calcDeleteKey(blockHeight uint64) []byte { + return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight)) +} + +func calcTxIndexKey(txID string) []byte { + return append(TxIndexPrefix, []byte(txID)...) +} + +func calcAnnotatedKey(formatKey string) []byte { + return append(TxPrefix, []byte(formatKey)...) +} + +func calcUnconfirmedTxKey(formatKey string) []byte { + return append(UnconfirmedTxPrefix, []byte(formatKey)...) +} + +func CalcGlobalTxIndexKey(txID string) []byte { + return append(GlobalTxIndexPrefix, []byte(txID)...) +} + +func CalcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte { + txIdx := make([]byte, 40) + copy(txIdx[:32], blockHash.Bytes()) + binary.BigEndian.PutUint64(txIdx[32:], position) + return txIdx +} + +func formatKey(blockHeight uint64, position uint32) string { + return fmt.Sprintf("%016x%08x", blockHeight, position) +} + +func contractIndexKey(accountID string) []byte { + return append([]byte(ContractIndexPrefix), []byte(accountID)...) +} + +func accountAliasKey(name string) []byte { + return append([]byte(AccountAliasPrefix), []byte(name)...) +} + +// MockWalletStore store wallet using leveldb +type MockWalletStore struct { + db dbm.DB + batch dbm.Batch +} + +// NewMockWalletStore create new MockWalletStore struct +func NewMockWalletStore(db dbm.DB) *MockWalletStore { + return &MockWalletStore{ + db: db, + batch: nil, } +} - tx := types.NewTx(*txData) - block := mockSingleBlock(tx) - txStatus := bc.NewTransactionStatus() - txStatus.SetStatus(0, false) - txStatus.SetStatus(1, false) - store.SaveBlock(block, txStatus) +// InitBatch initial new wallet store +func (store *MockWalletStore) InitBatch() WalletStore { + newStore := NewMockWalletStore(store.db) + newStore.batch = newStore.db.NewBatch() + return newStore +} - w := mockWallet(testDB, accountManager, reg, chain, dispatcher, true) - err = w.AttachBlock(block) - if err != nil { - t.Fatal(err) +// CommitBatch commit batch +func (store *MockWalletStore) CommitBatch() error { + if store.batch == nil { + return errors.New("MockWalletStore commit fail, store batch is nil.") } + store.batch.Write() + store.batch = nil + return nil +} - if _, err := w.GetTransactionByTxID(tx.ID.String()); err != nil { - t.Fatal(err) +// DeleteContractUTXO delete contract utxo by outputID +func (store *MockWalletStore) DeleteContractUTXO(outputID bc.Hash) { + if store.batch == nil { + store.db.Delete(ContractUTXOKey(outputID)) + } else { + store.batch.Delete(ContractUTXOKey(outputID)) } +} - wants, err := w.GetTransactions(testAccount.ID, "", 1, false) - if len(wants) != 1 { - t.Fatal(err) +// DeleteRecoveryStatus delete recovery status +func (store *MockWalletStore) DeleteRecoveryStatus() { + if store.batch == nil { + store.db.Delete(RecoveryKey) + } else { + store.batch.Delete(RecoveryKey) } +} - if wants[0].ID != tx.ID { - t.Fatal("account txID mismatch") +// DeleteTransactions delete transactions when orphan block rollback +func (store *MockWalletStore) DeleteTransactions(height uint64) { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch } + txIter := store.db.IteratorPrefix(calcDeleteKey(height)) + defer txIter.Release() - for position, tx := range block.Transactions { - get := w.DB.Get(calcGlobalTxIndexKey(tx.ID.String())) - bh := block.BlockHeader.Hash() - expect := calcGlobalTxIndex(&bh, uint64(position)) - if !reflect.DeepEqual(get, expect) { - t.Fatalf("position#%d: compare retrieved globalTxIdx err", position) + tmpTx := query.AnnotatedTx{} + for txIter.Next() { + if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil { + batch.Delete(calcTxIndexKey(tmpTx.ID.String())) } + batch.Delete(txIter.Key()) + } + if store.batch == nil { + batch.Write() } } -func TestRescanWallet(t *testing.T) { - // prepare wallet & db - dirPath, err := ioutil.TempDir(".", "") - if err != nil { - t.Fatal(err) +// DeleteUnconfirmedTransaction delete unconfirmed tx by txID +func (store *MockWalletStore) DeleteUnconfirmedTransaction(txID string) { + if store.batch == nil { + store.db.Delete(calcUnconfirmedTxKey(txID)) + } else { + store.batch.Delete(calcUnconfirmedTxKey(txID)) } - defer os.RemoveAll(dirPath) +} - config.CommonConfig = config.DefaultConfig() - testDB := dbm.NewDB("testdb", "leveldb", "temp") - defer func() { - testDB.Close() - os.RemoveAll("temp") - }() +// DeleteWalletTransactions delete all txs in wallet +func (store *MockWalletStore) DeleteWalletTransactions() { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + txIter := store.db.IteratorPrefix([]byte(TxPrefix)) + defer txIter.Release() - store := database.NewStore(testDB) - dispatcher := event.NewDispatcher() - txPool := protocol.NewTxPool(store, dispatcher) - chain, err := protocol.NewChain(store, txPool, dispatcher) - if err != nil { - t.Fatal(err) + for txIter.Next() { + batch.Delete(txIter.Key()) } - statusInfo := StatusInfo{ - Version: currentVersion, - WorkHash: bc.Hash{V0: 0xff}, + txIndexIter := store.db.IteratorPrefix([]byte(TxIndexPrefix)) + defer txIndexIter.Release() + + for txIndexIter.Next() { + batch.Delete(txIndexIter.Key()) } - rawWallet, err := json.Marshal(statusInfo) - if err != nil { - t.Fatal("save wallet info") + if store.batch == nil { + batch.Write() } +} - w := mockWallet(testDB, nil, nil, chain, dispatcher, false) - w.DB.Set(walletKey, rawWallet) - rawWallet = w.DB.Get(walletKey) - if rawWallet == nil { - t.Fatal("fail to load wallet StatusInfo") +// DeleteWalletUTXOs delete all txs in wallet +func (store *MockWalletStore) DeleteWalletUTXOs() { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + ruIter := store.db.IteratorPrefix([]byte(UTXOPrefix)) + defer ruIter.Release() + for ruIter.Next() { + batch.Delete(ruIter.Key()) } - if err := json.Unmarshal(rawWallet, &w.status); err != nil { - t.Fatal(err) + suIter := store.db.IteratorPrefix([]byte(SUTXOPrefix)) + defer suIter.Release() + for suIter.Next() { + batch.Delete(suIter.Key()) + } + if store.batch == nil { + batch.Write() } +} - // rescan wallet - if err := w.loadWalletInfo(); err != nil { - t.Fatal(err) +// GetAsset get asset by assetID +func (store *MockWalletStore) GetAsset(assetID *bc.AssetID) (*asset.Asset, error) { + definitionByte := store.db.Get(asset.ExtAssetKey(assetID)) + if definitionByte == nil { + return nil, errGetAsset + } + definitionMap := make(map[string]interface{}) + if err := json.Unmarshal(definitionByte, &definitionMap); err != nil { + return nil, err } + alias := assetID.String() + externalAsset := &asset.Asset{ + AssetID: *assetID, + Alias: &alias, + DefinitionMap: definitionMap, + RawDefinitionByte: definitionByte, + } + return externalAsset, nil +} - block := config.GenesisBlock() - if w.status.WorkHash != block.Hash() { - t.Fatal("reattach from genesis block") +// GetGlobalTransactionIndex get global tx by txID +func (store *MockWalletStore) GetGlobalTransactionIndex(txID string) []byte { + return store.db.Get(CalcGlobalTxIndexKey(txID)) +} + +// GetStandardUTXO get standard utxo by id +func (store *MockWalletStore) GetStandardUTXO(outid bc.Hash) (*acc.UTXO, error) { + rawUTXO := store.db.Get(StandardUTXOKey(outid)) + if rawUTXO == nil { + return nil, fmt.Errorf("failed get standard UTXO, outputID: %s ", outid.String()) + } + UTXO := new(acc.UTXO) + if err := json.Unmarshal(rawUTXO, UTXO); err != nil { + return nil, err } + return UTXO, nil } -func TestMemPoolTxQueryLoop(t *testing.T) { - dirPath, err := ioutil.TempDir(".", "") +// GetTransaction get tx by txid +func (store *MockWalletStore) GetTransaction(txID string) (*query.AnnotatedTx, error) { + formatKey := store.db.Get(calcTxIndexKey(txID)) + if formatKey == nil { + return nil, errAccntTxIDNotFound + } + rawTx := store.db.Get(calcAnnotatedKey(string(formatKey))) + tx := new(query.AnnotatedTx) + if err := json.Unmarshal(rawTx, tx); err != nil { + return nil, err + } + return tx, nil +} + +// GetUnconfirmedTransaction get unconfirmed tx by txID +func (store *MockWalletStore) GetUnconfirmedTransaction(txID string) (*query.AnnotatedTx, error) { + rawUnconfirmedTx := store.db.Get(calcUnconfirmedTxKey(txID)) + if rawUnconfirmedTx == nil { + return nil, fmt.Errorf("failed get unconfirmed tx, txID: %s ", txID) + } + tx := new(query.AnnotatedTx) + if err := json.Unmarshal(rawUnconfirmedTx, tx); err != nil { + return nil, err + } + return tx, nil +} + +// GetRecoveryStatus delete recovery status +func (store *MockWalletStore) GetRecoveryStatus() (*RecoveryState, error) { + rawStatus := store.db.Get(RecoveryKey) + if rawStatus == nil { + return nil, ErrGetRecoveryStatus + } + state := new(RecoveryState) + if err := json.Unmarshal(rawStatus, state); err != nil { + return nil, err + } + return state, nil +} + +// GetWalletInfo get wallet information +func (store *MockWalletStore) GetWalletInfo() (*StatusInfo, error) { + rawStatus := store.db.Get([]byte(WalletKey)) + if rawStatus == nil { + return nil, fmt.Errorf("failed get wallet info") + } + status := new(StatusInfo) + if err := json.Unmarshal(rawStatus, status); err != nil { + return nil, err + } + return status, nil +} + +// ListAccountUTXOs get all account unspent outputs +func (store *MockWalletStore) ListAccountUTXOs(id string, isSmartContract bool) ([]*acc.UTXO, error) { + prefix := UTXOPrefix + if isSmartContract { + prefix = SUTXOPrefix + } + + idBytes, err := hex.DecodeString(id) if err != nil { - t.Fatal(err) + return nil, err } - config.CommonConfig = config.DefaultConfig() - testDB := dbm.NewDB("testdb", "leveldb", dirPath) - defer func() { - testDB.Close() - os.RemoveAll(dirPath) - }() - store := database.NewStore(testDB) - dispatcher := event.NewDispatcher() - txPool := protocol.NewTxPool(store, dispatcher) + accountUtxoIter := store.db.IteratorPrefix(append(prefix, idBytes...)) + defer accountUtxoIter.Release() + + confirmedUTXOs := []*acc.UTXO{} + for accountUtxoIter.Next() { + utxo := new(acc.UTXO) + if err := json.Unmarshal(accountUtxoIter.Value(), utxo); err != nil { + return nil, err + } + + confirmedUTXOs = append(confirmedUTXOs, utxo) + } + return confirmedUTXOs, nil +} + +func (store *MockWalletStore) ListTransactions(accountID string, StartTxID string, count uint, unconfirmed bool) ([]*query.AnnotatedTx, error) { + annotatedTxs := []*query.AnnotatedTx{} + var startKey []byte + preFix := TxPrefix + + if StartTxID != "" { + if unconfirmed { + startKey = calcUnconfirmedTxKey(StartTxID) + } else { + formatKey := store.db.Get(calcTxIndexKey(StartTxID)) + if formatKey == nil { + return nil, errAccntTxIDNotFound + } + startKey = calcAnnotatedKey(string(formatKey)) + } + } + + if unconfirmed { + preFix = UnconfirmedTxPrefix + } + + itr := store.db.IteratorPrefixWithStart([]byte(preFix), startKey, true) + defer itr.Release() - chain, err := protocol.NewChain(store, txPool, dispatcher) + for txNum := count; itr.Next() && txNum > 0; txNum-- { + annotatedTx := new(query.AnnotatedTx) + if err := json.Unmarshal(itr.Value(), &annotatedTx); err != nil { + return nil, err + } + annotatedTxs = append(annotatedTxs, annotatedTx) + } + + return annotatedTxs, nil +} + +// ListUnconfirmedTransactions get all unconfirmed txs +func (store *MockWalletStore) ListUnconfirmedTransactions() ([]*query.AnnotatedTx, error) { + annotatedTxs := []*query.AnnotatedTx{} + txIter := store.db.IteratorPrefix([]byte(UnconfirmedTxPrefix)) + defer txIter.Release() + + for txIter.Next() { + annotatedTx := &query.AnnotatedTx{} + if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil { + return nil, err + } + annotatedTxs = append(annotatedTxs, annotatedTx) + } + return annotatedTxs, nil +} + +// SetAssetDefinition set assetID and definition +func (store *MockWalletStore) SetAssetDefinition(assetID *bc.AssetID, definition []byte) { + if store.batch == nil { + store.db.Set(asset.ExtAssetKey(assetID), definition) + } else { + store.batch.Set(asset.ExtAssetKey(assetID), definition) + } +} + +// SetContractUTXO set standard utxo +func (store *MockWalletStore) SetContractUTXO(outputID bc.Hash, utxo *acc.UTXO) error { + data, err := json.Marshal(utxo) if err != nil { - t.Fatal(err) + return err } + if store.batch == nil { + store.db.Set(ContractUTXOKey(outputID), data) + } else { + store.batch.Set(ContractUTXOKey(outputID), data) + } + return nil +} - accountManager := account.NewManager(testDB, chain) - hsm, err := pseudohsm.New(dirPath) +// SetGlobalTransactionIndex set global tx index by blockhash and position +func (store *MockWalletStore) SetGlobalTransactionIndex(globalTxID string, blockHash *bc.Hash, position uint64) { + if store.batch == nil { + store.db.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position)) + } else { + store.batch.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position)) + } +} + +// SetRecoveryStatus set recovery status +func (store *MockWalletStore) SetRecoveryStatus(recoveryState *RecoveryState) error { + rawStatus, err := json.Marshal(recoveryState) if err != nil { - t.Fatal(err) + return err + } + if store.batch == nil { + store.db.Set(RecoveryKey, rawStatus) + } else { + store.batch.Set(RecoveryKey, rawStatus) + } + return nil +} + +// SetTransaction set raw transaction by block height and tx position +func (store *MockWalletStore) SetTransaction(height uint64, tx *query.AnnotatedTx) error { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch } - xpub1, _, err := hsm.XCreate("test_pub1", "password", "en") + rawTx, err := json.Marshal(tx) if err != nil { - t.Fatal(err) + return err } + batch.Set(calcAnnotatedKey(formatKey(height, tx.Position)), rawTx) + batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(height, tx.Position))) + + if store.batch == nil { + batch.Write() + } + return nil +} - testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044) +// SetUnconfirmedTransaction set unconfirmed tx by txID +func (store *MockWalletStore) SetUnconfirmedTransaction(txID string, tx *query.AnnotatedTx) error { + rawTx, err := json.Marshal(tx) if err != nil { - t.Fatal(err) + return err + } + if store.batch == nil { + store.db.Set(calcUnconfirmedTxKey(txID), rawTx) + } else { + store.batch.Set(calcUnconfirmedTxKey(txID), rawTx) } + return nil +} - controlProg, err := accountManager.CreateAddress(testAccount.ID, false) +// SetWalletInfo get wallet information +func (store *MockWalletStore) SetWalletInfo(status *StatusInfo) error { + rawWallet, err := json.Marshal(status) if err != nil { - t.Fatal(err) + return err + } + + if store.batch == nil { + store.db.Set([]byte(WalletKey), rawWallet) + } else { + store.batch.Set([]byte(WalletKey), rawWallet) } + return nil +} + +type MockAccountStore struct { + db dbm.DB + batch dbm.Batch +} - controlProg.KeyIndex = 1 +// NewAccountStore create new MockAccountStore. +func NewMockAccountStore(db dbm.DB) *MockAccountStore { + return &MockAccountStore{ + db: db, + batch: nil, + } +} - reg := asset.NewRegistry(testDB, chain) - asset := bc.AssetID{V0: 5} +// InitBatch initial new account store +func (store *MockAccountStore) InitBatch() acc.AccountStore { + newStore := NewMockAccountStore(store.db) + newStore.batch = newStore.db.NewBatch() + return newStore +} - utxos := []*account.UTXO{} - btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID) - utxos = append(utxos, btmUtxo) - OtherUtxo := mockUTXO(controlProg, &asset) - utxos = append(utxos, OtherUtxo) +// CommitBatch commit batch +func (store *MockAccountStore) CommitBatch() error { + if store.batch == nil { + return errors.New("MockAccountStore commit fail, store batch is nil.") + } + store.batch.Write() + store.batch = nil + return nil +} - _, txData, err := mockTxData(utxos, testAccount) - if err != nil { - t.Fatal(err) +// DeleteAccount set account account ID, account alias and raw account. +func (store *MockAccountStore) DeleteAccount(account *acc.Account) error { + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch } - tx := types.NewTx(*txData) - //block := mockSingleBlock(tx) - txStatus := bc.NewTransactionStatus() - txStatus.SetStatus(0, false) - w, err := NewWallet(testDB, accountManager, reg, hsm, chain, dispatcher, false) - go w.memPoolTxQueryLoop() - w.eventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgNewTx}}) - time.Sleep(time.Millisecond * 10) - if _, err = w.GetUnconfirmedTxByTxID(tx.ID.String()); err != nil { - t.Fatal("disaptch new tx msg error:", err) - } - w.eventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgRemoveTx}}) - time.Sleep(time.Millisecond * 10) - txs, err := w.GetUnconfirmedTxs(testAccount.ID) + // delete account utxos + store.deleteAccountUTXOs(account.ID, batch) + + // delete account control program + if err := store.deleteAccountControlPrograms(account.ID, batch); err != nil { + return err + } + + // delete bip44 contract index + batch.Delete(Bip44ContractIndexKey(account.ID, false)) + batch.Delete(Bip44ContractIndexKey(account.ID, true)) + + // delete contract index + batch.Delete(contractIndexKey(account.ID)) + + // delete account id + batch.Delete(AccountIDKey(account.ID)) + batch.Delete(accountAliasKey(account.Alias)) + if store.batch == nil { + batch.Write() + } + return nil +} + +// deleteAccountUTXOs delete account utxos by accountID +func (store *MockAccountStore) deleteAccountUTXOs(accountID string, batch dbm.Batch) error { + accountUtxoIter := store.db.IteratorPrefix([]byte(UTXOPrefix)) + defer accountUtxoIter.Release() + + for accountUtxoIter.Next() { + accountUtxo := new(acc.UTXO) + if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil { + return err + } + + if accountID == accountUtxo.AccountID { + batch.Delete(StandardUTXOKey(accountUtxo.OutputID)) + } + } + + return nil +} + +// deleteAccountControlPrograms deletes account control program +func (store *MockAccountStore) deleteAccountControlPrograms(accountID string, batch dbm.Batch) error { + cps, err := store.ListControlPrograms() if err != nil { - t.Fatal("get unconfirmed tx error:", err) + return err } - if len(txs) != 0 { - t.Fatal("disaptch remove tx msg error") + var hash [32]byte + for _, cp := range cps { + if cp.AccountID == accountID { + sha3pool.Sum256(hash[:], cp.ControlProgram) + batch.Delete(ContractKey(bc.NewHash(hash))) + } } + return nil +} - w.eventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: 2}}) +// DeleteStandardUTXO delete utxo by outpu id +func (store *MockAccountStore) DeleteStandardUTXO(outputID bc.Hash) { + if store.batch == nil { + store.db.Delete(StandardUTXOKey(outputID)) + } else { + store.batch.Delete(StandardUTXOKey(outputID)) + } } -func mockUTXO(controlProg *account.CtrlProgram, assetID *bc.AssetID) *account.UTXO { - utxo := &account.UTXO{} - utxo.OutputID = bc.Hash{V0: 1} - utxo.SourceID = bc.Hash{V0: 2} - utxo.AssetID = *assetID - utxo.Amount = 1000000000 - utxo.SourcePos = 0 - utxo.ControlProgram = controlProg.ControlProgram - utxo.AccountID = controlProg.AccountID - utxo.Address = controlProg.Address - utxo.ControlProgramIndex = controlProg.KeyIndex - return utxo +// GetAccountByAlias get account by account alias +func (store *MockAccountStore) GetAccountByAlias(accountAlias string) (*acc.Account, error) { + accountID := store.db.Get(accountAliasKey(accountAlias)) + if accountID == nil { + return nil, acc.ErrFindAccount + } + return store.GetAccountByID(string(accountID)) } -func mockTxData(utxos []*account.UTXO, testAccount *account.Account) (*txbuilder.Template, *types.TxData, error) { - tplBuilder := txbuilder.NewBuilder(time.Now()) +// GetAccountByID get account by accountID +func (store *MockAccountStore) GetAccountByID(accountID string) (*acc.Account, error) { + rawAccount := store.db.Get(AccountIDKey(accountID)) + if rawAccount == nil { + return nil, acc.ErrFindAccount + } + account := new(acc.Account) + if err := json.Unmarshal(rawAccount, account); err != nil { + return nil, err + } + return account, nil +} - for _, utxo := range utxos { - txInput, sigInst, err := account.UtxoToInputs(testAccount.Signer, utxo) - if err != nil { - return nil, nil, err +// GetAccountIndex get account index by account xpubs +func (store *MockAccountStore) GetAccountIndex(xpubs []chainkd.XPub) uint64 { + currentIndex := uint64(0) + if rawIndexBytes := store.db.Get(accountIndexKey(xpubs)); rawIndexBytes != nil { + currentIndex = common.BytesToUnit64(rawIndexBytes) + } + return currentIndex +} + +// GetBip44ContractIndex get bip44 contract index +func (store *MockAccountStore) GetBip44ContractIndex(accountID string, change bool) uint64 { + index := uint64(0) + if rawIndexBytes := store.db.Get(Bip44ContractIndexKey(accountID, change)); rawIndexBytes != nil { + index = common.BytesToUnit64(rawIndexBytes) + } + return index +} + +// GetCoinbaseArbitrary get coinbase arbitrary +func (store *MockAccountStore) GetCoinbaseArbitrary() []byte { + return store.db.Get([]byte(CoinbaseAbKey)) +} + +// GetContractIndex get contract index +func (store *MockAccountStore) GetContractIndex(accountID string) uint64 { + index := uint64(0) + if rawIndexBytes := store.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil { + index = common.BytesToUnit64(rawIndexBytes) + } + return index +} + +// GetControlProgram get control program +func (store *MockAccountStore) GetControlProgram(hash bc.Hash) (*acc.CtrlProgram, error) { + rawProgram := store.db.Get(ContractKey(hash)) + if rawProgram == nil { + return nil, acc.ErrFindCtrlProgram + } + cp := new(acc.CtrlProgram) + if err := json.Unmarshal(rawProgram, cp); err != nil { + return nil, err + } + return cp, nil +} + +// GetMiningAddress get mining address +func (store *MockAccountStore) GetMiningAddress() (*acc.CtrlProgram, error) { + rawCP := store.db.Get([]byte(MiningAddressKey)) + if rawCP == nil { + return nil, acc.ErrFindMiningAddress + } + cp := new(acc.CtrlProgram) + if err := json.Unmarshal(rawCP, cp); err != nil { + return nil, err + } + return cp, nil +} + +// GetUTXO get standard utxo by id +func (store *MockAccountStore) GetUTXO(outid bc.Hash) (*acc.UTXO, error) { + u := new(acc.UTXO) + if data := store.db.Get(StandardUTXOKey(outid)); data != nil { + return u, json.Unmarshal(data, u) + } + if data := store.db.Get(ContractUTXOKey(outid)); data != nil { + return u, json.Unmarshal(data, u) + } + return nil, acc.ErrMatchUTXO +} + +// ListAccounts get all accounts which name prfix is id. +func (store *MockAccountStore) ListAccounts(id string) ([]*acc.Account, error) { + accounts := []*acc.Account{} + accountIter := store.db.IteratorPrefix(AccountIDKey(strings.TrimSpace(id))) + defer accountIter.Release() + + for accountIter.Next() { + account := new(acc.Account) + if err := json.Unmarshal(accountIter.Value(), &account); err != nil { + return nil, err } - tplBuilder.AddInput(txInput, sigInst) + accounts = append(accounts, account) + } + return accounts, nil +} - out := &types.TxOutput{} - if utxo.AssetID == *consensus.BTMAssetID { - out = types.NewIntraChainOutput(utxo.AssetID, 100, utxo.ControlProgram) +// ListControlPrograms get all local control programs +func (store *MockAccountStore) ListControlPrograms() ([]*acc.CtrlProgram, error) { + cps := []*acc.CtrlProgram{} + cpIter := store.db.IteratorPrefix([]byte(ContractPrefix)) + defer cpIter.Release() + + for cpIter.Next() { + cp := new(acc.CtrlProgram) + if err := json.Unmarshal(cpIter.Value(), cp); err != nil { + return nil, err + } + cps = append(cps, cp) + } + return cps, nil +} + +// ListUTXOs get utxos by accountID +func (store *MockAccountStore) ListUTXOs() ([]*acc.UTXO, error) { + utxoIter := store.db.IteratorPrefix([]byte(UTXOPrefix)) + defer utxoIter.Release() + + utxos := []*acc.UTXO{} + for utxoIter.Next() { + utxo := new(acc.UTXO) + if err := json.Unmarshal(utxoIter.Value(), utxo); err != nil { + return nil, err + } + utxos = append(utxos, utxo) + } + return utxos, nil +} + +// SetAccount set account account ID, account alias and raw account. +func (store *MockAccountStore) SetAccount(account *acc.Account) error { + rawAccount, err := json.Marshal(account) + if err != nil { + return acc.ErrMarshalAccount + } + + batch := store.db.NewBatch() + if store.batch != nil { + batch = store.batch + } + + batch.Set(AccountIDKey(account.ID), rawAccount) + batch.Set(accountAliasKey(account.Alias), []byte(account.ID)) + + if store.batch == nil { + batch.Write() + } + return nil +} + +// SetAccountIndex update account index +func (store *MockAccountStore) SetAccountIndex(account *acc.Account) { + currentIndex := store.GetAccountIndex(account.XPubs) + if account.KeyIndex > currentIndex { + if store.batch == nil { + store.db.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex)) } else { - out = types.NewIntraChainOutput(utxo.AssetID, utxo.Amount, utxo.ControlProgram) + store.batch.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex)) } - tplBuilder.AddOutput(out) } +} - return tplBuilder.Build() +// SetBip44ContractIndex set contract index +func (store *MockAccountStore) SetBip44ContractIndex(accountID string, change bool, index uint64) { + if store.batch == nil { + store.db.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index)) + } else { + store.batch.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index)) + } } -func mockWallet(walletDB dbm.DB, account *account.Manager, asset *asset.Registry, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) *Wallet { - wallet := &Wallet{ - DB: walletDB, - AccountMgr: account, - AssetReg: asset, - chain: chain, - RecoveryMgr: newRecoveryManager(walletDB, account), - eventDispatcher: dispatcher, - TxIndexFlag: txIndexFlag, +// SetCoinbaseArbitrary set coinbase arbitrary +func (store *MockAccountStore) SetCoinbaseArbitrary(arbitrary []byte) { + if store.batch == nil { + store.db.Set([]byte(CoinbaseAbKey), arbitrary) + } else { + store.batch.Set([]byte(CoinbaseAbKey), arbitrary) } - wallet.txMsgSub, _ = wallet.eventDispatcher.Subscribe(protocol.TxMsgEvent{}) - return wallet } -func mockSingleBlock(tx *types.Tx) *types.Block { - return &types.Block{ - BlockHeader: types.BlockHeader{ - Version: 1, - Height: 1, - }, - Transactions: []*types.Tx{config.GenesisTx(), tx}, +// SetContractIndex set contract index +func (store *MockAccountStore) SetContractIndex(accountID string, index uint64) { + if store.batch == nil { + store.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(index)) + } else { + store.batch.Set(contractIndexKey(accountID), common.Unit64ToBytes(index)) + } +} + +// SetControlProgram set raw program +func (store *MockAccountStore) SetControlProgram(hash bc.Hash, program *acc.CtrlProgram) error { + accountCP, err := json.Marshal(program) + if err != nil { + return err + } + if store.batch == nil { + store.db.Set(ContractKey(hash), accountCP) + } else { + store.batch.Set(ContractKey(hash), accountCP) + } + return nil +} + +// SetMiningAddress set mining address +func (store *MockAccountStore) SetMiningAddress(program *acc.CtrlProgram) error { + rawProgram, err := json.Marshal(program) + if err != nil { + return err + } + + if store.batch == nil { + store.db.Set([]byte(MiningAddressKey), rawProgram) + } else { + store.batch.Set([]byte(MiningAddressKey), rawProgram) + } + return nil +} + +// SetStandardUTXO set standard utxo +func (store *MockAccountStore) SetStandardUTXO(outputID bc.Hash, utxo *acc.UTXO) error { + data, err := json.Marshal(utxo) + if err != nil { + return err + } + if store.batch == nil { + store.db.Set(StandardUTXOKey(outputID), data) + } else { + store.batch.Set(StandardUTXOKey(outputID), data) } + return nil }