源码解析:bitshares-ui的钱包和帐号管理

本文试图从一个宏观架构的层面解析bitshares-ui这个应用中的钱包和账号管理,为作者接下来实现一个通用的私钥管理器做准备。

用到的库和标准

alt

altFlux架构 的轻量级和紧凑的实现,支持ES6语法。

Flux架构是Facebook开源的一种用户界面程序架构,特点是单向数据流,核心组件包括 Actions 、 Stores 、Views 和 Dispatcher 。其中

  • Actions表示动作,可能带参数,一般由Views根据用户操作,通过dispatcher广播出来
  • Stores存储应用的数据,监听并响应dispatcher广播的、与自身相关的Actions,修改自身的数据,当修改时,广播一个 change事件
  • Views代表用户界面,从Stores拿数据,展示给用户,并在stores的change事件发生时,重新获取数据刷新界面

Flux架构的好处:

  • 相对于MVC来说,去掉了Controller,强化了数据层;
  • Stores作为各个View的统一数据来源,为各个View提供了同步的数据;
  • 对数据的修改不是直接的,分离了用户操作意图和实际的数据修改,更容易调试。

下图是一个Facebook提供的直观的Flux架构图。

![](upload://hO7SB0LlcHZTVyQUzrRCdQlddqP.png)

alt库提供了Flux架构所需要的Actions、Stores和dispatcher API。

indexedDB

indexedDB 是一个W3C建议标准,用于在浏览器中存储结构化的对象数据库,是过时标准 WebSql 的替代。

indexedDB的存储分为以下几个层次:

  • 域,浏览器为不同的域(不同的应用)存储不同的数据库集合,避免跨域数据盗用
  • 数据库,同一个域下面可以有不同名称的数据库,每个数据库有相对独立的应用目的
  • 对象商店(Object Stores),每一个数据库可以包含多个对象商店,对象商店可类比Sql数据库中的表
  • 对象,对象商店中的一个实体,可类比Sql数据库中的行

对象商店可以有不同的形式,键值对形式和对象集合形式。

indexedDBShim

由于 IndxedDB标准比较新,各个浏览器实现有差异,有些还有BUG,因此为了更好的兼容性,indexedDBShim 项目被开发出来,为各种javascript环境(不同的浏览器甚至Nodejs)提供一致的indexedDB API。 下文引用github官网的项目说明

Use a single, indexable, offline storage API across all desktop and mobile browsers and Node.js.

Even if a browser natively supports IndexedDB, you may still want to use this shim. Some native
IndexedDB implementations are very buggy. Others are missing certain features. There are also
many minor inconsistencies between different browser implementations of IndexedDB, such as how
errors are handled, how transaction timing works, how records are sorted, how cursors behave,
etc. Using this shim will ensure consistent behavior across all browsers.

indexedDBShim在使用的时候,可以强制在支持indexedDB的浏览器也用shim(websql模拟),好处是啥?也许这样更稳定更不容易出错,因为websql的实现各个浏览器都是成熟的和一致的(sqlite)。

tcomb

javascript是动态强类型语言,由于缺乏静态类型检查,写代码容易出BUG。 tcomb 是javascript的运行时类型定义和检查库,作用类似Typescript,主要区别在于,tcomb是运行时检查,Typescript是编译时检查。与tcomb类似的库有 joi ,不过joi不直接支持浏览器环境,而tcomb同时支持浏览器和nodejs。tcomb定义的数据结构有助于我们理解程序逻辑。

相关文件

本文提到的文件,都以 bitshares-ui 项目的根目录为相对目录的起点。 下文给出简表。

文件名 说明
web/app/alt-instance.js alt全局Singleton
web/app/idb-instance.js indexeddb实例封装
web/app/stores/BaseStore.js 基于alt库store的其他store的基类,一种混合编程范式
web/app/stores/WalletDb.js 钱包Store
web/app/stores/PrivateKeyStore.js 私钥Store
web/app/stores/AccountStore.js 账号Store
web/app/stores/tcomb_structs.js 各种数据结构定义

存储的层次

存储的层次从最底层(离用于使用最远),到最上层,可分为钱包备份层,Web浏览器中的数据库层和内存层。

钱包备份层

钱包备份层是备份在硬盘上的钱包文件,可以跨浏览器,跨终端导入导出使用。钱包备份文件需要使用用户的主密钥解密才能导入,解密方法,请看 使用NODEJS解密bitshares网页钱包备份文件

Web浏览器中的数据库存储

如果在某个网页钱包(例如 比特帝国Openledger )上注册或者恢复了钱包,那么钱包中的数据会存储在浏览器的数据库中,操作的接口是 indexedDB,而由于bitshares-ui的实现强制使用了 indexedDBshim的shim模拟,实际上这些数据存储在Websql里面。 当钱包应用打开时,会读取一些钱包中的账号数据,与区块链API比对,可拿到用户的名称和余额等等信息。而当增加账号或者修改账号的公钥时,会通过上文所述Flux架构及其alt实现,最终修改数据库中的信息。私钥这种敏感信息在数据库中永远是加密状态。

Web浏览器的运行内存

网页钱包的运行数据会在内存中体现,表现形式是Flux架构的各种Store。

钱包相关的数据和加密方法

与钱包相关的数据,从web浏览器的数据库层来解析比较好理解。在这一层,包括三个对象商店,分别是 wallet, privatekey和 link_accounts。

wallet

wallet商店一般包含一个对象,表示用户的钱包的基本信息

wallet的结构,可参考文件 web/app/stores/tcom_structs.js 第27到41行

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 let WalletTcomb = t.struct({
   public_name: t.Str,
   created: t.Dat,
   last_modified: t.Dat,
   backup_date: t.maybe(t.Dat),
   password_pubkey: t.Str,
   encryption_key: t.Str,
   encrypted_brainkey: t.maybe(t.Str),
   brainkey_pubkey: t.Str,
   brainkey_sequence: t.Num,
   brainkey_backup_date: t.maybe(t.Dat),
   deposit_keys: t.maybe(t.Obj),
   // password_checksum: t.Str,
   chain_id: t.Str
}, "WalletTcomb");

各字段意义如下表:

字段名称 意义
public_name 钱包名字,一般为default,用户可管理多个钱包
created 钱包创建时间
last_modified 最后修改时间
backup_date 备份时间
password_pubkey 主密钥生成的ECC公私钥对中的公钥
encryption_key 由主密钥加密的,用于加密私钥的密钥(AES密钥)
encrypted_brainkey 加密的脑钱包种子,用于生成ECC公私钥对(HD)
brainkey_pubkey 脑钱包种子生成的ECC公钥(与HD无关)
brainkey_sequence brainkey序号,下一个密钥对从这里算
brainkey_backup_date brainkey备份时间
deposit_keys 不清楚
chain_id 石墨烯区块链ID,可区分主链测试链

注:HD表示Hierarchical Deterministic, 从一个种子开始,可序列化的、确定性的生成多个私钥,可参考 这篇文章 。 一般脑钱包(brain wallet)可以用例如12个随机英文单词作为种子,因此brainkey就指种子本身。

privatekey

privatekey表示用户的各种私钥:

  • owner key, 账号拥有者私钥,可以通过这个私钥修改其他密钥设置
  • active key, 活动私钥,可以通过这个私钥签署交易广播
  • memo key, 备注私钥,可以解密交易对手发过来的备注

privatekey结构,参考同一个源文件的第43-50行:

43
44
45
46
47
48
49
50
 let PrivateKeyTcomb = t.struct({
   id: t.maybe(t.Num),
   pubkey: t.Str,
   label: t.maybe(t.Str),
   import_account_names: t.maybe(t.Arr),
   brainkey_sequence: t.maybe(t.Num),
   encrypted_key: t.Str
 }, "PrivateKeyTcomb");

最有用就是2个字段:

  • pubkey,公钥,
  • encrypted_key, 加密的私钥

encrypted_key的加密密码在哪里?答案是加密存储在wallet对象的 encryption_key字段里(见上表),而后者的加密密码是用户的主密钥。

另外:

  • brainkey_sequence 表示这个私钥的生成序列号,wallet中 brainkey_sequence为所有privatekey中最大brainkey_sequence + 1。

linked_accounts

linked_accounts表示用户的账号信息,主要包含两个字段,账号名称和区块链id。

钱包锁定和解锁

在privatekey对象中,存在一个encrypted_key,需要解密才能使用。当用户解锁钱包时,wallet对象中的encryption_key被解密,并保留在内存一段时间。 通过解密后的密码,可以解密出私钥,进而进行签名操作。当钱包锁定时,wallet对象中没有明文的密码,无法解密出私钥进行计算。

小结

本文讨论了bitshares-ui源代码中关于钱包和账号的管理方法,希望对本文读者有所帮助。

原文链接:https://blog.xiaofuxing.name/2017/05/17/bitshares_ui_wallet_and_account_management.html