摘要:繼續看生成地址的方法由于這個方法里傳過來的是而不是對象,所以還需要再用查一遍,然后,再調用這個私有方法創建地址該方法可以分成部分在第塊中主要關注的是返回值。
作者:freewind
比原項目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockc...
在比原的dashboard中,我們可以為一個帳戶創建地址(address),這樣就可以在兩個地址之間轉帳了。在本文,我們將結合代碼先研究一下,比原是如何創建一個地址的。
首先看看我們在dashboard中的是如何操作的。
我們可以點擊左側的"Accounts",在右邊顯示我的帳戶信息。注意右上角有一個“Create Address”鏈接:
點擊后,比原會為我當前選擇的這個帳戶生成一個地址,馬上就可以使用了:
本文我們就要研究一下這個過程是怎么實現的,分成了兩個小問題:
前端是如何向后臺接口發送請求的?
比原后臺是如何創建地址的?
前端是如何向后臺接口發送請求的?在前一篇文章中,我們也是先從前端開始,在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() }) } render() { // ... view ={lang === "zh" ? "拷貝這個地址以用于交易中:" : "Copy this address to use in a transaction:"}
{lang === "zh" ? "新建地址" : "Create address"} , ]} /> // ... } // ... } }
上面的第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塊的關注點主要在參數這塊。可以看到,這個接口可以接收2個參數account_id和account_alias,但是剛才的前端代碼中傳過來了account_alias這一個,怎么回事?
從第2塊這里可以看出,如果傳了account_alias這個參數,則會以它為準,用它去查找相應的account,再拿到相應的id。否則的話,才使用account_id當作account的id
第3塊是為accountID相應的account創建一個地址
第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塊:
直接用alias在內存緩存aliasCache里找相應的id,找到的話調用FindByID找出完整的account數據
如果cache中沒找到,則將該alias變成數據庫需要的形式,在數據庫里找id。如果找不到,報錯
找到的話,把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 }
這個方法跟前面的套路一樣,也比較清楚:
先在內存緩存cache中找,找到就直接返回。m.cache也是定義于Manager中的一個lru.Cache對象
內存緩存中沒有,就到數據庫里找,根據id找到相應的JSON格式的account對象數據
把JSON格式的數據變成Account類型的數據,也就是前面需要的
把它放到內存緩存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塊中主要關注的是返回值。方法名為CreateAddress,但是返回值或者CtrlProgram,那么Address在哪兒?實際上Address是CtrlProgram中的一個字段,所以調用者可以拿到Address
在第2塊代碼這里有一個新的發現,原來一個帳戶是可以有多個密鑰對的(提醒:在橢圓算法中一個私鑰只能有一個公鑰)。因為這里將根據該account所擁有的公鑰數量不同,調用不同的方法。如果公鑰數量為1,說明該帳戶是一個獨享帳戶(由一個密鑰管理),將調用m.createP2PKH;否則的話,說明這個帳戶由多個公鑰共同管理(可能是一個聯合帳戶),需要調用m.createP2SH。這兩個方法,返回的對象cp,指的是ControlProgram,強調了它是一種控制程序,而不是一個地址,地址Address只是它的一個字段
創建好以后,把該控制程序插入到該帳戶中
我們先看第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創建地址的”已經解決的差不多了。
雖然很遺憾在過程中遇到的與核心相關的問題,比如創建地址的細節,我們目前還沒法理解,但是我們又再一次觸及到了核心。在之前的文章中我說過,比原的核心部分是很復雜的,所以我將嘗試多種從外圍向中心的試探方式,每次只觸及核心但不深入,直到積累了足夠的知識再深入研究核心。畢竟對于一個剛接觸區塊鏈的新人來說,以自己獨立的方式來解讀比原源代碼,還是一件很有挑戰的事情。比原的開發人員已經很辛苦了,我還是盡量少麻煩他們。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24180.html
摘要:啟動直到進入所以我們首先需要知道,比原在源代碼中是如何啟動,并且一步步走進了的世界。后面省略了一些代碼,主要是用來獲取當前監聽的實際以及外網,并記錄在日志中。 比原是如何監聽p2p端口的 我們知道,在使用bytomd init --chain_id mainnet/testnet/solonet初始化比原的時候,它會根據給定的chain_id的不同,使用不同的端口(參看config/t...
摘要:如果傳的是,就會在內部使用默認的隨機數生成器生成隨機數并生成密鑰。使用的是,生成的是一個形如這樣的全球唯一的隨機數把密鑰以文件形式保存在硬盤上。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇,我們探討了從瀏覽器的dashb...
摘要:而本文將繼續討論,比原是如何通過接口來創建帳戶的。把各信息打包在一起,稱之為另外,在第處還是一個需要注意的。比原在代碼中使用它保存各種數據,比如區塊帳戶等。到這里,我們已經差不多清楚了比原的是如何根據用戶提交的參數來創建帳戶的。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://git...
摘要:作者比原項目倉庫地址地址在前一篇中,我們說到,當比原向其它節點請求區塊數據時,會發送一個把需要的區塊告訴對方,并把該信息對應的二進制數據放入對應的通道中,等待發送。這個就是真正與連接對象綁定的一個緩存區,寫入到它里面的數據,會被發送出去。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https:...
摘要:前端是如何獲取交易數據并顯示出來的我們先在比原的前端代碼庫中尋找。這過程中的推導就不再詳說,需要的話可以看前面講解比原是如何顯示余額的那篇文章。的定義是其中的值是。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇文章中,我們...
閱讀 2247·2021-11-23 09:51
閱讀 1042·2021-11-18 10:02
閱讀 3434·2021-10-13 09:49
閱讀 1262·2021-09-22 14:57
閱讀 10391·2021-08-18 10:20
閱讀 1181·2019-08-30 15:55
閱讀 2225·2019-08-29 16:06
閱讀 3232·2019-08-29 11:14