做者:freewind前端
比原項目倉庫:node
Github地址:https://github.com/Bytom/bytomgit
Gitee地址:https://gitee.com/BytomBlockchain/bytomgithub
在前面,咱們探討了從瀏覽器的dashboard中進行註冊的時候,數據是如何從前端發到後端的,而且後端是如何建立密鑰的。而本文將繼續討論,比原是如何經過/create-account
接口來建立賬戶的。web
在前面咱們知道在API.buildHandler
中配置了與建立賬戶相關的接口配置:算法
func (a *API) buildHandler() { // ... if a.wallet != nil { // ... m.Handle("/create-account", jsonHandler(a.createAccount)) // ...
能夠看到,/create-account
對應的handler是a.createAccount
,它是咱們本文將研究的重點。外面套着的jsonHandler
是用來自動JSON與GO數據類型之間的轉換的,以前討論過,這裏再也不說。json
咱們先看一下a.createAccount
的代碼:後端
// POST /create-account func (a *API) createAccount(ctx context.Context, ins struct { RootXPubs []chainkd.XPub `json:"root_xpubs"` Quorum int `json:"quorum"` Alias string `json:"alias"` }) Response { // 1. acc, err := a.wallet.AccountMgr.Create(ctx, ins.RootXPubs, ins.Quorum, ins.Alias) if err != nil { return NewErrorResponse(err) } // 2. annotatedAccount := account.Annotated(acc) log.WithField("account ID", annotatedAccount.ID).Info("Created account") // 3. return NewSuccessResponse(annotatedAccount) }
能夠看到,它須要前端傳過來root_xpubs
、quorum
和alias
這三個參數,咱們在以前的文章中也看到,前端也的確傳了過來。這三個參數,經過jsonHandler
的轉換,到這個方法的時候,已經成了合適的GO類型,咱們能夠直接使用。
這個方法主要分紅了三塊:
a.wallet.AccountMgr.Create
以及用戶發送的參數去建立相應的賬戶account.Annotated(acc)
,把account對象轉換成能夠被JSON化的對象第3步沒什麼好說的,咱們主要把目光集中在前兩步,下面將依次結合源代碼詳解。
建立賬戶使用的是a.wallet.AccountMgr.Create
方法,先看代碼:
// Create creates a new Account. func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) { m.accountMu.Lock() defer m.accountMu.Unlock() // 1. normalizedAlias := strings.ToLower(strings.TrimSpace(alias)) // 2. if existed := m.db.Get(aliasKey(normalizedAlias)); existed != nil { return nil, ErrDuplicateAlias } // 3. signer, err := signers.Create("account", xpubs, quorum, m.getNextAccountIndex()) id := signers.IDGenerate() if err != nil { return nil, errors.Wrap(err) } // 4. account := &Account{Signer: signer, ID: id, Alias: normalizedAlias} // 5. rawAccount, err := json.Marshal(account) if err != nil { return nil, ErrMarshalAccount } // 6. storeBatch := m.db.NewBatch() accountID := Key(id) storeBatch.Set(accountID, rawAccount) storeBatch.Set(aliasKey(normalizedAlias), []byte(id)) storeBatch.Write() return account, nil }
咱們把該方法分紅了6塊,這裏依次講解:
Signer
,實際上就是對xpubs
、quorum
等參數的正確性進行檢查,沒問題的話會把這些信息捆綁在一塊兒,不然返回錯誤。這個Signer
我感受是檢查過沒問題簽個字的意思。這幾步中的第3步中涉及到的方法比較多,須要再細緻分析一下:
blockchain/signers/signers.go#L67-L90
// Create creates and stores a Signer in the database func Create(signerType string, xpubs []chainkd.XPub, quorum int, keyIndex uint64) (*Signer, error) { // 1. if len(xpubs) == 0 { return nil, errors.Wrap(ErrNoXPubs) } // 2. sort.Sort(sortKeys(xpubs)) // this transforms the input slice for i := 1; i < len(xpubs); i++ { if bytes.Equal(xpubs[i][:], xpubs[i-1][:]) { return nil, errors.WithDetailf(ErrDupeXPub, "duplicated key=%x", xpubs[i]) } } // 3. if quorum == 0 || quorum > len(xpubs) { return nil, errors.Wrap(ErrBadQuorum) } // 4. return &Signer{ Type: signerType, XPubs: xpubs, Quorum: quorum, KeyIndex: keyIndex, }, nil }
這個方法能夠分紅4塊,主要就是檢查參數是否正確,仍是比較清楚的:
findDuplicated
這樣的方法,直接放在這裏太過於細節了。quorum
,它是意思是「所需的簽名數量」,它必須小於等於xpubs的個數,但不能爲0。這個參數到底有什麼用這個可能已經觸及到比較核心的東西,放在之後研究。Singer
另外,在第2處仍是一個須要注意的sortKeys
。它實際上對應的是type sortKeys []chainkd.XPub
,爲何要這麼作,而不是直接把xpubs
傳給sort.Sort
呢?
這是由於,sort.Sort
須要傳進來的對象擁有如下接口:
type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) }
可是xpubs
是沒有的。因此咱們把它的類型從新定義成sortKeys
後,就能夠添加上這些方法了:
blockchain/signers/signers.go#L94-L96
func (s sortKeys) Len() int { return len(s) } func (s sortKeys) Less(i, j int) bool { return bytes.Compare(s[i][:], s[j][:]) < 0 } func (s sortKeys) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
而後是signers.Create("account", xpubs, quorum, m.getNextAccountIndex())
中的m.getNextAccountIndex()
,它的代碼以下:
func (m *Manager) getNextAccountIndex() uint64 { m.accIndexMu.Lock() defer m.accIndexMu.Unlock() var nextIndex uint64 = 1 if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil { nextIndex = common.BytesToUnit64(rawIndexBytes) + 1 } m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex)) return nextIndex }
從這個方法能夠看出,它用於產生自增的數字。這個數字保存在數據庫中,其key爲accountIndexKey
(常量,值爲[]byte("AccountIndex")
),value的值第一次爲1
,以後每次調用都會把它加1,返回的同時把它也保存在數據庫裏。這樣比原程序就算重啓該數字也不會丟失。
上代碼:
blockchain/signers/idgenerate.go#L21-L41
//IDGenerate generate signer unique id func IDGenerate() string { var ourEpochMS uint64 = 1496635208000 var n uint64 nowMS := uint64(time.Now().UnixNano() / 1e6) seqIndex := uint64(nextSeqID()) seqID := uint64(seqIndex % 1024) shardID := uint64(5) n = (nowMS - ourEpochMS) << 23 n = n | (shardID << 10) n = n | seqID bin := make([]byte, 8) binary.BigEndian.PutUint64(bin, n) encodeString := base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(bin) return encodeString }
從代碼中能夠看到,這個算法仍是至關複雜的,從註釋上來看,它是要生成一個「不重複」的id。若是咱們細看代碼中的算法,發現它沒並有和咱們的密鑰或者賬戶有關係,因此我不太明白,若是僅僅是須要一個不重複的id,爲何不能直接使用如uuid這樣的算法。另外這個算法是否有名字呢?已經提了issue向開發人員詢問:https://github.com/Bytom/bytom/issues/926
如今能夠回到咱們的主線a.wallet.AccountMgr.Create
上了。關於建立賬戶的流程,上面已經基本講了,可是還有一些地方咱們尚未分析:
a.wallet.AccountMgr.Create
方法中對應的AccountMgr
對象是在哪裏構造出來的?AccountMgr
的初始化比原在內部使用了leveldb這個數據庫,從配置文件config.toml
中就能夠看出來:
$ cat config.toml fast_sync = true db_backend = "leveldb"
這是一個由Google開發的性能很是高的Key-Value型的NoSql數據庫,比特幣也用的是它。
比原在代碼中使用它保存各類數據,好比區塊、賬戶等。
咱們看一下,它是在哪裏進行了初始化。
能夠看到,在建立比原節點對象的時候,有大量的與數據庫以及賬戶相關的初始化操做:
func NewNode(config *cfg.Config) *Node { // ... // Get store coreDB := dbm.NewDB("core", config.DBBackend, config.DBDir()) store := leveldb.NewStore(coreDB) tokenDB := dbm.NewDB("accesstoken", config.DBBackend, config.DBDir()) accessTokens := accesstoken.NewStore(tokenDB) // ... txFeedDB := dbm.NewDB("txfeeds", config.DBBackend, config.DBDir()) txFeed = txfeed.NewTracker(txFeedDB, chain) // ... if !config.Wallet.Disable { // 1. walletDB := dbm.NewDB("wallet", config.DBBackend, config.DBDir()) // 2. accounts = account.NewManager(walletDB, chain) assets = asset.NewRegistry(walletDB, chain) // 3. wallet, err = w.NewWallet(walletDB, accounts, assets, hsm, chain) // ... } // ... }
那麼咱們在本文中用到的,就是這裏的walletDB
,在上面代碼中的數字1對應的地方。
另外,AccountMgr
的初始化在也這個方法中進行了。能夠看到,在第2處,生成的accounts
對象,就是咱們前面提到的a.wallet.AccountMgr
中的AccountMgr
。這能夠從第3處看到,accounts
以參數形式傳給了NewWallet
生成了wallet
對象,它對應的字段就是AccountMgr
。
而後,當Node對象啓動時,它會啓動web api服務:
func (n *Node) OnStart() error { // ... n.initAndstartApiServer() // ... }
在initAndstartApiServer
方法裏,又會建立API
對應的對象:
func (n *Node) initAndstartApiServer() { n.api = api.NewAPI(n.syncManager, n.wallet, n.txfeed, n.cpuMiner, n.miningPool, n.chain, n.config, n.accessTokens) // ... }
能夠看到,它把n.wallet
對象傳給了NewAPI
,因此/create-account
對應的handlera.createAccount
中才可使用a.wallet.AccountMgr.Create
,由於這裏的a
指的就是api
。
這樣的話,與建立賬戶的流程及相關的對象的初始化咱們就都清楚了。
下面就回到咱們的API.createAccount
中的第2塊代碼:
// 2. annotatedAccount := account.Annotated(acc) log.WithField("account ID", annotatedAccount.ID).Info("Created account")
咱們來看一下account.Annotated(acc)
:
//Annotated init an annotated account object func Annotated(a *Account) *query.AnnotatedAccount { return &query.AnnotatedAccount{ ID: a.ID, Alias: a.Alias, Quorum: a.Quorum, XPubs: a.XPubs, KeyIndex: a.KeyIndex, } }
這裏出現的query
指的是比原項目中的一個包blockchain/query
,相應的AnnotatedAccount
的定義以下:
blockchain/query/annotated.go#L57-L63
type AnnotatedAccount struct { ID string `json:"id"` Alias string `json:"alias,omitempty"` XPubs []chainkd.XPub `json:"xpubs"` Quorum int `json:"quorum"` KeyIndex uint64 `json:"key_index"` }
能夠看到,它的字段與以前咱們在建立賬戶過程當中出現的字段都差很少,不一樣的是後面多了一些與json相關的註解。在後在前面的account.Annotated
方法中,也是簡單的把Account
對象裏的數字賦值給它。
爲何須要一個AnnotatedAccount
呢?緣由很簡單,由於咱們須要把這些數據傳給前端。在API.createAccount
的最後,第3步,會向前端返回NewSuccessResponse(annotatedAccount)
,因爲這個值將會被jsonHandler
轉換成JSON,因此它須要有一些跟json相關的註解才行。
同時,咱們也能夠根據AnnotatedAccount
的字段來了解,咱們最後將會向前端返回什麼樣的數據。
到這裏,咱們已經差很少清楚了比原的/create-account
是如何根據用戶提交的參數來建立賬戶的。
注:在閱讀代碼的過程當中,對部分代碼進行了重構,主要是從一些大方法分解出來了一些更具備描述性的小方法,以及一些變量名稱的修改,增長可讀性。#924