剝開比原看代碼11:比原是如何通過介面/create-account建立帳戶的

來源:互聯網
上載者:User

作者:freewind

比原項目倉庫:

Github地址:https://github.com/Bytom/bytom

Gitee地址:https://gitee.com/BytomBlockc...

在前面,我們探討了從瀏覽器的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-accountfunc (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_xpubsquorumalias這三個參數,我們在之前的文章中也看到,前端也的確傳了過來。這三個參數,通過jsonHandler的轉換,到這個方法的時候,已經成了合適的GO類型,我們可以直接使用。

這個方法主要分成了三塊:

  1. 使用a.wallet.AccountMgr.Create以及使用者發送的參數去建立相應的帳戶
  2. 調用account.Annotated(acc),把account對象轉換成可以被JSON化的對象
  3. 向前端發回成功資訊。該資訊會被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塊,這裡依次講解:

  1. 把傳進來的帳號別名進行標準化修正,比如去掉兩頭空白並小寫
  2. 從資料庫中尋找該別名是否已經用過。因為帳戶和別名是一一對應的,帳戶建立成功後,會在資料庫中把別名記錄下來。所以如果能從資料庫中尋找,說明已經被佔用,會返回一個錯誤資訊。這樣前台就可以提醒使用者更換。
  3. 建立一個Signer,實際上就是對xpubsquorum等參數的正確性進行檢查,沒問題的話會把這些資訊捆綁在一起,否則返回錯誤。這個Signer我感覺是檢查過沒問題籤個字的意思。
  4. 把第3步建立的signer和id,還有前面的標準化之後的別名拿起來,放在一起,就組成了一個帳戶
  5. 把帳戶對象變成JSON,方便後面往資料庫裡存
  6. 把帳戶相關的資料儲存在資料庫,其中別名與id對應(方便以後查詢別名是否存在),id與account對象(JSON格式)對應,儲存具體的資訊

這幾步中的第3步中涉及到的方法比較多,需要再細緻分析一下:

signers.Create

blockchain/signers/signers.go#L67-L90

// Create creates and stores a Signer in the databasefunc 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塊,主要就是檢查參數是否正確,還是比較清楚的:

  1. xpubs不可為空
  2. xpubs不能有重複的。檢查的時候就先排序,再看相鄰的兩個是否相等。我覺得這一塊代碼應該抽出來,比如findDuplicated這樣的方法,直接放在這裡太過於細節了。
  3. 檢查quorum,它是意思是“所需的簽名數量”,它必須小於等於xpubs的個數,但不能為0。這個參數到底有什麼用這個可能已經觸及到比較核心的東西,放在以後研究。
  4. 把各資訊打包在一起,稱之為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] }

m.getNextAccountIndex()

然後是signers.Create("account", xpubs, quorum, m.getNextAccountIndex())中的m.getNextAccountIndex(),它的代碼如下:

account/accounts.go#L119-L130

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,返回的同時把它也儲存在資料庫裡。這樣比原程式就算重啟該數字也不會丟失。

signers.IDGenerate()

上代碼:

blockchain/signers/idgenerate.go#L21-L41

//IDGenerate generate signer unique idfunc 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上了。關於建立帳戶的流程,上面已經基本講了,但是還有一些地方我們還沒有分析:

  1. 上面多次提到使用了資料庫,那麼使用的是什麼資料庫?在哪裡進行了初始化?
  2. 這個a.wallet.AccountMgr.Create方法中對應的AccountMgr對象是在哪裡構造出來的?

資料庫與AccountMgr的初始化

比原在內部使用了leveldb這個資料庫,從設定檔config.toml中就可以看出來:

$ cat config.tomlfast_sync = truedb_backend = "leveldb"

這是一個由Google開發的效能非常高的Key-Value型的NoSql資料庫,比特幣也用的是它。

比原在代碼中使用它儲存各種資料,比如區塊、帳戶等。

我們看一下,它是在哪裡進行了初始化。

可以看到,在建立比原節點對象的時候,有大量的與資料庫以及帳戶相關的初始化操作:

node/node.go#L59-L142

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服務:

node/node.go#L169-L180

func (n *Node) OnStart() error {    // ...    n.initAndstartApiServer()    // ...}

initAndstartApiServer方法裡,又會建立API對應的對象:

node/node.go#L161-L167

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

這樣的話,與建立帳戶的流程及相關的對象的初始化我們就都清楚了。

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 objectfunc 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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.