汪晓明对区块链、以太坊的思考

记录创业、生活的所思所感,探讨去中心化思想,推动区块链的发展。

HPB62:HPB开发节点搭建指南

导读: 社区开发者在开发HPB DAPP的时候首先面临的就是如何接入HPB主网的问题,本文介绍了开发者节点的搭建过程,本文基于Ubuntu 16.04搭建,开发节点相当于轻节点,可发起交易,进行区块同步,但不能进入候选节点、高性能节点的选举,也不能出块,可作为DAPP应用的接入节点。

进行开发节点搭建时您有以下两种方式选择:

(1) 方式一:通过源代码进行节点搭建,选择此种搭建方式需要具备一定软件编程基础。了解编译过程。本方式需要首先完成GO编译环境安装,然后按照源代码搭建步骤及示例进行;

(2) 方式二:通过HPB可执行文件进行节点搭建。本方式直接按照可执行文件搭建步骤及示例进行。

注意:

1.HPB程序必须以ROOT权限运行。

2. 任何与账户密码相关的操作不要委托他人操作,以免密码泄露。

3. 启动节点必须用户本人操作以防止密码泄露。

1. 检查网络连接

进行节点搭建前,用户需要检查服务器的网络连接,分别输入以下五个命令,以检测服务器连接bootnode的过程中是否存在延迟、丢包现象。

编号 命令 节点位置
1 ping -c 200 47.254.133.46 德国 |
2 ping -c 200 47.94.20.30 北京 |
3 ping -c 200 47.88.60.227 硅谷 |
4 ping -c 200 47.75.213.166 香港 |
5 ping -c 200 47.100.250.120 上海 |

示例:(仅以编号1为例,其余命令用户均需执行)

输入命令:ping 47.254.133.46后,等命令结束后控制台会输出总结信息,其中“200 packets transmitted”表示发送200次包,“186 received”表示接受186次包,“7% packet loss”表示连接过程中丢包比例为7%;“time 199386ms”表示200次连接总耗时199836ms,“rtt min/avg/max/mdev = 230.439/248.901/290.203/9.397 ms”表示200次连接中最短时间为234.439ms,平均时间为248.901ms,最长时间为290.203ms,平均方差为9.397ms。

提示:如服务器位置与节点位置为相同洲,则丢包现象需为0%、延迟小于100ms才能达标;(如国内服务器连北京或上海节点时,丢包现象应为0%);当为跨洲连接时,丢包现象一般存在,延迟一般不超过300ms,但其达标数值很难界定,当用户对其丢包比例以及延迟现象不确定时,可询问HPB社区工作人员这两项是否达标。

不达标的用户需联系网络服务提供商或者数据中心,解决网络问题。

2. 源代码搭建示例

(1) 步骤1确定程序执行路径

         输入**sudo mkdir** */home/ghpb-bin*创建程序执行路径;

​ 其中/home/ghpb-bin可改为指定路径

​ 切换成ROOT用户,根据提示输入ROOT账户密码;

(2) 步骤2 选择下载路径

    输入 **cd** */home/*;其中/home/可改为指定路径;       

(3) 步骤3 下载HPB主网可执行程序

    输入 **sudo git clone** *https://github.com/hpb-project/hpb-release*

​ 提示:如果提示go-hpb已存在,则输入命令:rm -rf go-hpb后再下载go-hpb源码。

(4) 步骤4 查看HPB主网可执行程序

         输入**cd** *hpb-release/*进入hpb-release目录,输入ls命令查看该目录文件,可看到bin、config和README.md三个文件。

(5) 步骤5 拷贝创世文件到执行路径

         输入**cd** *config/*进入config目录,继续输入**sudo** **cp** *gensis.json /home/ghpb-bin/**;*其中/hone/gphb-bin/为您所设置的程序执行路径。

(6) 步骤6 进入下载路径

         输入**cd** */home/*将源码下到home目录下,其中/home/可改为源码下载路径

索引:HPB源代码

(7) 步骤7编译go-hpb

     输入**cd** *go-hpb/*;继续输入**make all**编译go-hpb;

(8) 步骤8拷贝程序到执行路径

         输入**sudo cp** *build/bin/\* /home/ghpb-bin/*即可;
         其中/home/ghpb-bin/为您设置的程序执行路径;   

(9) 步骤9 初始化节点

         输入**cd** */home/ghpb-bin/*进入程序执行路径;继续输入**sudo** *./ghpb* **--datadir** *node/data* **init** *gensis.json*,当出现”Successfully wrote genesis state database=chaindata”时,继续下一步;其中/home/ghpb-bin/为您设置的程序执行路径; 

(10) 步骤10导出账户

      从HPB钱包导出您的账户信息文件,输入cd node/data/后继续输入mkdir keystore即可,输入ls可看到ghpb和keystore两个文件;

​ 按顺序进入路径/home/ghpb-bin/node/data/keystore,将账户信息文件拷入keystore文件夹中;

​ 提示:如果没有权限进入这个目录,输入命令 chmod 777 /home/ghpb-bin -R,之后再重新进入。

(11) 步骤11 新建账户

          输入 ./ghpb --datadir node/data account new,稍等片刻根据提示设置新账户的密码,重复输入后将返回新账户地址,用户需记录该地址;

(12) 步骤12启动节点

        **启动方式一**:输入**cd** */home/ghpb-bin/*进入ghpb-bin目录,继续输入
1
sudo ./ghpb --datadir node/data  --networkid 100 --port 3004 --syncmode full  --nodetype synnode console;

​ 当出现“Welcome to the GHPB JavaScript console!”信息时,节点启动成功。

(13) 提示:节点间测试带宽的端口号为本地ghpb端口号加100;开发节点防火墙中本地端口(如3004)必须打开,测试带宽端口可不打开,选择启动方式一时,如果用户退出远程服务器或者关掉终端,节点程序将停止运行。

启动方式二:输入cd /home/ghpb-bin/进入ghpb-bin目录后,继续输入

1
sudo nohup ./ghpb --datadir node/data --networkid 100   --verbosity 3 --syncmode full --rpc --rpcapi hpb,web3,admin,txpool,debug,personal,net,miner,prometheus  --nodetype synnode  &

然后,按两次回车;

提示:节点间测试带宽的端口号为本地ghpb端口号加100;开发节点防火墙中本地端口(默认为30303)必须打开,测试带宽端口可不打开,等待15s后继续输入命令:

1
sudo *./ghpb* attach http://127.0.0.1:8545

3. 可执行文件搭建示例

(1) 步骤1确定程序执行路径

             输入**sudo mkdir** */home/ghpb-bin*创建程序执行路径;其中/home/ghpb-bin可改为指定路径

切换成ROOT用户:输入su root;根据提示输入ROOT账户密码;

(2) 步骤2 选择下载路径

          输入 **cd** */home/*;其中/home/可改为指定路径;

(3) 步骤3 下载HPB主网可执行程序

          输入**sudo git clone** <https://github.com/hpb-project/hpb-release>

如果提示hpb-release已存在,则输入命令:rm -rf hpb-release后再下载hpb-release文件

(4) 步骤4 查看HPB主网可执行程序

           输入**cd** *hpb-release/*进入hpb-release目录,输入ls命令查看该目录文件,可看到bin、config和README.md三个文件。

(5) 步骤5 拷贝创世文件到执行路径

            输入**cd** *config/*进入config目录,继续输入**sudo** **cp** gensis.json  /home/ghpb-bin/,其中/hone/gphb-bin/为您所设置的程序执行路径。

(6) 步骤6 进入下载路径

​ 输入cd .. ,继续输入cd bin/,进入到bin目录,然后解压HPB主网程序

​ 输入sudo tar zxvf ghpb-vx.x.x.x.tar.gz 命令解压ghpb-vx.x.x.x.tar.gz文件,其中x.x.x.x为HPB软件的最新版本号

(7) 步骤7修改文件权限:

​ 输入 sudo chmod +x ghpb-v0.0.0.1 -R

(8) 步骤8 拷贝程序到执行路径

​ 输入sudo cp ghpb-vX.X.X.X/* /home/ghpb-bin/,其中/home/ghpb-bin/为您设置的程序执行路径;

(9) 步骤9 初始化节点

            输入**cd** */home/ghpb-bin/*进入程序执行路径;继续输入**sudo** *./ghpb* **--datadir** *node/data* **init** *gensis.json*,当出现”Successfully wrote genesis state database=chaindata”时,继续下一步;其中/home/ghpb-bin/为您设置的程序执行路径;

(10) 步骤10 导出账户

              从HPB钱包导出您的账户信息文件,创建keystore,输入cd node/data/后继续输入mkdir keystore即可;

​ 输入ls可看到ghpb和keystore两个文件;

(11) 步骤11导入节点

        按顺序进入路径/home/ghpb-bin/node/data/keystore,将账户信息文件拷入keystore文件夹中;

​ 提示:如果没有权限进入这个目录,输入命令 chmod 777 /home/ghpb-bin -R,之后再重新进入.

(12) 步骤12 新建账户

            输入*./ghpb* **--datadir** *node/data* **account new**,稍等片刻根据提示设置新账户的密码,重复输入后将返回新账户地址,用户需记录该地址;

(13) 步骤13启动节点

​ 启动方式一:输入cd /home/ghpb-bin/进入ghpb-bin目录;

​ 继续输入

1
sudo ./ghpb --datadir node/data   --networkid 100 --port 3004 --syncmode full --nodetype synnode console;

当出现“Welcome to the GHPB JavaScript console!”信息时,节点启动成功。

提示:节点间测试带宽的端口号为本地ghpb端口号加100;开发节点防火墙中本地端口(如3004)必须打开,测试带宽端口可不打开,选择启动方式一时,如果用户退出远程服务器或者关掉终端,节点程序将停止运行。

​ 启动方式二:输入cd /home/ghpb-bin/进入ghpb-bin目录;

继续输入

1
sudo nohup ./ghpb --datadir node/data --networkid 100  --verbosity 3 --syncmode full --rpc --rpcapi hpb,web3,admin,txpool,debug,personal,net,miner,prometheus  --nodetype synnode  &

然后,按两次回车;

提示:节点间测试带宽的端口号为本地ghpb端口号加100;开发节点防火墙中本地端口(默认为30303)必须打开,测试带宽端口可不打开,等待10s后继续输入命令:

1
sudo ./ghpb attach http://127.0.0.1:8545

我们创立了HPB的技术讨论专栏,如有任何技术问题,请访问HPB Talk

感谢HPB技术团队整理。

HPB61:HPB BOE 版卡功能介绍

BOE功能介绍

BOE(Blockchain Offload Engine)系统是区块链卸载引擎的缩写,利用硬件FPGA的并发处理能力对区块链节点上的交易、区块等处理过程进行加速。共识算法也与BOE的伪随机序列深度结合,以保证网络内节点能够快速、稳定、安全的运行。下面分别从交易验签和伪随机序列两个方面详细介绍BOE的工作流程。

1 交易验签

在HPB中,从整体上看交易的处理流程有下面几步:

  • 从客户端收到交易数据

  • 由BOE恢复公钥,得到发送者的公钥

  • 根据公钥计算发送者的账户地址

  • 根据账户余额和Nonce验证交易的合法性

  • 交易合法则放入交易池等待打包入块

  • 入块后交易完成

    flow1

​ 图1 交易流程图

其中第二步恢复公钥为BOE的主要工作,恢复公钥的主要流程:

  • 主程序将交易数据打包成网络包
  • BOE从网卡接收数据
  • 协议模块进行协议解析
  • 根据运算类型放入对应的任务队列
  • 任务调度模块将任务从队列中放到 ECC运算模块
  • 重组数据包,将结果发给网卡
  • 主程序接收到BOE返回的数据,解析后得到公钥

​ 图2 BOE框图

2 共识算法随机序列

HPB的共识算法(Prometheus)与BOE的结合之处在于每一次的出块节点是通过BOE产生的伪随机序列来指定的.

其主要过程如下:

  • 各节点准备生成区块
  • 从前一个块头中获取Hash序列
  • 根据当前的Hash送给BOE计算得到NewHash
  • 根据NewHash与当前快照中的节点总数取余,得到本轮次的出块节点索引
  • 如果本节点并不是上面计算得到的出块节点,那么将延时一段时间
  • 如果本节点是上面计算得到的出块节点,那么马上生成区块并广播出去
  • 其他节点同步到区块后,重复执行步骤 2-4 验证该区块的生成者是否为选出的出块节点

flow3

​ 图3 BOE出块流程

其中计算随机数是BOE的主要工作,计算随机数的主要流程:

  • 主程序从块头中获取当前的序列Hash,组成网络包发给BOE
  • BOE从网卡接收数据
  • 协议模块进行协议解析
  • 根据运算类型放入对应的任务队列
  • 任务调度模块将任务从队列中放到 随机序列运算模块
  • 重组数据包,将结果发给网卡
  • 主程序接收到BOE返回的数据,解析后得到NewHash

1548774223928

​ 图4 BOE工作框图

我们创立了HPB的技术讨论专栏,如有任何技术问题,请访问HPB Talk

感谢HPB技术团队整理。

HPB60:HPB共识算法选举机制描述

简介

为了适应BOE技术的要求,同时尽可能的提升安全TPS, HPB共识算法采用了高效的双层选举机制,即外层选举和内层选举。

外层选举:由具有BOE板卡的所有用户中选取出。选取周期为3个月。选取方式为通过HPB钱包进行投票选举。

内层选举:采用节点贡献值评价指标,从众多候选节点中选出高贡献值节点成员。基于Hash队列记名投票机制,在每次区块生成时,计算高贡献值节点生成区块的优先级,优先级高的高贡献值节点享有优先生成区块的权利。

在整个共识算法设计中, HPB共识算法的轻量级消息交换机制使其在共识效率上远高于其他共识算法, 同时在安全性,隐私性等方面也做了较大幅度的提升。

外层选举

由于在申请BOE板卡时已经考虑了除用户持币量的其他因素,所以外层选举在钱包投票节点主要考虑持币量的因素,以及用户的认可度,用户认可度高,则用户会对他进行投票,反之则投票数量少。

注1:如果具有BOE的用户较多,会存在一定数量的用户在外层选举中落选。落选的用户仍然可以入网,但是不会作为候选节点或者高性能节点,并且也不会收到奖励。

内层选举

本文重点关注内层选举。将会详细描述内层选举的实现方式和关键因素。

选举包括2部分,首先是投票节点,然后是唱票阶段。

关键因素

内层选举阶段发生在网络运行中的特定阶段,每间隔一定数量的区块进行一次选举,并且根据关键因素进行排名,选择优秀的节点进行投票。

节点带宽:在节点运行期间,节点会定期测试与其他节点间的带宽数据,并保存在节点中,提供给共识使用,节点的带宽数据会记录在区块中。

用户持币量:用户在投票时的持币数量。

投票数量:外层选举过程中用户节点获得的投票数量。

投票规则

节点根据外层选举得出的节点数据为集合,根据关键因素的排名加权选择最优节点,进行投票。为了保证选举结果数量,在投票过程中进行了随机化操作,并不是在所有的外层选举集合内进行最有选择,而是随机选择出特定数量的集合,然后选择最优节点投票。这个目的是保证选举结果的数量达到稳定值,保证网络的稳定性,并且排除节点排名较靠后的节点。

唱票

已经有了投票的基础数据后,在特定阶段环节进行唱票工作,所有节点在唱票阶段通过读取区块进行唱票,将区块中所有的投票数据提取出来进行统计。在投票阶段会将投票的数据写入voteIndex。

唱票规则

将获得投票的所有节点作为一个集合,从中选出特定数量的节点,作为下一轮的高性能节点。因此需要对该结合中的节点进行排名操作,排名的依据是voteIndex的均值。

最后

在注2中可以看到几个与次文关系较密切的字段,分别是candAddress,miner以及voteIndex。

miner:产生区块的节点。

candAddress:是由miner根据内层选举的3个关键因素确定的。

voteIndex:是candAddress所对应的因素加权结果值。

从投票到唱票的所有环节,所有节点均存在校验机制,确保节点无法进行虚假投票和唱票,如果投票伪造,则该区块会被其他节点拒收;如果唱票伪造,则会被其他节点踢出网络。

注2:区块部分内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
**candAddress: “0x4a8111ecec1f9150d366ae319d0585303085748f”**,
comdAddress: “0x4a8111ecec1f9150d366ae319d0585303085748f”,
difficulty: 2,
extraData: “0x00000000000…”,
gasLimit: 100000000,
gasUsed: 0,
hardwareRandom: “0x2bce19ff44fbf1b05edfb93fcb7c7d3ab04c50fd7dc947e2fce65d66493d0dff”,
hash: “0x1fceb9c0d5a822fdddaa72bb9378f5ce24cd168b0281082a83d8f3a00c62d79a”,
logsBloom: “0x00000000000…”,
**miner: “0x4a8111ecec1f9150d366ae319d0585303085748f”**,
mixHash: “0x0000000000000000000000000000000000000000000000000000000000000000”,
nonce: “0x0000000000000000”,
number: 100,
parentHash: “0xab7299002317fecbfdd835b28bacb470e24a7933b41102f75cb76491957baa98”,
receiptsRoot: “0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421”,
sha3Uncles: “0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347”,
size: 683,
stateRoot: “0xc46fc99654813b2f92e9be58f7e69957499fa2c9b1c0ac31d1da86679f3b9a62”,
timestamp: 1541416798,
totalDifficulty: 201,
transactions: [],
transactionsRoot: “0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421”,
uncles: [],
**voteIndex: "0x0"**
}

具体投票机制可能根据最新的需求会调整,请及时参考最新的代码

我么创立了HPB的技术讨论专栏,如有任何技术问题,请访问HPB Talk

感谢HPB技术团队整理。

HPB59:HPB 是什么

创新之处

HPB芯链采用全新的区块链软硬件体系架构,结合高性能 区块链开源硬件(BOE)以及软件,实现了区块链分布式 应用的性能扩展,在保证安全性和去中心化等特性的情况 下实现了高TPS和低延迟。HPB芯链的共识算法采用双层 选举机制验证交易,在选举制和邀请制之间达成平衡。

开源公链

HPB芯链作为开源公链,公众可自由调用其智能合约,贡 献数据,以及使用该公链平台。HPB芯链致力于建设一个 被全世界广泛接受并采用的DApp生态系统,与产业深度 结合,以满足现实世界的真实商业需求。

区块链专用硬件-区块链卸载引擎(BOE)

HPB自主研发的区块链卸载引擎(BOE),引领区块链技 术走向了更广泛的应用,重新定义了区块链行业的技术要 求。HPB芯链节点网络通过使用BOE硬件和专用高规格服 务器,提升了区块链的交易处理速度;而BOE硬件设计与 区块链技术的无缝融合,也极大地加强了区块链性能及其 安全性。

HPB节点分布和共识算法

BOE硬件构成了HPB节点网络的基石,节点由社区投票和 HPB基金会邀请组成,70%的节点由投票决定,24%的节 点采取邀请制(其中包含非营利组织、非政府组织及 Dapps开发者等),6%的节点由基金会维护。 HPB采用动态共识算法,在节点轮换时考虑不同的变量。 节点网络最初预计将在150个BOE节点上运行,负责生成 区块和验证交易。

节点收益

节点收益分为2部分(1/3奖励 和 2/3奖励)

1/3部分:当选BOE节点: 收益根据投票阶段 所获得的票数比例分配;受邀节点:受邀节点无法参与 投票,因此没有此 部分收益分配。

2/3部分:所有BOE节点 35%收益用于负责出块的高性能节点 65%收益由其他所有候选节点平分。

我么创立了HPB的技术讨论专栏,如有任何技术问题,请访问HPB Talk

感谢HPB技术团队整理。

HPB-Wallet:HPB钱包助记词生成和备份

助记词生成

1 BIP32, BIP39, BIP44

  • BIP32:定义 Hierarchical Deterministic wallet (简称 “HD Wallet”),是一个系统可以从单一个 seed 产生一树状结构储存多组 keypairs(私钥和公钥)。好处是可以方便的备份、转移到其他相容装置(因为都只需要 seed),以及分层的权限控制等
  • BIP39:将 seed 用方便记忆和书写的单字表示。一般由 12 个单词组成,称为 mnemonic code(phrase),中文称为助记词或助记码。例如:

rose rocket invest real refuse margin festival danger anger border idle brown

  • BIP44:基于 BIP32 的系统,赋予树状结构中的各层特殊的意义。让同一个 seed 可以支持多币种、多帐户等。各层定义如下:

m / purpose' / coin_type' / account' / change / address_index

其中的 purporse’ 固定是 44’,代表使用 BIP44。而 coin_type’ 用来表示不同币种,例如 Bitcoin 就是 0’,Ethereum 是 60’。

2 助记词生成

HPB Wallet目前使用的BIP39,将64位私钥变化为12个单词的形式便于记忆。

打开BIP39.swift文件,可以通过调用下面方法,随机生成一个助记词:

1
2
3
4
5
6
static public func generateMnemonics(bitsOfEntropy: Int, language: BIP39Language = BIP39Language.english) throws -> String? {
        guard bitsOfEntropy >= 128 && bitsOfEntropy <= 256 && bitsOfEntropy % 32 == 0 else {return nil}
        guard let entropy = Data.randomBytes(length: bitsOfEntropy/8) else {throw AbstractKeystoreError.noEntropyError}
        return BIP39.generateMnemonicsFromEntropy(entropy: entropy, language: language)
        
    }     

可以根据助记词去获取seed,然后通过sha256得到明文私钥

1
2
 let seed = BIP39.seedFromMmemonics(mnemonic, language: BIP39Language.english)
 let privateKey = seed.sha256()

2.1 助记词备份

拥有助记词就可以掌控这个账户,因此助记词生成后,要提醒用户去备份助记词。对于去中心化的APP,用户备份后要从本地删除,用户备份之前可以加密存放在本地。

作者:感谢HPB Wallet 开发团队整理供稿

HPB-Wallet:HPB钱包基本概念

1 基本概念

1.1 什么是账户?

一个钱包地址就代表着一个账户。地址是账户的标识。地址表示的是该账户公钥的后20字节(通常会以0x开头,例如,0xed37f755e56b1d49642dce8ff2b788ae33263c94`)。每个账户都由一对钥匙定义,一个私钥(Private Key)和一个公钥(Public Key)。 账户以地址为索引,地址由公钥衍生而来,取公钥的最后20个字节。

1.2 私钥、公钥和地址

  1. 先生成一个私钥,由随机的256bit组成。
  2. 使用加密算法椭圆曲线签名算法elliptic curve cryptography将私钥映射生成公钥。一个私钥只能映射出一个公钥。
  3. 用公钥低位的160bit通过SHA-3加密hash算法计算得到地址。

1.3 Keystore文件

Keystore文件是JSON格式的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{  
   "address":"0xed37f755e56b1d49642dce8ff2b788ae33263c94",
   "crypto":{  
      "cipher":"aes-128-ctr",
      "ciphertext":"41c14f88ec8f35c9fe57cd39121a76c2dadbd82ea8fec59866468bc0d7371f2e",
      "cipherparams":{  
         "iv":"43443bf394e8f6ebcc687e13bc0effb9"
      },
      "kdf":"scrypt",
      "kdfparams":{  
         "dklen":32,
         "n":262144,
         "p":1,
         "r":8,
         "salt":"aaef6847d09cb1e9f5ceadaf5865d96a7493df1cae146b24e31092cc0a7844af"
      },
      "mac":"5e9781c587db5795c6d41cb4f001bf086cc3db33b6e7eefcc2ef472145e76821"
   },
   "id":"bcd61a88-283f-4d81-8457-30ec9c11521f",
   "version":3
}

通过keystore文件中的内容,我们可以看到其中包括了私钥加密的相关信息:

  • address:该账户的地址
  • cipher:加密方法使用的是AES-128-CTR算法4
  • ciphertext:加密后的密文
  • cipherparams:AES-128-CTR算法加密所需的相关参数
  • kdf:秘钥生成函数,用于使用密码对keystore文件进行加密
  • kdfparams:kdf算法所需的参数
  • mac:用于验证密码的编码

2 创建代码

2.1 创建EthereumKeystoreV3对象

在web3siwft中找到EthereumKeystoreV3.swift文件,创建EthereumKeystoreV3对象。构造函数生成对象:

1
2
3
4
5
6
//随机生成私钥
public init? (password: String = "BANKEXFOUNDATION") 
 
//指定私钥   
public init? (privateKey: Data, password: String = "BANKEXFOUNDATION") 
   

在构造方法中会调用encryptDataToStorage方法通过ECC去给keystoreParams赋值。

2.2 创建kstore文件并存入本地

对EthereumKeystoreV3的keystoreParams属性进行编码并存在本地。

具体代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static func generateKstoreFileBy(_ privateKey: Data,password: String,complete: ((String,String)->Void)?) 
   -> WalletManagerResult{
    
    guard let ethereumKeystore = try? EthereumKeystoreV3(privateKey: privateKey, password: password),let ks = ethereumKeystore else{
        return WalletManagerResult(false,"生成Keystore文件出错")
    }
    guard  let keydata = try? JSONEncoder().encode(ks.keystoreParams)
        else{
        return WalletManagerResult(false,"获取Keystore文件出错")
    }
    guard let adress = ks.getAddress() else{
       return WalletManagerResult(false,"获取Keystore文件出错")
    }
    //创建普通的keystore文件
    let filename = HPBFileManager.generateFileName(address: adress.addressData)
    if FileManager.default.createFile(atPath: HPBFileManager.getKstoreDirectory() + filename, contents: keydata, attributes: nil){
        complete?(filename,adress.address.lowercased())
        return WalletManagerResult(true,nil)
    }else{
        return  WalletManagerResult(false,"生成Keystore文件出错")
    }
}

到此,一个新的账户就创建成功了,keystore文件就是加密的账户私钥,需要配合密码使用。

下一节我们会讲解助记词生成和备份。

作者:感谢HPB Wallet 开发团队整理供稿

HPB-Wallet:HPB钱包Xcode集成

1 集成说明

由于HPB主网账户底层算法和以太坊相同,因此我们可以采用以太坊的底层算法对接HPB。熟悉以太坊的朋友都知道,以太坊提供了一个Web3.js API 中文文档的库,通过调用js的代码去实现区块链DApp的开发。

但是对于iOS的小伙伴来说,直接调用js无疑是很困难的,并且从实现上来说也很复杂。因此我们找到了一个swift代码实现的web3swift的库,通过这个我们可以使用swift语言去开发应用,这让实现变得简单。接下来,我们就以这个库为基础,详细介绍钱包的开发过程。

2 Xcode工程设置

2.1 开发环境

  • 支持iOS 9.0以上系统
  • 使用Xcode 9或更高版本
  • swift4.1开发语言

2.2 Xcode工程设置

库文件的导入使用cocoapods,详细安装步骤,请点击

1. 创建Podfile
1
touch Podfile

创建Podfile

2. 使用CocoaPods 安装 web3swift

目前HPB Wallet采用 web3swift 0.7.0版本,后续也会以该版本作为基础进行说明。

1
2
3
4
5
6
platform :ios, '9.0'

target '<Your Target Name>' do
    use_frameworks!
    pod 'web3swift', '~> 0.7.0'
end

pod

在Podfile所在的文件夹下输入命令:

1
pod install 
3. 导入成功,启动工程

在使用的地方导入头文件。

导入

2.3使用CocoaPods的问题

  • pod search无法搜索到类库的解决办法(找不到类库)

(1)执行pod setup

(2)删除~/Library/Caches/CocoaPods目录下的search_index.json文件

1
2
3
pod setup成功后会生成~/Library/Caches/CocoaPods/search_index.json文件。
终端输入rm ~/Library/Caches/CocoaPods/search_index.json
删除成功后再执行pod search

(3)执行pod search

安装好了web3swift库,接下来我们会讲解如何创建钱包。

作者:感谢HPB Wallet 开发团队整理供稿

HPB-Wallet:HPB 钱包简介

1 去中心化钱包基本概念

1.1 简介

我们生活中常用的钱包(支付宝,微信,银行卡等)是由中心化的机构发放,并进行资产管理,我们密码丢失,可以通过相关证明去找回密码。同样的,区块链交易所(bibBox,火币,OKEx等)也是中心化的,由交易所保存我们的私钥。使用交易所非常方便快捷,但其实不建议把大量的数字资产长期保存在交易所中,因为中心化交易所拥有大量的数字代币,容易成为黑客的重点攻击对象。因此我们就需要一款去中心化的区块链钱包保存我们的资产。HPB Wallet 就是一款去中心化的钱包。

1.2 钱包名词

区块链钱包一般包含这几个名词:公钥、私钥、助记词、keystore、密码。

1.2.1 地址

生成区块链钱包开发后会生成一个以 0x 开头的 42 位字符串,这个字符串就是区块链钱包地址。

1.2.2 私钥

这个私钥属于明文私钥,由 64 位字符串组成,一个钱包只有一个私钥且不能修改。谁拥有私钥(不需要原密码)就拥有这个钱包的掌控权。因此,创建完钱包要及时备份私钥,并妥善保存。

1.2.3 助记词

私钥一般太难记忆了,使用也不方便,所以从钱包设计的角度,就为简化操作同时不丢失安全性,就出现了助记词的方法。助记词是明文私钥的另一种表现形式,具有和私钥同样的功能,在导入区块链钱包中,输入助记词并设置一个密码(不用输入原密码)就拥有这个钱包的掌控权,因此,助记词和明文私钥同等重要。

1.2.4 keystore

keystore 属于加密私钥,和区块链钱包密码有很大关联,钱包密码修改后,keystore 也就相应变化,在用 keystore 导入钱包时,需要输入原密码。

1.3 优缺点

1.3.1 交易所的弊端

数字资产交易所未来是否会引入监管还存在极大的不确定性。最重要的一点是人们在交易所上存放的数字资产本质上并不掌握所有权,人们只是把数字资产转到了交易所指定的账号上,由交易所代为管理。所以交易所完全有可能直接用这些数字资产去做短期套利。交易所由于缺乏监管,理论上完全有可能携款跑路,并且极易受黑客攻击。

1.3.2 去中心化钱包

前面讲到了,区块链交易所越来越多的安全事件,让大家意识到使用去中心化钱包的重要性。所谓去中心化钱包,就是创建钱包的过程在本地生成,钱包服务商不保存我们的私钥,所以,相比中心化交易所不会出现交易所被攻击,自己资产丢失的情况。钱包安全程度与你私钥保管有很大的关系。所以,这里要提醒大家,如果使用去中心化的钱包,千万不要将私钥在手机上截图,或通过邮箱、QQ等方式传送,也不要放在云盘等云服务器,建议断网保存。大家可以在一张纸上写下私钥,然后妥善保管。

2 HPB Wallet钱包介绍

HPB Wallet 就是一款去中心化的钱包,可用于存储HPB资产。目前,HPB Wallet提供了用户创建,导入账号,存储HPB资产,转账,交易记录查询,映射,投票等功能。

2.1 钱包核心功能介绍

HPB Wallet 可以在首页查看当前的HPB资产,以及收款和转账功能

转账交易

HPB Wallet 提供用户管理钱包的功能,用户可以创建多个钱包,也可以导入钱包。 钱包管理

HPB Wallet 提供便捷的方式,让以太坊的HPB搬家,并且可以参与投票选出心目中的高性能节点 未命名_meitu_3.jpg

2.2 HPB Wallet的优势

HPB Wallet 基于HPB高性能主链,转账近乎秒级确认,这在业内是很具挑战性的突破。HPB Wallet 让交易更加便捷。

基本概念掌握清楚了,那么接下来的文章我们会讲解如何在HPB主网上开发钱包。

作者:感谢HPB Wallet 开发团队整理供稿

HPB58:以太坊数据结构与存储分析

一.概述

在以太坊中,数据的存储大致分为三个部分,分别是:状态数据、区块链和底层数据。

其中,底层数据存放以太坊中全部数据,存储形式是[k,v]键值对,目前使用数据库是LevelDB;所有与交易,操作相关的数据,都存储在链上;StateDB 是用来管理账户的,每个账户都是一个 stateObject。

二.区块部分

区块结构:

区块链是以太坊的核心之一,所有交易以及结构都存于一个个区块中,接下来我们看看以太坊中的区块结构是怎样的。

以太坊中所有结构定义基本都可以在 core/types 中找到,区块结构定义在 block.go

中:

受比特币区块链数据结构的影响,我以为 block 可以简单地分为 head 和 body 两部分。但读了以太坊源码后,我发现 Ethereum 不是这样设计的。在 block.go 中,定义了以太坊区块链的区块结构为:

可以看到 body+head!=block。除此之外,block.go 文件中还定义了许多结构,比如用于

协议的 extblock,用于存储的 storageblock 等。

可以看到,header 结构用的非常频繁,接下来看看 header 结构是如何定义的:

Header 成员变量的解释如下:

  • ParentHash:指向父区块(parentBlock)的指针。除了创世块(Genesis Block)外,每个区块有且只有一个父区块。
  • Coinbase:挖掘出这个区块的矿工地址。用于发放奖励。
  • UncleHash:Block 结构体的成员 uncles 的哈希值。注意这个字段是一个 Header 数
  • 组。
  • Root:StateDB 中的“state Trie”的根节点的哈希值。Block 中,每个账户以 stateObject 对象表示,账户以 Address 为唯一标示,其信息在相关交易(Transaction)的执行中被修改。所有账户对象可以逐个插入一个 Merkle-PatricaTrie(MPT)结构里,形成“state
  • Trie”。
  • TxHash: Block 中 “tx Trie”的根节点的哈希值。Block 的成员变量 transactions 中所有的
  • tx 对象,被逐个插入一个 MPT 结构,形成“tx Trie”。
  • ReceiptHash:Block 中的 “Receipt Trie”的根节点的哈希值。Block 的所有 Transaction 执行完后会生成一个 Receipt 数组,这个数组中的所有 Receipt 被逐个插入一个 MPT 结构中,形成”Receipt Trie”。(比特币区块中有一颗用于存放交易的梅克尔树,而以太坊中有不同功能的三棵 M-P 树)
  • Bloom:Bloom 过滤器(Filter),用来快速判断一个参数 Log 对象是否存在于一组已知的 Log 集合中。
  • Difficulty:区块的难度。Block 的 Difficulty 由共识算法基于 parentBlock 的 Time 和Difficulty 计算得出,它会应用在区块的‘挖掘’阶段。
  • Number:区块的序号。Block 的 Number 等于其父区块 Number +1。
  • Time:区块“应该”被创建的时间。由共识算法确定,一般来说,要么等于parentBlock.Time + 10s,要么等于当前系统时间。
  • GasLimit:区块内所有 Gas 消耗的理论上限。该数值在区块创建时设置,与父区块有关。具体来说,根据父区块的 GasUsed 同 GasLimit * 2/3 的大小关系来计算得出。
  • GasUsed:区块内所有 Transaction 执行时所实际消耗的 Gas 总和。
  • Nonce:一个 64bit 的哈希数,用于“挖矿”。

区块存储:

区块的数据最终都是以[k,v]的形式存储在数据库中,数据库在主机中的存储位置为:

datadir/geth/chaindata。具体代码在 core/database_util.go 中。区块在储存是,head 和

body 是分开存储的。

以 writeHeader 为例:

在存储 header 时,内容部分的 key 为:前缀+num(uint64 big endian)+hash;value 是区块

头的 rlp 编码。其余的模块存储方式类似:

各种前缀也都定义在 database_util.go 文件中,值得注意的是,在此文件中还有用于只存储区块头的一套函数,应该是为提高以太坊的灵活性。

三.交易部分

交易部分的内容比较多,已写在专门的一个文档,请查看上一篇内容。

四.Merkle-PatriciaTrie

Merkle-PatriciaTrie 简介:

Ethereum 使用的 Merkle-PatriciaTrie(MPT)结构,源自于 Trie 结构,又分别继承了

PatriciaTrie 和 MerkleTree 的优点,并基于内部数据的特性,设计了全新的节点体系和插入载入机制。

Trie,又称为字典树或者前缀树(prefix tree),属于查找树的一种。它与平衡二叉树的主要不同点包括:每个节点数据所携带的 key 不会存储在 Trie 的节点中,而是通过该节点在整个树形结构里位置来体现;同一个父节点的子节点,共享该父节点的 key 作为它们各自 key 的前缀,因此根节点 key 为空;待存储的数据只存于叶子节点中,非叶子节点帮助形成叶子节点 key 的前缀。下图来自 wiki-Trie,展示了一个简单的 Trie 结构。

PatriciaTrie,又被称为 RadixTree 或紧凑前缀树(compact prefix tree),是一种空间使用率经过优化的 Trie。与 Trie 不同的是,PatriciaTrie 里如果存在一个父节点只有一个子节点, 那么这个父节点将与其子节点合并。这样可以缩短 Trie 中不必要的深度,大大加快搜索节点速度。

MerkleTree,也叫哈希树(hash tree),是密码学的一个概念,注意理论上它不一定是 Trie。在哈希树中,叶子节点的标签是它所关联数据块的哈希值,而非叶子节点的标签是它的所有子节点的标签拼接而成字符串的哈希值。哈希树的优势在于,它能够对大量的数据内容迅速作出高效且安全的验证。假设一个 hash tree 中有 n 个叶子节点,如果想要验证其中一个叶子节点是否正确-即该节点数据属于源数据集合并且数据本身完整,所需哈希计算的时间复杂度是是 O(log(n)),相比之下 hash list 大约需要时间复杂度 O(n)的哈希计算,hash

tree 的表现无疑是优秀的。

上图来自 wiki-MerkleTree,展示了一个简单的二叉哈希树。四个有效数据块 L1-L4, 分别被关联到一个叶子节点上。Hash0-0 和 Hash0-1 分别等于数据块 L1 和 L2 的哈希值, 而 Hash0 则等于 Hash0-0 和 Hash0-1 二者拼接成的新字符串的哈希值,依次类推,根节点的标签 topHash 等于 Hash0 和 Hash1 二者拼接成的新字符串的哈希值。比特币在存储交易时就用的此种数据结构。

实现部分

MPT 的相关代码在 tire 文件夹中。

Node.go 中定义了四种类型的节点,分别是:

其中,nodeFlag 是用于在创建/修改节点是存放缓存数据的。

fullNode 是一个可以携带多个子节点的父(枝)节点。它有一个容量为 17 的 node 数组成员变量 Children,数组中前 16 个空位分别对应 16 进制(hex)下的 0-9a-f,这样对于每个子节点,根据其 key 值 16 进制形式下的第一位的值,就可挂载到 Children 数组的某个位置,fullNode 本身不再需要额外 key 变量;Children 数组的第 17 位,留给该 fullNode 的数据部分。fullNode 明显继承了原生 trie 的特点,而每个父节点最多拥有 16 个分支也包含了基于总体效率的考量。

shortNode 是一个仅有一个子节点的父(枝)节点。它的成员变量 Val 指向一个子节点,而成员 Key 是一个任意长度的字符串(字节数组[]byte)。显然 shortNode 的设计体现了PatriciaTrie 的特点,通过合并只有一个子节点的父节点和其子节点来缩短 trie 的深度,结果就是有些节点会有长度更长的 key。

valueNode 充当 MPT 的叶子节点。它其实是字节数组[]byte 的一个别名,不带子节点。在使用中,valueNode 就是所携带数据部分的 RLP 哈希值,长度 32byte,数据的 RLP 编码值作为 valueNode 的匹配项存储在数据库里。

这三种类型覆盖了一个普通 Trie(也许是 PatriciaTrie)的所有节点需求。任何一个[k,v]类型数据被插入一个 MPT 时,会以 k 字符串为路径沿着 root 向下延伸,在此次插入结束时首先成为一个 valueNode,k 会以自顶点 root 起到到该节点止的 key path 形式存在。但之后随着其他节点的不断插入和删除,根据 MPT 结构的要求,原有节点可能会变化成其他node 实现类型,同时 MPT 中也会不断裂变或者合并出新的(父)节点。比如:

假设一个 shortNode S 已经有一个子节点 A,现在要新插入一个子节点 B,那么会有两种可能,要么新节点 B 沿着 A 的路径继续向下,这样 S 的子节点会被更新;要么 S 的Key 分裂成两段,前一段分配给 S 作为新的 Key,同时裂变出一个新的 fullNode 作为 S 的子节点,以同时容纳 B,以及需要更新的 A。

如果一个 fullNode 原本只有两个子节点,现在要删除其中一个子节点,那么这个fullNode 就会退化为 shortNode,同时保留的子节点如果是 shortNode,还可以跟它再合并。

如果一个 shortNode 的子节点是叶子节点同时又被删除了,那么这个 shortNode 就会退化成一个 valueNode,成为一个叶子节点。诸如此类的情形还有很多,提前设想过这些案例,才能正确实现 MPT 的插入/删除/查找等操作。当然,所有查找树(search tree)结构的操作,免不了用到递归。

hashNode 跟 valueNode 一样,也是字符数组[]byte 的一个别名,同样存放 32byte 的哈希值,也没有子节点。不同的是,hashNode 是 fullNode 或者 shortNode 对象的 RLP 哈希值,所以它跟 valueNode 在使用上有着莫大的不同。

在 MPT 中,hashNode 几乎不会单独存在(有时遍历遇到一个 hashNode 往往因为原本的 node 被折叠了),而是以 nodeFlag 结构体的成员(nodeFlag.hash)的形式,被 fullNode 和shortNode 间接持有。一旦 fullNode 或 shortNode 的成员变量(包括子结构)发生任何变化,它们的 hashNode 就一定需要更新。所以在 trie.Trie 结构体的 insert(),delete()等函数实现中,可以看到除了新创建的 fullNode、shortNode,那些子结构有所改变的 fullNode、shortNode 的 nodeFlag 成员也会被重设,hashNode 会被清空。在下次 trie.Hash()调用时,整个 MPT 自底向上的遍历过程中,所有清空的 hashNode 会被重新赋值。这样trie.Hash()结束后,我们可以得到一个根节点 root 的 hashNode,它就是此时此刻这个 MPT结构的哈希值。

Header 中的成员变量 Root、TxHash、ReceiptHash 的生成,正是源于此。明显的,hashNode 体现了 MerkleTree 的特点:每个父节点的哈希值来源于所有子节点哈希值的组合,一个顶点的哈希值能够代表一整个树形结构。hashNode 加上之前的fullNode,shortNode,valueNode,构成了一个完整的 Merkle-PatriciaTrie 结构,很好的融合了各种原型结构的优点,非常值得研究。

节点增删查改用到的函数都定义于 trie.go。在 MPT 的查找,插入,删除过程中,如果在遍历时遇到一个 hashNode,首先需要从数据库里以这个哈希值为 k,读取出相匹配的v,然后再将 v 解码恢复成 fullNode 或 shortNode。在代码中这个过程叫 resolve。

五.StateDB

在系统设计中,底层数据库模块和业务模型之间,往往需要设置本地存储模块,它面向业务模型,可以根据业务需求灵活的设计各种存储格式和单元,同时又连接底层数据库,如果底层数据库(或者第三方 API)有变动,可以大大减少对业务模块的影响。在以太坊中,StateDB 就担任这个角色,它通过大量的 stateObject 对象集合,管理所有“账户”信息。

StateDB 有一个 trie.Trie 类型成员 trie,它又被称为 storage trie 或 stte trie,这个 MPT 结构中存储的都是 stateObject 对象,每个 stateObject 对象以其地址(20 bytes)作为插入节点的 Key;每次在一个区块的交易开始执行前,trie 由一个哈希值(hashNode)恢复出来。另外还有一个 map 结构,也是存放 stateObject,每个 stateObject 的地址作为 map 的 key。那么问题来了,这些数据结构之间是怎样的关系呢?

如上图所示,每当一个 stateObject 有改动,亦即“账户”信息有变动时,这个stateObject 对象会更新,并且这个 stateObject 会标为 dirty,此时所有的数据改动还仅仅存储在 map 里。当 IntermediateRoot()调用时,所有标为 dirty 的 stateObject 才会被一起写入 trie。而整个 trie 中的内容只有在 CommitTo()调用时被一起提交到底层数据库。可见,这个 map 被用作本地的一级缓存,trie 是二级缓存,底层数据库是第三级,各级数据结构的界限非常清晰,这样逐级缓存数据,每一级数据向上一级提交的时机也根据业务需求做了合理的选择。

StateDB 还可以管理账户状态的版本。这个功能用到了几个结构体:journal,revision,先来看看 UML 关系图:

其中 journal 对象是 journalEntry 的散列,长度不固定,可任意添加元素,接口journalEntry 存在若干种实现体,描述了从单个账户操作(账户余额,发起合约次数等),到account trie 变化(创建新账户对象,账户消亡)等各种最小事件。revision 结构体,用来描述一个‘版本’,它的两个整型成员 jd 和 journalIndex,都是基于 journal 散列进行操作的。

上图简述了 StateDB 中账户状态的版本是如何管理的。首先 journal 散列会随着系统运行不断的增长,记录所有发生过的单位事件;当某个时刻需要产生一个账户状态版本时, 代码中相应的是 Snapshop()调用,会产生一个新 revision 对象,记录下当前 journal 散列的长度,和一个自增 1 的版本号。

基于以上的设计,当发生回退要求时,只要根据相应的 revision 中的 journalIndex,在journal 散列上,根据所记录的所有 journalEntry,即可使所有账户回退到那个状态。每个 stateObject 对象管理着以太坊中的一个“账户”。stateObject 有一个成员变量data,类型是 Accunt 结构体,里面存有账户 Ether 余额,合约发起次数,最新发起合约指令集的哈希值,以及一个 MPT 结构的顶点哈希值。

stateObject 内部也有一个 Trie 类型的成员 trie,被称为 storage trie,它里面存放的是一种被称为 State 的数据。State 跟每个账户相关,格式是[Hash, Hash]键值对。有意思的是,stateObject 内部也有类似 StateDB 一样的二级数据缓存机制,用来缓存和更新这些State。

stateObject 定义了一种类型名为 storage 的 map 结构,用来存放[Hash,Hash]类型的数据对,也就是 State 数据。当 SetState()调用发生时,storage 内部 State 数据被更新,相应标示为”dirty”。之后,待有需要时(比如 updateRoot()调用),那些标为”dirty”的 State 数据被一起写入 storage trie,而 storage trie 中的所有内容在 CommitTo()调用时再一起提交到底层数据库。

感谢HPB团队整理。

HPB57:ETH交易部分分析

1.交易结构

交易结构定义在 core/types/transaction.go 中:

这个 atomic 是 go 语言的一个包 sync/atomic,用来实现原子操作。在这个结构体中, data 为数据字段,其余三个为缓存。下面是计算 hash 的函数:

计算哈希前,首先会从缓存 tx.hash 中获取,如果取到,则直接返回值。没有,则使用

rlpHash 计算:

hash 的计算方式为:先将交易的 tx.data 进行 rlpEncode 编码(定义在:core/types/transaction.go 中)

然后再进行算法为 Keccak256 的哈希计算。即:txhash=Keccak256(rlpEncode(tx.data))

Transaction 中,data 为 txdata 类型的,定义于同文件中,里面详细规定了交易的具体字段:

这些字段的详细解释如下:

  • AccountNonce:此交易的发送者已发送过的交易数(可防止重放攻击)
  • Price:此交易的 gas price
  • GasLimit:本交易允许消耗的最大 gas 数量
  • Recipient:交易的接收者地址,如果这个字段为 nil 的话,则这个交易为“合约创建”类型交易
  • Amount:交易转移的以太币数量,单位是 wei
  • Payload:交易可以携带的数据,在不同类型的交易中有不同的含义
  • V R S:交易的签名数据

我们会发现,交易中没有包含发送者地址这条数据,这是因为这个地址已包含在签名信息中,后面我们会分析到相关代码,另外,以太坊节点还会提供 JSON RPC 服务,供外部调用来传输数据。传输的数据格式为 json,因此,本文件中,还定义了交易的 json 类型数据结构,以及相关的转换函数。

函数为:MarshalJSON()和 UnmarshlJSON(),这两个函数会调用core/types/gen_tx_json.go 文件中的同名函数进行内外部数据类型的转换。

2.交易存储

交易的获取与存储函数为:Get/WriteTXLookupEntries ,定义在 core/database_util.go

中。

对于每个传入的区块,该函数会读取块中的每一条交易来分别处理。首先建立条目(entry),数据类型为:txLookupEntry。内容包括区块哈希、区块号以及交易索引(交易 在区块中的位置),然后将此 entry 进行 rlp 编码作为存入数据库的 value。key 部分与区块存储类似,组成结构为交易前缀+交易哈希。

此函数的调用主要在 core/blockchain.go 中,比如 WriteBlockAndState()会将区块写入数据库,处理 body 部分时需要分别处理每条交易。而 WriteBlockAndState 是在miner/worker.go 中 wait 函数调用的。mainer/worker.go 中 newWorker 函数在创建新矿工时,会调用 worker.wait().

3.交易类型

在源码中交易只有一种数据结构,如果非要给交易分个类的话,我认为交易可以分为三种:转账的交易、创建合约的交易、执行合约的交易。web3.js 提供了发送交易的接口:

web3.eth.sendTransaction(transactionObject [, callback]) (web3.js 在internal/jsre/deps 中)

参数是一个对象,如果在发送交易的时候指定不同的字段,区块链节点就可以识别出对应类型的交易。

转账交易

​ 转账是最简单的一种交易,这里转账是指从一个账户向另一个账户发送以太币。发送转账交易的时候只需要指定交易的发送者、接收者、转币的数量。使用 web3.js 发送转账交易应该像这样:

value 是转移的以太币数量,单位是 wei,对应的是源码中的 Amount 字段。to 对应的是源码中的 Recipient

创建合约交易

​ 创建合约指的是将合约部署到区块链上,这也是通过发送交易来实现。在创建合约的交易中,to 字段要留空不填,在 data 字段中指定合约的二进制代码,from 字段是交易的发送者也是合约的创建者。

data 字段对应的是源码中的 Payload 字段。

执行合约交易

调用合约中的方法,需要将交易的 to 字段指定为要调用的合约的地址,通过 data 字段指定要调用的方法以及向该方法传递的参数。

data 字段需要特殊的编码规则,具体细节可以参考 Ethereum Contract ABI(自己拼接字段既不方便又容易出错,所以一般都使用封装好的 SDK(比如 web3.js) 来调用合约)。

4.交易执行

​ 按照以太坊架构设计,交易的执行可大致分为内外两层结构:第一层是虚拟机外,包括执行前将 Transaction 类型转化成 Message,创建虚拟机(EVM)对象,计算一些 Gas 消耗,以及执行交易完毕后创建收据(Receipt)对象并返回等;第二层是虚拟机内,包括执行 转帐,和创建合约并执行合约的指令数组。

虚拟机外

执行 tx 的入口函数是 Process()函数,在 core/state_processor.go 中。

​ Process()函数的核心是一个 for 循环,它将 Block 里的所有 tx 逐个遍历执行。具体的执行函数为同个 go 文件中的 ApplyTransaction()函数,它每次执行 tx, 会返回一个收据(Receipt)对象。Receipt 结构体的声明如下(core/types/receipt.go):

​ Receipt 中有一个 Log 类型的数组,其中每一个 Log 对象记录了 Tx 中一小步的操作。所以,每一个 tx 的执行结果,由一个 Receipt 对象来表示;更详细的内容,由一组 Log 对象来记录。这个 Log 数组很重要,比如在不同 Ethereum 节点(Node)的相互同步过程中, 待同步区块的 Log 数组有助于验证同步中收到的 block 是否正确和完整,所以会被单独同步(传输)。

Receipt 的 PostState 保存了创建该 Receipt 对象时,整个 Block 内所有“帐户”的当时状态。Ethereum 里用 stateObject 来表示一个账户 Account,这个账户可转帐(transfer value), 可执行 tx, 它的唯一标示符是一个 Address 类型变量。 这个 Receipt.PostState 就是当时所在 Block 里所有 stateObject 对象的 RLP Hash 值。

Bloom 类型是一个 Ethereum 内部实现的一个 256bit 长 Bloom Filter。 Bloom Filter 概念定义可见 wikipediahttp://blog.csdn.net/jiaomeng/article/details/1495500 它可用来快速验证一个新收到的对象是否处于一个已知的大量对象集合之中。这里 Receipt 的 Bloom, 被用以验证某个给定的 Log 是否处于 Receipt 已有的 Log 数组中。

​ 我们来看下 StateProcessor.ApplyTransaction()的具体实现,它的基本流程如下图:

​ ApplyTransaction()首先根据输入参数分别封装出一个 Message 对象和一个 EVM 对象,然后加上一个传入的 GasPool 类型变量,执行 core/state_transition.go 中的ApplyMessage(),而这个函数又调用同 go 文件中 TransitionDb()函数完成 tx 的执行,待TransitionDb()返回之后,创建一个收据 Receipt 对象,最后返回该 Recetip 对象,以及整个tx 执行过程所消耗 Gas 数量。

GasPool 对象是在一个 Block 执行开始时创建,并在该 Block 内所有 tx 的执行过程中共享,对于一个 tx 的执行可视为“全局”存储对象; Message 由此次待执行的 tx 对象转化而来,并携带了解析出的 tx 的(转帐)转出方地址,属于待处理的数据对象;EVM 作为Ethereum 世界里的虚拟机(Virtual Machine),作为此次 tx 的实际执行者,完成转帐和合约(Contract)的相关操作。

我们来细看下 TransitioinDb()的执行过程(/core/state_transition.go)。假设有StateTransition 对象 st, 其成员变量 initialGas 表示初始可用 Gas 数量,gas 表示即时可用Gas 数量,初始值均为 0,于是 st.TransitionDb() 可由以下步骤展开:

首先执行 preCheck()函数,检查:1.交易中的 nonce 和账户 nonce 是否为同一个。2. 检查 gas 值是否合适(<=64 )

  • 购买 Gas。首先从交易的(转帐)转出方账户扣除一笔 Ether,费用等于tx.data.GasLimit * tx.data.Price; 同 时 st.initialGas = st.gas = tx.data.GasLimit; 然 后(GasPool) gp –= st.gas 。
  • 计算 tx 的固有 Gas 消耗 – intrinsicGas。它分为两个部分,每一个 tx 预设的消耗量,这个消耗量还因 tx 是否含有(转帐)转入方地址而略有不同;以及针对tx.data.Payload 的 Gas 消耗,Payload 类型是[]byte,关于它的固有消耗依赖于[]byte 中非 0 字节和 0 字节的长度。最终,st.gas –= intrinsicGas
  • EVM 执行。如果交易的(转帐)转入方地址(tx.data.Recipient)为空,即contractCreation,调用 EVM 的 Create()函数;否则,调用 Call()函数。无论哪个函数返回后,更新 st.gas。
  • 计算本次执行交易的实际 Gas 消耗: requiredGas = st.initialGas – st.gas
  • 偿退 Gas。它包括两个部分:首先将剩余 st.gas 折算成 Ether,归还给交易的(转帐)转出方账户;然后,基于实际消耗量 requiredGas,系统提供一定的补偿,数量为 refundGas。refundGas 所折算的 Ether 会被立即加在(转帐)转出方账户上, 同时 st.gas += refundGas,gp += st.gas,即剩余的 Gas 加上系统补偿的 Gas,被一起归并进 GasPool,供之后的交易执行使用。
  • 奖励所属区块的挖掘者:系统给所属区块的作者,亦即挖掘者账户,增加一笔金额,数额等于 st.data,Price * (st.initialGas – st.gas)。注意,这里的 st.gas 在步骤 5 中被加上了 refundGas, 所以这笔奖励金所对应的 Gas,其数量小于该交易实际消耗量 requiredGas。

由上可见,除了步骤 3 中 EVM 函数的执行,其他每个步骤都在围绕着 Gas 消耗量作文章。

步骤 5 的偿退机制很有意思,设立它的目的何在?目前为止我只能理解它可以避免交易执行过程中过快消耗 Gas,至于对其全面准确的理解尚需时日。

步骤 6 是奖励机制,没什么好说的。

Ethereum 中每个交易(transaction,tx)对象在被放进 block 时,都是经过数字签名的, 这样可以在后续传输和处理中随时验证 tx 是否经过篡改。Ethereum 采用的数字签名是椭圆曲线数字签名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)。ECDSA 相比于基于大质数分解的 RSA 数字签名算法,可以在提供相同安全级别(in bits)的同时,仅需更短的公钥(public key)。这里需要特别留意的是,tx 的转帐转出方(发送方)地址,就是对该 tx 对象作 ECDSA 签名计算时所用的公钥 publicKey。

Ethereum 中的数字签名计算过程所生成的签名(signature), 是一个长度为 65bytes 的字节数组,它被截成三段放进 tx 中,前 32bytes 赋值给成员变量 R, 再 32bytes 赋值给 S,1byte 赋给 V,当然由于 R、S、V 声明的类型都是*big.Int, 上述赋值存在[]byte –> big.Int 的类型转换。

当需要恢复出 tx 对象的转帐转出方地址时(比如在需要执行该交易时),Ethereum 会先从 tx 的 signature 中恢复出公钥,再将公钥转化成一个 common.Address 类型的地址,signature 由 tx 对象的三个成员变量 R,S,V 转化成字节数组[]byte 后拼接得到。

Ethereum 对此定义了一个接口 Signer, 用来执行挂载签名,恢复公钥,对 tx 对象做哈希等操作。 接口定义是在:/ core/types/transaction_signing.go 的:

这个接口主要做的就是恢复发送地址、生成签名格式、生成交易哈希、验证等。

生成数字签名的函数叫 SignTx(),最根源是定义在 core/types/transaction_signing.go(mobile/accounts.go 中也有 SignTx,但是这个函数是调用 accounts/keystore/keystore.go中的 SignTX,最终又调用 types.SignTx),它会先调用其函数生成 signature, 然后调用tx.WithSignature()将 signature 分段赋值给 tx 的成员变量 R,S,V。

​ Signer 接口中,恢复(提取?)转出方地址的函数为:Sender,Sender returns the address derived from the signature (V, R, S) using secp256k1。使用到的参数是:Signer 和 Transaction ,该函数定义在core/types/transaction_signing.go 中

​ Sender()函数体中,signer.Sender()会从本次数字签名的签名字符串(signature)中恢复出公钥,并转化为 tx 的(转帐)转出方地址。此函数最终会调用同文件下的 recoverPlain 函数来进行恢复

在上文提到的 ApplyTransaction()实现中,Transaction 对象需要首先被转化成 Message接口,用到的AsMessage()函数即调用了此处的 Sender()。调用路径为: AsMessage->transaction_signing.Sender(两个参数的)–>sender(单个参数的) 在 Transaction 对象 tx 的转帐转出方地址被解析出以后,tx 就被完全转换成了Message 类型,可以提供给虚拟机 EVM 执行了。

虚拟机内:

​ 每个交易(Transaction)带有两部分内容(参数)需要执行:

  1. 转帐,由转出方地址向转入方地址转帐一笔以太币 Ether;
  2. 携带的[]byte 类型成员变量 Payload,其每一个 byte 都对应了一个单独虚拟机指令。这些内容都是由 EVM(Ethereum Virtual Machine)对象来完成 的。EVM 结构体是 Ethereum 虚拟机机制的核心,它与协同类的 UML 关系图如下:

​ 其中 Context 结构体分别携带了 Transaction 的信息(GasPrice, GasLimit),Block 的信息(Number, Difficulty),以及转帐函数等,提供给 EVM;StateDB 接口是针对 state.StateDB 结构体设计的本地行为接口,可为 EVM 提供 statedb 的相关操作; Interpreter 结构体作为解释器,用来解释执行 EVM 中合约(Contract)的指令(Code)。

​ 注意,EVM 中定义的成员变量 Context 和 StateDB, 仅仅声明了变量名而无类型,而变量名同时又是其类型名,在 Golang 中,这种方式意味着宗主结构体可以直接调用该成员变量的所有方法和成员变量,比如 EVM 调用 Context 中的 Transfer()。

交易的转帐操作由 Context 对象中的 TransferFunc 类型函数来实现,类似的函数类型,还有 CanTransferFunc, 和 GetHashFunc。这三个类型的函数变量 CanTransfer, Transfer, GetHash,在 Context 初始化时从外部传入,目前使用的均是一个本地实现。可见目前的转帐函数 Transfer()的逻辑非常简单,转帐的转出账户减掉一笔以太币,转入账户加上一笔以太币。由于 EVM 调用的 Transfer()函数实现完全由 Context 提供,所以,假设如果基于 Ethereum 平台开发,需要设计一种全新的“转帐”模式,那么只需写一个新的 Transfer()函数实现,在 Context 初始化时赋值即可。

有朋友或许会问,这里 Transfer()函数中对转出和转入账户的操作会立即生效么?万一两步操作之间有错误发生怎么办?答案是不会立即生效。StateDB 并不是真正的数据库, 只是一行为类似数据库的结构体。它在内部以 Trie 的数据结构来管理各个基于地址的账 户,可以理解成一个 cache;当该账户的信息有变化时,变化先存储在 Trie 中。仅当整个Block 要被插入到 BlockChain 时,StateDB 里缓存的所有账户的所有改动,才会被真正的提交到底层数据库。

合约的创建和赋值:

合约(Contract)是 EVM 用来执行(虚拟机)指令的结构体。Contract 的结构定义于:core/vm/contract.go 中,在这些成员变量里,caller 是转帐转出方地址(账户),self 是转入方地址,不过它们的类型都用接口 ContractRef 来表示;Code 是指令数组,其中每一个 byte 都对应于一个预定义的虚拟机指令;CodeHash 是 Code 的 RLP 哈希值;Input 是数据数组,是指令所操作的数据集合;Args 是参数。

​ 有意思的是 self 这个变量,为什么转入方地址要被命名成 self 呢? Contract 实现了ContractRef 接口,返回的恰恰就是这个 self 地址。

func (c *Contract) Address() common.Address { return c.self.Address()

}

​ 所以当 Contract 对象作为一个 ContractRef 接口出现时,它返回的地址就是它的 self地址。那什么时候 Contract 会被类型转换成 ContractRef 呢?当 Contract A 调用另一个Contract B 时,A 就会作为 B 的 caller 成员变量出现。Contract 可以调用 Contract,这就为系统在业务上的潜在扩展,提供了空间。

创建一个 Contract 对象时,重点关注对 self 的初始化,以及对 Code, CodeAddr 和Input 的赋值。

另外,StateDB 提供方法 SetCode(),可以将指令数组 Code 存储在某个 stateObject 对象中; 方法 GetCode(),可以从某个 stateObject 对象中读取已有的指令数组 Code。

1
2
3
func (self *StateDB) SetCode(addr common.Address, code []byte) /func (self

*StateDB) GetCode(addr common.Address) code []byte

​ stateObject (core/state/state_object.go)是 Ethereum 里用来管理一个账户所有信息修改的结构体,它以一个 Address 类型变量为唯一标示符。StateDB 在内部用一个巨大的map 结构来管理这些 stateObject 对象。所有账户信息-包括 Ether 余额,指令数组 Code,该账户发起合约次数 nonce 等-它们发生的所有变化,会首先缓存到 StateDB 里的某个stateObject 里,然后在合适的时候,被 StateDB 一起提交到底层数据库。

​ EVM(core/vm/evm.go)中 目前有五个函数可以创建并执行 Contract,按照作用和调用方式,可以分成两类:

  • ​ Create(), Call(): 二者均在 StateProcessor 的 ApplyTransaction()被调用以执行单个交易,并且都有调用转帐函数完成转帐。
  • ​ CallCode(), DelegateCall(), StaticCall():三者由于分别对应于不同的虚拟机指令(1 byte)操作,不会用以执行单个交易,也都不能处理转帐。

    考虑到与执行交易的相关性,这里着重探讨 Create()和 Call()。先来看 Call(),它用来处理(转帐)转入方地址不为空的情况:

Call()函数的逻辑可以简单分为以上 6 步。其中步骤(3)调用了转帐函数 Transfer(),转入账户 caller, 转出账户 addr;步骤(4)创建一个 Contract 对象,并初始化其成员变量 caller, self(addr), value 和 gas; 步骤(5)赋值 Contract 对象的 Code, CodeHash, CodeAddr 成员变量;步骤(6) 调用 run()函数执行该合约的指令,最后 Call()函数返回。相关代码可见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) 

{

if evm.vmConfig.NoRecursion && evm.depth > 0 {//如果设置了“禁用 call”,并且depth 正确,直接返回

return nil, gas, nil

}

// Fail if we're trying to execute above the call depth limit

  if evm.depth > int(params.CallCreateDepth) {//如果 call 的栈深度超过了预设值, 报错

return nil, gas, ErrDepth

}

// Fail if we're trying to transfer more than the available balance

    if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {//检查发出账户是否有足够的钱(实际实现的函数定义在     core/evm.go/CanTransfer()中)但目前还不知道是怎么调用的

  return nil, gas, ErrInsufficientBalance

}

var (

to = AccountRef(addr)

snapshot  = evm.StateDB.Snapshot()

)

if !evm.StateDB.Exist(addr) {//建立账户

precompiles := PrecompiledContractsHomestead

if evm.ChainConfig().IsByzantium(evm.BlockNumber) { precompiles = PrecompiledContractsByzantium

}

if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {

return nil, gas, nil

}

evm.StateDB.CreateAccount(addr)

}

evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)//转移

// initialise a new contract and set the code that is to be used by the

// E The contract is a scoped environment for this execution context

// only.

contract := NewContract(caller, to, value, gas)//建立合约contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr),

evm.StateDB.GetCode(addr))

ret, err = run(evm, snapshot, contract, input)

// When an error was returned by the EVM or when setting the creation code

// above we revert to the snapshot and consume any gas remaining. Additionally

// when we're in homestead this also counts for code storage gas errors. if err != nil {

evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted {

contract.UseGas(contract.Gas)

}

}

return ret, contract.Gas, err

}

​ 因为此时(转帐)转入地址不为空,所以直接将入参 addr 初始化 Contract 对象的 self 地址,并可从 StateDB 中(其实是以 addr 标识的账户 stateObject 对象)读取出相关的 Code 和CodeHash 并赋值给 contract 的成员变量。注意,此时转入方地址参数 addr 同时亦被赋值予 contract.CodeAddr。

再来看看 EVM.Create(),它用来处理(转帐)转入方地址为空的情况。

与 Call()相比,Create()因为没有 Address 类型的入参 addr,其流程有几处明显不同:

  • ​ 步骤(3)中创建一个新地址 contractAddr,作为(转帐)转入方地址,亦作为

    Contract 的 self 地址;

  • ​ 步骤(6)由于 contracrAddr 刚刚新建,db 中尚无与该地址相关的 Code 信息, 所以会将类型为[]byte 的入参 code,赋值予 Contract 对象的 Code 成员;

  • ​ 步骤(8)将本次执行合约的返回结果,作为 contractAddr 所对应账户

    (stateObject 对象)的 Code 储存起来,以备下次调用。

​ 还有一点隐藏的比较深,Call()有一个入参 input 类型为[]byte,而 Create()有一个入参code 类型同样为[]byte,没有入参 input,它们之间有无关系?其实,它们来源都是Transaction 对象 tx 的成员变量 Payload!调用 EVM.Create()或 Call()的入口在StateTransition.TransitionDb()中,当 tx.Recipent 为空时,tx.data.Payload 被当作所创建Contract 的 Code;当 tx.Recipient 不为空时,tx.data.Payload 被当作 Contract 的 Input。

预编译合约

​ EVM 中执行合约(指令)的函数是 run(),在 core/vm/evm.go 中其实现代码如下: 可见如果待执行的 Contract 对象恰好属于一组预编译的合约集合-此时以指令地址CodeAddr 为匹配项-那么它可以直接运行;没有经过预编译的 Contract,才会由Interpreter 解释执行。这里的”预编译”,可理解为不需要编译(解释)指令(Code)。预编译的合约,其逻辑全部固定且已知,所以执行中不再需要 Code,仅需 Input 即可。

在代码实现中,预编译合约只需实现两个方法 Required()和 Run()即可,这两方法仅需一个入参 input。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/ core/vm/contracts.go

type PrecompiledContract interface { RequiredGas(input []byte) uint64 Run(input []byte) ([]byte, error)

}

func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contrat) (ret []byte, err error) {

gas := p.RequiredGas(input) if contract.UseGas(gas) {

return p.Run(input)

}

return nil, ErrOutOfGas

}

目前,Ethereuem 代码中已经加入了多个预编译合约,功能覆盖了包括椭圆曲线密钥恢复,SHA-3(256bits)哈希算法,RIPEMD-160 加密算法等等。相信基于自身业务的需求,二次开发者完全可以加入自己的预编译合约,大大加快合约的执行速度。

解释器执行合约的指令

解释器 Interpreter 用来执行(非预编译的)合约指令。它的结构体 UML 关系图如下所示:

​ Interpreter 结构体通过一个 Config 类型的成员变量,间接持有一个包括 256 个operation 对象在内的数组 JumpTable。operation 是做什么的呢?

每个 operation 对象正对 应 一 个 已 定 义 的 虚 拟 机 指 令 , 它 所 含 有 的 四 个 函 数 变 量 execute, gasCost, validateStack, memorySize 提供了这个虚拟机指令所代表的所有操作。每个指令长度1byte,Contract 对象的成员变量 Code 类型为[]byte,就是这些虚拟机指令的任意集合,operation 对象的函数操作,主要会用到 Stack,Memory, IntPool 这几个自定义的数据结构。

​ 这样一来,Interpreter 的 Run()函数就很好理解了,其核心流程就是逐个 byte 遍历入参 Contract 对象的 Code 变量,将其解释为一个已知的 operation,然后依次调用该operation 对象的四个函数,流程示意图如下:

operation 在操作过程中,会需要几个数据结构: Stack,实现了标准容器 -栈的行为;Memory,一个字节数组,可表示线性排列的任意数据;还有一个 intPool,提供对big.Int 数据的存储和读取。

已定义的 operation,种类很丰富,包括:

  • ​ 算术运算:ADD,MUL,SUB,DIV,SDIV,MOD,SMOD,EXP…;
  • ​ 逻辑运算:LT,GT,EQ,ISZERO,AND,XOR,OR,NOT…;
  • ​ 业务功能:SHA3,ADDRESS,BALANCE,ORIGIN,CALLER,GASPRICE,LOG1,LOG2…等等

    需要特别注意的是 LOGn 指令操作,它用来创建 n 个 Log 对象,这里 n 最大是 4。还记得 Log 在何时被用到么?每个交易(Transaction,tx)执行完成后,会创建一个 Receipt 对象用来记录这个交易的执行结果。Receipt 携带一个 Log 数组,用来记录 tx 操作过程中的所有变动细节,而这些 Log,正是通过合适的 LOGn 指令-即合约指令数组(Contract.Code) 中的单个 byte,在其对应的 operation 里被创建出来的。每个新创建的 Log 对象被缓存在StateDB 中的相对应的 stateObject 里,待需要时从 StateDB 中读取。

感谢HPB团队整理。