1 // Package account stores and tracks accounts within a Chain Core.
6 // stdsql "database/sql"
12 "github.com/golang/groupcache/lru"
16 "github.com/bytom/blockchain/signers"
17 "github.com/bytom/blockchain/txbuilder"
18 "github.com/bytom/crypto/ed25519/chainkd"
19 // "chain/database/pg"
20 dbm "github.com/tendermint/tmlibs/db"
21 "github.com/bytom/errors"
22 "github.com/bytom/log"
23 "github.com/bytom/protocol"
24 "github.com/bytom/protocol/vm/vmutil"
27 const maxAccountCache = 1000
30 ErrDuplicateAlias = errors.New("duplicate account alias")
31 ErrBadIdentifier = errors.New("either ID or alias must be specified, and not both")
34 func NewManager(db dbm.DB, chain *protocol.Chain/*, pinStore *pin.Store*/) *Manager {
38 utxoDB: newReserver(db, chain/*, pinStore*/),
39 // pinStore: pinStore,
40 cache: lru.New(maxAccountCache),
41 aliasCache: lru.New(maxAccountCache),
42 delayedACPs: make(map[*txbuilder.TemplateBuilder][]*controlProgram),
46 // Manager stores accounts and their associated control programs.
52 // pinStore *pin.Store
58 delayedACPsMu sync.Mutex
59 delayedACPs map[*txbuilder.TemplateBuilder][]*controlProgram
62 acpIndexNext uint64 // next acp index in our block
63 acpIndexCap uint64 // points to end of block
66 func (m *Manager) IndexAccounts(indexer Saver) {
70 // ExpireReservations removes reservations that have expired periodically.
71 // It blocks until the context is canceled.
72 func (m *Manager) ExpireReservations(ctx context.Context, period time.Duration) {
73 ticks := time.Tick(period)
77 log.Printf(ctx, "Deposed, ExpireReservations exiting")
80 err := m.utxoDB.ExpireReservations(ctx)
91 Tags map[string]interface{}
94 // Create creates a new Account.
95 func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string, tags map[string]interface{}, clientToken string) (*Account, error) {
96 signer, err := signers.Create(ctx, m.db, "account", xpubs, quorum, clientToken)
98 return nil, errors.Wrap(err)
101 tagsParam , err := tagsToNullString(tags)
108 /* aliasSQL := stdsql.NullString{
114 INSERT INTO accounts (account_id, alias, tags) VALUES ($1, $2, $3)
115 ON CONFLICT (account_id) DO UPDATE SET alias = $2, tags = $3
117 _, err = m.db.ExecContext(ctx, q, signer.ID, aliasSQL, tagsParam)
118 if pg.IsUniqueViolation(err) {
119 return nil, errors.WithDetail(ErrDuplicateAlias, "an account with the provided alias already exists")
120 } else if err != nil {
121 return nil, errors.Wrap(err)
123 account_alias := []byte(fmt.Sprintf("account_alias:%v", signer.ID))
124 account_tags := []byte(fmt.Sprintf("account_tags:%v", signer.ID))
125 m.db.Set(account_alias, []byte(alias))
126 m.db.Set(account_tags, []byte(tagsParam))
127 alias_account := []byte(fmt.Sprintf("alias_account:%v", alias))
128 m.db.Set(alias_account, []byte(signer.ID))
136 err = m.indexAnnotatedAccount(ctx, account)
138 return nil, errors.Wrap(err, "indexing annotated account")
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, id, alias *string, tags map[string]interface{}) error {
147 if (id == nil) == (alias == nil) {
148 return errors.Wrap(ErrBadIdentifier)
151 tagsParam , err := tagsToNullString(tags)
153 return errors.Wrap(err, "convert tags")
157 signer *signers.Signer
162 signer, err = m.findByID(ctx, *id)
164 return errors.Wrap(err, "get account by ID")
167 // An alias is required by indexAnnotatedAccount. The latter is a somewhat
168 // complex function, so in the interest of not making a near-duplicate,
169 // we'll satisfy its contract and provide an alias.
170 const q = `SELECT alias FROM accounts WHERE account_id = $1`
171 var a stdsql.NullString
172 err := m.db.QueryRowContext(ctx, q, *id).Scan(&a)
174 return errors.Wrap(err, "alias lookup")
179 } else { // alias is guaranteed to be not nil due to bad identifier check
181 signer, err = m.FindByAlias(ctx, aliasStr)
183 return errors.Wrap(err, "get account by alias")
190 WHERE account_id = $2
192 _, err = m.db.ExecContext(ctx, q, tagsParam, signer.ID)
194 return errors.Wrap(err, "update entry in accounts table")
197 return errors.Wrap(m.indexAnnotatedAccount(ctx, &Account{
201 }), "update account index")
205 // FindByAlias retrieves an account's Signer record by its alias
206 func (m *Manager) FindByAlias(ctx context.Context, alias string) (*signers.Signer, error) {
210 cachedID, ok := m.aliasCache.Get(alias)
213 accountID = cachedID.(string)
215 /*const q = `SELECT account_id FROM accounts WHERE alias=$1`
216 err := m.db.QueryRowContext(ctx, q, alias).Scan(&accountID)
217 if err == stdsql.ErrNoRows {
218 return nil, errors.WithDetailf(pg.ErrUserInputNotFound, "alias: %s", alias)
221 return nil, errors.Wrap(err)
223 bytez := m.db.Get([]byte(fmt.Sprintf("alias_account:%v", alias)))
224 accountID = string(bytez[:])
226 m.aliasCache.Add(alias, accountID)
229 return m.findByID(ctx, accountID)
232 // findByID returns an account's Signer record by its ID.
233 func (m *Manager) findByID(ctx context.Context, id string) (*signers.Signer, error) {
235 cached, ok := m.cache.Get(id)
238 return cached.(*signers.Signer), nil
240 account, err := signers.Find(ctx, m.db, "account", id)
245 m.cache.Add(id, account)
250 type controlProgram struct {
253 controlProgram []byte
258 func (m *Manager) createControlProgram(ctx context.Context, accountID string, change bool, expiresAt time.Time) (*controlProgram, error) {
259 account, err := m.findByID(ctx, accountID)
264 idx, err := m.nextIndex(ctx)
269 path := signers.Path(account, signers.AccountKeySpace, idx)
270 derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
271 derivedPKs := chainkd.XPubKeys(derivedXPubs)
272 control, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
276 return &controlProgram{
277 accountID: account.ID,
279 controlProgram: control,
281 expiresAt: expiresAt,
285 // CreateControlProgram creates a control program
286 // that is tied to the Account and stores it in the database.
287 func (m *Manager) CreateControlProgram(ctx context.Context, accountID string, change bool, expiresAt time.Time) ([]byte, error) {
288 cp, err := m.createControlProgram(ctx, accountID, change, expiresAt)
292 err = m.insertAccountControlProgram(ctx, cp)
296 return cp.controlProgram, nil
300 func (m *Manager) insertAccountControlProgram(ctx context.Context, progs ...*controlProgram) error {
302 INSERT INTO account_control_programs (signer_id, key_index, control_program, change, expires_at)
303 SELECT unnest($1::text[]), unnest($2::bigint[]), unnest($3::bytea[]), unnest($4::boolean[]),
304 unnest($5::timestamp with time zone[])
307 accountIDs pq.StringArray
308 keyIndexes pq.Int64Array
309 controlProgs pq.ByteaArray
311 expirations []stdsql.NullString
313 for _, p := range progs {
314 accountIDs = append(accountIDs, p.accountID)
315 keyIndexes = append(keyIndexes, int64(p.keyIndex))
316 controlProgs = append(controlProgs, p.controlProgram)
317 change = append(change, p.change)
318 expirations = append(expirations, stdsql.NullString{
319 String: p.expiresAt.Format(time.RFC3339),
320 Valid: !p.expiresAt.IsZero(),
324 // _, err := m.dbm.ExecContext(ctx, q, accountIDs, keyIndexes, controlProgs, change, pq.Array(expirations))
325 return errors.Wrap(nil)
329 func (m *Manager) nextIndex(ctx context.Context) (uint64, error) {
331 defer m.acpMu.Unlock()
333 if m.acpIndexNext >= m.acpIndexCap {
335 const incrby = 10000 // account_control_program_seq increments by 10,000
336 const q = `SELECT nextval('account_control_program_seq')`
337 err := m.db.QueryRowContext(ctx, q).Scan(&cap)
339 return 0, errors.Wrap(err, "scan")
342 m.acpIndexNext = cap - incrby*/
351 func tagsToNullString(tags map[string]interface{}) (*stdsql.NullString, error) {
355 tagsJSON, err = json.Marshal(tags)
357 return nil, errors.Wrap(err)
360 return &stdsql.NullString{String: string(tagsJSON), Valid: len(tagsJSON) > 0}, nil