作者:freewind
比原項目倉庫:
Github位址:
https://github.com/Bytom/bytom Gitee位址: https://gitee.com/BytomBlockchain/bytom 在前面,我們探讨了從浏覽器的dashboard中進行注冊的時候,資料是如何從前端發到後端的,并且後端是如何建立密鑰的。而本文将繼續讨論,比原是如何通過/create-account
接口來建立帳戶的。
在前面我們知道在
API.buildHandler
中配置了與建立帳戶相關的接口配置: api/api.go#L164-L244 func (a *API) buildHandler() {
// ...
if a.wallet != nil {
// ...
m.Handle("/create-account", jsonHandler(a.createAccount))
// ...
可以看到,
/create-account
對應的handler是
a.createAccount
,它是我們本文将研究的重點。外面套着的
jsonHandler
是用來自動JSON與GO資料類型之間的轉換的,之前讨論過,這裡不再說。
我們先看一下
a.createAccount
的代碼:
api/accounts.go#L15-L30// 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對象轉換成可以被JSON化的對象account.Annotated(acc)
- 向前端發回成功資訊。該資訊會被jsonHandler自動轉為JSON發到前端,用于顯示提示資訊
第3步沒什麼好說的,我們主要把目光集中在前兩步,下面将依次結合源代碼詳解。
建立相應的帳戶
建立帳戶使用的是
a.wallet.AccountMgr.Create
方法,先看代碼:
account/accounts.go#L145-L174// 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步建立的signer和id,還有前面的标準化之後的别名拿起來,放在一起,就組成了一個帳戶
- 把帳戶對象變成JSON,友善後面往資料庫裡存
- 把帳戶相關的資料儲存在資料庫,其中别名與id對應(友善以後查詢别名是否存在),id與account對象(JSON格式)對應,儲存具體的資訊
這幾步中的第3步中涉及到的方法比較多,需要再細緻分析一下:
signers.Create
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塊,主要就是檢查參數是否正确,還是比較清楚的:
- xpubs不能為空
- xpubs不能有重複的。檢查的時候就先排序,再看相鄰的兩個是否相等。我覺得這一塊代碼應該抽出來,比如
這樣的方法,直接放在這裡太過于細節了。findDuplicated
- 檢查
,它是意思是“所需的簽名數量”,它必須小于等于xpubs的個數,但不能為0。這個參數到底有什麼用這個可能已經觸及到比較核心的東西,放在以後研究。quorum
- 把各資訊打包在一起,稱之為
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-L96func (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] }
m.getNextAccountIndex()
然後是
signers.Create("account", xpubs, quorum, m.getNextAccountIndex())
中的
m.getNextAccountIndex()
,它的代碼如下:
account/accounts.go#L119-L130func (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,傳回的同時把它也儲存在資料庫裡。這樣比原程式就算重新開機該數字也不會丢失。
signers.IDGenerate()
上代碼:
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
的初始化
AccountMgr
比原在内部使用了
leveldb這個資料庫,從配置檔案
config.toml
中就可以看出來:
$ cat config.toml
fast_sync = true
db_backend = "leveldb"
這是一個由Google開發的性能非常高的Key-Value型的NoSql資料庫,比特币也用的是它。
比原在代碼中使用它儲存各種資料,比如區塊、帳戶等。
我們看一下,它是在哪裡進行了初始化。
可以看到,在建立比原節點對象的時候,有大量的與資料庫以及帳戶相關的初始化操作:
node/node.go#L59-L142func 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服務:
node/node.go#L169-L180func (n *Node) OnStart() error {
// ...
n.initAndstartApiServer()
// ...
}
在
initAndstartApiServer
方法裡,又會建立
API
對應的對象:
node/node.go#L161-L167func (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
對應的handler
a.createAccount
中才可以使用
a.wallet.AccountMgr.Create
,因為這裡的
a
指的就是
api
這樣的話,與建立帳戶的流程及相關的對象的初始化我們就都清楚了。
Annotated(acc)
下面就回到我們的
API.createAccount
中的第2塊代碼:
// 2.
annotatedAccount := account.Annotated(acc)
log.WithField("account ID", annotatedAccount.ID).Info("Created account")
我們來看一下
account.Annotated(acc)
:
account/indexer.go#L27-L36//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-L63type 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