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

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

HPB55:ETH基于POA的环境搭建

ETH基于POA的环境搭建

许多同学有研究POA的想法,那么今天我们尝试创建以太坊基于 POA 的环境,首先搭建环境前的准备

1.安装 go-ethereum,下载 go 的源码

1
git clone https://github.com/ethereum/go-ethereum

2.编译go

1
2
3
cd go-ethereum

Make geth

3.创建4个文件夹

执行完成之后再bin的目录下会生成可执行文件geth,分别命名为node1.node2.signer1.signer2.node 是普通节点,用于后期节点间发起交易,在接下来的实验中,signer1 设置为创世块指定的授权节点;signer2为后期加入信任名单授权节点,目录如下:

1
2
3
root@test # ls

geth, node1,node2,signer1,signer2 

4.私有链搭建创建新账户,将启动的数据分别存在不同的文件夹中。

1
./geth --datadir node1/data account new

至此我们有了四个账户,四个账户分别为

1
2
3
4
5
6
7
Node1: 93ab57b1c1ae82537d6c7956d2817916166f6389

Node2: c2fdb2dffa3a14740e3d8b3baeeea504a699229f

Signer1: 7d848a70962ad3830c59ad35c6811b1fc3d07360 

Signer2:  f5413187e29113841db0ec73a2c559a53b6be7fe

5.初始化创世块

在 1.6 之后的以太坊中提供了初始化创世块的工具:puppeth, 请选择含有 Geth & Tools 的版本下载,如下图标红处。下载地址:https://geth.ethereum.org/downloads/ , 也可以按照上文自行编译。

puppeth 是个互动式的程序,直接启动照着指示输入相关信息。设置 Private chain 名称,假定为 poa_test,如下:

6.指定每隔 30 秒生成一个区块

我们指定了第一个 signer1 这个用户用来挖矿,指定了 node1和 signer1 开始被填充了余额。

7.按照如下步骤将生成的初始块进行导出

8.导出后的配置文件如下如所示:

9.创世块为 json 格式

我们将创世块的内容列举如下:

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
{

"config": { "chainId": 40783,

"homesteadBlock": 1,

"eip150Block": 2, "eip150Hash":

"0x0000000000000000000000000000000000000000000000000000000000000000",

"eip155Block": 3,

"eip158Block": 3,

"byzantiumBlock": 4, "clique": {

"period": 30,

"epoch": 30000

}

},

"nonce": "0x0", "timestamp": "0x59fa93a0", "extraData":

"0x00000000000000000000000000000000000000000000000000000000000000007d848a70962a d3830c59ad35c6811b1fc3d07360000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000

0",

"gasLimit": "0x47b760", "difficulty": "0x1",

"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "coinbase": "0x0000000000000000000000000000000000000000",

"alloc": { "0000000000000000000000000000000000000000": {

"balance": "0x1"

},

….. // 此部分列举了部分内容,剩下为从 00 到 ff "00000000000000000000000000000000000000ff": {

"balance": "0x1"

}, "7d848a70962ad3830c59ad35c6811b1fc3d07360": {

"balance": "0x200000000000000000000000000000000000000000000000000000000000000"

}, "93ab57b1c1ae82537d6c7956d2817916166f6389": {

"balance": "0x200000000000000000000000000000000000000000000000000000000000000"

}

},

"number": "0x0",

"gasUsed": "0x0",

"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"

}

参数解释如下:

mixhash 与 nonce 配合用于挖矿,由上一个区块的一部分生成的 hash。注意他和 nonce 的设置需要满足以太坊的 Yellow paper, 4.3.4. Block Header Validity, (44)章节所描述的 条件。.
nonce nonce 就是一个 64 位随机数,用于挖矿,注意他和 mixhash 的设置需要满足以太 坊的 Yellow paper, 4.3.4. Block Header Validity, (44)章节所描述的条件。 |
difficulty 设置当前区块的难度,如果难度过大,cpu 挖矿就很难,这里设置较小难度 |
alloc 用来预置账号以及账号的以太币数量,因为私有链挖矿比较容易,所以我们不需要 预置有币的账号,需要的时候自己创建即可以。 |
coinbase 矿工的账号,随便填 |
timestamp 设置创世块的时间戳 |
parentHash 上一个区块的 hash 值,因为是创世块,所以这个值是 0 |
extraData 附加信息,在 POA 挖矿中 |
gasLimit 该值设置对 GAS 的消耗总量限制,用来限制区块能包含的交易信息总和,因为我 们是私有链,所以填最大。 |

10.初始化私有链

使用 geth init 指令,分别初始化 4 个节点的 datadir,比如初始化 node1 可执行:

1
./geth --datadir node1/data init poa_test.json

初始化完成之后,在每个文件夹中会生成两个子文件夹。

其中 geth 用来存储区块链的数据信息,keystore 用来存储账户信息,启动节点,分别执行以下命令:

1
2
3
4
5
6
7
./geth --datadir node1/data --networkid 66300 --port 3000 console

./geth --datadir node2/data --networkid 66300 --port 3001 console

./geth --datadir signer1/data --networkid 66300 --port 3002 --unlock 7d848a70962ad3830c59ad35c6811b1fc3d07360 console

./geth --datadir signer2/data --networkid 66300 --port 3003 console

11.建立data目录

datadir先前的步骤已经在每个节点各自的目录下都建立了data目录,networkid geths之间一定都要用同一个值才可以互相通信,比如实验中的66300. Portgeths 之间通信时,监听的一个port,由于四个节点都在本机,所以这里必须指定不同的值,使用 node1 对应 3000, node2 对应 3001, signer1 对应 3002, signer2 对应 3003.

Geth 参数含义如下:

identity 区块链的标示,随便填写,用于标示目前网络的名字
init 指定创世块文件的位置,并创建初始块 |
datadir 设置当前区块链网络数据存放的位置 |
port 网络监听端口 |
rpc 启动 rpc 通信,可以进行智能合约的部署和调试 |
rpcapi 设置允许连接的 rpc 的客户端,一般为 db,eth,net,web3 |
networkid 设置当前区块链的网络 ID,用于区分不同的网络,是一个数字 |
console 启动命令行模式,可以在 Geth 中执行命令 |

12.启动成功后的截图如下

因为 signer1 为挖矿节点,所以在启动的时候需要进行解锁。

13.建立节点之前的通信

目前各个节点虽然已经启动,但是各个节点之间仍然处于孤立状态。各节点启动后无法相互通信的,所以geth要连上对方的节点就必须先设置好enode://@:,复制刚刚启动node1时出现的enode信息,将[::]替换为127.0.0.1,这样就可以让其他节点加入。如上面提示的的红色提示所见, node1 的 enode 为

1
enode://720d04c0fcf239fa773493e85d3393ef3a2ce581858cbaf1756288c2decc8f08c738fabbd6b4c79fa300292755601714522fcf4febe3f6f224ea119eaac41f72@127.0.0.1:3000

在node2,singer1,singer2的geth console界面下分别运行如下指令:

完成后,在 node1 的 geth console 输入 admin.peers 应该要看到三个节点资讯,如下:(每个id 对应其他节点的 encode 字段信息)

至此四个节点之前的关系已经初步建立。

14.开始挖矿

按照实验的设计,在本实验中 signer1 为挖矿节点,我们下面开始进行模拟挖矿,在 signer1 的console 界面,键入 miner.start(),geth 就会开始挖矿了,在 signer1 的 console 会出现正在mining 的信息。

从上面可以看出挖矿的时间间隔为 30 秒,与我们创世块的中参数一致,同时在其他节点(node1 和 node2 和 signer2)则会收到 import block 的信息。如下:

15.交易转账

按照我们的预设,在 node1 和 signer1 上是已经初始化了余额的。我进行查询如下:

但是目前的账户处于锁定状态,所以首先我们要进行解锁。

进行解锁的命令如下:

1
personal.unlockAccount("0x93ab57b1c1ae82537d6c7956d2817916166f6389")

我们开始对 node2 进行转账,如下图所示:

16.查看矿机的挖矿状态

此时已经将挖到的交易打入到区块中。查看 node2 账户的余额,展示如下:

到目前为止已经完成了 POA 私有链的所有内容的搭建。

17.加入新可信节点

按照预先实验设计,signer2 也为挖矿节点,如果此时启动 signer2 节点进行挖矿,则会出现未授权的异常,如下图所示:

18.节点授权

必须回到已经在授权名单内的节点的 console 界面下,将新的节点加入。在 signer1 的 console输入指令:

此时 signer1 挖矿节点的日志已经发生变化,已经签名,等待其他签名者。

19.重新启动

此时需要以输入密码的形式重新启动 signer2

1
./geth --datadir signer2/data --networkid 66300 --port 3003 --unlock f5413187e29113841db0ec73a2c559a53b6be7fe console   

建立连接,使用 admin.addPeer()重新建立连接

1
 admin.addPeer("enode://0d2d75b5e6fd5a7e3f82da882e49b4f74206c136a241b04a2f8ba95246df 8eb55f79de9562ffe68d5582f6e0424cfea950166236a3a7cee3f3d4cd084fc33cef@127.0.0.1:3003")

20.启动 singer2 开始挖矿

可以看到 signer1 和 signer2 在交替进行挖矿。

在 40 分 36 时,singner1,挖到了一个合适的区块, 进行签名后并立即进行广播,同时 signer2 获取到了最新的区块段,签名后并提交到网络中。singner1 在收到签名后进行打包,合并到区块中,并广播最新的消息。

至此,环境搭建全部完成

引用:

https://geth.ethereum.org/downloads/

http://blog.csdn.net/sportshark/article/details/51855007

http://blog.csdn.net/aaa123524457/article/details/52836408

http://blog.csdn.net/code_segment/article/details/78160660

感谢HPB团队整理。

HPB54:以太坊交易收发机制

交易的主要数据结构

序号 数据 描述 备注
1 AccountNonce 发送者的发起的交易总数量 |
2 Recipient 交易接受者的地址 以太坊地址 |
3 Price 此次交易的 gas price |
4 GasLimit 本交易允许消耗的最大 Gas 数 |
5 Gas 要转换的 gas 数 |
6 Amount 此次交易转移的以太币数量 |
7 V 签名数据 |
8 R 签名数据 |
9 S 签名数据 |
10 Payload 其他数据 |

交易收发相关协程

交易数据验证流程

交易入池流程

感谢HPB团队整理。

HPB53:以太坊网络服务分析

网络分层

以太坊所有网络功能如下图所示:

所有网络功能建立在以太网的传输层之上,TCP 及 UDP 均有应用。

会话层

会话层主要包括 Peer 管理,NodeTable 管理和 RPC 协议,本文着重介绍 Peer 管理,

NodeTable 请参考《P2P 网络及邻居节点发现机制》。

涉及到会话层的关键代码:

源文件 类描述 主要方法
Server.go Type Server struct Server 是网络功能的基本类,所有功能均封装为 server,并进行启动。 Start:启动 server Run: 运行具体任. 务, 包括创建 Peers map, peer 拨号,peer 添加, 删除处理。Startlistening : 监 听 TCP 端口。 AddPeer: 添 加 peer RemovePeer : 移 除 Peer |
Peer.go Type Peer struct Peer 类提供各种Peer 的操作方法 NewPeer:生成一个新的 peer Run:peer 链接建立后继续进行应用层通信,并启动协议处理。 startProtocols:启动应 用层 eth 协议。 |
Dial.go Type Dialer struct Dialer 类,即拨号类,拨号即节 点向邻居节点发起 Peer 通信 Dial:实现一次拨号 |
Table.go Type Table struct 邻居节点及 K 桶相关类 |
Udp.go 提供 Kad 协议相关方法及 Peer 握手相关方法 |

Peer 介绍

Peer 指通过了通信握手的邻居节点,只有邻居节点才能变为 Peer,只有 Peer 列表中的节点,才能进行正常的通信。

Peer 管理

Peers 在代码中以 map 的结构存在,由 server 运行方法 run 创建,并在 run 方法中进行添加和删除维护。Pees 最大默认数量为 25(node/defaults.go 定义)

Peer 动态添加删除流程

Peer 添加分为两种:被动添加和主动添加。

1) 被动添加指其他节点发起握手,流程如下:

2) 每当当前 peers 有变动时,如添加,删除,或者一次 dial 任务完成,则会执行一次主动握手流程如下,其中要进行 Dial(拨号,即握手通信)的节点有以下几部分组成:

  • 静态节点,系统启动时配置文件写入
  • nodeTable 中随机选取(当前 needDynDials 的二分之一,needDynDials 的值为(s.MaxPeer+1)/2=13)
  • loobbuf 中的节点(discovery task 中的邻居节点)
  • lookbuf 中的节点 Peer 数量不足时,会强制进行一次 nodetable 刷新,刷新到的node 写入lookuf.

3) Peer 删除有三种方式: RPC 命令删除,一次应用层通信完成自动删除,通信过程读写错误。

Peer 握手机制

参考《以太坊底层技术研究:Peer 握手机制》

表示层:RLP 编码

以太坊所相关有网络上 x 发送的数据均遵循 RLP 编码,参考《RLP 机制分析》

应用层:Eth 协议

Peer握手成功后,即可进行应用层通信,协议数据包如下表所示:

序号 分类 描述
1 Code 数据包命令分类 |
2 Size Payload大小 |
3 Payload 负载数据 |
4 ReceivedAt 接受到数据包的时间,接受端节点赋值 |

ETH协议应用层包括如下命令:

序号 分类 描述
1 StatusMsg = 0x00 Handshake 使用,握 手成功后不再发送 |
2 NewBlockHashesMsg = 0x01 新区块哈希(广播) |
3 TxMsg = 0x02 交易信息(广播数据) |
4 GetBlockHeadersMsg = 0x03 获取去块头信息 |
5 BlockHeadersMsg = 0x04 获取区块头信息响应 |
6 GetBlockBodiesMsg = 0x05 获取区块信息 |
7 BlockBodiesMsg = 0x06 获取区块信息相应 |
8 NewBlockMsg = 0x07 新块信息(广播数据) |
9 GetNodeDataMsg = 0x0d 获取节点数据 |
10 NodeDataMsg = 0x0e 获取节点数据响应 |
11 GetReceiptsMsg = 0x0f 获取收据信息 |
12 ReceiptsMsg = 0x10 获取收据信息响应 |

感谢HPB团队整理。

HPB52:实战以太坊搭建联盟链

1 安装前的准备

1.1 配置Linux系统

本文配置的为windows下的VMware软件下搭建的Linux下的cent0S-7系统,推荐系统有CentOS 7和Ubuntu 16.04有条件的可以用云服务器上的Linux系统,好处是可以搭建广域网联盟链。

本文默认你已经对以太坊和docker技术有一定的掌握和了解,并部署好相应的环境。如果你还未准备好,请参考芯链公众号前期发布的环境搭建和部署文章。

1.2 安装Docker

请使用管理员权限进行安装,

# su root

安装Docker(请参考芯链公众号发布的安装指导文章):

CentOS: yum -y install docker-io

Ubuntu: apt-get install docker-engine

安装完毕如图:

然后启动Docker服务:

#service docker start

校验docker是否安装成功:

#docker run hello-world

这个命令会下载一个测试镜像,并且运行在一个容器中。当容器运行时,他会打印一些信息,并且退出。下图表示Docker已经安装完成。

2 安装bootnode

2.1 下载bootnode镜像

运行命令如下命令:

#docker pull docker.io/hawyasunaga/ethereum-bootnode

查看镜像:docker images

2.2 Docker创建bootnode容器节点

生成引导节点:

1
#docker run -itd -m 512M --privileged=true --memory-swap -1 --net=host -p 30301:30301/udp -p 30301:30301/tcp -v /path/docker/bootnode:/root/bootnode --name genbootnode docker.io/hawyasunaga/ethereum-bootnode bootnode --genkey=/root/bootnode/boot.key

运行引导节点:

1
#docker run -itd -m 512M --privileged=true --memory-swap -1 --net=host -p 30301:30301/udp -p 30301:30301/tcp -v /path/docker/bootnode:/root/bootnode --name bootnode docker.io/hawyasunaga/ethereum-bootnode bootnode --nodekey=/root/bootnode/boot.key

注意:这两个命令参数中,-v /path/docker/bootnode:/root/bootnode为映射路径,在docker的这个bootnode容器中,出现容器内/root/bootnode路径都映射为外部路径/path/docker/bootnode。下方以太坊容器搭建节点命令同理。

2.3 查看bootnode日志得到节点

1
#docker logs -f bootnode 

得到节点如下:

将[::]替换为本机IP地址

好了,用于连接的根节点运行完毕。

3 安装以太坊节点

可以找台Linux进行节点联盟链的搭建了,这里继续以centos7继续演示了。

3.1 创建以太坊的创世文件

新建创世文件genesis.json,内容如下:

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
{

  "config": {

        "chainId": 90,

        "homesteadBlock": 0,

        "eip155Block": 0,

        "eip158Block": 0

    },

  "alloc": {"0x5f38056f45091ee992298e53681b0a60c999ff95":{"balance": "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7"}},

  "coinbase"   : "0x5f38056f45091ee992298e53681b0a60c999ff95",

  "extraData"  : "0x2017",

  "nonce"      : "0xdeadbeefdeadbeeF",

  "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",

  "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",

  "timestamp"  : "0x00",

  "gasLimit": "0xffffffff",

  "difficulty": "0x20000"

}

3.2 初始化创世文件

首先新建一个准备放置以太坊目录的文件,将genesis.json文件放入该地址,本文为/home/admin下:

运行如下命令

1
#docker run -itd --privileged=true -v /home/admin:/root/ethdev --name gethDev1 ethereum/client-go --datadir /root/ethdev --networkid 8765639736937780 init /root/ethdev/genesis.json

请记住—networkid 8765639736937780,这是当前搭建联盟链的ID。

查看日志文件:docker logs -f gethDev1

初始化成功。

3.3 Docker创建以太坊容器节点

运行如下命令

1
#docker rm -f gethDev1

1
#docker run -itd -m 512M --privileged=true --network=host --memory-swap -1 --net=host -p 8545:8545 -p 40303:40303 -v /home/admin:/root/ethdev --name gethDev1 ethereum/client-go --ipcdisable --port 40303 --bootnodes "enode://ad6aff917c6e8bd40cb20af4eac6ce05c16d285125b46f17fc9b5c3b0a833bd21667231215949c6ff771ba512eb8f87f138ac6679852997c3eaec1d349561d20@120.25.162.110:30301" --bootnodesv4 "enode://ad6aff917c6e8bd40cb20af4eac6ce05c16d285125b46f17fc9b5c3b0a833bd21667231215949c6ff771ba512eb8f87f138ac6679852997c3eaec1d349561d20@120.25.162.110:30301" --bootnodesv5 "enode://ad6aff917c6e8bd40cb20af4eac6ce05c16d285125b46f17fc9b5c3b0a833bd21667231215949c6ff771ba512eb8f87f138ac6679852997c3eaec1d349561d20@120.25.162.110:30301" --debug --rpcapi "db,eth,net,web3,personal,admin,miner,txpool" --datadir /root/ethdev --networkid 8765639736937780 --wsapi "db,eth,net,web3,personal,admin,miner,txpool" --ws --wsaddr "0.0.0.0" --rpc --rpcaddr "0.0.0.0" --cache=512 --verbosity 3 console

//enode为上面运行bootnode得到的节点,并将预备的私钥文件放入keystore中,并且—networkid 8765639736937780为联盟链的标识ID。

查看控制台日志:docker logs -f gethDev1

Ok,节点搭建成功。

3.4 验证联盟链的连接

在上一步搭建好节点后,等待一段时间,让节点自动连接根节点bootnode。

进入以太坊容器中:

1
#docker attach gethDev1

输入命令:admin

查看peer是否连接:

OK,连接完成,联盟链搭建完成。

连接完成后,开始同步区块:

注意:这里连接的是之前已经加入bootnode根节点的其他节点,与根节点的连接并不会显示在peers中。如果是第一个连接bootnode的节点,连接了bootnode的时候输入admin,peers是空的,需要继续搭建一个节点来测试他们互相连接。

感谢HPB团队整理。

HPB51:RLPx加密握手协议研究

加密握手协议全局位置

Server服务器作为上层使用的接口,直接调用Start启动。Start完成启动监听端口、连接以及建立通信的任务。在运行中,Star通过创建多个goroutine,并将goroutine的结果通过channel形式汇聚给run进行集中处理。当用户间建立节点以后,通过Msg格式进行通信。其中

newTransport 是和节点建立连接(tcp或者udp)后进行协商密钥、协议握手的地方,通过rlpx协议来进行握手。除了建立rlpx握手,在运行通信的整个过程中,需要进行如下判断:

加密握手协议流程

加密握手细节描述

关键步骤补充:

生成Authpacket(Dialing执行) 接收AuthPacket(Listening执行)
1.创建enchandshake结构体,该结构体包含 Initiator(bool) RemoteID(discover.NodeID) remotePub(ecies.pk) initNonce, respNonce(byte) randomPrivateKey(ecies.sk) remoteRandomPub(ecies.pk) 1.创建authMsgV4结构体 gotPlain(bool) initiatorPubkey(byte) Signature(byte) Nonce(byte) Version(unit) |
2.执行makeAuthMsg方法: 1)将romteID恢复为公钥remotePub 其中ECDSA公钥作为ECIES公钥 2) 生成随机intiator nonce 3)生成随机ECDH密钥randomPrivKey 4)对信息签名得到signature(见附录) 处理后输出AuthMsgV4格式的数据 发送的数据包括: Version(=4) Nonce(随机生成) InitiatorPubKey(本地sk参与) Signature 2.读取authMsg操作readHandshakeMsg ◑ 若为pre-EIP8: 1) 将ECDSA私钥作为ECIES私钥 2) 通过私钥解密ECIES密文为明文 3) 对明文进行解码 ◑ 若为EIP8格式: 1) 增加前缀并连接 2) 解密为明文 3) 对明文进行解码 解码后的[]byte赋值给s作为authPacket |
3.生成经过封装的AuthPacket AuthPacket=sealEIP8(AuthMsg) 封装过程如下: 1) rlp编码 2) 填充随机数据使总长至少到100byte 3) 加前缀 4) 进行ECIES加密(remotePub) |
生成authRespPacket(Listening执行) 接收authRespPacket(Dialing执行)
1.创建enchandshake结构体(同前) 1.创建authRespV4结构体 RandomPubkey(byte) Nonce(byte) Version(unit) |
2.处理authMsg操作, 通过handleAuthMsg方法 1)将远端的身份传入,其中包括 ◑ nonce值传入给initNonce ◑ 将initiatorPK恢复为remotePub 其中ECDSA公钥作为ECIES公钥 2)生成随机ECDH私钥randomPrivKey 3)检查签名signature,并 恢复出remoteRandomPub 2.读取authRespMsg操作, 使用readHandshakeMsg方法 ◑ 若为pre-EIP8: 1) 将ECDSA私钥作为ECIES私钥 2) 通过私钥解密ECIES密文为明文 3) 对明文进行解码 ◑ 若为EIP8格式: 1) 增加前缀并连接 2) 解密为明文 3) 对明文进行解码 解码后byte赋值给s作为authRespPacket |
3. 执行makeAuthMsg方法: 1)生成随机respNonce 2)创建authRespV4结构体 该结构体包括: RandomPubKey Nonce Version 3)对结构体进行copy赋值 处理后输出AuthRespV4格式的数据 发送的数据包括: Version(=4) Nonce(随机生成) RespPubKey(本地sk参与) 3.创建enchandshake结构体(同前) |
4.生成经过封装的AuthRespPacket ◑ 若为明文格式: 需通过ECIES进行加密(remotePub) authRespPacket=sealPlain(AuthMsg) ◑ 若为非明文格式: 进行EIP8封装(同前) authRespPacket=sealEIP8(AuthMsg) 4.处理authResp操作handleAuthResp 1)读取nonce 2)将RandomPubKey恢复为remoteRandomPub 其中将输入的PK去编组化,并 将ECDSA公钥作为ECIES公钥 |

感谢HPB团队整理。

HPB50:主流共识算法分析

区块链核心框架

区块链是一个不断增长的分布式账本[2],账本用“区块”的形式衔接在一起,区块中包含交易, 时间戳,随机数等元数据,每个区块中含有一个指针指向上一个交易链接,区块链的设计是安 全的,因为其具有良好的拜占庭容错能力。区块链可以概括为一个分布式的高频交易系统,如 下图 1 所示,区块链的核心技术可以总结为四部分:分布式的数据库,密码学相关理论,共识机制和 P2P 网络。

中分布式数据库负责数据的写入与读取,密码学中非对称密钥和 HASH 等算法来标识交易者的身份和保证系统的完整性;对等网络是系统运行的基础;共识算法用来保证交易信息在整个账本不同节点中写入的一致性,常用的共识算法有 POW, POS, DPOS 等。

共识算法与 CAP 理论

共识算法是为了解决在对等网络中(P2P),相互独立的节点如何达成一项决议问题的过程。简而言之,共识算法是在解决分布式系统中如何保持一致性的问题。关于此部分的讨论较为成熟和最为广泛接受的理论是 CAP 理论。CAP 由 Eric Brewer )在 2000 年 PODC 会议上提出[4],并提出分布式系统不能同时完全满足 CAP 三个要求的假设,其中包括如下三个方面:

Consistency: 一致性 从不同节点读取的数据一致。一致性是指数据的原子性,在经典的数据库中通过事务来保障,事务完成时,无论成功或回滚,数据都会处于一致的状态,在分布式环境下,一致性是指多个节点数据是否一致。

Availability: 可用性是指服务及时非错误地响应,服务一直保持可用的状态,当用户发出一个请求,服务能在一定的时间内返回结果,响应可终止、不会一直等待。

Partition tolerance:分区容错性即可靠性。可靠性的量化指标是周期内系统平均无故障运行时间. 即使有些消息延迟或者无法到达,并不影响系统的整体运行。简而言之,在网络分区的情况下,被分隔的节点仍能正常对外服务。

和所有分布式系统一样,区块链共识算法设计也是在权衡上面的三个因素,假如区块链中节点能立即确认交易数据,好处是不依赖其他节点立即可用,满足了 CAP 理论中的 AP,可风险是失去了强一致性,其他节点可能丢弃这个区块,因为区块所在的区块链分叉在竞争性的选举中失败了[5]。 为了获得 CP,客户端应该等待区块链大多数节点接受了这笔交易在真正接受它, 说明这笔交易所在分叉已经选举胜利,获得大部分共识,获得了强一致性,但是风险是可能unavailable ,丧失 CAP 的 A,因为网络分区通信等问题可能阻止这种共识。

研究定位

区块链系统是一个将交易数据正确地固化在分布式节点上的系统。共识算法为了解决如何更安全有效的将交易数据写入到区块链上,本质上讲,共识算法旨在解决以下问题:

  • 哪个服务节点有权利生成下一个用新区块?
  • 上一个区块与下一个区块之间应如何衔接
  • 下一个区块什么时间产生?
  • 区块中应该包含了哪些内容?
  • 区块的大小是多少,一个区块中包含多少交易数据?
  • 确认机制如果解决区块链分叉的问题?

本文档从多个角度分析不同共识算法关于以上问题的解决方案,旨在为将来实际算法设计提供相关理论参考,分析方法为以下两点:

  • 纵向分析:我们以一个交易的被确认的完整过程,勾画出整个区块链系统的工作过程,纵向的分析共识算法在整个区块链系统中所扮演的角色。
  • 横向对比:我们陈列出当前加密货币中常用的共识算法,如 POW,POS,DPOS,PBFT 等,然后从算法的一致性,容错性,网络组织情况等方面进行对比分析。

纵向定位分析

研究共识机制旨在设计更安全,高效的区块产生方案。为了让读者更加清晰的认识共识算法在整个区块链中所扮演的角色,在本章中我们勾画出区块产生的完整周期,并用以比特币的例子详细的讲解区块产生的过程。

图 2. 交易数据在区块链中被确认的过程

图 2 中展示了交易数据在区块链中一个完整的流转过程,在起始阶段,交易信息被客户端组装, 其中交易信息包含了交易的输入金额,输入账户信息和输出账户信息等,客户端可以被认为是 全节点钱包,轻钱包和各大交易平台。在一个完整的交易被生成后被称为“原始交易(Raw

Transaction)”。 原始交易并不能被矿机接收,因为缺乏相应转账人的签名。在转账人签名完成后允许将其广播到区块链系统中,矿机采集相关交易后,经过共识算法将交易数据打包并确认到对等网络中的其他节点上。下面我们以比特的例子详细阐述以上过程。

交易数据的组装

假设用户 A 给用户 B 进行转账,用户 A 的的公钥为 Pk_a,私钥为 Pr_b, 用户 B 的的公钥为Pk_b,私钥为 Pr_b. 我们按照表 1 给出的协议一步一步的给出最终可广播的内容。备注: 以下数据均为十六进制表示,我们采用比特币中最常用的 Pay-to-PubkeyHash 进行分解。经过客户端的数据组合,我们展示一个完整的交易协议如下,其中输入数据 inputs 数据可以从UTXO(Unspent Transaction Output,未开销的比特币交易输出)中获取。

表 1. 比特币原始区块链交易协议

Version (版本) 01000000
Input count (输入长度) 01 |
inputs previous output hash (上一个脚本的 hash) be66e10da854e7aea9338c1f91cd4897 68d1d6d7189f586d7a3613f2a24d5396 |
inputs previous output index (上一个交易的索引) 00000000 |
inputs scriptSig length (表示脚本的长度) 19 |
inputs scriptSig(脚本签名,实际此部分为脚本的前半部分) 76a914010966776006953d5567439e5e 39f86a0d273bee88ac |
inputs Sequence (序列) ffffffff |
outputs count (输出长度) 01 |
outputs Value (需要转出的比特币的值,上面的输入的值减去) 605af40500000000 |
outputs script length (表示脚本的长度) 19 |
outputs script(脚本签名,实际此部分为脚本的前后半部分) 76a914097072524438d003d23a2f23ed b65aae1bb3e46988ac |
lock time (锁定时间) 00000000 |

交易数据的签名

交易数据的完成组装后并不能立即被矿机所接受,因为交易的输出方并没有对其进行有效的签名,我们用 sha256 整体对上面的数据的 hash 进行签名,我们假设发送者的公钥是 Pk_a, 签名后的结果为 Sig_a. 为更好的理解签名后在区块链中执行的过程,我们将上面 inputs 中的scriptSig 进行分解76a914010966776006953d5567439e5e39f86a0d273bee88ac 分解后的内容如下表格,表 2. 未签名的 ScriptSig 数据格式分解,备注数字与操作符的对应关系可以通过https://en.bitcoin.it/wiki/Script 查询到,ScriptSig 格式如下:

OP_DUP 76
OP_HASH160 a9 |
length 14 |
pubKeyHash 010966776006953d5567439e5e39f86a0d273bee |
OP_EQUALVERIFY 88 |
OP_CHECKSIG ac |

经过签名之后我们将签名后的数据衔接在 ScriptSig 上面,因此最终的 ScriptSig 变成如下格式。表 3. 签名后的 ScriptSig 数据格式分解。

Sig_a Sig_a.
Pk_a Pk_a |
OP_DUP 76 |
OP_HASH160 a9 |
pubKeyHash 14010966776006953d5567439e5e39f86a0d273bee |
OP_EQUALVERIFY 88 |
OP_CHECKSIG ac |

比特币的区块链中采用的是堆栈式的语言,ScriptSig 的执行过程描述如下 :

堆栈 脚本 描述
Sig_a \ Pk_a \| OP_DUP \| OP_HASH160 \| pubKeyHash \| OP_EQUALVERIFY \| OP_CHECKSIG | 将 Sig_a 和 Pk_a 抛出 |
Sig_a \ Pk_a OP_DUP \| OP_HASH160 \| pubKeyHash \| OP_EQUALVERIFY \| OP_CHECKSIG | 将常量加入到堆栈中 |
Sig_a \ Pk_a\ Pk_a | OP_HASH160 \| pubKeyHash \| OP_EQUALVERIFY \| OP_CHECKSIG | OP_DUP 作用是复制 Pk_a, 目前状态堆栈中有两个 Pk_a |
Sig_a \ Pk_a\ Pk_a_hash | pubKeyHash \| OP_EQUALVERIFY \| OP_CHECKSIG | OP_HASH160 的作用是计算出最顶层 stack 的 hash |
Sig_a \ Pk_a\ Pk_a_hash \|pubKeyHash | OP_EQUALVERIFY \| OP_CHECKSIG | 将 pubKeyHash 推入堆顶 |
Sig_a \ Pk_a OP_CHECKSIG OP_EQUALVERIFY 是检查栈顶的两个值是否相同
true OP_CHECKSIG 作用是检查栈顶的签名是否正确,正确则返回 true

交易数据的广播

在原始交易组装完成后,讲交易进行广播出去。非严格意义上讲,消息广播出去分为两种形式:直接调用 API,自身加入 P2P 节点。

交易数据打包确认

交易数据的打包和确认非正式术语称为“挖矿”,进行挖矿之前,首先要将交易合并在区块中, 区块对于交易的数据打包采用的 Merkle tree 算法。将多个交易的 hash 合并到树中,然后将Tree 的树根合并到块中。

需要说明的是在区块中不仅仅含有 Merkle root ,还有其他的辅助信息如图 4 所示。共识机制作用于此部分,共识算法旨在将上面的 Merkle Root 所在的区块衔接在上一个区块中,不同的区块链产品所采用的共识算法不同,我们将在下面的章节中选取典型的算法进行分析。

纵向分析总结

宏观上讲,共识算法作用于图 4 中的打包确认阶段,共识算法负责将交易数据打包到新的区块中,同时负责将该区块衔接到之前的链上。微观上从服务节点的角度上讲,共识算法包括 4 个阶段,如下图 5 所示,分发阶段,验证阶段,挖矿阶段,宣布阶段。矿机在分发阶段进行对交易进行收集,验证阶段开始验证交易的正确性,经过验证的交易在挖矿阶段进行确认,然后在T5 阶段进行下一轮的共识。

横向对比分析

在本章中我们选取了的业界常用的共识算法进行分析,这些共识算法包括工作量证明POW, 权益证明 POS, 授权股权证明 DPOS, 瑞波共识算法 RC 和用于 Hyperledger 的拜占庭算法 PBFT。在工作量证明 POW 中我们会以 Bitcoin 和 Ethereum 中不同的 POW 做阐述;权益证明 POS 主要以点点币为代表进行分析;授权股权证明 DPOS 分别以Bitshares 和 Casper 算法进行讲解;瑞波共识算法 RC 和 拜占庭算法 PBFT 的分析依附于瑞波加密货币和 Hyperledger。同时在本章节的最后我们会从网络组织,算法的效率和货币的发行机制等多个方面进行横向分析。

工作量证明 POW

工作量证明 POW(Proof-of-work)最早由 Markus Jakobsson 在反垃圾邮件系统实现中提出[6]。反垃圾邮件系统能够使垃圾邮件发送者需要更多的时间来发送邮件,就可以增 大他们的成本, 起到抵挡攻击的作用。2008 年被中本聪在论文《a peer to peer electronic cash system》[1] 中再次提及并使用,其设计理念是整个系统中每个节点为整个 系统提供计算能力(简称算力),通过一个竞争机制,让计算工作完成最出色的节点获得系统的奖励, 完成新生成货币的分配。

目前采用 POW 的算法代表有 Bitcoin 和 Ethereum(早期版本),他们虽然同时都成为POW,并都采用全节点竞争的方式对交易进行确认,但算法本质却截然不同。我们下面将对两个系统不同的 POW 算法进行分析。

Bitcoin的POW 算法分析

下面我们采用一个例子来描述在比特币中挖矿的过程。 上面的区块产生的过程章节中已经对下面的参数进行了交代,黄色的部分是块头,他将随着交易一起被被打包到区块链,第一个交易称为 coinbase , coinbase 是用来奖励矿工的,它的具体的工作原理是coinbase收敛“正常交易”中的交易费组成一个新的交易,然后交易指向矿工的地址“正常交易”只指用来转移比特币用的交易。

图 6. 比特币区块结构图

Bitcoin 的 POW 核心机制是找 hash 碰撞, 从上面的分析我们知道,区块链是一个持续增长的顺序块组成的,区块链是密码上的安全,对于每一轮只要找到相应的 hash 的碰撞就算成功, hash 碰撞的意思可以理解为 hash 值的前多少位相同,我们知道何难找到两个 hash 一模一样的文件,但我们可以找到前几位相同的,我们将一个完整的挖矿过程整理如下:

f(Di)>SHA256(SHA256(Hi−1||Ti||TXi||di||Ni)))

Hi−1 表示上一个区块的 HASH,Ti 表示时间戳,di 表示本轮的难度,Ni 表示需要找出的随机数。我们从下图 7(比特币的官网上截取)可以清晰的看出上一个区块和下一个区块之间的关系。直观上讲,下一个区块比上一个区块前面多个一个 0,就是前 N 位对撞成功。

Ethereum 的 Pow 算法分析

以太坊 Etherum 目前采用的是 Ethash 算法,最初设计的目标是“GPU 友好,阻断 ASIC”鼓励一个机器一票,抵制大型的集成电路挖矿,Ethash 是基于一个固定的只读数据集的随机路径,受启发与内存限制的工作证明谜题和相关的学术著作。Ethash 所使用的定制随机功能是非标准的,难以进行密码分析,但它们可以进行简单的统计测试。算法的主题思路是从缓存得到固定数据集容易,反之十分困难,完整的算法描述如下:

固定数据集合代码 :

缓存集合代码 :

权益证明机制 POS

POS (Proof of Stake) 即权益证明机制,最早出现在点点币的白皮书中 [7],其核心思想是将货币持有人的数目和持有的时间累计作为被选为共识节点的资本。

协议描述

这种新型区块里 POS 是一种特殊的交易称利息币(coinstake)(依据 BTC 当中的一类特殊交易:币基 coinbase 而命名,如上图 6 中所提及)。在利息币(coinstake) 交易中,区块持有人可以消耗币龄获得利息,同时获得为网络产生一个区块和用 POS 造币的优先权。利息币的第一个输入被称为核心(Kernel),并需要符合某一 Hash 目标协议。由此 POS 区块的产生具有随机性,这一过程与 POW 相似。但有一个重要的区别在 POS 随机散列运算是在一个有限制的空间里完成的(具体来说为 1 hash / 未消费钱包的输出*秒),而不是像 POW 那样在无限制的空间里寻找,因此无需大量的能源消耗。

权益核心(kernel)所要符合的随机散列目标是以在核心中消耗币龄的目标值(币* 天 coin-day) 这与 BTC 的 POW 是不同的,BTC 的每个节点都是相同的目标值。 因此核心消耗的币龄越多, 就越容易符合目标协议。

点点币源码地址:https://github.com/peercoin/peercoin/blob/master/src/kernel.cpp

授权股权证明机制 DPOS

授权股权证明机制 (Delegated Proof of Stake) 是一种新的共识算法,有程序员 Daniel Larimer 提出 [8],旨在优化 POW 和 POS 中的问题,这些问题集中在共识效率和严重集中化上。DPOS 使用技术民主用来抵消集中化的负面影响。

解决集中化的问题

授权股权证明机制通过使用证人(称为代表)减轻集权化的潜在负面影响。总共 N 名证人签署了这些区块,并由分散在 P2P 网络的节点进行投票,并进行了每一笔交易。通过使用分散的投票程序,DPOS 的设计比同类系统更加民主。每个被签名的区块在被接收信任节点签名之前都要被检验。

解决共识效率的问题

DPOS 消除了在确定事务之前必须等待一定数量的不可信节点进行验证的过程。这减少了对确认的需求,提高了交易速度。通过网络决定,通过有意向最可信赖的潜在的块签名者进行信任, 不需要施加人为的负担来减缓块签名过程。 DPOS 允许将多个事务包含在块中,而不是工作证明或证据证明系统。DPOS 系统中的每个客户端都有能力决定谁是信任的,而不是将信任集中在资源最多的人手中。在授权的证据证明系统集权仍然发生,但它是受控制的。与其他保护密码安全网络的方法不同,

DPOS 系统中的每个客户端都有能力决定谁是信任的,而不是将信任集中在资源最多的人手中。DPOS 允许网络获得集中化的一些主要优点,同时仍然保持一些计算的权力下放衡量标准。这个制度是通过公正的选举程序执行的,任何人都可能成为大多数用户的代表。下面我们选取了BitShares 和 Ethereum 的 DOPS 算法进行简要分析。

BitShares 算法分析

BitShares 是第一个提出并采用 DPOS 的分布式账本 [8]。按照它的设计原则分类帐本必须按照正确的顺序进行验证和确认。以保证数据库的的一致性和普遍确认。

协议描述

在现实生活中见证人发挥着中立担保的作用。例如一份重要的合同签证往往需要公共仲裁机构的担保。在 BitShares 系统中,见证人担任着相同的角色,由于它可以验证签名和交易中的时间戳信息。

在 BitShares 系统设计中,利益相关者可以选举一定数量的见证人来生成区块。每个账户允许对每个见证人投一票,这个投票的过程被称为“批准投票”。选择出来的 N 个见证人被认为是对至少 50%的投票利益相关者的代表。每次见证人产生一个区块,见证人将得到一定的奖励,如果见证人因为违规没有生成区块,将不能到奖励,并且会被加入“黑名单”,再次获取 见证人的机会将会大大降低。

每组见证人的活跃状态在每一个周期将会被更新,这个周期的通常设置为 1 天,随后这组见证人将会被解散。每个见证人给一个 2 秒的流转机会用来区块,当所有的见证人被流转完成,改组见证人也会被解散,如果一个见证人在它的时间周期内没有产生区块,他的时间机会将会被错过,下一个见证人将产生下一个区块。任何节点都可以通过观察证人参与率来监控网络健康状况。历史上 BitShares 曾经维持了 99%的见证参与。

代表们以类似证人的方式当选。代表成为特权帐户的共同签署者,该帐户有权提出对网络参数 的更改。这个帐户被称为起源帐户。这些参数包括从交易费用到块大小,见证支付和块间隔的 一切。在大多数代表批准了一项拟议的变更后,利益相关者将获得 2 周的审查期间,在此期间, 他们可以对代表进行投票,并根据建议变更或者取消。选择这种设计是为了确保代表在技术上 不具有直接的权力,所有对网络参数的更改最终都得到利益相关者的批准。这样做是为了保护 代表免受可能适用于加密货币的管理者或管理员的规定。在 DPOS 下,我们可以真正地说, 行政权力由用户掌握,而不是代表或证人。

Casper 算法分析

Casper 是近期 Ethereum 改进型方案。下面我们简单的描述下 Casper 算法。

我们给出以下定义, b(block)表示每个块,c (checkpoint)表示检查点,其中b和c的关系可以表示成下图:

C0 被定义为起始指针,一个“纪元”被定义为两个检查点之间的连续的块序列,这个块序列包括后面的检查点,而不是较早的检查点。块的“纪元“是包含该散列的历元的索引,例如, 区块 599 的纪元为 5。

每个代表都需要提交一定的准备金,和现实的世界一样,这份准备金直接关系到将来的奖励和违规罚金。系统中所提及的 2/3 的验证者,实际并不是 2/3 的节点,而是指拥有 2/3 保证金的节点。代表可以广播两种类型的消息,第一种为“准备消息”显示格式为 <prepare, h, e, h, e, S> , 其中的含义如下图所示。

“准备消息” 的内容描述 :

符号 描述
h 检查点的 hash |
e 检查点的纪元 |
h* 最近调整的 hash |
e* h* 的纪元 |
S 每个代表的签名 |

另一种为“提交消息” 显示格式为 <commit, h, e, S> , 下表“提交消息”的含义描述

符号 描述
h 检查点的 hash |
e 检查点的纪元 |
S 每个代表的签名 |

协议描述

一个检查点 h 满足以下条件是被认为是“调整过的”,这个阶段可以认为是选举的阶段。

  • 2/3 的准备金持有代表已经发出 <prepare, h, e, h, e, S>消息
  • h 自身被调整

一个检查点 h 满足以下条件是被认为是“被确认成功的”。这个阶段可以认为是共识阶段。

  • h 被调整
  • 2/3 的准备金持有代表已经发出<commit, h, e, S>消息

需要指出的是调整 h 时候有超过 2/3 代表的消息中必须含有相同的 h*. 同时起始指针 C0 默认为是已经调整和已经确认过的。

协议规定

Casper 的创新点在于不可能两个冲突的检查点同时被确认,除非有超过 1/3 的人违反了规定, 规定的内容如下:

  • 代表不得在同一纪元发布两个或多个不一致的准备消息。换句话说,一个代表在一个纪元只能发布一个准备消息。
  • 代表不能在同一个纪元发布确认消息早于准备消息。言外之意,确认消息一定晚于准备消息。

如果代表违反了上面的协议,准备金将全部被没收,用来奖励发现它违规的见证人。我们给出一个理想的例子,在纪元 n 期间,所有的代表准备好 Cn h* = Cn-1 同时提交了 Cn。

瑞波共识机制 RCA

RCA 即瑞波公式算法(ripple consensus algorithm),在分布式支付系统总是会出现由于网络中所有节点同步通信的要求导致节点遭受高延迟的问题 [9] 。瑞波共识算法通过在较大的网络中利用集体可信的子网来解决这些问题。下面我们将详细的解读瑞波共识算法.

参数定义

服务节点(Server): 服务节点可以是 P2P 网络中的任意一个,用来参与共识算法。账本

(Ledger):用来记录交易记录的数据库。账本有服务节点在完成共识之后进行维护。最后 一个关闭的账本(Last-Closed Ledger):经过公式算法写入的账本,最近写入的账本,代表了网络中账本的最新状态。活动账本(Open Ledger):每个节点上会维护一个活动账本, 共识的过程就是将活动账本变为最后一个关闭的账本的过程。独特节点列表(Unique Node

List):每个服务节点都会维护这一组服务节点列表,服务列表中的服务节点是被认为是将来有能力进行选举算法的, 我们可以认为这些服务节点组成的网络是可信网络。提案人

(Proposer):任何服务器都可以广播交易,在一次交易循环开始的时候,每个服务器都尝

试着确认有效的交易,但是在这个确认过程中,只有服务上的独特节点列表的服务器确认的才有效。

协议描述

第一步:所有的服务节点采集有效的交易,并将这些交易打包成一个“候选交易集合”。

第二步:每个服务节点上的 UNL 合并“候选交易集合”,并验证“候选交易集合”的真实性。

第三步:候选交易集合中的集合只有在收集到 UNL 足够的认同后才能被选中,如果没有获得足够的认同,将被丢弃或者作为新一轮的候选交易。

第四步: 一个交易需要 80%的服务节点上的 UNL 同意,所有满足要求的交易才允许挂载到链上。一旦挂载成功,当前的链自动关闭。

详细过程

如下图所示:每个区块中都包含了账本序号,账户信息,交易信息,时间戳等信息。整个账本每隔几秒会生成一个新的账本,一个新的账本用账本序号进行标识。假如当前的账本的序号为

N,则前一个账本的序号为 N-1,下一个账本的 N+1。

瑞波网络由接受和处理事务的分布式服务器(称为节点)和客户端应用程序组成。客户端应用程序向节点进行签名和发送交易,客户端应用程序包括钱包和金融机构的电子交易平台。

接收,中继和处理事务的节点可以是跟踪节点或验证节点。跟踪节点的主要功能包括分发来自客户端的事务和响应关于账本的查询。验证节点执行与跟踪节点相同的功能,并另外有助于推进账本序列。在接受客户应用程序提交的交易时,每个跟踪节点使用最后一个验证的账本作为起点。

网络上的节点共享关于候选交易的信息。通过协商一致的过程,验证节点就下一个账本考虑的候选交易的特定子集达成一致。共识是一个迭代过程,其中节点转发提案或一组候选事务。节点沟通和更新建议,直到超过多个对等节点(80%以上,可调整)同意一组候选交易。这个 过程决定了哪些交易被接受,哪些交易被抛弃或者被载入到下一轮提案中。在交易选择过程中付出交易费用更多的节点更容易被接受。

当一组交易完成的时候,交易蔓延到不同的节点并进行签名,当收集到足够多的节点的时候, 当前的备选交易将被打包到区块链中。如果网络节点在交易繁忙或者交易共识将很难被达成, 此时算法可以自主调节交易费用和等待时间。

总结

我们将一个完整的交易周期总计如下:

步骤 描述
1. 一个交易被创建并且被签名 |
2. 交易信息被提交到网络中 错误的格式的交易,将直接被拒绝 正确格式的数据暂时会被拒绝,可能下一轮 会被确认。 |
3. 经过共识算法,交易将被打包到账本中 验证成功的交易被加入到账本中。 |

瑞波共识算法面临中心化的问题,使一组节点能够基于特殊节点列表达成共识。初始特殊节点列表就像一个俱乐部,要接纳一个新成员,必须由 51%的该俱乐部会员投票通过。共识遵循这核心成员的     51%权力,外部人员则没有影响力。由于该俱乐部由“中心化”开始,它将一直是“中心化的”,而如果它开始腐化,股东们什么也做不了。与比特币及点点币一样,瑞波  系统将股东们与其投票权隔开,并因此比其他系统更中心化。

拜占庭共识机制 PBFT

PBFT(Practical Byzantine Fault Tolerance),意为实用拜占庭容错算法,是目前最常用的 BFT 算法之一。该算法是 Miguel Castro (卡斯特罗)和 Barbara Liskov(利斯科夫)在

1999 年提出来的,解决了原始拜占庭容错算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用中变得可行。下面我们来分析这个算法。

参数定义

  • client:客户端,发出调用请求的实体
  • view:视图,内容为连续的编号
  • replica:网络节点
  • primary:主节点,负责生成消息序列号
  • backup:支撑节点,辅助整体共识过程
  • state:节点状态

协议描述

PBFT 算法要求整个系统流程要在同一个视图(view)下完成,所有节点采取一致的行动。一个客户端会发送请求<REQUEST, o ,t, c>给 replicas。其中,o 表示具体的操作,t 表示

timestamp,给每一个请求加上时间戳,这样后来的请求会有高于前面的时间戳。Replicas

接收到请求后,如果验证通过,它就会将其写入自己的 log 中。在此请求执行完成后,

replicas 会返回 client 一个回复 <REPLY,v,t,c,i,r>,其中:v 是当前的 view 序号,t 就是对应请求的时间戳,i 是replica 节点的编号,r 是执行结果。每一个replica 会与每一个处于active 状态的 client 共享一份秘钥。秘钥所占据空间较少,加上会限制 active client 的数量,所以不必担心以后出现的扩展性问题。

PBFT 采用三阶段协议来广播请求给 replicas : pre-prepare, prepare, commit 。pre-prepare 阶段和 prepare 阶段用来把在同一个 view 里发送的请求排序,然后让各个 replicas节点都认可这个序列,照序执行。prepare 阶段和 commit 阶段用来确保那些已经达到commit 状态的请求即使在发生视图改变(view change, 之后会提到)后,在新的 view 里依然保持原有的序列不变,比如一开始在 view 0 中,共有 req 0, req 1, req2 三个请求依次进入了 commit 阶段,假设没有恶意节点,那么这四个 replicas 即将要依次执行者三条请求并返回给Client。但这时主节点问题导致 view change 的发生,view 0 变成 view 1,在新的 view 里,原本的 req 0,req1, req2 三条请求的序列将被保留。但是处于 pre-prepare 和 prepare 阶段的请求在 view change 发生后,在新的 view 里都将被遗弃。

pre-prepare 阶段

主节点收到来自 Client 的一条请求并分配了一个编号给这个请求,然后主节点会广播一条<<PRE-PREPARE,v,n,d>, m>信息给备份节点,这里 v 是视图编号,m 是客户端发送的请求消息,d 是请求消息 m 的摘要(digest)。该信息会送达到每一个备份节点,收到信息的备份节点会进行一系列验证,验证通过后会 accept 这条 PRE-PREPARE 信息。验证内容主要为:

  • 请求和预准备消息的签名正确,并且 d 与 m 的摘要一致。
  • 当前视图编号是 v。
  • 该备份节点从未在视图 v 中接受过序号为 n 但是摘要 d 不同的消息 m。
  • 预准备消息的序号 n 必须在水线(watermark)上下限 h 和 H 之间(水线存在的意义在于防止一个失效节点使用一个很大的序号消耗序号空间)。

当一个备份节点 accept 了这条 PRE-PREPARE 后,它就会进入下面的 prepare 阶段。

prepare 阶段

一个备份节点进入到自己的 prepare 阶段后,开始将一条信息<PREPARE,v,n,d,i>,广播给主节点和其它的备份节点,与此同时,该备份节点也会收到来自其它备份节点的 PREPARE 信息。该备份节点将验证消息的签名是否正确,视图编号是否一致,以及消息序号是否满足水线限制, 然后综合验证信息做出自己对编号 n 的最终裁决。如果验证通过,则把这个准备消息写入消息日志中。当一个备份节点收到来自至少 2/3 个节点的准备消息,并且验证请求消息一致时,那么我们就说该请求在这个节点上的状态是 prepared, 同时该节点也拥有了一个证书叫prepared certificate 。

commit 阶段

紧接着 prepare 阶段,当一个 replica 节点发现有一个 quorum 同意编号分配时,它就会广播一条 COMMIT 信息给其它所有节点告诉他们它有一个 prepared certificate 了。与此同时它也会陆续收到来自其它节点的 COMMIT 信息,如果它收到了至少 2/3 条 COMMIT 后,我们就说该节点拥有了一个叫 committed certificate 的证书, 请求在这个节点上达到了committed 状态。此时只通过这一个节点, 我们就能断定该请求已经在一个有效团体(quorum)中到达了 prepared 状态,即一个有效团体的节点们都同意了编号 n 的分配。当请求 m 在一个节点中到达 commited 状态后,该请求就会被该节点执行。

横向分析小节

本章将从三个角度进行总结:货币控制权,货币发行机制,以上提及算法的综合对比。

控制权的争夺

比特币的设计之初,系统默认节点和算力是均匀分布的,因为通过 CPU 来进行投票,拥有钱包(节点)数和算力值应该是大致匹配的,每一个比特币钱包的拥有者都能够参与整个系统的决策机制, 如果有任何人试图对系统作恶,或者某一部分节点收到损失,都可以让其他节点迅速补上,并且只要有 51%的节点(算力)投票就可以选择对系统发展更有利的方向。

在实际操作中 POW 的主要问题是算力过于集中的安全风险,这种风险体现在比特币的控制权上, 挖矿的人和持有比特币的人已经完全被隔开,许多矿工可能完全不了解比特币的生态,甚至不关心比特币的未来,却拥有对比特币的绝对控制权,因为他们是新币产生的起始点。一种极端的想法,如果几个大型的矿池联合在一起,那么最新发行的币将囤积,会造成原有币种的进一步通货紧缩。简而言之,比特币的命运掌握在并不一定关心比特币命运的人手上,而持有比特币的人并没有控制权。

这就有点像,一个公司的命运并不是那些持有公司股份的股东来决定的,而是那些有可能根本不拥有股份,而只要有钱的人来决定的。那些持有比特币的人完全无法对比特币的未来做出自己的决定。我们仿佛从中本聪设定的一 CPU 一票的文明世界,一下子沦为纯粹是靠蛮力,看谁力气更大的原始社会。

DPOS 机制似乎又重新把权利归还到那些持有数字货币的人手上。DPOS 机制是让每一个持有 BTS 的人对整个系统资源当代表的人进行投票,而获得最多票数的 101 个代表进行交易打包计算。这个可以理解为 101 个矿池,而这 101 个矿池彼此的权利是完全相等的。那些握着 BTS 选票的人可以随时通过投票更换这些代表(矿池),只要他们提供的算力不稳定,计算机宕机、或者试图利用手中的权力作恶,他们将会立刻被愤怒的选民踢出整个系统,而后备代表可以随时顶上去。

货币发行机制

总体而言,目前的共识算法所对应的货币发行机制有 3 种:以比特币为代表的通过挖矿产生新货币,以点点币为例子的通过持有者的利息产生新货币和以瑞波币的恒量发行。

POW 的新增机制是“挖矿”,即矿工每完成一定量的计算,有可能获得一块新 block 中的新增比特币。这个过程是一个纯粹的通胀过程,即无中生有新增比特币。但获得新增的比特币有一定的要求,必须全球第一个找出特定的 HASH 值。因此发行机制是算力比例分配的。

POS 和 DPOS 的新增机制是“利息”,即持有一定的 POS 币一定时间,将获得一定量的固定“利息”。这部分“利息”是新增的币。只要你持有 POS 币并开机,你就能获得一定比例的“利息”。因此 POS 体系将新增 POS 币投放社会的机制,其投向是以已有 POS 币等比例增加的。

RCA 所对应的瑞波币为恒量发行,不再增发,总发行量1000 亿个,瑞波币计划最终向外发行75% 的 Ripple 货币供应,并承诺永不增发。用户在进行每次交易时要花费一定的 Ripple 币(金额非常非常低,大约是 1/1000 美分),这个交易费不交给任何人,只是凭空消失。因此 Ripple 币只会越来越少,但减少的速度非常慢。瑞波币是一个不断通货紧缩的过程。

共识算法对比

POW POS DPOS RCA PBFT
一致性 最终一致性 最终一致性 最终一致性 最终一致性 强一致性 |
允许失败的 节点数目 <=25% 根据不同的算 法而定 <=33% <=20% <=33% |
网络扩展性 低 |
区块链类型 无权限 无权限 无权限 无权限 有权限 |
交易速度 极快 慢 |

结论

区块链的共识算法是加密货币的核心,良好的区块链算法可以安全高效的解决分布式加密货币系统中的“双花”和交易数据一致性的问题,本文从纵向定位分析和横向分析对比分析多角度研究了不同加密货币系统中共识算法,旨在为区块链项目设计自己的共识算法提供理论参考和设计依据。

引用

[1]. S. Nakamoto, “Bitcoin: A peer-to-peer electronic cash system,” Oct. 2008

[2]. “Blockchain.” Wikipedia. October 15, 2017. Accessed October 17, 2017. https://en.wikipedia.org/wiki/Blockchain.

[3]. RuJia . “【区块链技术系列】 区块链共识机制总结(上).” Ehcoo. October 09, 2017. Accessed October 17, 2017. http://www.ehcoo.com/blockchain_confirmation_mechanism.html.

[4]. Towards Robust Distributed Systems, Eric Brewer, 2000

[5]. Anon, (2017). [online] Available at: https://kknews.cc/finance/5m4ye5k.html [Accessed 17 Oct. 2017].

[6]. Jakobsson, Markus, and Ari Juels. “Proofs of Work and Bread Pudding Protocols(Extended Abstract).” Secure Information Networks, 1999, 258-72. doi:10.1007/978-0-387-35568-9_18.

[7]. Sunny King, Scott Nadal . PPCoin: Peer-to-Peer Crypto-Currency with Proof-of-Stake. August 19th, 2012

[8]. “Delegated Proof of Stake.” Graphene Documentation. Accessed October 17, 2017. http://docs.bitshares.org/bitshares/dpos.html.

[9]. David Schwartz, Noah Youngs, Arthur Britto. The Ripple Protocol Consensus Algorithm.2014

[10]. “The XRP Ledger Consensus Process.” Ripple. Accessed October 17, 2017. https://ripple.com/build/xrp-ledger-consensus-process/#the-xrp-ledger-protocol-consensus-and- validation.

[11]. 梧桐树. “共识算法 区块链实用手册.” 区块链实用手册. Accessed October 17, 2017. http://wutongtree.github.io/hyperledger/consensus.

感谢HPB团队整理。

HPB49:P2P网络数据交互

P2P网络数据交互

1. 发送交易数据SendTransactions

事件触发交易广播txBroadcastLoop

本地发送了一个交易,或者是接收到别人发来的交易信息。 txpool会产生一条消息,消息被传递到txCh通道。然后被goroutine txBroadcastLoop()处理, 发送给其他不知道这个交易的peer。

ProtocolManager在Start的时候,订阅TxPreEvent并启动txBroadcastLoop协程监听事件。

当监听到事件后,调用BroadcastTx进行广播,广播按照委员及候选委员,接入节点,轻节点逐层广播。

发送交易之前,会把tx.Hash放到peer的knownTxs中:

新连接建立txsyncLoop

txsyncLoop负责每个新连接的初始事务同步。 当新的peer出现时,我们转发所有当前待处理的事务。

在txsyncLoop函数中定义了一个send函数来广播交易信息:

2. 发送区块哈希值SendNewBlockHashes

广播挖矿区块 NewMinedBlockEvent

ProtocolManager在Start的时候,订阅NewMinedBlockEvent并启动 minedBroadcastLoop()协程监听事件。

监听到事件后,开始广播区块信息。

先根据BroadcastBlock输入的参数propagate决定是否广播区块,当propagate为true时,广播区块信息。之后开始广播区块哈希。

广播时,先把hash放到knownBlocks里面,在广播区块和区块哈希

基于块通知的同步Fetcher

Fetcher Start函数中启动协程:

Fetcher模块的queue里面缓存了已经完成fetch的block,等待按照顺序插入到本地的区块链中。优先级别就是他们的区块号,这样区块数小的排在最前面。最后调用insert方法把给定的区块插入本地的区块链。

在insert函数中,有两处广播:一是如果区块头通过验证,那么马上对区块进行广播;二是如果插入成功, 那么广播区块,第二个参数为false,那么只会对区块的hash进行广播。

定时同步syncer

syncer中会定时的同BestPeer()来同步信息: 当有新的Peer增加的时候 会同步, 这个时候可能触发区块广播; 定时触发 10秒一次。

3. 发送区块内容SendNewBlock

参照SendNewBlockHashes的处理流程。

4. 发送区块头信息SendBlockHeaders

在通过握手后runPeer时,会运行protocol的run函数,接着调用startProtocols函数,进而进入NewProtocolManager的时候定义的Run,每一个SubProtocols都有一个Run。

这个run方法首先创建了一个peer对象,然后调用了handle方法来处理这个peer。注意,这里的peer区别于p2p中的peer,但是它包含p2p的peer。

在handle最后,循环调用handleMsg, 这个方法很长,主要是处理接收到各种消息之后的应对措施。

对于GetBlockHeadersMsg的消息处理,结果调用SendBlockHeaders返回给对端:

首先解码msg,解析出getBlockHeadersData结构体。

查找方式:

从Hash指定的开始朝创世区块移动,也就是反向移动。

从Hash指定的开始正向移动。

通过Number反向查找。

通过Number正向查找。

查找结果发给对端:

5. 发送区块体信息SendBlockBodies

没有用到。

6. RLP编码发送区块体信息SendBlockBodiesRLP

调用流程参考“4 发送区块头信息SendBlockHeaders”。

收到GetBlockBodiesMsg,解析msg信息,组织bodies并发送给对端。

7. 发送节点信息SendNodeData

调用流程参考“4 发送区块头信息SendBlockHeaders”。

GetNodeDataMsg对应的协议版本要大于等于eth63。

8. RLP编码发送节点信息SendReceiptsRLP

调用流程参考“4 发送区块头信息SendBlockHeaders”。

9. 请求一个区块头RequestOneHeader

调用流程参考“4 发送区块头信息SendBlockHeaders”。

10. 通过Hash请求区块头RequestHeadersByHash

首先,在协议初始化的时候,调用protocolManager.Start

之后启动syncer(), syncer中会定时的同BestPeer()来同步信息: 当有新的Peer增加的时候 会同步; 定时触发 10秒一次同步。

)

pm.synchronise会调用中 Downloader中的同步函数。 Synchronise试图和一个peer来同步,如果同步过程中遇到一些错误,那么会删除掉Peer。然后会被重试。

最后,在syncWithPeer中会启动几个fetcher 分别负责header,bodies,receipts处理。spawnSync给每个fetcher启动一个goroutine, 然后阻塞的等待fetcher出错。

在fetchHeight中,会发出RequestHeadersByHash请求。

fetchHeaders方法用来获取header。 然后根据获取的header去获取body和receipt等信息。fetchHeaders不断的重复这样的操作,发送header请求,等待所有的返回,直到完成所有的header请求。

11. 通过Number请求区块头RequestHeadersByNumber

调用流程参考“10 通过Hash请求区块头RequestHeadersByHash”。

12. 请求区块体RequestBodies

调用流程参考“10 通过Hash请求区块头RequestHeadersByHash”。

13. 请求收据RequestReceipts

调用流程参考“10 通过Hash请求区块头RequestHeadersByHash”。

14. 请求节点信息RequestNodeData

在创建Downloader的时候,会同时启动协程 startFetcher,进而启动runStateSync。

15. 握手Handshake

head是当前的区块头,genesis是创世区块的信息,只有创世区块相同才能握手成功。如果接收到任何一个错误(发送,接收),或者是超时,那么就断开连接,握手失败。

readStatus,检查对端返回的各种情况。

感谢HPB团队整理。

HPB48:P2P网络数据处理流程

P2P网络数据处理流程

监听(ListenLoop)+拨号(Dial) –> 建立连接(SetupConn) –> Enc 握手(doEncHandshake) –> 协议握手(doProtoHandshake) –> 添加Peer Addpeer –> Run Peer

1. Enc握手 doEncHandshake

监听时接收到Enc握手:receiverEncHandshake

拨号时发起初始End握手:initiatorEncHandshake

链接的发起者被称为initiator(主动拨号),链接的被动接受者被成为receiver(被动监听)。 这两种模式下处理的流程是不同的,完成握手后, 生成了一个sec可以理解为拿到了对称加密的密钥。 然后创建了一个newRLPXFrameRW帧读写器,完成加密信道的创建过程。

initiatorEncHandshake 和receiverEncHandshake有些像,但逻辑处理是相反的过程。

makeAuthMsg

makeAuthMsg这个方法创建了handshake message。 首先对端的公钥可以通过对端的ID来获取。对端的公钥对于发起者来说是知道的;对于接收者来说是不知道的。

  • 根据对端的ID计算出对端公钥remotePub
  • 生成一个随机的初始值initNonce
  • 生成一个随机的私钥
  • 使用自己的私钥和对方的公钥生成的一个共享秘密
  • 用共享秘密来加密这个initNonce
  • 这里把发起者的公钥告知对方

这一步,主要是构建authMsgV4结构体。

sealEIP8

sealEIP8对msg进行rlp的编码,填充一下数据,然后使用对方的公钥把数据进行加密。

readHandshakeMsg

readHandshakeMsg有两个地方调用: 一个是在initiatorEncHandshake,另外一个就是在receiverEncHandshake。 这个方法比较简单, 首先用一种格式尝试解码,如果不行就换另外一种。基本上就是使用自己的私钥进行解码然后调用rlp解码成结构体。 结构体的描述就是authRespV4,里面最重要的就是对端的随机公钥。 双方通过自己的私钥和对端的随机公钥可以得到一样的共享秘密。 而这个共享秘密是第三方拿不到的。

secrets

secrets函数是在handshake完成之后调用。它通过自己的随机私钥和对端的公钥来生成一个共享秘密,这个共享秘密是瞬时的(只在当前这个链接中存在)。

这个函数计算出IngressMAC和EgressMAC用于rlpxFrameRW中ReadMsg,WriteMsg数据的接收发送。

数据帧结构

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
normal = not chunked

  chunked-0 = First frame of a multi-frame packet

  chunked-n = Subsequent frames for multi-frame packet

  || is concatenate

  ^ is xor

 

Single-frame packet:

header || header-mac || frame || frame-mac

 

Multi-frame packet:

header || header-mac || frame-0 ||

[ header || header-mac || frame-n || ... || ]

header || header-mac || frame-last || frame-mac

 

header: frame-size || header-data || padding

frame-size: 3-byte integer size of frame, big endian encoded (excludes padding)

header-data:

    normal: rlp.list(protocol-type[, context-id])

    chunked-0: rlp.list(protocol-type, context-id, total-packet-size)

    chunked-n: rlp.list(protocol-type, context-id)

    values:

        protocol-type: < 2**16

        context-id: < 2**16 (optional for normal frames)

        total-packet-size: < 2**32

padding: zero-fill to 16-byte boundary

 

header-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest

 

frame:

    normal: rlp(packet-type) [|| rlp(packet-data)] || padding

    chunked-0: rlp(packet-type) || rlp(packet-data...)

    chunked-n: rlp(...packet-data) || padding

padding: zero-fill to 16-byte boundary (only necessary for last frame)

 

frame-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ right128(egress-mac.update(frame-ciphertext).digest))

 

egress-mac: h256, continuously updated with egress-bytes*

ingress-mac: h256, continuously updated with ingress-bytes*

2. 协议握手doProtoHandshake

这个方法比较简单,加密信道已经创建完毕。 我们看到这里只是约定了是否使用Snappy加密然后就退出了。

在这个函数,发送给对方 handshakeMsg = 0x00,在readProtocolHandshake中读取接收对方发过来的handshakeMsg。

3. RLPX 数据分帧

在完成Encode握手之后,调用newRLPXFrameRW方法创建rlpxFrameRW对象,这的对象提供ReadMsg和WriteMsg方法

ReadMsg

)

1读取帧头header

2 验证帧头MAC

3 获取帧体Frame大小

4 读取帧体数据

5 验证帧体MAC信息

6 解密帧体内容(NewCTR à XORKeyStream)

7 解码帧体(RLP Decode)

8 解析帧体结构(msg.Size & msg.Payload)

9 snappy解码

WriteMsg

1 RLP编码msg.Code

2 如果snappy,就对读取payload并进行snappy编码

3 写帧头header (32字节)

4 写帧头MAC

5 写帧体信息(ptype+payload+padding)

6 写帧体MAC

4. runPeer

newPeerHook,建立peer的钩子函数

广播PeerEventTypeAdd事件

运行protocol

广播PeerEventTypeDrop事件

删除peer

run protocol

1 启动协程readLoop,读取消息并根据msg.Code处理消息:

pingMsg->pongMsg

discMsg->RLP解码msg.Payload返回reason

其他协议消息处理,根据msg.Code的取值范围,把msg分给注册的协议进行处理。

2 启动协程pingLoop

根据pingInterval(15秒)定时发送pingMsg消息

3 启动协议

startProtocols主要功能是启动协程运行注册协议的run函数proto.Run(p, rw),这个rw参数类型是protoRW,它实现的ReadMsg和WriteMsg增加msg.Code取值范围的处理。不同的protocol有不同的code取值范围,根据offset和Length确定。

感谢HPB团队整理。

HPB47:ETH-Pow算法分析

1. Ethash 算法

1.1 Ethash

Ethash是以太坊1.0中使用的PoW(工作量证明)算法,它是Hashimoto算法结合Dagger之后产生的一个变种。它的特点是计算的效率基本与CPU无关,却和内存大小和内存带宽正相关。因此通过共享内存的方式大规模部署的矿机芯片并不能在挖矿效率上有线性或者超线性的增长。

该算法的一般流程如下:

  • 首先根据块信息计算一个种子(seed, c++代码中为seedhash)
  • 使用这个种子,计算出一个16MB的cache数据。轻客户端需要存储这份cache.
  • 通过cache,计算出一个1GB(初始大小)的数据集(DAG),DAG可以理解为是一个完整的搜索空间,全客户端和矿工需要存储完整的DAG,挖矿过程中需要从DAG中重复的随机抽取数据拿去和其他数据计算mixhash,DAG中每个元素的生成只依赖于cache中的少量数据。每到一个新的纪元DAG会完全不一样,并且它的大小也随时间线性增长。
  • 由于仅根据cache就可以使用少量内存快速的计算出DAG中指定位置的数据,所以轻客户端只需要存储cache就可以高效的进行校验。

1.2 内存难解

由于比特币将hash算法作为pow工作量证明的重要手段,后续的各种采用pow的数字货币也延续了这个设计,以SHA256、MD5(MD5后来被证明不具备强碰撞性数字货币一般不用)为代表算法。在设计之初都是算力敏感型,意味着计算资源是瓶颈,主频越高的 CPU 进行 Hash 的速度也越快。这个设计直接导致后来的矿机出现,采用ASIC芯片的矿机更是将这种运算能力成倍提升,更多矿场的出现使得当时的比特币面临算力中心化的威胁。为了限制计算能力的依赖,人们开始寻求新的算法,既然要限制CPU的能力,目光自然投向存储依赖,也就是内存依赖。

​ Hashimoto算法采用IO饱和的策略来对抗ASIC,使内存读取成为采矿过程中的限制因素。

​ Dagger算法使用DAG(directed acyclic graphs 有向无环图)来同时实现内存难解和内存易验证两个特点。 主要原理是,计算每个nonce需要DAG中的一小部分,采矿过程需要存储完整的DAG,禁止每次计算DAG的相应子集,而验证过程是允许的。

1.3 参数定义

WORD_BYTES 4 Word的字节数
DATASET_BYTES_INIT 2**30 1GB Dataset的初始大小 |
DATASET_BYTES_GROWTH 2**23 8MB 每个纪元dataset的增长量 |
CACHE_BYTES_INIT 2**24 16MB Cache的初始大小 |
CHCHE_BYTES_GROWTH 2**17 128KB 每个纪元cache的增长量 |
CACHE_MULTIPLIER 1024 Size of the DAG relative to the cache |
EPOCH_LENGTH 30000 每个epoch的块数 |
MIX_BYTES 128 Mix的宽度 |
HASH_BYTES 64 Hash的长度 |
DATASET_PARENTS 256 每个数据集元素的parents数量 |
CACHE_ROUNDS 3 计算cache时的轮数 |
ACCESSES 64 Hashimoto循环的次数 |

2 DAG

DAG是ethash算法中需要频繁访问的数据集,这个为每个epoch生成的。DAG要花很长时间生成,如果客户端至少按照需要生成它,那么在找到新epoch第一个区块之前,每个epoch过渡都要等待很长时间。然而,DAG的生成只取决于区块数量,所以可以预先计算出DAG来避免在每个epoch过渡过长的等待时间。

DAG的生成流程如下:

2.1 Dag_size 和Cache_size

每个epoch的dagsize和cachesize都不同,上面已经定义了创世时的初始值,以太坊还提供了一个表来存储接下来2048个纪元(大约20年)的各个值。详见官网或源码cpp-ethereum/libethash/data_sizes.h.

获取datasize 和cachesize的方法如下:

2.2 Seedhash

算法中需要一个seedhash,由下面程序生成,从程序可见每个epoch的seed是不变的。

2.3 Cache

使用seedhash计算cache。

2.4 DAG

最后使用cache计算DAG,light参数中保存的是cache数据.

2.5 DAG文件

DAG每次生成都需要很长时间,因此生成时候需要存在文件中,再使用mmap映射到内存中。DAG文件路径一般如下

Mac/Linux : $HOME/.ethash/full-R

Windows: $HOME/Appdata/Local/Ethash/full-R

是ethash算法的版本号,在libethash/ethash.h 中REVISION定义。

是上面计算出来的seedhash

路径下可能会有多个DAG文件,这取决于用户或者客户端是否删除过时的DAG文件。

格式:

DAG文件以8字节的幻数开头,值为0xfee1deadbaddcafe, 以小端格式写入。接下来是小端格式写入的dataset数据。

3 Ethash实现

3.1 Ethash

图1 算法流程图

参数说明:

Header_hash: 是当前块头部数据的hash值,在矿机调用get_ethwork时从任务参数中获取。

Nonce: 是每次计算ethash使用不同的数,不能重复。可以取时间戳或随机数作为起始值,然后递增。

对于矿工来说,如果result的值小于或等于target,那么就完成了挖矿过程,将当前的nonce和mix_hash作为工作量证明提交工作;如果result的值大于target,那么就需要改变nonce的值,再次调用ethash算法.

Ethash算法程序如下:

从图中看,每次ethash从DAG随机取64128=8192Bytes, 以GTX1070显卡为例,带宽为256GB/s, 那么每秒能承受256102410241024/8192=33554432次ethash运算,即33MH/s的算力。可见,该算法对内存带宽的要求很高。

3.2 快速验证

当验证一个工作提交是否有效时,速度很快。

下面是快速验证程序:

感谢HPB团队整理。

HPB46:Solidity编译器和简单调试

Solidity编译器和简单调试

作者:HPB团队整理

1 安装Solidity编译器

1.1 通过docker安装Solc

搜索docker的Solc镜像

docker search —no-trunc ethereum/solc

通过docker安装Solc

docker pull docker.io/ethereum/solc:stable

1.2 运行Solc容器

运行如下命令

docker run —rm -it —privileged=true —net=host -v /home/hpbroot/ethereum_go/contract:/contract —name solc ethereum/solc:stable –version

查看是否成功

2 新建spring boot工程

2.1 通过Eclipse新建工程

首先,新建ContractCompile工程

在springboot配置文件application.properties中添加如下

1
2
3
4
5
6
7
web3j.contract.solcCmd=docker run --rm -it --privileged=true --net=host -v /home/hpbroot/ethereum_go/contract:/contract --name solc ethereum/solc:stable
在pom文件添加依赖
<dependency>
  <groupId>org.ethereum</groupId>
  <artifactId>solcJ-all</artifactId>
  <version>0.4.10</version>
</dependency>

新建ContractConfig类

1
2
3
4
5
6
7
8
9
10
11
@Component
@ConfigurationProperties(prefix = "web3j.contract")
public class ContractConfig {
  private String solcCmd;
  public String getSolcCmd() {
      return solcCmd;
  }
  public void setSolcCmd(String solcCmd) {
      this.solcCmd = solcCmd;
  }
}

2.2 调用智能合约编译器的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String soliditySrcCode =MapUtils.getString(preParam, "soliditySrcCode");
if(StringUtils.isBlank(soliditySrcCode)) {
  param.put(ContractConstant.RETURN_CODE, ContractConstant.ERROR_CODE);
  param.put(ContractConstant.RETURN_MSG, ContractConstant.NOSRCCODE);
  return param;
}
SolidityCompiler solidityCompiler = SolidityCompiler.getInstance(getLog(),contractConfig.getSolcCmd());
byte[] source = soliditySrcCode.getBytes(StandardCharsets.UTF_8);
CompilerResult compilerResult = solidityCompiler.compileSrc(source,
SolidityCompiler.Options.ABI, SolidityCompiler.Options.BIN);
param.put(ContractConstant.RETURN_CODE, ContractConstant.SUCCESS_CODE);
param.put(ContractConstant.RETURN_MSG, ContractConstant.SUCCESS_MSG);
if(compilerResult.isFailed()) {
  param.put(ContractConstant.RETURN_CODE, ContractConstant.ERROR_CODE);
  param.put(ContractConstant.RETURN_MSG, compilerResult.getErrors());
}

3 调用编译智能合约源文件的代码

3.1 编写智能合约源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract SampleRecipientSuccess {
  address public from;
  uint256 public value;
  address public tokenContract;
  bytes public extraData;
  event ReceivedApproval(uint256 _value);
  function receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) {
    from = _from;
    value = _value;
    tokenContract = _tokenContract;
    extraData = _extraData;
    ReceivedApproval(_value);
  }
}

3.2 通过HTTP调用智能合约的J2EE组件

1
2
3
4
5
6
7
8
9
10
11
12
13
String contractPath="/SampleRecipientSuccess.sol";
      String contractString = FileUtils.readFileToString(new File(contractPath),StandardCharsets.UTF_8);
      HashMap<String, Object> hashMap = new HashMap<String,Object>();
      hashMap.put("soliditySrcCode", contractString);
      String url = "http://192.168.3.43:18080/ContractCompile/compileContractCmd";
      ResponseEntity<Map> postForEntity = getRestTemplate().postForEntity(url, hashMap, Map.class);
      Map body = postForEntity.getBody();
      String returnCode = MapUtils.getString(body, ContractConstant.RETURN_CODE);
      if(ContractConstant.SUCCESS_CODE.equals(returnCode)) {
          String result = MapUtils.getString(body, "result");
          Map<String, Object> parseResult = parseResult(result);
          System.out.println(AppObjectUtil.toJson(parseResult));
      }

3.3 智能合约编译器组件返回的编译数据

1
{"abis":[{"constant":true,"inputs":[],"name":"value","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"tokenContract","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"extraData","outputs":[{"name":"","type":"bytes"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_value","type":"uint256"},{"name":"_tokenContract","type":"address"},{"name":"_extraData","type":"bytes"}],"name":"receiveApproval","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"from","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_value","type":"uint256"}],"name":"ReceivedApproval","type":"event"}],"bin":"6060604052341561000c57fe5b5b6103d38061001c6000396000f300606060405263ffffffff60e060020a6000350416633fa4f245811461004d57806355a373d61461006f578063609d33341461009b5780638f4ffcb11461012b578063d5ce338914610199575bfe5b341561005557fe5b61005d6101c5565b60408051918252519081900360200190f35b341561007757fe5b61007f6101cb565b60408051600160a060020a039092168252519081900360200190f35b34156100a357fe5b6100ab6101da565b6040805160208082528351818301528351919283929083019185019080838382156100f1575b8051825260208311156100f157601f1990920191602091820191016100d1565b505050905090810190601f16801561011d5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561013357fe5b604080516020600460643581810135601f8101849004840285018401909552848452610197948235600160a060020a039081169560248035966044359093169594608494929391019190819084018382808284375094965061026895505050505050565b005b34156101a157fe5b61007f6102f8565b60408051600160a060020a039092168252519081900360200190f35b60015481565b600254600160a060020a031681565b6003805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156102605780601f1061023557610100808354040283529160200191610260565b820191906000526020600020905b81548152906001019060200180831161024357829003601f168201915b505050505081565b60008054600160a060020a0380871673ffffffffffffffffffffffffffffffffffffffff19928316179092556001859055600280549285169290911691909117905580516102bd906003906020840190610307565b506040805184815290517f2db24179b782aab7c5ab64add7f84d4f6c845d0779695371f29be1f658d043cd9181900360200190a15b50505050565b600054600160a060020a031681565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061034857805160ff1916838001178555610375565b82800160010185558215610375579182015b8281111561037557825182559160200191906001019061035a565b5b50610382929150610386565b5090565b6103a491905b80821115610382576000815560010161038c565b5090565b905600a165627a7a723058209522849948e8cc25a7d6717d5c10836c97c36425936be5edf399206b3e5d7fa30029"}

总结

通过J2EE组件的接口调用,可以为大多数基于java的区块链应用提供了便利,可以利用J2EE成熟稳定的框架无缝集成到项目中,也是为了以后安卓开发和联盟链提供在线编译智能合约功能,如果是私有的局域网络的企业级联盟链,可以发布该智能合约的J2EE组件到该局域网的机器上去,可以实现联盟中的智能合约统一的编译器,便于快速升级编译器。

感谢HPB团队整理。