剥开比原看代码12:比原是如何通过/create-account-receiver创建地址的?
作者:freewind
比原项目仓库:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockchain/bytom
在比原的dashboard中,我们可以为一个帐户创建地址(address),这样就可以在两个地址之间转帐了。在本文,我们将结合代码先研究一下,比原是如何创建一个地址的。
首先看看我们在dashboard中的是如何操作的。
我们可以点击左侧的"Accounts",在右边显示我的帐户信息。注意右上角有一个“Create Address”链接: 点击后,比原会为我当前选择的这个帐户生成一个地址,马上就可以使用了:
本文我们就要研究一下这个过程是怎么实现的,分成了两个小问题:
- 前端是如何向后台接口发送请求的?
- 比原后台是如何创建地址的?
前端是如何向后台接口发送请求的?
在前一篇文章中,我们也是先从前端开始,在React组件中一步步找到了使用了接口,以前发送的数据。由于这些过程比较相似,在本文我们就简化了,直接给出找到的代码。
首先是页面中的"Create Address"对应的React组件:
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块,看起来还是比较清楚:
- 第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
直接上代码:
// 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
类型中的一个字段:
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
对象 - 内存缓存中没有,就到数据库里找,根据id找到相应的JSON格式的account对象数据
- 把JSON格式的数据变成
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部分:
- 在第1块中主要关注的是返回值。方法名为
CreateAddress
,但是返回值或者CtrlProgram
,那么Address
在哪儿?实际上Address
是CtrlProgram
中的一个字段,所以调用者可以拿到Address - 在第2块代码这里有一个新的发现,原来一个帐户是可以有多个密钥对的(提醒:在椭圆算法中一个私钥只能有一个公钥)。因为这里将根据该account所拥有的公钥数量不同,调用不同的方法。如果公钥数量为1,说明该帐户是一个独享帐户(由一个密钥管理),将调用
m.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
创建地址的”已经解决的差不多了。
虽然很遗憾在过程中遇到的与核心相关的问题,比如创建地址的细节,我们目前还没法理解,但是我们又再一次触及到了核心。在之前的文章中我说过,比原的核心部分是很复杂的,所以我将尝试多种从外围向中心的试探方式,每次只触及核心但不深入,直到积累了足够的知识再深入研究核心。毕竟对于一个刚接触区块链的新人来说,以自己独立的方式来解读比原源代码,还是一件很有挑战的事情。比原的开发人员已经很辛苦了,我还是尽量少麻烦他们。
剥开比原看代码12:比原是如何通过/create-account-receiver创建地址的?的更多相关文章
- 剥开比原看代码16:比原是如何通过/list-transactions显示交易信息的
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...
- 剥开比原看代码13:比原是如何通过/list-balances显示帐户余额的?
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...
- 剥开比原看代码11:比原是如何通过接口/create-account创建帐户的
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...
- 剥开比原看代码09:通过dashboard创建密钥时,前端的数据是如何传到后端的?
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...
- 剥开比原看代码08:比原的Dashboard是怎么做出来的?
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...
- 剥开比原看代码03:比原是如何监听p2p端口的
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...
- 剥开比原看代码10:比原是如何通过/create-key接口创建密钥的
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...
- Derek解读Bytom源码-protobuf生成比原核心代码
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom ...
- Java多线程系列--“JUC原子类”03之 AtomicLongArray原子类
概要 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray这3个数组类型的原子类的原理和用法相似.本章以AtomicLongArray对数 ...
随机推荐
- ReactiveObjC
简介: RAC 指的就是 RactiveCocoa ,是 Github 的一个开源框架,能够帮我们提供大量方便的事件处理方案,让我们更简单粗暴地去处理事件,现在分为 ReactiveObjC 和 Re ...
- 简单的图像显著性区域特征提取方法-----opencv实现LC,AC,FT
https://blog.csdn.net/cai13160674275/article/details/72991049?locationNum=7&fps=1 四种简单的图像显著性区域特征 ...
- Caused by: java.sql.SQLException: Field 'category_id' doesn't have a default value
### The error may involve com.qingmu.core.dao.ad.ContentDao.insertSelective-Inline ### The error occ ...
- Python文件读写、StringIO和BytesIO
1 IO的含义 在计算机中,IO是Input/Output的简写,也就是输入和输出. 由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘.网络等,就 ...
- git小技巧--如何从其他分支merge个别文件或文件夹
在实际工作中,一个大型的项目或版本迭代可能不是一次上线,可能会分好几次上线,这时候就会涉及创建多个分支,进行分别开发. 创建分支 功能分为2个分支,分别为A.B. A上面有个列表页功能 B上面有个详情 ...
- Program terminated with signal 6, Aborted,有可能啥原因呢?
Program terminated with signal 6, Aborted,有可能啥原因呢?其中一种原因就是事实上的OOM(虽然/var/log/message中没有标明操作系统kill了进行 ...
- Eureka-zookeeper的服务发现替代方案
参考: https://my.oschina.net/thinwonton/blog/1622905 http://www.open-open.com/lib/view/open14269407225 ...
- NPM安装步骤
一.使用之前,我们先来掌握3个东西是用来干什么的. npm: Nodejs下的包管理器. webpack: 它主要的用途是通过CommonJS的语法把所有浏览器端需要发布的静态资源做相应的准备,比如资 ...
- 火车时刻表WebApp
关键词 :Ajax 跨域访问 php 同源策略 JQueryMobile 前言 在面试的过程中,兄弟连的徐老师提出要求我用JQuery Mobile(前端框架)来实现一个具有“火车时刻表”功能的Web ...
- 12: nginx原理及常用配置
1.1 nginx基本介绍 1.nginx高并发原理( 多进程+epoll实现高并发 ) 1. Nginx 在启动后,会有一个 master 进程和多个相互独立的 worker 进程. 2. 每个子进 ...