作者:freewind

比原项目仓库:

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

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

在前面,我们探讨了从浏览器的dashboard中进行注册的时候,数据是如何从前端发到后端的,并且后端是如何创建密钥的。而本文将继续讨论,比原是如何通过/create-account接口来创建帐户的。

在前面我们知道在API.buildHandler中配置了与创建帐户相关的接口配置:

api/api.go#L164-L244

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

可以看到,/create-account对应的handler是a.createAccount,它是我们本文将研究的重点。外面套着的jsonHandler是用来自动JSON与GO数据类型之间的转换的,之前讨论过,这里不再说。

我们先看一下a.createAccount的代码:

api/accounts.go#L15-L30

// POST /create-account
func (a *API) createAccount(ctx context.Context, ins struct {
RootXPubs []chainkd.XPub `json:"root_xpubs"`
Quorum int `json:"quorum"`
Alias string `json:"alias"`
}) Response { // 1.
acc, err := a.wallet.AccountMgr.Create(ctx, ins.RootXPubs, ins.Quorum, ins.Alias)
if err != nil {
return NewErrorResponse(err)
} // 2.
annotatedAccount := account.Annotated(acc)
log.WithField("account ID", annotatedAccount.ID).Info("Created account") // 3.
return NewSuccessResponse(annotatedAccount)
}

可以看到,它需要前端传过来root_xpubsquorumalias这三个参数,我们在之前的文章中也看到,前端也的确传了过来。这三个参数,通过jsonHandler的转换,到这个方法的时候,已经成了合适的GO类型,我们可以直接使用。

这个方法主要分成了三块:

  1. 使用a.wallet.AccountMgr.Create以及用户发送的参数去创建相应的帐户
  2. 调用account.Annotated(acc),把account对象转换成可以被JSON化的对象
  3. 向前端发回成功信息。该信息会被jsonHandler自动转为JSON发到前端,用于显示提示信息

第3步没什么好说的,我们主要把目光集中在前两步,下面将依次结合源代码详解。

创建相应的帐户

创建帐户使用的是a.wallet.AccountMgr.Create方法,先看代码:

account/accounts.go#L145-L174

// Create creates a new Account.
func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) {
m.accountMu.Lock()
defer m.accountMu.Unlock() // 1.
normalizedAlias := strings.ToLower(strings.TrimSpace(alias)) // 2.
if existed := m.db.Get(aliasKey(normalizedAlias)); existed != nil {
return nil, ErrDuplicateAlias
} // 3.
signer, err := signers.Create("account", xpubs, quorum, m.getNextAccountIndex())
id := signers.IDGenerate()
if err != nil {
return nil, errors.Wrap(err)
} // 4.
account := &Account{Signer: signer, ID: id, Alias: normalizedAlias} // 5.
rawAccount, err := json.Marshal(account)
if err != nil {
return nil, ErrMarshalAccount
} // 6.
storeBatch := m.db.NewBatch()
accountID := Key(id)
storeBatch.Set(accountID, rawAccount)
storeBatch.Set(aliasKey(normalizedAlias), []byte(id))
storeBatch.Write() return account, nil
}

我们把该方法分成了6块,这里依次讲解:

  1. 把传进来的帐户别名进行标准化修正,比如去掉两头空白并小写
  2. 从数据库中寻找该别名是否已经用过。因为帐户和别名是一一对应的,帐户创建成功后,会在数据库中把别名记录下来。所以如果能从数据库中查找,说明已经被占用,会返回一个错误信息。这样前台就可以提醒用户更换。
  3. 创建一个Signer,实际上就是对xpubsquorum等参数的正确性进行检查,没问题的话会把这些信息捆绑在一起,否则返回错误。这个Signer我感觉是检查过没问题签个字的意思。
  4. 把第3步创建的signer和id,还有前面的标准化之后的别名拿起来,放在一起,就组成了一个帐户
  5. 把帐户对象变成JSON,方便后面往数据库里存
  6. 把帐户相关的数据保存在数据库,其中别名与id对应(方便以后查询别名是否存在),id与account对象(JSON格式)对应,保存具体的信息

这几步中的第3步中涉及到的方法比较多,需要再细致分析一下:

signers.Create

blockchain/signers/signers.go#L67-L90

// Create creates and stores a Signer in the database
func Create(signerType string, xpubs []chainkd.XPub, quorum int, keyIndex uint64) (*Signer, error) {
// 1.
if len(xpubs) == 0 {
return nil, errors.Wrap(ErrNoXPubs)
} // 2.
sort.Sort(sortKeys(xpubs)) // this transforms the input slice
for i := 1; i < len(xpubs); i++ {
if bytes.Equal(xpubs[i][:], xpubs[i-1][:]) {
return nil, errors.WithDetailf(ErrDupeXPub, "duplicated key=%x", xpubs[i])
}
} // 3.
if quorum == 0 || quorum > len(xpubs) {
return nil, errors.Wrap(ErrBadQuorum)
} // 4.
return &Signer{
Type: signerType,
XPubs: xpubs,
Quorum: quorum,
KeyIndex: keyIndex,
}, nil
}

这个方法可以分成4块,主要就是检查参数是否正确,还是比较清楚的:

  1. xpubs不能为空
  2. xpubs不能有重复的。检查的时候就先排序,再看相邻的两个是否相等。我觉得这一块代码应该抽出来,比如findDuplicated这样的方法,直接放在这里太过于细节了。
  3. 检查quorum,它是意思是“所需的签名数量”,它必须小于等于xpubs的个数,但不能为0。这个参数到底有什么用这个可能已经触及到比较核心的东西,放在以后研究。
  4. 把各信息打包在一起,称之为Singer

另外,在第2处还是一个需要注意的sortKeys。它实际上对应的是type sortKeys []chainkd.XPub,为什么要这么做,而不是直接把xpubs传给sort.Sort呢?

这是因为,sort.Sort需要传进来的对象拥有以下接口:

type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}

但是xpubs是没有的。所以我们把它的类型重新定义成sortKeys后,就可以添加上这些方法了:

blockchain/signers/signers.go#L94-L96

func (s sortKeys) Len() int           { return len(s) }
func (s sortKeys) Less(i, j int) bool { return bytes.Compare(s[i][:], s[j][:]) < 0 }
func (s sortKeys) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

m.getNextAccountIndex()

然后是signers.Create("account", xpubs, quorum, m.getNextAccountIndex())中的m.getNextAccountIndex(),它的代码如下:

account/accounts.go#L119-L130

func (m *Manager) getNextAccountIndex() uint64 {
m.accIndexMu.Lock()
defer m.accIndexMu.Unlock() var nextIndex uint64 = 1
if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil {
nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
} m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex))
return nextIndex
}

从这个方法可以看出,它用于产生自增的数字。这个数字保存在数据库中,其key为accountIndexKey(常量,值为[]byte("AccountIndex")),value的值第一次为1,之后每次调用都会把它加1,返回的同时把它也保存在数据库里。这样比原程序就算重启该数字也不会丢失。

signers.IDGenerate()

上代码:

blockchain/signers/idgenerate.go#L21-L41

//IDGenerate generate signer unique id
func IDGenerate() string {
var ourEpochMS uint64 = 1496635208000
var n uint64 nowMS := uint64(time.Now().UnixNano() / 1e6)
seqIndex := uint64(nextSeqID())
seqID := uint64(seqIndex % 1024)
shardID := uint64(5) n = (nowMS - ourEpochMS) << 23
n = n | (shardID << 10)
n = n | seqID bin := make([]byte, 8)
binary.BigEndian.PutUint64(bin, n)
encodeString := base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(bin) return encodeString }

从代码中可以看到,这个算法还是相当复杂的,从注释上来看,它是要生成一个“不重复”的id。如果我们细看代码中的算法,发现它没并有和我们的密钥或者帐户有关系,所以我不太明白,如果仅仅是需要一个不重复的id,为什么不能直接使用如uuid这样的算法。另外这个算法是否有名字呢?已经提了issue向开发人员询问:https://github.com/Bytom/bytom/issues/926

现在可以回到我们的主线a.wallet.AccountMgr.Create上了。关于创建帐户的流程,上面已经基本讲了,但是还有一些地方我们还没有分析:

  1. 上面多次提到使用了数据库,那么使用的是什么数据库?在哪里进行了初始化?
  2. 这个a.wallet.AccountMgr.Create方法中对应的AccountMgr对象是在哪里构造出来的?

数据库与AccountMgr的初始化

比原在内部使用了leveldb这个数据库,从配置文件config.toml中就可以看出来:

$ cat config.toml
fast_sync = true
db_backend = "leveldb"

这是一个由Google开发的性能非常高的Key-Value型的NoSql数据库,比特币也用的是它。

比原在代码中使用它保存各种数据,比如区块、帐户等。

我们看一下,它是在哪里进行了初始化。

可以看到,在创建比原节点对象的时候,有大量的与数据库以及帐户相关的初始化操作:

node/node.go#L59-L142

func NewNode(config *cfg.Config) *Node {
// ... // Get store
coreDB := dbm.NewDB("core", config.DBBackend, config.DBDir())
store := leveldb.NewStore(coreDB) tokenDB := dbm.NewDB("accesstoken", config.DBBackend, config.DBDir())
accessTokens := accesstoken.NewStore(tokenDB) // ... txFeedDB := dbm.NewDB("txfeeds", config.DBBackend, config.DBDir())
txFeed = txfeed.NewTracker(txFeedDB, chain) // ... if !config.Wallet.Disable {
// 1.
walletDB := dbm.NewDB("wallet", config.DBBackend, config.DBDir())
// 2.
accounts = account.NewManager(walletDB, chain)
assets = asset.NewRegistry(walletDB, chain)
// 3.
wallet, err = w.NewWallet(walletDB, accounts, assets, hsm, chain)
// ...
}
// ...
}

那么我们在本文中用到的,就是这里的walletDB,在上面代码中的数字1对应的地方。

另外,AccountMgr的初始化在也这个方法中进行了。可以看到,在第2处,生成的accounts对象,就是我们前面提到的a.wallet.AccountMgr中的AccountMgr。这可以从第3处看到,accounts以参数形式传给了NewWallet生成了wallet对象,它对应的字段就是AccountMgr

然后,当Node对象启动时,它会启动web api服务:

node/node.go#L169-L180

func (n *Node) OnStart() error {
// ...
n.initAndstartApiServer()
// ...
}

initAndstartApiServer方法里,又会创建API对应的对象:

node/node.go#L161-L167

func (n *Node) initAndstartApiServer() {
n.api = api.NewAPI(n.syncManager, n.wallet, n.txfeed, n.cpuMiner, n.miningPool, n.chain, n.config, n.accessTokens)
// ...
}

可以看到,它把n.wallet对象传给了NewAPI,所以/create-account对应的handlera.createAccount中才可以使用a.wallet.AccountMgr.Create,因为这里的a指的就是api

这样的话,与创建帐户的流程及相关的对象的初始化我们就都清楚了。

Annotated(acc)

下面就回到我们的API.createAccount中的第2块代码:

    // 2.
annotatedAccount := account.Annotated(acc)
log.WithField("account ID", annotatedAccount.ID).Info("Created account")

我们来看一下account.Annotated(acc)

account/indexer.go#L27-L36

//Annotated init an annotated account object
func Annotated(a *Account) *query.AnnotatedAccount {
return &query.AnnotatedAccount{
ID: a.ID,
Alias: a.Alias,
Quorum: a.Quorum,
XPubs: a.XPubs,
KeyIndex: a.KeyIndex,
}
}

这里出现的query指的是比原项目中的一个包blockchain/query,相应的AnnotatedAccount的定义如下:

blockchain/query/annotated.go#L57-L63

type AnnotatedAccount struct {
ID string `json:"id"`
Alias string `json:"alias,omitempty"`
XPubs []chainkd.XPub `json:"xpubs"`
Quorum int `json:"quorum"`
KeyIndex uint64 `json:"key_index"`
}

可以看到,它的字段与之前我们在创建帐户过程中出现的字段都差不多,不同的是后面多了一些与json相关的注解。在后在前面的account.Annotated方法中,也是简单的把Account对象里的数字赋值给它。

为什么需要一个AnnotatedAccount呢?原因很简单,因为我们需要把这些数据传给前端。在API.createAccount的最后,第3步,会向前端返回NewSuccessResponse(annotatedAccount),由于这个值将会被jsonHandler转换成JSON,所以它需要有一些跟json相关的注解才行。

同时,我们也可以根据AnnotatedAccount的字段来了解,我们最后将会向前端返回什么样的数据。

到这里,我们已经差不多清楚了比原的/create-account是如何根据用户提交的参数来创建帐户的。

注:在阅读代码的过程中,对部分代码进行了重构,主要是从一些大方法分解出来了一些更具有描述性的小方法,以及一些变量名称的修改,增加可读性。#924

剥开比原看代码11:比原是如何通过接口/create-account创建帐户的的更多相关文章

  1. 剥开比原看代码09:通过dashboard创建密钥时,前端的数据是如何传到后端的?

    作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...

  2. 剥开比原看代码16:比原是如何通过/list-transactions显示交易信息的

    作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...

  3. 剥开比原看代码13:比原是如何通过/list-balances显示帐户余额的?

    作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...

  4. 剥开比原看代码12:比原是如何通过/create-account-receiver创建地址的?

    作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...

  5. 剥开比原看代码10:比原是如何通过/create-key接口创建密钥的

    作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...

  6. 剥开比原看代码08:比原的Dashboard是怎么做出来的?

    作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...

  7. 剥开比原看代码03:比原是如何监听p2p端口的

    作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchai ...

  8. Derek解读Bytom源码-protobuf生成比原核心代码

    作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom ...

  9. 成为Java顶尖高手要看的11本书

    成为Java顶尖高手要看的11本书 学习的最好途径就是看书",这是我自己学习并且小有了一定的积累之后的第一体会.个人认为看书有两点好处: 1.能出版出来的书一定是经过反复的思考.雕琢和审核的 ...

随机推荐

  1. linux 安装 Python

    一. 打开终端,输入:wget https://www.python.org/ftp/python/3.5.0/Python-3.5.0b4.tgz 下载完毕后 输入解压命令:tar –zxvf Py ...

  2. jfinal 项目 控制层、ORM层、AOP层,在发表之前一定要记得保存

    一.ORM简介 对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术.简单的说,ORM是通过使用描述对象和数据 ...

  3. javascript实现异步编程的4种方法

    1.回调函数. 2.事件监听 .  思路:采用事件驱动模式.任务的执行不取决于代码的顺序,而取决于某个事件是否发生 3.观察者模式 (发布/订阅模式)   代码如下: jQuery.subscribe ...

  4. webpack系统配置

    简言之,webpack 是一个模块打包器 (module bundler),能够将任何资源如 JavaScript 文件.CSS 文件.图片等打包成一个或少数文件. 为什么要用Webpack? 首先, ...

  5. 一个Java系统测试

    实验感受: 本次实验最大的感受,就是不要改代码,自己写,代码改起来真的没完没了,不知道会出现什么问题.还有就是一定要清楚自己要怎么去写,流程很重要,一个个功能去实现. 主界面 数据库 主页面代码 &l ...

  6. django模板-if标签和for标签

    在django中,标签写在{%  标签  %}中 if else标签 ①通过if进行条件判断 views.py from django.shortcuts import render def inde ...

  7. 树莓派安装cobbler,自动化安装CentOS

    安装python.相关python模块.apache sudo apt-get install python python2.7 python-django python-netaddr python ...

  8. 批处理:根据进程名称查询进程,如果有进程就输出up没有就输出donw

    需求:windows系统上  根据进程名称查询进程,如果有进程就输出 up ,没有就输出  donw. ::Final interpretation is owned by chenglee ::@e ...

  9. android之发送Get或Post请求至服务器接口

    import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; imp ...

  10. Codeforces 675E Trains and Statistic - 线段树 - 动态规划

    题目传送门 快速的vjudge通道 快速的Codeforces通道 题目大意 有$n$个火车站,第$i$个火车站出售第$i + 1$到第$a_{i}$个火车站的车票,特殊地,第$n$个火车站不出售车票 ...