做者:freewind前端
比原項目倉庫:git
Github地址:https://github.com/Bytom/bytomgithub
Gitee地址:https://gitee.com/BytomBlockchain/bytom算法
在比原的dashboard中,咱們能夠爲一個賬戶建立地址(address),這樣就能夠在兩個地址之間轉賬了。在本文,咱們將結合代碼先研究一下,比原是如何建立一個地址的。數據庫
首先看看咱們在dashboard中的是如何操做的。json
咱們能夠點擊左側的"Accounts",在右邊顯示個人賬戶信息。注意右上角有一個「Create Address」連接: 點擊後,比原會爲我當前選擇的這個賬戶生成一個地址,立刻就可使用了: 本文咱們就要研究一下這個過程是怎麼實現的,分紅了兩個小問題:api
在前一篇文章中,咱們也是先從前端開始,在React組件中一步步找到了使用了接口,之前發送的數據。因爲這些過程比較類似,在本文咱們就簡化了,直接給出找到的代碼。緩存
首先是頁面中的"Create Address"對應的React組件:app
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
的定義:
const accountsAPI = (client) => { return { // ... createAddress: (params, cb) => shared.create(client, '/create-account-receiver', params, {cb, skipArray: true}), // ... } }
能夠看到,它調用的比原接口是/create-account-receiver
。
而後咱們就將進入比原後臺。
在比原的代碼中,咱們能夠找到接口/create-account-receiver
對應的handler:
func (a *API) buildHandler() { // ... if a.wallet != nil { // ... m.Handle("/create-account-receiver", jsonHandler(a.createAccountReceiver))
原來是a.createAccountReceiver
。咱們繼續進去:
// 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塊,看起來仍是比較清楚:
account_id
和account_alias
,可是剛纔的前端代碼中傳過來了account_alias
這一個,怎麼回事?account_alias
這個參數,則會以它爲準,用它去查找相應的account,再拿到相應的id。不然的話,才使用account_id
看成account的idaccountID
相應的account建立一個地址jsonHandler
轉換爲JSON對象後發給前端這裏面,須要咱們關注的只有兩個方法,即第2塊中的a.wallet.AccountMgr.FindByAlias
和第3塊中的a.wallet.AccountMgr.CreateAddress
,咱們依次研究。
a.wallet.AccountMgr.FindByAlias
直接上代碼:
// 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塊:
aliasCache
裏找相應的id,找到的話調用FindByID
找出完整的account數據FindByID
找出完整的account數據上面提到的aliasCache
是定義於Manager
類型中的一個字段:
type Manager struct { // ... aliasCache *lru.Cache
lru.Cache
是由Go語言提供的,咱們就不深究了。
而後就是用到屢次的FindByID
:
// 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
對象Account
類型的數據,也就是前面須要的cache
中,以id
爲key這裏感受沒什麼說的,由於基本上在前一篇都涉及到了。
a.wallet.AccountMgr.CreateAddress
繼續看生成地址的方法:
// 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
這個私有方法建立地址:
// 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部分:
CreateAddress
,可是返回值或者CtrlProgram
,那麼Address
在哪兒?實際上Address
是CtrlProgram
中的一個字段,因此調用者能夠拿到Addressm.createP2PKH
;不然的話,說明這個賬戶由多個公鑰共同管理(多是一個聯合賬戶),須要調用m.createP2SH
。這兩個方法,返回的對象cp
,指的是ControlProgram
,強調了它是一種控制程序,而不是一個地址,地址Address
只是它的一個字段咱們先看第2塊代碼中的賬戶只有一個密鑰的狀況,所調用的方法爲createP2PKH
:
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
方法:
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
函數,很簡單:
func ContractKey(hash common.Hash) []byte { return append(contractPrefix, hash[:]...) }
其中的contractPrefix
爲常量[]byte("Contract:")
。從這個名字咱們能夠又將接觸到一個新的概念:合約(Contract),看來前面的CtrlProgram
就是一個合約,而賬戶只是合約中的一部分(是否如此,留待咱們之後驗證)
寫到這裏,我以爲此次要解決的問題「比原是如何經過/create-account-receiver
建立地址的」已經解決的差很少了。
雖然很遺憾在過程當中遇到的與核心相關的問題,好比建立地址的細節,咱們目前還無法理解,可是咱們又再一次觸及到了核心。在以前的文章中我說過,比原的核心部分是很複雜的,因此我將嘗試多種從外圍向中心的試探方式,每次只觸及核心但不深刻,直到積累了足夠的知識再深刻研究核心。畢竟對於一個剛接觸區塊鏈的新人來講,以本身獨立的方式來解讀比原源代碼,仍是一件頗有挑戰的事情。比原的開發人員已經很辛苦了,我仍是儘可能少麻煩他們。