剝開比原看代碼11:比原是如何經過接口/create-account建立賬戶的

做者:freewind前端

比原項目倉庫:node

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

Gitee地址:https://gitee.com/BytomBlockchain/bytomgithub

在前面,咱們探討了從瀏覽器的dashboard中進行註冊的時候,數據是如何從前端發到後端的,而且後端是如何建立密鑰的。而本文將繼續討論,比原是如何經過/create-account接口來建立賬戶的。web

在前面咱們知道在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數據類型之間的轉換的,以前討論過,這裏再也不說。json

咱們先看一下a.createAccount的代碼:後端

api/accounts.go#L15-L30api

// 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_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 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塊,主要就是檢查參數是否正確,仍是比較清楚的:

  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 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上了。關於建立賬戶的流程,上面已經基本講了,可是還有一些地方咱們尚未分析:

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

數據庫與AccountMgr的初始化

比原在內部使用了leveldb這個數據庫,從配置文件config.toml中就能夠看出來:

$ cat config.toml
fast_sync = true
db_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 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

相關文章
相關標籤/搜索