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

来源:https://www.cnblogs.com/bytom/archive/2018/07/23/9356922.html
-Advertisement-
Play Games

作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在比原的dashboard中,我們可以為一個帳戶創建地址(address),這樣就可 ...


作者:freewind

比原項目倉庫:

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

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

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

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

我們可以點擊左側的"Accounts",在右邊顯示我的帳戶信息。註意右上角有一個“Create Address”鏈接:  點擊後,比原會為我當前選擇的這個帳戶生成一個地址,馬上就可以使用了:  

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

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

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

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

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

https://github.com/Bytom/dashboard/blob/0cc300fd0a9cbc52940b2d5119cf05230392a75f/src/features/accounts/components/AccountShow.jsx#L12-L132

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/dashboard/blob/674d3b1be8ec420d75f0ab0b792fd97e11ffe352/src/sdk/api/accounts.js#L3-L32

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 alias
func (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 account
func (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創建地址的”已經解決的差不多了。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、構造方法 在使用類創建對象的時候(就是類後面加括弧)就自動執行__init__方法。 Python中派生類可以繼承父類的構造方法 1.基於super() 遇到super()就表示去執行父類的xxx屬性 2.通過父類的名稱執行父類的構造方法。 這兩種方式中推薦super,使用第二中方法的時候,進行 ...
  • 原創 尋求圖中最短路徑的方法有很多,最近剛開始學習,先用深搜實現,用鄰接矩陣來存儲圖。 直接上圖上代碼: 尋求從0~4的最短路徑 利用深搜找出所有從0~4的路徑,一一比較選擇出最小的 測試數據: 輸入: 5 8 0 1 2 0 4 10 1 2 3 1 4 7 2 0 4 2 3 4 3 4 5 4 ...
  • java.io包下的File類用於描述和創建一個文件或文件夾對象,只能對文件或文件夾做一些簡單操作,不能修改文件的內容,功能比較有限。下麵是對於File類中常用方法的程式演示。 [1] 演示程式一 [2] 演示程式二 註:希望與各位讀者相互交流,共同學習進步。 ...
  • 在debug VS c工程文件時,碰到cannot convert from 'int (__cdecl *)(char *)' to 'xxx'這個問題,發現問題在於typedef函數指針類型時,將函數調用方法__cdecl寫成了_stdcall。 後來百度了兩者的區別,如下: __cdecl 是 ...
  • 需求分析 要做安全方面的內容,依靠人臉識別通過和庫中的臉比對後判定相似率來驗證用戶身份。 快速從圖片中識別出人的信息,用於尋人功能等。 其實從安全的角度出發有很多可以囊括的。比如智能家居中的刷臉開門,支付軟體的刷臉支付等都是例子。 人臉特征提取的步驟 真正的人臉識別需要很多的知識,大體上粗略的可以分 ...
  • 互聯網這股東風不久前刮到了甘涼國,國王老甘獨具慧眼,想趕緊趁著東風未停大力發展移動互聯網,因為他篤信布斯雷的理論:“站在風口上,豬都能飛起來”。無奈地方偏僻落後,國內無可用之才啊。老甘一籌莫展的低頭凝思應聲被打斷,“啟奏陛下,有四個從東土大唐來的和尚前來更換通關文牒”,聽到“東土大唐”四個字,老甘心 ...
  • 因為IDEA和Goland來自同一家非常有名的捷克公司:JetBrains 很好用,智能化程度高 最新版下載地址:http://www.jetbrains.com/go/?fromMenu 相關使用手冊:https://www.jetbrains.com/help/go/discover-golan ...
  • 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在前一篇文章中,我們試圖理解比原是如何交易的,但是由於內容太多,我們把它分成了幾個小 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...