1 // Package account stores and tracks accounts within a Bytom Core.
11 "github.com/golang/groupcache/lru"
12 log "github.com/sirupsen/logrus"
13 dbm "github.com/tendermint/tmlibs/db"
15 "github.com/bytom/blockchain/signers"
16 "github.com/bytom/blockchain/txbuilder"
17 "github.com/bytom/common"
18 "github.com/bytom/consensus"
19 "github.com/bytom/crypto"
20 "github.com/bytom/crypto/ed25519/chainkd"
21 "github.com/bytom/crypto/sha3pool"
22 "github.com/bytom/errors"
23 "github.com/bytom/protocol"
24 "github.com/bytom/protocol/vm/vmutil"
28 maxAccountCache = 1000
32 accountIndexKey = []byte("AccountIndex")
33 accountPrefix = []byte("Account:")
34 aliasPrefix = []byte("AccountAlias:")
35 contractIndexPrefix = []byte("ContractIndex")
36 contractPrefix = []byte("Contract:")
37 miningAddressKey = []byte("MiningAddress")
40 // pre-define errors for supporting bytom errorFormatter
42 ErrDuplicateAlias = errors.New("duplicate account alias")
43 ErrFindAccount = errors.New("fail to find account")
44 ErrMarshalAccount = errors.New("failed marshal account")
45 ErrInvalidAddress = errors.New("invalid address")
46 ErrFindCtrlProgram = errors.New("fail to find account control program")
49 func aliasKey(name string) []byte {
50 return append(aliasPrefix, []byte(name)...)
53 // Key account store prefix
54 func Key(name string) []byte {
55 return append(accountPrefix, []byte(name)...)
58 // ContractKey account control promgram store prefix
59 func ContractKey(hash common.Hash) []byte {
60 return append(contractPrefix, hash[:]...)
63 func contractIndexKey(accountID string) []byte {
64 return append(contractIndexPrefix, []byte(accountID)...)
67 // NewManager creates a new account manager
68 func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
72 utxoDB: newReserver(chain, walletDB),
73 cache: lru.New(maxAccountCache),
74 aliasCache: lru.New(maxAccountCache),
75 delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
79 // Manager stores accounts and their associated control programs.
89 delayedACPsMu sync.Mutex
90 delayedACPs map[*txbuilder.TemplateBuilder][]*CtrlProgram
96 // ExpireReservations removes reservations that have expired periodically.
97 // It blocks until the context is canceled.
98 func (m *Manager) ExpireReservations(ctx context.Context, period time.Duration) {
99 ticks := time.Tick(period)
103 log.Info("Deposed, ExpireReservations exiting")
106 err := m.utxoDB.ExpireReservations(ctx)
108 log.WithField("error", err).Error("Expire reservations")
114 // Account is structure of Bytom account
115 type Account struct {
117 ID string `json:"id"`
118 Alias string `json:"alias"`
121 func (m *Manager) getNextAccountIndex() uint64 {
123 defer m.accIndexMu.Unlock()
125 var nextIndex uint64 = 1
126 if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil {
127 nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
130 m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex))
134 func (m *Manager) getNextContractIndex(accountID string) uint64 {
136 defer m.accIndexMu.Unlock()
138 nextIndex := uint64(1)
139 if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
140 nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
143 m.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(nextIndex))
147 // Create creates a new Account.
148 func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) {
150 defer m.accountMu.Unlock()
152 normalizedAlias := strings.ToLower(strings.TrimSpace(alias))
153 if existed := m.db.Get(aliasKey(normalizedAlias)); existed != nil {
154 return nil, ErrDuplicateAlias
157 signer, err := signers.Create("account", xpubs, quorum, m.getNextAccountIndex())
158 id := signers.IDGenerate()
160 return nil, errors.Wrap(err)
163 account := &Account{Signer: signer, ID: id, Alias: normalizedAlias}
164 rawAccount, err := json.Marshal(account)
166 return nil, ErrMarshalAccount
168 storeBatch := m.db.NewBatch()
171 storeBatch.Set(accountID, rawAccount)
172 storeBatch.Set(aliasKey(normalizedAlias), []byte(id))
178 // FindByAlias retrieves an account's Signer record by its alias
179 func (m *Manager) FindByAlias(ctx context.Context, alias string) (*Account, error) {
181 cachedID, ok := m.aliasCache.Get(alias)
184 return m.FindByID(ctx, cachedID.(string))
187 rawID := m.db.Get(aliasKey(alias))
189 return nil, ErrFindAccount
192 accountID := string(rawID)
194 m.aliasCache.Add(alias, accountID)
196 return m.FindByID(ctx, accountID)
199 // FindByID returns an account's Signer record by its ID.
200 func (m *Manager) FindByID(ctx context.Context, id string) (*Account, error) {
202 cachedAccount, ok := m.cache.Get(id)
205 return cachedAccount.(*Account), nil
208 rawAccount := m.db.Get(Key(id))
209 if rawAccount == nil {
210 return nil, ErrFindAccount
213 account := &Account{}
214 if err := json.Unmarshal(rawAccount, account); err != nil {
219 m.cache.Add(id, account)
224 // GetAliasByID return the account alias by given ID
225 func (m *Manager) GetAliasByID(id string) string {
226 account := &Account{}
228 rawAccount := m.db.Get(Key(id))
229 if rawAccount == nil {
230 log.Warn("fail to find account")
234 if err := json.Unmarshal(rawAccount, account); err != nil {
241 // CreateAddress generate an address for the select account
242 func (m *Manager) CreateAddress(ctx context.Context, accountID string, change bool) (cp *CtrlProgram, err error) {
243 account, err := m.FindByID(ctx, accountID)
247 return m.createAddress(ctx, account, change)
250 // CreateAddress generate an address for the select account
251 func (m *Manager) createAddress(ctx context.Context, account *Account, change bool) (cp *CtrlProgram, err error) {
252 if len(account.XPubs) == 1 {
253 cp, err = m.createP2PKH(ctx, account, change)
255 cp, err = m.createP2SH(ctx, account, change)
261 if err = m.insertAccountControlProgram(ctx, cp); err != nil {
267 func (m *Manager) createP2PKH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
268 idx := m.getNextContractIndex(account.ID)
269 path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
270 derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
271 derivedPK := derivedXPubs[0].PublicKey()
272 pubHash := crypto.Ripemd160(derivedPK)
274 // TODO: pass different params due to config
275 address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)
280 control, err := vmutil.P2WPKHProgram([]byte(pubHash))
286 AccountID: account.ID,
287 Address: address.EncodeAddress(),
289 ControlProgram: control,
294 func (m *Manager) createP2SH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
295 idx := m.getNextContractIndex(account.ID)
296 path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
297 derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
298 derivedPKs := chainkd.XPubKeys(derivedXPubs)
299 signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
303 scriptHash := crypto.Sha256(signScript)
305 // TODO: pass different params due to config
306 address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
311 control, err := vmutil.P2WSHProgram(scriptHash)
317 AccountID: account.ID,
318 Address: address.EncodeAddress(),
320 ControlProgram: control,
325 //CtrlProgram is structure of account control program
326 type CtrlProgram struct {
330 ControlProgram []byte
331 Change bool // Mark whether this control program is for UTXO change
334 func (m *Manager) insertAccountControlProgram(ctx context.Context, progs ...*CtrlProgram) error {
336 for _, prog := range progs {
337 accountCP, err := json.Marshal(prog)
342 sha3pool.Sum256(hash[:], prog.ControlProgram)
343 m.db.Set(ContractKey(hash), accountCP)
348 // IsLocalControlProgram check is the input control program belong to local
349 func (m *Manager) IsLocalControlProgram(prog []byte) bool {
351 sha3pool.Sum256(hash[:], prog)
352 bytes := m.db.Get(ContractKey(hash))
356 // GetCoinbaseControlProgram will return a coinbase script
357 func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
358 if data := m.db.Get(miningAddressKey); data != nil {
360 return cp.ControlProgram, json.Unmarshal(data, cp)
363 accountIter := m.db.IteratorPrefix([]byte(accountPrefix))
364 defer accountIter.Release()
365 if !accountIter.Next() {
366 log.Warningf("GetCoinbaseControlProgram: can't find any account in db")
367 return vmutil.DefaultCoinbaseProgram()
370 account := &Account{}
371 if err := json.Unmarshal(accountIter.Value(), account); err != nil {
375 program, err := m.createAddress(nil, account, false)
380 rawCP, err := json.Marshal(program)
385 m.db.Set(miningAddressKey, rawCP)
386 return program.ControlProgram, nil
389 // DeleteAccount deletes the account's ID or alias matching accountInfo.
390 func (m *Manager) DeleteAccount(aliasOrID string) (err error) {
391 account := &Account{}
392 if account, err = m.FindByAlias(nil, aliasOrID); err != nil {
393 if account, err = m.FindByID(nil, aliasOrID); err != nil {
398 storeBatch := m.db.NewBatch()
401 m.aliasCache.Remove(account.Alias)
404 storeBatch.Delete(aliasKey(account.Alias))
405 storeBatch.Delete(Key(account.ID))
411 // ListAccounts will return the accounts in the db
412 func (m *Manager) ListAccounts(id string) ([]*Account, error) {
413 accounts := []*Account{}
414 accountIter := m.db.IteratorPrefix(Key(strings.TrimSpace(id)))
415 defer accountIter.Release()
417 for accountIter.Next() {
418 account := &Account{}
419 if err := json.Unmarshal(accountIter.Value(), &account); err != nil {
422 accounts = append(accounts, account)
428 // ListControlProgram return all the local control program
429 func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) {
430 var cps []*CtrlProgram
431 cpIter := m.db.IteratorPrefix(contractPrefix)
432 defer cpIter.Release()
436 if err := json.Unmarshal(cpIter.Value(), cp); err != nil {
439 cps = append(cps, cp)
445 // GetProgramByAddress return CtrlProgram by given address
446 func (m *Manager) GetProgramByAddress(address string) (*CtrlProgram, error) {
447 addr, err := common.DecodeAddress(address, &consensus.ActiveNetParams)
452 redeemContract := addr.ScriptAddress()
455 case *common.AddressWitnessPubKeyHash:
456 program, err = vmutil.P2WPKHProgram(redeemContract)
457 case *common.AddressWitnessScriptHash:
458 program, err = vmutil.P2WSHProgram(redeemContract)
460 return nil, ErrInvalidAddress
468 sha3pool.Sum256(hash[:], program)
469 rawProgram := m.db.Get(ContractKey(hash))
470 if rawProgram == nil {
471 return nil, ErrFindCtrlProgram
474 if err := json.Unmarshal(rawProgram, cp); err != nil {
481 // GetAccountByProgram return Account by given CtrlProgram
482 func (m *Manager) GetAccountByProgram(program *CtrlProgram) (*Account, error) {
483 rawAccount := m.db.Get(Key(program.AccountID))
484 if rawAccount == nil {
485 return nil, ErrFindAccount
488 account := &Account{}
489 if err := json.Unmarshal(rawAccount, account); err != nil {