1 // Package account stores and tracks accounts within a Chain 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
30 accountPrefix = "ACC:"
31 accountCPPrefix = "ACP:"
32 keyNextIndex = "NextIndex"
35 // pre-define errors for supporting bytom errorFormatter
37 ErrDuplicateAlias = errors.New("duplicate account alias")
38 ErrFindAccount = errors.New("fail to find account")
39 ErrMarshalAccount = errors.New("failed marshal account")
40 ErrMarshalTags = errors.New("failed marshal account to update tags")
41 ErrStandardQuorum = errors.New("need single key pair account to create standard transaction")
44 func aliasKey(name string) []byte {
45 return []byte(aliasPrefix + name)
48 //Key account store prefix
49 func Key(name string) []byte {
50 return []byte(accountPrefix + name)
53 //CPKey account control promgram store prefix
54 func CPKey(hash common.Hash) []byte {
55 return append([]byte(accountCPPrefix), hash[:]...)
58 // NewManager creates a new account manager
59 func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
61 if index := walletDB.Get([]byte(keyNextIndex)); index != nil {
62 nextIndex = uint64(binary.LittleEndian.Uint64(index))
67 utxoDB: newReserver(chain, walletDB),
68 cache: lru.New(maxAccountCache),
69 aliasCache: lru.New(maxAccountCache),
70 delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
71 acpIndexNext: nextIndex,
75 // Manager stores accounts and their associated control programs.
85 delayedACPsMu sync.Mutex
86 delayedACPs map[*txbuilder.TemplateBuilder][]*CtrlProgram
89 acpIndexNext uint64 // next acp index in our block
90 acpIndexCap uint64 // points to end of block
93 // ExpireReservations removes reservations that have expired periodically.
94 // It blocks until the context is canceled.
95 func (m *Manager) ExpireReservations(ctx context.Context, period time.Duration) {
96 ticks := time.Tick(period)
100 log.Info("Deposed, ExpireReservations exiting")
103 err := m.utxoDB.ExpireReservations(ctx)
105 log.WithField("error", err).Error("Expire reservations")
111 // Account is structure of Bytom account
112 type Account struct {
115 Tags map[string]interface{} `json:"tags,omitempty"`
118 // Create creates a new Account.
119 func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string, tags map[string]interface{}, accessToken string) (*Account, error) {
120 if existed := m.db.Get(aliasKey(alias)); existed != nil {
121 return nil, ErrDuplicateAlias
124 signer, err := signers.Create(ctx, m.db, "account", xpubs, quorum, accessToken)
126 return nil, errors.Wrap(err)
129 account := &Account{Signer: signer, Alias: alias, Tags: tags}
130 rawAccount, err := json.Marshal(account)
132 return nil, ErrMarshalAccount
134 storeBatch := m.db.NewBatch()
136 accountID := Key(signer.ID)
137 storeBatch.Set(accountID, rawAccount)
138 storeBatch.Set(aliasKey(alias), []byte(signer.ID))
144 // UpdateTags modifies the tags of the specified account. The account may be
145 // identified either by ID or Alias, but not both.
146 func (m *Manager) UpdateTags(ctx context.Context, accountInfo string, tags map[string]interface{}) error {
149 accountID := accountInfo
150 if s, err := m.FindByAlias(nil, accountInfo); err == nil {
154 rawAccount := m.db.Get(Key(accountID))
155 if rawAccount == nil {
156 return ErrFindAccount
158 if err := json.Unmarshal(rawAccount, &account); err != nil {
162 for k, v := range tags {
165 delete(account.Tags, k)
167 if account.Tags == nil {
168 account.Tags = make(map[string]interface{})
174 rawAccount, err := json.Marshal(account)
176 return ErrMarshalTags
179 m.db.Set(Key(accountID), rawAccount)
183 // FindByAlias retrieves an account's Signer record by its alias
184 func (m *Manager) FindByAlias(ctx context.Context, alias string) (*signers.Signer, error) {
186 cachedID, ok := m.aliasCache.Get(alias)
189 return m.findByID(ctx, cachedID.(string))
192 rawID := m.db.Get(aliasKey(alias))
194 return nil, ErrFindAccount
197 accountID := string(rawID)
199 m.aliasCache.Add(alias, accountID)
201 return m.findByID(ctx, accountID)
204 // findByID returns an account's Signer record by its ID.
205 func (m *Manager) findByID(ctx context.Context, id string) (*signers.Signer, error) {
207 cachedSigner, ok := m.cache.Get(id)
210 return cachedSigner.(*signers.Signer), nil
213 rawAccount := m.db.Get(Key(id))
214 if rawAccount == nil {
215 return nil, ErrFindAccount
219 if err := json.Unmarshal(rawAccount, &account); err != nil {
224 m.cache.Add(id, account.Signer)
226 return account.Signer, nil
229 // GetAliasByID return the account alias by given ID
230 func (m *Manager) GetAliasByID(id string) string {
233 rawAccount := m.db.Get(Key(id))
234 if rawAccount == nil {
235 log.Warn("fail to find account")
239 if err := json.Unmarshal(rawAccount, &account); err != nil {
247 // CreateAddress generate an address for the select account
248 func (m *Manager) CreateAddress(ctx context.Context, accountID string, change bool, expiresAt time.Time) (cp *CtrlProgram, err error) {
249 account, err := m.findByID(ctx, accountID)
254 if account.Quorum == 1 {
255 cp, err = m.createP2PKH(ctx, account, change, expiresAt)
257 cp, err = m.createP2SH(ctx, account, change, expiresAt)
263 if err = m.insertAccountControlProgram(ctx, cp); err != nil {
269 func (m *Manager) createP2PKH(ctx context.Context, account *signers.Signer, change bool, expiresAt time.Time) (*CtrlProgram, error) {
270 idx, err := m.nextIndex(ctx)
274 path := signers.Path(account, signers.AccountKeySpace, idx)
275 derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
276 derivedPK := derivedXPubs[0].PublicKey()
277 pubHash := crypto.Ripemd160(derivedPK)
279 // TODO: pass different params due to config
280 address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.MainNetParams)
285 control, err := vmutil.P2PKHSigProgram([]byte(pubHash))
291 AccountID: account.ID,
292 Address: address.EncodeAddress(),
294 ControlProgram: control,
296 ExpiresAt: expiresAt,
300 func (m *Manager) createP2SH(ctx context.Context, account *signers.Signer, change bool, expiresAt time.Time) (*CtrlProgram, error) {
301 idx, err := m.nextIndex(ctx)
306 path := signers.Path(account, signers.AccountKeySpace, idx)
307 derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
308 derivedPKs := chainkd.XPubKeys(derivedXPubs)
309 signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
313 scriptHash := crypto.Sha256(signScript)
315 // TODO: pass different params due to config
316 address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.MainNetParams)
321 control, err := vmutil.P2SHProgram(scriptHash)
327 AccountID: account.ID,
328 Address: address.EncodeAddress(),
330 ControlProgram: control,
332 ExpiresAt: expiresAt,
336 func (m *Manager) createControlProgram(ctx context.Context, accountID string, change bool, expiresAt time.Time) (*CtrlProgram, error) {
337 account, err := m.findByID(ctx, accountID)
342 idx, err := m.nextIndex(ctx)
347 path := signers.Path(account, signers.AccountKeySpace, idx)
348 derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
349 derivedPKs := chainkd.XPubKeys(derivedXPubs)
350 control, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
356 AccountID: account.ID,
358 ControlProgram: control,
360 ExpiresAt: expiresAt,
364 // CreateControlProgram creates a control program
365 // that is tied to the Account and stores it in the database.
366 func (m *Manager) CreateControlProgram(ctx context.Context, accountID string, change bool, expiresAt time.Time) ([]byte, error) {
367 cp, err := m.createControlProgram(ctx, accountID, change, expiresAt)
372 if err = m.insertAccountControlProgram(ctx, cp); err != nil {
375 return cp.ControlProgram, nil
378 //CtrlProgram is structure of account control program
379 type CtrlProgram struct {
383 ControlProgram []byte
388 func (m *Manager) insertAccountControlProgram(ctx context.Context, progs ...*CtrlProgram) error {
390 for _, prog := range progs {
391 accountCP, err := json.Marshal(prog)
396 sha3pool.Sum256(hash[:], prog.ControlProgram)
397 m.db.Set(CPKey(hash), accountCP)
402 // GetCoinbaseControlProgram will return a coinbase script
403 func (m *Manager) GetCoinbaseControlProgram(height uint64) ([]byte, error) {
404 signerIter := m.db.IteratorPrefix([]byte(accountPrefix))
405 if !signerIter.Next() {
406 log.Warningf("GetCoinbaseControlProgram: can't find any account in db")
407 return vmutil.CoinbaseProgram(nil, 0, height)
409 rawSigner := signerIter.Value()
412 signer := &signers.Signer{}
413 if err := json.Unmarshal(rawSigner, signer); err != nil {
414 log.Errorf("GetCoinbaseControlProgram: fail to unmarshal signer %v", err)
415 return vmutil.CoinbaseProgram(nil, 0, height)
418 ctx := context.Background()
419 idx, err := m.nextIndex(ctx)
421 log.Errorf("GetCoinbaseControlProgram: fail to get nextIndex %v", err)
422 return vmutil.CoinbaseProgram(nil, 0, height)
424 path := signers.Path(signer, signers.AccountKeySpace, idx)
425 derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)
426 derivedPKs := chainkd.XPubKeys(derivedXPubs)
428 script, err := vmutil.CoinbaseProgram(derivedPKs, signer.Quorum, height)
433 err = m.insertAccountControlProgram(ctx, &CtrlProgram{
434 AccountID: signer.ID,
436 ControlProgram: script,
440 log.Errorf("GetCoinbaseControlProgram: fail to insertAccountControlProgram %v", err)
445 func saveIndex(db dbm.DB, index uint64) {
446 buf := make([]byte, 8)
447 binary.LittleEndian.PutUint64(buf, index)
448 db.Set([]byte(keyNextIndex), buf)
451 func (m *Manager) nextIndex(ctx context.Context) (uint64, error) {
453 defer m.acpMu.Unlock()
457 saveIndex(m.db, m.acpIndexNext)
461 // DeleteAccount deletes the account's ID or alias matching accountInfo.
462 func (m *Manager) DeleteAccount(in struct {
463 AccountInfo string `json:"account_info"`
467 storeBatch := m.db.NewBatch()
469 accountID := in.AccountInfo
470 if s, err := m.FindByAlias(nil, in.AccountInfo); err == nil {
474 rawAccount := m.db.Get(Key(accountID))
475 if rawAccount == nil {
478 if err := json.Unmarshal(rawAccount, &account); err != nil {
482 storeBatch.Delete(aliasKey(account.Alias))
483 storeBatch.Delete(Key(account.ID))
489 type annotatedAccount struct {
490 Alias string `json:"alias"`
491 ID string `json:"id"`
492 Quorum int `json:"quorum"`
493 KeyIndex uint64 `json:"key_index"`
494 XPubs []chainkd.XPub `json:"xpubs"`
495 Tags *json.RawMessage `json:"tags"`
498 // ListAccounts will return the accounts in the db
499 func (m *Manager) ListAccounts(id string) ([]annotatedAccount, error) {
501 tmpAccount := annotatedAccount{}
502 accounts := make([]annotatedAccount, 0)
503 jsonTags := json.RawMessage(`{}`)
505 accountIter := m.db.IteratorPrefix([]byte(accountPrefix + id))
506 defer accountIter.Release()
508 for accountIter.Next() {
509 if err := json.Unmarshal(accountIter.Value(), &account); err != nil {
513 tmpAccount.Alias = account.Alias
514 tmpAccount.ID = account.ID
515 tmpAccount.Quorum = account.Quorum
516 tmpAccount.KeyIndex = account.KeyIndex
517 tmpAccount.XPubs = account.XPubs
518 if account.Tags != nil {
519 t, err := json.Marshal(account.Tags)
525 tmpAccount.Tags = &jsonTags
527 accounts = append(accounts, tmpAccount)