-// Package account stores and tracks accounts within a Chain Core.
+// Package account stores and tracks accounts within a Bytom Core.
package account
import (
- "context"
- "encoding/binary"
"encoding/json"
- "sort"
"strings"
"sync"
- "time"
"github.com/golang/groupcache/lru"
log "github.com/sirupsen/logrus"
"github.com/bytom/blockchain/txbuilder"
"github.com/bytom/common"
"github.com/bytom/consensus"
+ "github.com/bytom/consensus/segwit"
"github.com/bytom/crypto"
"github.com/bytom/crypto/ed25519/chainkd"
"github.com/bytom/crypto/sha3pool"
"github.com/bytom/errors"
"github.com/bytom/protocol"
+ "github.com/bytom/protocol/bc"
"github.com/bytom/protocol/vm/vmutil"
)
const (
maxAccountCache = 1000
- aliasPrefix = "ALI:"
- accountPrefix = "ACC:"
- accountCPPrefix = "ACP:"
- indexPrefix = "ACIDX:"
)
-var miningAddressKey = []byte("miningAddress")
+var (
+ accountIndexKey = []byte("AccountIndex")
+ accountPrefix = []byte("Account:")
+ aliasPrefix = []byte("AccountAlias:")
+ contractIndexPrefix = []byte("ContractIndex")
+ contractPrefix = []byte("Contract:")
+ miningAddressKey = []byte("MiningAddress")
+)
// pre-define errors for supporting bytom errorFormatter
var (
- ErrDuplicateAlias = errors.New("duplicate account alias")
- ErrFindAccount = errors.New("fail to find account")
- ErrMarshalAccount = errors.New("failed marshal account")
- ErrMarshalTags = errors.New("failed marshal account to update tags")
+ ErrDuplicateAlias = errors.New("duplicate account alias")
+ ErrFindAccount = errors.New("fail to find account")
+ ErrMarshalAccount = errors.New("failed marshal account")
+ ErrInvalidAddress = errors.New("invalid address")
+ ErrFindCtrlProgram = errors.New("fail to find account control program")
)
func aliasKey(name string) []byte {
- return []byte(aliasPrefix + name)
+ return append(aliasPrefix, []byte(name)...)
}
-func indexKeys(xpubs []chainkd.XPub) []byte {
- xpubStrings := make([]string, len(xpubs))
- for i, xpub := range xpubs {
- xpubStrings[i] = xpub.String()
- }
- sort.Strings(xpubStrings)
- suffix := strings.Join(xpubStrings, "")
-
- return []byte(indexPrefix + suffix)
-}
-
-//Key account store prefix
+// Key account store prefix
func Key(name string) []byte {
- return []byte(accountPrefix + name)
+ return append(accountPrefix, []byte(name)...)
}
-//CPKey account control promgram store prefix
-func CPKey(hash common.Hash) []byte {
- return append([]byte(accountCPPrefix), hash[:]...)
+// ContractKey account control promgram store prefix
+func ContractKey(hash common.Hash) []byte {
+ return append(contractPrefix, hash[:]...)
}
-func convertUnit64ToBytes(nextIndex uint64) []byte {
- buf := make([]byte, 8)
- binary.PutUvarint(buf, nextIndex)
- return buf
+func contractIndexKey(accountID string) []byte {
+ return append(contractIndexPrefix, []byte(accountID)...)
}
-func convertBytesToUint64(rawIndex []byte) uint64 {
- result, _ := binary.Uvarint(rawIndex)
- return result
+// Account is structure of Bytom account
+type Account struct {
+ *signers.Signer
+ ID string `json:"id"`
+ Alias string `json:"alias"`
}
-// NewManager creates a new account manager
-func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
- return &Manager{
- db: walletDB,
- chain: chain,
- utxoDB: newReserver(chain, walletDB),
- cache: lru.New(maxAccountCache),
- aliasCache: lru.New(maxAccountCache),
- delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
- }
+//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 {
- db dbm.DB
- chain *protocol.Chain
- utxoDB *reserver
+ db dbm.DB
+ chain *protocol.Chain
+ utxoKeeper *utxoKeeper
cacheMu sync.Mutex
cache *lru.Cache
delayedACPsMu sync.Mutex
delayedACPs map[*txbuilder.TemplateBuilder][]*CtrlProgram
- accIndexMu sync.Mutex
+ accIndexMu sync.Mutex
+ accountMu sync.Mutex
}
-// ExpireReservations removes reservations that have expired periodically.
-// It blocks until the context is canceled.
-func (m *Manager) ExpireReservations(ctx context.Context, period time.Duration) {
- ticks := time.Tick(period)
- for {
- select {
- case <-ctx.Done():
- log.Info("Deposed, ExpireReservations exiting")
- return
- case <-ticks:
- err := m.utxoDB.ExpireReservations(ctx)
- if err != nil {
- log.WithField("error", err).Error("Expire reservations")
- }
- }
+// NewManager creates a new account manager
+func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
+ return &Manager{
+ db: walletDB,
+ chain: chain,
+ utxoKeeper: newUtxoKeeper(chain.BestBlockHeight, walletDB),
+ cache: lru.New(maxAccountCache),
+ aliasCache: lru.New(maxAccountCache),
+ delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
}
}
-// Account is structure of Bytom account
-type Account struct {
- *signers.Signer
- ID string
- Alias string
- Tags map[string]interface{}
-}
-
-func (m *Manager) getNextXpubsIndex(xpubs []chainkd.XPub) uint64 {
- m.accIndexMu.Lock()
- defer m.accIndexMu.Unlock()
-
- var nextIndex uint64 = 1
- if rawIndexBytes := m.db.Get(indexKeys(xpubs)); rawIndexBytes != nil {
- nextIndex = convertBytesToUint64(rawIndexBytes) + 1
- }
-
- m.db.Set(indexKeys(xpubs), convertUnit64ToBytes(nextIndex))
-
- return nextIndex
+// AddUnconfirmedUtxo add untxo list to utxoKeeper
+func (m *Manager) AddUnconfirmedUtxo(utxos []*UTXO) {
+ m.utxoKeeper.AddUnconfirmedUtxo(utxos)
}
// Create creates a new Account.
-func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string, tags map[string]interface{}) (*Account, error) {
+func (m *Manager) Create(xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) {
+ m.accountMu.Lock()
+ defer m.accountMu.Unlock()
+
normalizedAlias := strings.ToLower(strings.TrimSpace(alias))
if existed := m.db.Get(aliasKey(normalizedAlias)); existed != nil {
return nil, ErrDuplicateAlias
}
- nextAccountIndex := m.getNextXpubsIndex(xpubs)
-
- signer, err := signers.Create("account", xpubs, quorum, nextAccountIndex)
+ signer, err := signers.Create("account", xpubs, quorum, m.getNextAccountIndex())
id := signers.IDGenerate()
if err != nil {
return nil, errors.Wrap(err)
}
- account := &Account{Signer: signer, ID: id, Alias: normalizedAlias, Tags: tags}
+ account := &Account{Signer: signer, ID: id, Alias: normalizedAlias}
rawAccount, err := json.Marshal(account)
if err != nil {
return nil, ErrMarshalAccount
}
- storeBatch := m.db.NewBatch()
accountID := Key(id)
+ storeBatch := m.db.NewBatch()
storeBatch.Set(accountID, rawAccount)
- storeBatch.Set(aliasKey(alias), []byte(id))
+ storeBatch.Set(aliasKey(normalizedAlias), []byte(id))
storeBatch.Write()
-
return account, nil
}
-// UpdateTags modifies the tags of the specified account. The account may be
-// identified either by ID or Alias, but not both.
-func (m *Manager) UpdateTags(ctx context.Context, accountInfo string, tags map[string]interface{}) (err error) {
+// CreateAddress generate an address for the select account
+func (m *Manager) CreateAddress(accountID string, change bool) (cp *CtrlProgram, err error) {
+ account, err := m.FindByID(accountID)
+ if err != nil {
+ return nil, err
+ }
+ return m.createAddress(account, change)
+}
+
+// DeleteAccount deletes the account's ID or alias matching accountInfo.
+func (m *Manager) DeleteAccount(aliasOrID string) (err error) {
account := &Account{}
- if account, err = m.FindByAlias(nil, accountInfo); err != nil {
- if account, err = m.findByID(ctx, accountInfo); err != nil {
+ if account, err = m.FindByAlias(aliasOrID); err != nil {
+ if account, err = m.FindByID(aliasOrID); err != nil {
return err
}
}
- account.Tags = tags
- rawAccount, err := json.Marshal(account)
- if err != nil {
- return ErrMarshalTags
- }
-
- m.db.Set(Key(account.ID), rawAccount)
m.cacheMu.Lock()
- m.cache.Add(account.ID, account)
+ 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
}
// FindByAlias retrieves an account's Signer record by its alias
-func (m *Manager) FindByAlias(ctx context.Context, alias string) (*Account, error) {
+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(ctx, cachedID.(string))
+ return m.FindByID(cachedID.(string))
}
rawID := m.db.Get(aliasKey(alias))
m.cacheMu.Lock()
m.aliasCache.Add(alias, accountID)
m.cacheMu.Unlock()
- return m.findByID(ctx, accountID)
+ return m.FindByID(accountID)
}
-// findByID returns an account's Signer record by its ID.
-func (m *Manager) findByID(ctx context.Context, id string) (*Account, error) {
+// 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()
return account, nil
}
-// GetAliasByID return the account alias by given ID
-func (m *Manager) GetAliasByID(id string) string {
+// 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)
+}
+// GetAliasByID return the account alias by given ID
+func (m *Manager) GetAliasByID(id string) string {
rawAccount := m.db.Get(Key(id))
if rawAccount == nil {
- log.Warn("fail to find account")
+ log.Warn("GetAliasByID fail to find account")
return ""
}
+ account := &Account{}
if err := json.Unmarshal(rawAccount, account); err != nil {
log.Warn(err)
- return ""
}
return account.Alias
}
-// CreateAddress generate an address for the select account
-func (m *Manager) CreateAddress(ctx context.Context, accountID string, change bool) (cp *CtrlProgram, err error) {
- account, err := m.findByID(ctx, accountID)
+// GetCoinbaseControlProgram will return a coinbase script
+func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
+ if data := m.db.Get(miningAddressKey); data != nil {
+ cp := &CtrlProgram{}
+ return cp.ControlProgram, json.Unmarshal(data, cp)
+ }
+
+ accountIter := m.db.IteratorPrefix([]byte(accountPrefix))
+ defer accountIter.Release()
+ if !accountIter.Next() {
+ log.Warningf("GetCoinbaseControlProgram: can't find any account in db")
+ return vmutil.DefaultCoinbaseProgram()
+ }
+
+ account := &Account{}
+ if err := json.Unmarshal(accountIter.Value(), account); err != nil {
+ return nil, err
+ }
+
+ program, err := m.createAddress(account, false)
if err != nil {
return nil, err
}
- return m.createAddress(ctx, account, change)
+
+ rawCP, err := json.Marshal(program)
+ if err != nil {
+ return nil, err
+ }
+
+ m.db.Set(miningAddressKey, rawCP)
+ return program.ControlProgram, nil
}
-// CreateAddress generate an address for the select account
-func (m *Manager) createAddress(ctx context.Context, account *Account, change bool) (cp *CtrlProgram, err error) {
- if len(account.XPubs) == 1 {
- cp, err = m.createP2PKH(ctx, account, change)
- } else {
- cp, err = m.createP2SH(ctx, account, change)
+// GetContractIndex return the current index
+func (m *Manager) GetContractIndex(accountID string) uint64 {
+ m.accIndexMu.Lock()
+ defer m.accIndexMu.Unlock()
+
+ index := uint64(1)
+ if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+ index = common.BytesToUnit64(rawIndexBytes)
}
+ return index
+}
+
+// GetProgramByAddress return CtrlProgram by given address
+func (m *Manager) GetProgramByAddress(address string) (*CtrlProgram, error) {
+ addr, err := common.DecodeAddress(address, &consensus.ActiveNetParams)
if err != nil {
return nil, err
}
- if err = m.insertAccountControlProgram(ctx, cp); err != nil {
+ 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 cp, nil
+
+ var hash [32]byte
+ sha3pool.Sum256(hash[:], program)
+ rawProgram := m.db.Get(ContractKey(hash))
+ if rawProgram == nil {
+ return nil, ErrFindCtrlProgram
+ }
+
+ cp := &CtrlProgram{}
+ return cp, json.Unmarshal(rawProgram, cp)
}
-func (m *Manager) createP2PKH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
- idx := m.getNextXpubsIndex(account.Signer.XPubs)
+// IsLocalControlProgram check is the input control program belong to local
+func (m *Manager) IsLocalControlProgram(prog []byte) bool {
+ var hash common.Hash
+ sha3pool.Sum256(hash[:], prog)
+ bytes := m.db.Get(ContractKey(hash))
+ return bytes != nil
+}
+
+// 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
+}
+
+// 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
+}
+
+func (m *Manager) ListUnconfirmedUtxo(isSmartContract bool) []*UTXO {
+ utxos := m.utxoKeeper.ListUnconfirmed()
+ result := []*UTXO{}
+ for _, utxo := range utxos {
+ if segwit.IsP2WScript(utxo.ControlProgram) != isSmartContract {
+ result = append(result, utxo)
+ }
+ }
+ return result
+}
+
+// RemoveUnconfirmedUtxo remove utxos from the utxoKeeper
+func (m *Manager) RemoveUnconfirmedUtxo(hashes []*bc.Hash) {
+ m.utxoKeeper.RemoveUnconfirmedUtxo(hashes)
+}
+
+// CreateAddress generate an address for the select account
+func (m *Manager) createAddress(account *Account, change bool) (cp *CtrlProgram, err error) {
+ if len(account.XPubs) == 1 {
+ cp, err = m.createP2PKH(account, change)
+ } else {
+ cp, err = m.createP2SH(account, change)
+ }
+ if err != nil {
+ return nil, err
+ }
+ return cp, m.insertControlPrograms(cp)
+}
+
+func (m *Manager) createP2PKH(account *Account, change bool) (*CtrlProgram, error) {
+ idx := m.getNextContractIndex(account.ID)
path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
derivedPK := derivedXPubs[0].PublicKey()
pubHash := crypto.Ripemd160(derivedPK)
- // TODO: pass different params due to config
- address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.MainNetParams)
+ address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)
if err != nil {
return nil, err
}
}, nil
}
-func (m *Manager) createP2SH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
- idx := m.getNextXpubsIndex(account.Signer.XPubs)
+func (m *Manager) createP2SH(account *Account, change bool) (*CtrlProgram, error) {
+ idx := m.getNextContractIndex(account.ID)
path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
derivedPKs := chainkd.XPubKeys(derivedXPubs)
}
scriptHash := crypto.Sha256(signScript)
- // TODO: pass different params due to config
- address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.MainNetParams)
+ address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
if err != nil {
return nil, err
}
}, nil
}
-//CtrlProgram is structure of account control program
-type CtrlProgram struct {
- AccountID string
- Address string
- KeyIndex uint64
- ControlProgram []byte
- Change bool
-}
-
-func (m *Manager) insertAccountControlProgram(ctx context.Context, progs ...*CtrlProgram) error {
- var hash common.Hash
- for _, prog := range progs {
- accountCP, err := json.Marshal(prog)
- if err != nil {
- return err
- }
+func (m *Manager) getNextAccountIndex() uint64 {
+ m.accIndexMu.Lock()
+ defer m.accIndexMu.Unlock()
- sha3pool.Sum256(hash[:], prog.ControlProgram)
- m.db.Set(CPKey(hash), accountCP)
+ var nextIndex uint64 = 1
+ if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil {
+ nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
}
- return nil
-}
-
-// IsLocalControlProgram check is the input control program belong to local
-func (m *Manager) IsLocalControlProgram(prog []byte) bool {
- var hash common.Hash
- sha3pool.Sum256(hash[:], prog)
- bytes := m.db.Get(CPKey(hash))
- return bytes != nil
+ m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex))
+ return nextIndex
}
-// GetCoinbaseControlProgram will return a coinbase script
-func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
- if data := m.db.Get(miningAddressKey); data != nil {
- cp := &CtrlProgram{}
- return cp.ControlProgram, json.Unmarshal(data, cp)
- }
-
- accountIter := m.db.IteratorPrefix([]byte(accountPrefix))
- defer accountIter.Release()
- if !accountIter.Next() {
- log.Warningf("GetCoinbaseControlProgram: can't find any account in db")
- return vmutil.DefaultCoinbaseProgram()
- }
-
- account := &Account{}
- if err := json.Unmarshal(accountIter.Value(), account); err != nil {
- return nil, err
- }
-
- program, err := m.createAddress(nil, account, false)
- if err != nil {
- return nil, err
- }
+func (m *Manager) getNextContractIndex(accountID string) uint64 {
+ m.accIndexMu.Lock()
+ defer m.accIndexMu.Unlock()
- rawCP, err := json.Marshal(program)
- if err != nil {
- return nil, err
+ nextIndex := uint64(1)
+ if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+ nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
}
-
- m.db.Set(miningAddressKey, rawCP)
- return program.ControlProgram, nil
+ m.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(nextIndex))
+ return nextIndex
}
-// DeleteAccount deletes the account's ID or alias matching accountInfo.
-func (m *Manager) DeleteAccount(in struct {
- AccountInfo string `json:"account_info"`
-}) (err error) {
- account := &Account{}
- if account, err = m.FindByAlias(nil, in.AccountInfo); err != nil {
- if account, err = m.findByID(nil, in.AccountInfo); err != nil {
+func (m *Manager) insertControlPrograms(progs ...*CtrlProgram) error {
+ var hash common.Hash
+ for _, prog := range progs {
+ accountCP, err := json.Marshal(prog)
+ if err != nil {
return err
}
- }
-
- storeBatch := m.db.NewBatch()
-
- m.cacheMu.Lock()
- m.aliasCache.Remove(account.Alias)
- m.cacheMu.Unlock()
-
- storeBatch.Delete(aliasKey(account.Alias))
- storeBatch.Delete(Key(account.ID))
- storeBatch.Write()
-
- return nil
-}
-
-// ListAccounts will return the accounts in the db
-func (m *Manager) ListAccounts(id string) ([]*Account, error) {
- accounts := []*Account{}
- accountIter := m.db.IteratorPrefix([]byte(accountPrefix + 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
-}
-
-// ListControlProgram return all the local control program
-func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) {
- cps := []*CtrlProgram{}
- cpIter := m.db.IteratorPrefix([]byte(accountCPPrefix))
- defer cpIter.Release()
-
- for cpIter.Next() {
- cp := &CtrlProgram{}
- if err := json.Unmarshal(cpIter.Value(), cp); err != nil {
- return nil, err
- }
- cps = append(cps, cp)
+ sha3pool.Sum256(hash[:], prog.ControlProgram)
+ m.db.Set(ContractKey(hash), accountCP)
}
-
- return cps, nil
+ return nil
}