以太坊搭建联盟链 Posted 2020-12-29 dqh123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以太坊搭建联盟链相关的知识,希望对你有一定的参考价值。
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 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://ad6aff917c6e8bd40cb20af4eac6ce05c16d285125b46f17fc9b5c3b0a833bd21[email protected] 120.25.162.110:30301" --bootnodesv4 "enode://ad6aff917c6e8bd40cb20af4eac6ce05c16d285125b46f17fc9b5c3b0a833bd21[email protected] 120.25.162.110:30301" --bootnodesv5 "enode://ad6aff917c6e8bd40cb20af4eac6ce05c16d285125b46f17fc9b5c3b0a833bd21[email protected] 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团队整理。
JUL 26TH, 2018
加密握手协议全局位置
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.生成经过封装的AuthPacketAuthPacket =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团队整理。
JUL 3RD, 2018
区块链核心框架
区块链是一个不断增长的分布式账本[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团队整理。
JUN 29TH, 2018
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团队整理。
JUN 28TH, 2018
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团队整理。
JUN 26TH, 2018
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, 那么每秒能承受256 10241024 1024/8192=33554432次ethash运算,即33MH/s的算力。可见,该算法对内存带宽的要求很高。
3.2 快速验证
当验证一个工作提交是否有效时,速度很快。
下面是快速验证程序:
感谢HPB团队整理。
MAY 26TH, 2018
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团队整理。
APR 20TH, 2018
Solidity概述及基本代码展示
作者:HPB团队整理
Solidity是实施智能合约的契约导向的高级语言。它受到C ++,Python和javascript 的影响,旨在针对以太坊虚拟机(EVM)。
Solidity是静态类型的,支持继承,库和复杂的用户定义类型等功能。
您将会看到,可以创建投票,众筹,盲目拍卖,多重签名钱包等等的合约。
1 Solidity智能合约例子
我们从一个基础的solidity例子开始。开始的时候,你可能看不懂每一行具体的意思,但是没关系,我们会在后续的讲解中介绍每一个细节。
1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) {
storedData = x;
}
function get() constant returns (uint) {
return storedData;
}
}
第一行告诉该合约用的是0.4.0版本的solidity编写,并且这些代码具有向上兼容性。保证不会在不同solidity编译版本下编译会出现不同的行为。
从Solidity角度来看,合约就是存在于以太坊区块链中的一个特定地址中的代码和数据集合。uint storedData 声明了一个类型为 uint(256位的无符号整型)的变量,变量名称为 storedData。你可以把它想象为数据库中的一个字段,该字段是可以被数据库中的方法进行查询,修改。在以太坊中,这个字段是属于一个合约字段。在这个例子中,该变量可以通过提供的get,set方法进行获取或是修改。在Solidity中,访问一个变量是不需要通过this来引用的。
这个合约很简单,只是允许以太坊上的任何人来存储一个数据到某个节点,同时把这个操作发布到以太坊中,当然,以太坊上的其他节点同样可以通过调用set方法来修改你已经存储好的值。虽然有被修改,但是对该值操作的任何历史记录都是保存在以太坊中的。不用担心你的存储记录或是修改记录会丢失。后面我们会将到如何对合约进行限制,只允许你一个人修改这个数据。
2 Solidity子货币例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.0;
contract Coin {
//public关键字可以让外部访问该变量
address public minter;
mapping (address => uint) public balances;
//事件可以让轻客户端快速的响应变化
event Sent(address from, address to, uint amount);
// 构造方法
function Coin() {
minter = msg.sender;
}
function mint(address receiver, uint amount) {
if (msg.sender != minter) return;
balances[receiver] += amount;
}
function send(address receiver, uint amount) {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Sent(msg.sender, receiver, amount);
}
}
下面的例子将实现一个简单的加密货币例子。无中生币不在是梦想,当然只有合约的创建人才有这个特权。此外,任何人只要有一个以太坊密钥对就可以进行货币交易,根本不需要注册用户名和密码。这个合约引入了一些新的概念,让我们一个个都过一遍。
address public minter;
声明了一个public,类型为address的状态变量。Address类型是一个160位的值,不允许任何的算术操作。它适合于存储合约地址或是其他人的密钥对。Public关键字会自动产生用于外部访问该变量值的方法。如果不声明public,其他的合约是无法访问该变量的。自动产生的方法类似于:
function minter() returns (address) { return minter; }
当然如果你增加了一个和上面完全一样的方法是没有任何作用的,我们需要变量和产生的方法名完全一致。这块其实编译器会帮助我们完成,不需要我们自己动手编写,我们只要知道这个概念就可以。
mapping (address => uint) public balances;
还是创建了一个公有状态变量,这是一个比address更复杂的数据类型,类似java里的Map<address,uint>,它描述了一个地址和一个uinit数据类型的map关系。Mappings的关系可以看成是一个hash表,所有可能的key都对应了一个0的值。当然在map里不存在只有key值或是只有value值的情况。所以我们需要记住添加了一个什么样的map关系或是像这个例子一样,如何使用它。因为这是个public变量,所以系统会自动为它生成一个get方法,类似于:
function balances(address _account) returns (uint) {
return balances[_account];
}
通过上面的方法我们可以很容易的查询一个账号的余额。
event Sent(address from, address to, uint amount);
这一行创建了一个名为event 的事件。该事件会在该示例的最后一行被触发。用户或是server应用可以花很低的代价(后面会讲代价是什么)来监听事件的触发。一旦这个事件被触发了,监听者接收到三个参数:from, to,amount.也是说从哪个账号,到哪个账号,金额是多少。通过这三个参数可以很容易追踪到具体的交易。为了监听这个事件,我们需要使用如下代码:
1
2
3
4
5
6
7
8
9
10
Coin.Sent().watch({}, ‘‘, function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:
" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})
注意 :用户是如何调用系统自动生成的balances方法。
Coin方法 是构造方法,是在合约产生的时候系统会调用,而且之后不允许被调用。Msg(以及tx和block)是一个全局变量,保存了可以被区块链访问的一些属性。它持久化了创建合约的节点的地址。 Msg.sender是值该方法调用者的地址。
最后,真正完成合约功能的,并且被其他用户调用的是mint和send方法。如果mint是被不是创建该合约的账号调用,不会起任何作用。但是,send可以被任何账号(必须有以太币的账号)调用并发送以太币给另外一个账号。注意,如果你用合约发送以太币到另外一个账号,通过区块链浏览器查看是查看不到任何变化的,因为发送以太币的过程和金额的变化都被存储在了特殊的以太币合约里。而不是体现在账号上。通过使用事件,可以很容易的创建一个区块链浏览器,用来查看交易和账号余额。
感谢HPB团队整理。
MAR 15TH, 2018
以太坊虚拟机的基本介绍
作者:HPB团队整理
此文简要的介绍了以太坊虚拟机的基本要素,在以后的文章我们会向大家展示如何安装应用调试等基本技术。
1.1 概述
以太坊虚拟机(EVM)是以太网上智能合约的运行环境。这不仅仅是个沙盒,更确实的是一个完全独立的环境,也就是说代码运行在EVM里是没有网络,文件系统或是其他进程的。智能合约甚至被限制访问其他的智能合约
1.2 账号
? 在以太坊中有两种账号共享地址空间:外部账号和合约账号。外部账号是由公钥和私钥控制的(如人),合约账号是由账号存储的代码所控制。
外部账号的地址是由公钥决定的,而合约地址是在智能合约被创建的时候决定的(这个地址由创建者的地址和发送方发送过来的交易数字衍生而来,这个数字通常被叫做“nonce”)
不管是否账号存有代码(合约账号存储了代码,而外部账号没有),对于EVM来说这两种账号是相等的。
每一个账号都有持久化存储一个key和value长度都为256位字的键值对,被称为“storage”
而且,在以太坊中,每个账号都有一个余额(确切的是用“Wei”来作为基本单位),该余额可以被发送方发送过来带有以太币的交易所更改。
1.3 交易
交易是一个账号和另外一个账号之间的信息交换。它包含了二进制数据(消费数据)和以太数据。如果目标账号包含了代码,这个代码一旦被执行,那么它的消费数据就会作为一个输入数据。如果目标账号是一个0账号(地址为0的账号),交易会生成一个新的合约。这个合约的地址不为0,但是是来源于发送方,之后这个账号的交易数据会被发送。这个合约消费会被编译为EVM的二进制代码,并执行。这次的执行会被作为这个合约的代码持久化。这就是说:为了创建一个合约,你不需要发送真正的代码到这个合约上,事实上是代码的返回作为合约代码。
1.4 Gas
以太坊上的每笔进行一笔交易都会被收取一定数量的Gas.这是为了限制交易的数量,同时对每一笔交易的进行支付额外费用。当EVM执行一个交易,交易发起方就会根据定义的规则消耗对应的Gas。
交易的创造者定义了的Gas 价格。所以交易发起方每次需要支付 gas_price * gas 。如果有gas在执行后有剩余,会以同样的方法返回给交易发起方。如果gas在任何时候消耗完,out-of-gas 异常会被抛出,那当前的这边交易所执行的后的状态全部会被回滚到初始状态。
1.5 存储,主存和栈
每个账号都有持久化的内存空间叫做存储. 存储是一个key和value长度都为256位的key-value键值对。从一个合约里列举存储是不大可能的。读取存储里的内容是需要一定的代价的,修改storage里的内容代价则会更大。一个合约只能读取或是修改自己的存储内容。
第二内存区域叫做主存。系统会为每个消息的调用分配一个新的,被清空的主存空间。主存是线性并且以字节粒度寻址。读的粒度为32字节(256位),写可以是1个字节(8位)或是32个字节(256字节)。当访问一个字(256位)内存时,主存会按照字的大小来扩展。主存扩展时候,消耗Gas也必须要支付,主存的开销会随着其增长而增大(指数增长)。
EVM不是一个基于寄存器,而是基于栈的。所以所有的计算都是在栈中执行。最大的size为1024个元素,每个元素为256位的字。栈的访问限于顶端,按照如下方式:允许拷贝最上面的16个元素中的一个到栈顶或是栈顶和它下面的16个元素中的一个进行交换。所有其他操作会从栈中取出两个(有可能是1个,多个,取决于操作)元素,把操作结果在放回栈中。当然也有可能把栈中元素放入到存储或是主存中,但是不可能在没有移除上层元素的时候,随意访问下层元素。
1.6 指令集
为了避免错误的实现而导致的一致性问题,EVM的指令集保留最小集合。所有的指令操作都是基于256位的字。包含有常用的算术,位操作,逻辑操作和比较操作。条件跳转或是非条件跳转都是允许的。而且合约可以访问当前区块的相关属性比如编号和时间戳。
1.7 消息调用
合约可以通过消息调用来实现调用其他合约或是发送以太币到非合约账号。消息调用和交易类似,他们都有一个源,一个目标,数据负载,以太币,gas和返回的数据。事实上,每个交易都包含有一个顶层消息调用,这个顶层消息可以依次创建更多的消息调用。
一个合约可以定义内部消息调用需要消耗多少gas,多少gas需要被保留。如果在内部消息调用中出现out-of-gas异常,合约会被通知,会在栈里用一个错误值来标记。这种情况只是这次调用的gas被消耗完。在Solidity,这种情况下调用合约会引起一个人为异常,这种异常会抛出栈的信息。
上面提到,调用合约会被分配到一个新的,并且是清空的主存,并能访问调用的负载。调用负载时被称为calldata的一个独立区域。调用结束后,返回一个存储在调用主存空间里的数据。这个存储空间是被调用者预先分配好的。调用限制的深度为1024.对于更加复杂的操作,我们更倾向于使用循环而不是递归。
1.8 代理调用/ 代码调用和库
存在一种特殊的消息调用,叫做代理调用。除了目标地址的代码在调用方的上下文中被执行,而且msg.sender和msg.value不会改变他们的值,其他都和消息调用一样。这就意味着合约可以在运行时动态的加载其他地址的代码。存储,当前地址,余额都和调用合约有关系。只有代码是从被调用方中获取。这就使得我们可以在Solidity中使用库。比如为了实现复杂的数据结构,可重用的代码可以应用于合约存储中。
1.9 日志
我们可以把数据存储在一个特殊索引的数据结构中。这个结构映射到区块层面的各个地方。为了实现这个事件,在Solidity把这个特性称为日志。合约在被创建出来后是不可以访问日志数据的。但是他们可以从区块链外面有效的访问这些数据。因为日志的部分数据是存储在bloom filters上。我们可以用有效并且安全加密的方式来查询这些数据。即使不用下载整个区块链数据(轻客户端)也能找到这些日志
1.10 创建
合约可以通过特殊的指令来创建其他合约。这些创建调用指令和普通的消息调用唯一区别是:负载数据被执行,结果作为代码被存储,调用者在栈里收到了新合约的地址。
1.11 自毁
从区块链中移除代码的唯一方法是合约在它的地址上执行了selfdestruct操作。这个账号下剩余的以太币会发送给指定的目标,存储和代码从栈中删除。
本文由HPB团队整理.
FEB 28TH, 2018
1 Java Web3J概述
Web3j是一个轻量级,Reactive(响应式),类型安全的Java库,用于与Ethereum网络上的客户端(节点)集成,这允许您使用Ethereum 块链,而不需要为平台编写自己的集成代码的额外开销。
1.1 Web3J的提供的功能
通过HTTP和IPC 完成Ethereum的JSON-RPC客户端API的实现
1.Ethereum钱包支持
2.使用过滤器的函数式编程功能的API
3.自动生成Java智能合约包装器,以创建、部署、处理和调用来自本地Java代码的智能合约
4.支持Parity的 个人 和Geth的 个人 客户端API
5.支持Infura ,所以您不必自己运行一个Ethereum客户端
6.综合整合测试展示了上述一些场景
7.命令行工具
1.2 Web3J的依赖的库(中间件)
1.RxJava函数式编程的的API中间件
2.Apache HTTP Client 中间件
3.Jackson Core 用于快速JSON序列化/反序列化中间件
4.Bouncy Castle 加密解密和 Java Scrypt 加密中间件
5.生成智能合约java包装器类的java源代码(.java)的JavaPoet 中间件
6.Java的UNIX域套接字的*nix系统进程间通信API中间件
1.3 启动Ethereum客户端
1
$ geth --fast --cache = 512 –networkid 2 - -rpcapi “personal,db,eth,net,web3” --rpc --dev
1.4 Web3J的进程间通信IPC
Web3j还支持通过文件套接字快速进行进程间通信(IPC)到在与web3j相同的主机上运行的客户端。在创建服务时,连接只需使用相关的IpcService 实现而不是 HttpService :
?
1.5 Web3J的过滤器
Web3j的函数式编程的特性让我们设置观察者很容易,这样通知订阅者在区块链以便知道区块链上设置的事件。
1.5.1 区块过滤器
当所有新的块被添加到块链中的时候,接收到这些区块
如果您希望查看最近的当前的区块,以便于新的后续块的创建:
1.5.2 交易过滤器
当所有新的交易被添加到块链中的时候,接收到这些交易
1.5.3 待处理的交易过滤器
当所有待处理的交易被提交到网络时(也就是说,在它们被分组到一个块之前),接收这些交易
1.5.4 使用Web3J交易
Web3j支持使用Ethereum钱包文件(推荐)和用于发送交易的Ethereum客户端管理命令。
使用您的Ethereum钱包文件将Ether发送到另一方:
如果想自定义交易
获取可用的nonce
创建交易
签名并发送交易
使用web3j的智能合约包装器要简单得多
1.6 使用Web3J智能合约
使用Java智能合约包装器处理智能合约
web3j可以自动生成智能合约包装器代码,以便在不离开Java的情况下部署和与智能合约进行交互,生成包装代码
1.6.1 编译智能合约
$ solc .sol —bin —abi —optimize -o <output-dir>
1.6.2 然后使用Web3J的命令行工具生成包装器代码
Web3j solidity生成/path/to/<smart-contract>.bin /path/to/<smart-contract>.abi -o / path / to / src / main / java -p com.your.organisation.name
1.6.3 创建并部署智能合约
1.6.4 使用已存在的智能合约
2 以太坊的Web3J交易
2.1 Web3J交易类型
从广义上讲,以太坊有三种类型的交易
从以太坊的一方转移到另一方
创建智能合约
交易智能合约
2.2 在TestNet /私有块链上挖矿
在Ethereme测试网络(TestNet)中,采矿难度低于主网络(MainNet)。这意味着您可以使用常规CPU(如笔记本电脑)挖掘新的Ether,您需要做的就是运行一个Ethereum客户端
2.3 以太坊GAS
在使用Ethereum客户端时,这意味着有两个参数(GasPrice,GasLimit)用来决定你想花多少时间来完成一项任务:
2.3.1 Gas Price
这是你在每单位Gas中所准备的量。它的价格是9000 Wei (9 x 10 ^15 Ether)
这是你在交易执行过程中愿意花费的总金额。在以太坊中,一个交易的大小是有上限的,它限制了这个值,通常限制为1,500,000
2.3.2 Gas Limit
这些参数组合在一起,决定了您愿意花费在交易成本上的最大数量。也就是说,你不能再用Gas的价格限制了。GasPrice也会影响交易发生的速度,这取决于其他交易的价格,而这些交易对矿商来说更有利可图,您可能需要对这些参数进行调整,以确保交易能够及时地进行。
2.4 交易机制
当您使用某种Ether创建有效的帐户时,可以使用两种机制来与Ethereum进行交易,这两种机制都通过web3j来支持。
[通过Ethereum客户端进行交易签名](https://docs.web3j.io/transactions.html #signing-via-client)
[离线交易签名](https://docs.web3j.io/transactions.html#offline-signing)
2.4.1 通过Ethereum客户端进行交易签名
为了通过Ethereum客户端进行交易,您首先需要确保您所交易的客户知道您的钱包地址。为了做到这一点,你最好运行自己的Ethereum客户端,如Geth / Parity。一旦您有客户端运行,您可以通过以下方式创建一个钱包:
Geth Wiki包含了Geth支持的不同机制的良好运行,比如导入私钥文件,并通过它的控制台创建一个新帐户或者,您可以为客户端使用JSON-RPC管理命令,例如,用于[Parity](https://github.com/paritytech/parity/wiki/JSONRPC-personal-module#personal_newaccount) 或[Geth的personal_newAccount](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_newaccount)
在客户端使用json-rpc管理命令,对于 [Parity](https://github.com/paritytech/parity/wiki/JSONRPC-personal-module#personal_newaccount) or [Geth](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_newaccount)使用personal_newAccount
创建您的钱包文件后,您可以通过web3j解锁帐户,首先创建一个支持Parity / Geth管理命令的web3j实例
2.4.2 脱机交易签名
如果您不想管理自己的Ethereum客户端,或者不想向Ethereum客户端提供电子钱包详细信息(如密码),那么离线交易签名就是要走的路。
离线交易签名允许您使用您在Web3j中的Ethereum Ethereum钱包签署交易,从而可以完全控制您的私人凭据。离线创建的交易可以发送到网络上的任何Ethereum客户端,这将会将交易传播到其他节点,前提是它是一个有效的交易。
2.4.3 创建和使用钱包文件
为了离线签署交易,您需要使用您的Ethereum钱包文件或与Ethereum钱包/帐户相关的公钥和私钥,web3j能够为您生成一个新的安全Ethereum钱包文件,或者使用现有的钱包文件。
要创建一个新的钱包文件:
String fileName = WalletUtils.generateNewWalletFile(“your password”,new File(“/path/to/destination”));
2.4.4 脱机签名交易
如果具有达到脱机签名能力目的交易应该使用 RawTransaction 类型。RawTransaction类似于之前提到的交易类型,但是它不需要from地址,因为这可以从签名中推断出来。
为了创建和签名一个raw交易,顺序如下:
确定发件人帐户的下一个可用随机数
创建RawTransaction对象
编码RawTransaction对象
签名RawTransaction对象
将RawTransaction对象发送到节点进行处理
获取下一个可用的[随机数后](https://docs.web3j.io/transactions.html#nonce),该值就可以用于创建交易对象:
然后可以对交易进行签名和编码:
这些凭证是在创建和处理钱包文件时加载的。
然后使用ethsendrawtransaction发送该交易:
2.4.5 交易 Nonce
Nonce是一个递增的数值,它用于惟一地标识交易。一个nonce只能被使用一次,直到一个交易被挖矿确认,它可以用同一个nonce发送多个版本的交易,然而,一旦被挖矿确认,任何后续的提交都将被拒绝,可以通过
2.4.6 Nonce使用规则
为了防止交易重播,ETH(ETC)节点要求每笔交易必须有一个nonce数值。每一个账户从同一个节点发起交易时,这个nonce值从0开始计数,发送一笔nonce对应加1。当前面的nonce处理完成之后才会处理后面的nonce。
注意这里的前提条件是相同的地址在相同的节点发送交易。
当nonce太小(小于之前已经有交易使用的nonce值),交易会被直接拒绝。
当nonce太大,交易会一直处于队列之中,这也就是导致我们上面描述的问题的原因;
当发送一个比较大的nonce值,然后补齐开始nonce到那个值之间的nonce,那么交易依旧可以被执行。
当交易处于queue中时停止geth客户端,那么交易queue中的交易会被清除掉。
2.5 创建智能合约
2.5.1 使用Raw Transaction
2.5.2 获取合约地址
如果智能合约包含构造函数,则相关的构造函数字段值必须编码并附加到编译后的智能合约代码中.
2.5.3 执行智能合约
web3j负责为您提供功能编码,进一步的详细信息可以在Ethereum维基百科的电子合同ABI节中找到。
等待响应就使用EthGetTransactionReceipt…
不管消息签名的返回类型是什么,都不可能从事务性函数调用返回值。但是,可以使用过滤器捕获函数返回的值.
2.5.4 查询智能合约的状态
此功能由ethcall json-rpc调用实现, ethcall允许您在智能合约中调用一个方法来查询一个值。这个函数不存在交易成本,这是因为它不会改变任何智能合约方法的状态,它只是简单地返回它们的值:
注意:如果生成了一个无效的函数调用,或者获得了一个null结果,那么返回值将是一个集合。emptylist的实例.
以上是关于以太坊搭建联盟链的主要内容,如果未能解决你的问题,请参考以下文章
搭建以太坊POA联盟链
第74篇 Ubuntu环境搭建以太坊POA联盟链
第01篇 Windows平台以太坊联盟链搭建步骤
运维老鸟中秋新作以太坊联盟链-多节点私链搭建手册
区块链之旅以太坊超级账本公链联盟链
第一行代码:以太坊-创建自己的私有区块链