// Package account stores and tracks accounts within a Bytom Core. package account import ( "reflect" "strings" "sync" "github.com/golang/groupcache/lru" "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/vapor/blockchain/signers" "github.com/vapor/blockchain/txbuilder" "github.com/vapor/common" "github.com/vapor/consensus" "github.com/vapor/consensus/segwit" "github.com/vapor/crypto" "github.com/vapor/crypto/ed25519/chainkd" "github.com/vapor/crypto/sha3pool" "github.com/vapor/errors" "github.com/vapor/protocol" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/vm/vmutil" ) const ( maxAccountCache = 1000 // HardenedKeyStart bip32 hierarchical deterministic wallets // keys with index ≥ 0x80000000 are hardened keys HardenedKeyStart = 0x80000000 logModule = "account" ) // 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") ErrFindMiningAddress = errors.New("Failed to find mining address") ErrAccountIDEmpty = errors.New("account_id is empty") ) // Account is structure of Bytom account type Account struct { *signers.Signer ID string `json:"id"` Alias string `json:"alias"` } //CtrlProgram is structure of account control program type CtrlProgram struct { AccountID string Address string KeyIndex uint64 ControlProgram []byte Change bool // Mark whether this control program is for UTXO change } // Manager stores accounts and their associated control programs. type Manager struct { store AccountStore chain *protocol.Chain utxoKeeper *utxoKeeper cacheMu sync.Mutex cache *lru.Cache aliasCache *lru.Cache delayedACPsMu sync.Mutex delayedACPs map[*txbuilder.TemplateBuilder][]*CtrlProgram addressMu sync.Mutex accountMu sync.Mutex } // NewManager creates a new account manager func NewManager(store AccountStore, chain *protocol.Chain) *Manager { return &Manager{ store: store, chain: chain, utxoKeeper: newUtxoKeeper(chain.BestBlockHeight, store), cache: lru.New(maxAccountCache), aliasCache: lru.New(maxAccountCache), delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram), } } // AddUnconfirmedUtxo add untxo list to utxoKeeper func (m *Manager) AddUnconfirmedUtxo(utxos []*UTXO) { m.utxoKeeper.AddUnconfirmedUtxo(utxos) } // CreateAccount creates a new Account. func CreateAccount(xpubs []chainkd.XPub, quorum int, alias string, acctIndex uint64, deriveRule uint8) (*Account, error) { if acctIndex >= HardenedKeyStart { return nil, ErrAccountIndex } signer, err := signers.Create("account", xpubs, quorum, acctIndex, deriveRule) if err != nil { return nil, errors.Wrap(err) } id := uuid.New().String() return &Account{Signer: signer, ID: id, Alias: strings.ToLower(strings.TrimSpace(alias))}, nil } 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 } if err := newStore.CommitBatch(); err != nil { return err } return nil } // SaveAccount save a new account. func (m *Manager) SaveAccount(account *Account) error { m.accountMu.Lock() defer m.accountMu.Unlock() _, err := m.store.GetAccountByAlias(account.Alias) if err == nil { return ErrDuplicateAlias } if err != ErrFindAccount { return err } acct, err := m.GetAccountByXPubsIndex(account.XPubs, account.KeyIndex) if err != nil && err != ErrFindAccount { return err } else if acct != nil { return ErrDuplicateIndex } return m.saveAccount(account) } // Create creates and save a new Account. func (m *Manager) Create(xpubs []chainkd.XPub, quorum int, alias string, deriveRule uint8) (*Account, error) { m.accountMu.Lock() defer m.accountMu.Unlock() _, err := m.store.GetAccountByAlias(alias) if err == nil { return nil, ErrDuplicateAlias } else if err != ErrFindAccount { return nil, err } acctIndex := uint64(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); err != nil { return nil, err } return account, nil } func (m *Manager) UpdateAccountAlias(accountID string, newAlias string) error { m.accountMu.Lock() defer m.accountMu.Unlock() account, err := m.FindByID(accountID) if err != nil { return err } oldAccount := *account normalizedAlias := strings.ToLower(strings.TrimSpace(newAlias)) _, err = m.store.GetAccountByAlias(normalizedAlias) if err == nil { return ErrDuplicateAlias } if err != ErrFindAccount { return err } m.cacheMu.Lock() m.aliasCache.Remove(oldAccount.Alias) m.cacheMu.Unlock() account.Alias = normalizedAlias 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 } return nil } // CreateAddress generate an address for the select account func (m *Manager) CreateAddress(accountID string, change bool) (cp *CtrlProgram, err error) { m.addressMu.Lock() defer m.addressMu.Unlock() account, err := m.FindByID(accountID) if err != nil { return nil, err } currentIdx, err := m.getCurrentContractIndex(account, change) if err != nil { return nil, err } cp, err = CreateCtrlProgram(account, currentIdx+1, change) if err != nil { return nil, err } return cp, m.saveControlProgram(cp, true) } // CreateBatchAddresses generate a batch of addresses for the select account func (m *Manager) CreateBatchAddresses(accountID string, change bool, stopIndex uint64) error { m.addressMu.Lock() defer m.addressMu.Unlock() account, err := m.FindByID(accountID) if err != nil { return err } currentIndex, err := m.getCurrentContractIndex(account, change) if err != nil { return err } for currentIndex++; currentIndex <= stopIndex; currentIndex++ { cp, err := CreateCtrlProgram(account, currentIndex, change) if err != nil { return err } if err := m.saveControlProgram(cp, true); err != nil { return err } } return nil } // DeleteAccount deletes the account's ID or alias matching account ID. func (m *Manager) DeleteAccount(accountID string) error { m.accountMu.Lock() defer m.accountMu.Unlock() account, err := m.FindByID(accountID) if err != nil { return err } m.cacheMu.Lock() m.aliasCache.Remove(account.Alias) m.cacheMu.Unlock() return m.store.DeleteAccount(account) } // FindByAlias retrieves an account's Signer record by its alias func (m *Manager) FindByAlias(alias string) (*Account, error) { m.cacheMu.Lock() cachedID, ok := m.aliasCache.Get(alias) m.cacheMu.Unlock() if ok { return m.FindByID(cachedID.(string)) } return m.store.GetAccountByAlias(alias) } // FindByID returns an account's Signer record by its ID. func (m *Manager) FindByID(id string) (*Account, error) { m.cacheMu.Lock() cachedAccount, ok := m.cache.Get(id) m.cacheMu.Unlock() if ok { return cachedAccount.(*Account), nil } account, err := m.store.GetAccountByID(id) if err != nil { return nil, err } m.cacheMu.Lock() m.cache.Add(id, account) m.cacheMu.Unlock() return account, nil } // GetAccountByProgram return Account by given CtrlProgram func (m *Manager) GetAccountByProgram(program *CtrlProgram) (*Account, error) { return m.store.GetAccountByID(program.AccountID) } // GetAccountByXPubsIndex get account by xPubs and index func (m *Manager) GetAccountByXPubsIndex(xPubs []chainkd.XPub, index uint64) (*Account, error) { accounts, err := m.ListAccounts("") if err != nil { return nil, err } for _, account := range accounts { if reflect.DeepEqual(account.XPubs, xPubs) && account.KeyIndex == index { return account, nil } } return nil, ErrFindAccount } // GetAliasByID return the account alias by given ID func (m *Manager) GetAliasByID(id string) string { account, err := m.store.GetAccountByID(id) if err != nil { log.Warn("GetAliasByID fail to find account") return "" } return account.Alias } func (m *Manager) GetCoinbaseArbitrary() []byte { if arbitrary := m.store.GetCoinbaseArbitrary(); arbitrary != nil { return arbitrary } return []byte{} } // GetCoinbaseControlProgram will return a coinbase script func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) { cp, err := m.GetCoinbaseCtrlProgram() if err == ErrFindAccount { 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 cp, err := m.store.GetMiningAddress(); err == nil { return cp, nil } else if err != ErrFindMiningAddress { return nil, err } account := new(Account) accounts, err := m.store.ListAccounts("") if err != nil { return nil, err } if len(accounts) > 0 { account = accounts[0] } else { return nil, ErrFindAccount } program, err := m.CreateAddress(account.ID, false) if err != nil { return nil, err } if err := m.store.SetMiningAddress(program); err != nil { return nil, err } return program, nil } // GetBip44ContractIndex return the current bip44 contract index func (m *Manager) GetBip44ContractIndex(accountID string, change bool) uint64 { return m.store.GetBip44ContractIndex(accountID, change) } // GetLocalCtrlProgramByAddress return CtrlProgram by given address func (m *Manager) GetLocalCtrlProgramByAddress(address string) (*CtrlProgram, error) { program, err := m.getProgramByAddress(address) if err != nil { return nil, err } var hash [32]byte sha3pool.Sum256(hash[:], program) cp, err := m.store.GetControlProgram(bc.NewHash(hash)) if err != nil { return nil, err } return cp, nil } // GetMiningAddress will return the mining address func (m *Manager) GetMiningAddress() (string, error) { cp, err := m.GetCoinbaseCtrlProgram() 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 [32]byte sha3pool.Sum256(hash[:], prog) 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) { return m.store.ListAccounts(id) } // ListControlProgram return all the local control program func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) { return m.store.ListControlPrograms() } func (m *Manager) ListUnconfirmedUtxo(accountID string, isSmartContract bool) []*UTXO { utxos := m.utxoKeeper.ListUnconfirmed() result := []*UTXO{} for _, utxo := range utxos { if segwit.IsP2WScript(utxo.ControlProgram) != isSmartContract && (accountID == utxo.AccountID || accountID == "") { result = append(result, utxo) } } return result } // RemoveUnconfirmedUtxo remove utxos from the utxoKeeper func (m *Manager) RemoveUnconfirmedUtxo(hashes []*bc.Hash) { m.utxoKeeper.RemoveUnconfirmedUtxo(hashes) } // SetMiningAddress will set the mining address func (m *Manager) SetMiningAddress(miningAddress string) (string, error) { program, err := m.getProgramByAddress(miningAddress) if err != nil { return "", err } cp := &CtrlProgram{ Address: miningAddress, ControlProgram: program, } if err := m.store.SetMiningAddress(cp); err != nil { return cp.Address, err } return m.GetMiningAddress() } func (m *Manager) SetCoinbaseArbitrary(arbitrary []byte) { m.store.SetCoinbaseArbitrary(arbitrary) } // CreateCtrlProgram generate an address for the select account func CreateCtrlProgram(account *Account, addrIdx uint64, change bool) (cp *CtrlProgram, err error) { path, err := signers.Path(account.Signer, signers.AccountKeySpace, change, addrIdx) if err != nil { return nil, err } if len(account.XPubs) == 1 { cp, err = createP2PKH(account, path) } else { cp, err = createP2SH(account, path) } if err != nil { return nil, err } cp.KeyIndex, cp.Change = addrIdx, change return cp, nil } func createP2PKH(account *Account, path [][]byte) (*CtrlProgram, error) { derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path) derivedPK := derivedXPubs[0].PublicKey() pubHash := crypto.Ripemd160(derivedPK) address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams) if err != nil { return nil, err } control, err := vmutil.P2WPKHProgram([]byte(pubHash)) if err != nil { return nil, err } return &CtrlProgram{ AccountID: account.ID, Address: address.EncodeAddress(), ControlProgram: control, }, nil } func createP2SH(account *Account, path [][]byte) (*CtrlProgram, error) { derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path) derivedPKs := chainkd.XPubKeys(derivedXPubs) signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum) if err != nil { return nil, err } scriptHash := crypto.Sha256(signScript) address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams) if err != nil { return nil, err } control, err := vmutil.P2WSHProgram(scriptHash) if err != nil { return nil, err } return &CtrlProgram{ AccountID: account.ID, Address: address.EncodeAddress(), ControlProgram: control, }, nil } 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.store.GetContractIndex(account.ID), nil case signers.BIP0044: return m.store.GetBip44ContractIndex(account.ID, change), nil } return 0, ErrDeriveRule } func (m *Manager) getProgramByAddress(address string) ([]byte, error) { addr, err := common.DecodeAddress(address, &consensus.ActiveNetParams) if err != nil { return nil, err } redeemContract := addr.ScriptAddress() program := []byte{} switch addr.(type) { case *common.AddressWitnessPubKeyHash: program, err = vmutil.P2WPKHProgram(redeemContract) case *common.AddressWitnessScriptHash: program, err = vmutil.P2WSHProgram(redeemContract) default: return nil, ErrInvalidAddress } if err != nil { return nil, err } return program, nil } func (m *Manager) saveControlProgram(prog *CtrlProgram, updateIndex bool) error { var hash [32]byte sha3pool.Sum256(hash[:], prog.ControlProgram) acct, err := m.GetAccountByProgram(prog) if err != nil { return err } newStore := m.store.InitBatch() if err := newStore.SetControlProgram(bc.NewHash(hash), prog); err != nil { return nil } if updateIndex { switch acct.DeriveRule { case signers.BIP0032: newStore.SetContractIndex(acct.ID, prog.KeyIndex) case signers.BIP0044: newStore.SetBip44ContractIndex(acct.ID, prog.Change, prog.KeyIndex) } } return newStore.CommitBatch() } // SaveControlPrograms save account control programs func (m *Manager) SaveControlPrograms(progs ...*CtrlProgram) error { m.addressMu.Lock() defer m.addressMu.Unlock() for _, prog := range progs { acct, err := m.GetAccountByProgram(prog) if err != nil { return err } currentIndex, err := m.getCurrentContractIndex(acct, prog.Change) if err != nil { return err } m.saveControlProgram(prog, prog.KeyIndex > currentIndex) } 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) }