剝開比原看代碼12:比原是如何通過/create-account-receiver建立地址的?

來源:互聯網
上載者:User

作者:freewind

比原項目倉庫:

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

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

在比原的dashboard中,我們可以為一個帳戶建立地址(address),這樣就可以在兩個地址之間轉帳了。在本文,我們將結合代碼先研究一下,比原是如何建立一個地址的。

首先看看我們在dashboard中的是如何操作的。

我們可以點擊左側的"Accounts",在右邊顯示我的帳戶資訊。注意右上方有一個“Create Address”連結:

點擊後,比原會為我當前選擇的這個帳戶產生一個地址,馬上就可以使用了:

本文我們就要研究一下這個過程是怎麼實現的,分成了兩個小問題:

  1. 前端是如何向後台介面發送請求的?
  2. 比原後台是如何建立地址的?

前端是如何向後台介面發送請求的?

在前一篇文章中,我們也是先從前端開始,在React組件中一步步找到了使用了介面,以前發送的資料。由於這些過程比較相似,在本文我們就簡化了,直接給出找到的代碼。

首先是頁面中的"Create Address"對應的React組件:

https://github.com/Bytom/dash...

class AccountShow extends BaseShow {  // ...  // 2.   createAddress() {    // ...    // 3.     this.props.createAddress({      account_alias: this.props.item.alias    }).then(({data}) => {      this.listAddress()      this.props.showModal(<div>        <p>{lang === 'zh' ? '拷貝這個地址以用於交易中:' : 'Copy this address to use in a transaction:'}</p>        <CopyableBlock value={data.address} lang={lang}/>      </div>)    })  }  render() {      // ...      view =         <PageTitle          title={title}          actions={[            // 1.            <button className='btn btn-link' onClick={this.createAddress}>              {lang === 'zh' ? '建立地址' : 'Create address'}            </button>,          ]}        />       // ...    }    // ...  }}

上面的第1處就是"Create Address"連結對應的代碼,它實際上是一個Button,當點擊後,會調用createAddress方法。而第2處就是這個createAddress方法,在它裡面的第3處,又將調用this.props.createAddress,也就是由外部傳進來的createAddress函數。同時,它還要發送一個參數account_alias,它對應就是當前帳戶的alias。

繼續可以找到createAddress的定義:

https://github.com/Bytom/dash...

const accountsAPI = (client) => {  return {    // ...    createAddress: (params, cb) => shared.create(client, '/create-account-receiver', params, {cb, skipArray: true}),    // ...  }}

可以看到,它調用的比原介面是/create-account-receiver

然後我們就將進入比原後台。

比原後台是如何建立地址的?

在比原的代碼中,我們可以找到介面/create-account-receiver對應的handler:

api/api.go#L164-L174

func (a *API) buildHandler() {    // ...    if a.wallet != nil {        // ...        m.Handle("/create-account-receiver", jsonHandler(a.createAccountReceiver))

原來是a.createAccountReceiver。我們繼續進去:

api/receivers.go#L9-L32

// 1.func (a *API) createAccountReceiver(ctx context.Context, ins struct {    AccountID    string `json:"account_id"`    AccountAlias string `json:"account_alias"`}) Response {    // 2.    accountID := ins.AccountID    if ins.AccountAlias != "" {        account, err := a.wallet.AccountMgr.FindByAlias(ctx, ins.AccountAlias)        if err != nil {            return NewErrorResponse(err)        }        accountID = account.ID    }    // 3.    program, err := a.wallet.AccountMgr.CreateAddress(ctx, accountID, false)    if err != nil {        return NewErrorResponse(err)    }    // 4.     return NewSuccessResponse(&txbuilder.Receiver{        ControlProgram: program.ControlProgram,        Address:        program.Address,    })}

方法中的代碼可以分成4塊,看起來還是比較清楚:

  1. 第1塊的關注點主要在參數這塊。可以看到,這個介面可以接收2個參數account_idaccount_alias,但是剛才的前端代碼中傳過來了account_alias這一個,怎麼回事?
  2. 從第2塊這裡可以看出,如果傳了account_alias這個參數,則會以它為準,用它去尋找相應的account,再拿到相應的id。否則的話,才使用account_id當作account的id
  3. 第3塊是為accountID相應的account建立一個地址
  4. 第4塊返回成功資訊,經由外面的jsonHandler轉換為JSON對象後發給前端

這裡面,需要我們關注的只有兩個方法,即第2塊中的a.wallet.AccountMgr.FindByAlias和第3塊中的a.wallet.AccountMgr.CreateAddress,我們依次研究。

a.wallet.AccountMgr.FindByAlias

直接上代碼:

account/accounts.go#L176-L195

// FindByAlias retrieves an account's Signer record by its aliasfunc (m *Manager) FindByAlias(ctx context.Context, alias string) (*Account, error) {    // 1.     m.cacheMu.Lock()    cachedID, ok := m.aliasCache.Get(alias)    m.cacheMu.Unlock()    if ok {        return m.FindByID(ctx, cachedID.(string))    }    // 2.     rawID := m.db.Get(aliasKey(alias))    if rawID == nil {        return nil, ErrFindAccount    }    // 3.    accountID := string(rawID)    m.cacheMu.Lock()    m.aliasCache.Add(alias, accountID)    m.cacheMu.Unlock()    return m.FindByID(ctx, accountID)}

該方法的結構同樣比較簡單,分成了3塊:

  1. 直接用alias在記憶體緩衝aliasCache裡找相應的id,找到的話調用FindByID找出完整的account資料
  2. 如果cache中沒找到,則將該alias變成資料庫需要的形式,在資料庫裡找id。如果找不到,報錯
  3. 找到的話,把alias和id放在記憶體cache中,以備後用,同時調用FindByID找出完整的account資料

上面提到的aliasCache是定義於Manager類型中的一個欄位:

account/accounts.go#L78-L85

type Manager struct {    // ...    aliasCache *lru.Cache

lru.Cache是由Go語言提供的,我們就不深究了。

然後就是用到多次的FindByID

account/accounts.go#L197-L220

// FindByID returns an account's Signer record by its ID.func (m *Manager) FindByID(ctx context.Context, id string) (*Account, error) {    // 1.     m.cacheMu.Lock()    cachedAccount, ok := m.cache.Get(id)    m.cacheMu.Unlock()    if ok {        return cachedAccount.(*Account), nil    }    // 2.    rawAccount := m.db.Get(Key(id))    if rawAccount == nil {        return nil, ErrFindAccount    }    // 3.    account := &Account{}    if err := json.Unmarshal(rawAccount, account); err != nil {        return nil, err    }    // 4.    m.cacheMu.Lock()    m.cache.Add(id, account)    m.cacheMu.Unlock()    return account, nil}

這個方法跟前面的套路一樣,也比較清楚:

  1. 先在記憶體緩衝cache中找,找到就直接返回。m.cache也是定義於Manager中的一個lru.Cache對象
  2. 記憶體緩衝中沒有,就到資料庫裡找,根據id找到相應的JSON格式的account對象資料
  3. 把JSON格式的資料變成Account類型的資料,也就是前面需要的
  4. 把它放到記憶體緩衝cache中,以id為key

這裡感覺沒什麼說的,因為基本上在前一篇都涉及到了。

a.wallet.AccountMgr.CreateAddress

繼續看產生地址的方法:

account/accounts.go#L239-L246

// CreateAddress generate an address for the select accountfunc (m *Manager) CreateAddress(ctx context.Context, accountID string, change bool) (cp *CtrlProgram, err error) {    account, err := m.FindByID(ctx, accountID)    if err != nil {        return nil, err    }    return m.createAddress(ctx, account, change)}

由於這個方法裡傳過來的是accountID而不是account對象,所以還需要再用FindByID查一遍,然後,再調用createAddress這個私人方法建立地址:

account/accounts.go#L248-L263

// 1.func (m *Manager) createAddress(ctx context.Context, account *Account, change bool) (cp *CtrlProgram, err error) {    // 2.     if len(account.XPubs) == 1 {        cp, err = m.createP2PKH(ctx, account, change)    } else {        cp, err = m.createP2SH(ctx, account, change)    }    if err != nil {        return nil, err    }    // 3.    if err = m.insertAccountControlProgram(ctx, cp); err != nil {        return nil, err    }    return cp, nil}

該方法可以分成3部分:

  1. 在第1塊中主要關注的是傳回值。方法名為CreateAddress,但是傳回值或者CtrlProgram,那麼Address在哪兒?實際上AddressCtrlProgram中的一個欄位,所以調用者可以拿到Address
  2. 在第2塊代碼這裡有一個新的發現,原來一個帳戶是可以有多個金鑰組的(提醒:在橢圓演算法中一個私密金鑰只能有一個公開金鑰)。因為這裡將根據該account所擁有的公開金鑰數量不同,調用不同的方法。如果公開金鑰數量為1,說明該帳戶是一個獨享帳戶(由一個密鑰管理),將調用m.createP2PKH;否則的話,說明這個帳戶由多個公開金鑰共同管理(可能是一個聯合帳戶),需要調用m.createP2SH。這兩個方法,返回的對象cp,指的是ControlProgram,強調了它是一種控製程序,而不是一個地址,地址Address只是它的一個欄位
  3. 建立好以後,把該控製程序插入到該帳戶中

我們先看第2塊代碼中的帳戶只有一個密鑰的情況,所調用的方法為createP2PKH

account/accounts.go#L265-L290

func (m *Manager) createP2PKH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {    idx := m.getNextContractIndex(account.ID)    path := signers.Path(account.Signer, signers.AccountKeySpace, idx)    derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)    derivedPK := derivedXPubs[0].PublicKey()    pubHash := crypto.Ripemd160(derivedPK)    // TODO: pass different params due to config    address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)    if err != nil {        return nil, err    }    control, err := vmutil.P2WPKHProgram([]byte(pubHash))    if err != nil {        return nil, err    }    return &CtrlProgram{        AccountID:      account.ID,        Address:        address.EncodeAddress(),        KeyIndex:       idx,        ControlProgram: control,        Change:         change,    }, nil}

不好意思,這個方法的代碼一看我就搞不定了,看起來是觸及到了比較比原鏈中比較核心的地方。我們很難通過這幾行代碼以及快速的查閱來對它進行合理的解釋,所以本篇只能跳過,以後再專門研究。同樣,m.createP2SH也是一樣的,我們也先跳過。我們早晚要把這一塊解決的,請等待。

我們繼續看第3塊中m.insertAccountControlProgram方法:

account/accounts.go#L332-L344

func (m *Manager) insertAccountControlProgram(ctx context.Context, progs ...*CtrlProgram) error {    var hash common.Hash    for _, prog := range progs {        accountCP, err := json.Marshal(prog)        if err != nil {            return err        }        sha3pool.Sum256(hash[:], prog.ControlProgram)        m.db.Set(ContractKey(hash), accountCP)    }    return nil}

這個方法看起來就容易多了,主要是把前面建立好的CtrlProgram傳過來,對它進行儲存資料庫的操作。注意這個方法的第2個參數是...*CtrlProgram,它是一個可變參數,不過在本文中用到的時候,只傳了一個值(在其它使用的地方有傳入多個的)。

在方法中,對progs進行變數,對其中的每一個,都先把它轉換成JSON格式,然後再對它進行摘要,最後通過ContractKey函數給摘要加一個Contract:的首碼,放在資料庫中。這裡的m.db在之前文章中分析過,它就是那個名為wallet的leveldb資料庫。這個資料庫的Key挺雜的,儲存了各種類型的資料,以首碼區分。

我們看一下ContractKey函數,很簡單:

account/accounts.go#L57-L59

func ContractKey(hash common.Hash) []byte {    return append(contractPrefix, hash[:]...)}

其中的contractPrefix為常量[]byte("Contract:")。從這個名字我們可以又將接觸到一個新的概念:合約(Contract),看來前面的CtrlProgram就是一個合約,而帳戶只是合約中的一部分(是否如此,留待我們以後驗證)

寫到這裡,我覺得這次要解決的問題“比原是如何通過/create-account-receiver建立地址的”已經解決的差不多了。

雖然很遺憾在過程中遇到的與核心相關的問題,比如建立地址的細節,我們目前還沒法理解,但是我們又再一次觸及到了核心。在之前的文章中我說過,比原的核心部分是很複雜的,所以我將嘗試多種從外圍向中心的試探方式,每次只觸及核心但不深入,直到積累了足夠的知識再深入研究核心。畢竟對於一個剛接觸區塊鏈的新人來說,以自己獨立的方式來解讀比原原始碼,還是一件很有挑戰的事情。比原的開發人員已經很辛苦了,我還是盡量少麻煩他們。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.