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

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

HPB-Wallet:HPB 钱包简介

1 去中心化钱包基本概念

1.1 简介

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

1.2 钱包名词

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

1.2.1 地址

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

1.2.2 私钥

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

1.2.3 助记词

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

1.2.4 keystore

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

1.3 优缺点

1.3.1 交易所的弊端

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

1.3.2 去中心化钱包

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

2 HPB Wallet钱包介绍

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

2.1 钱包核心功能介绍

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

转账交易

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

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

2.2 HPB Wallet的优势

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

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

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

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

一.概述

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

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

二.区块部分

区块结构:

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

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

中:

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

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

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

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

Header 成员变量的解释如下:

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

区块存储:

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

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

body 是分开存储的。

以 writeHeader 为例:

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

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

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

三.交易部分

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

四.Merkle-PatriciaTrie

Merkle-PatriciaTrie 简介:

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

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

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

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

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

tree 的表现无疑是优秀的。

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

实现部分

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

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

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

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

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

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

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

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

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

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

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

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

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

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

五.StateDB

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

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

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

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

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

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

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

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

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

感谢HPB团队整理。

HPB57:ETH交易部分分析

1.交易结构

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

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

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

rlpHash 计算:

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

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

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

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

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

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

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

2.交易存储

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

中。

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

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

3.交易类型

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

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

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

转账交易

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

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

创建合约交易

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

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

执行合约交易

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

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

4.交易执行

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

虚拟机外

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

虚拟机内:

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

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

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

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

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

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

合约的创建和赋值:

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

{

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

return nil, gas, nil

}

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

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

return nil, gas, ErrDepth

}

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

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

  return nil, gas, ErrInsufficientBalance

}

var (

to = AccountRef(addr)

snapshot  = evm.StateDB.Snapshot()

)

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

precompiles := PrecompiledContractsHomestead

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

}

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

return nil, gas, nil

}

evm.StateDB.CreateAccount(addr)

}

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

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

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

// only.

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

evm.StateDB.GetCode(addr))

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

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

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

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

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

contract.UseGas(contract.Gas)

}

}

return ret, contract.Gas, err

}

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

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

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

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

    Contract 的 self 地址;

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

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

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

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

预编译合约

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

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

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

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

}

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

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

return p.Run(input)

}

return nil, ErrOutOfGas

}

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

解释器执行合约的指令

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

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

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

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

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

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

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

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

感谢HPB团队整理。

HPB56:Windows平台下eclipse配置

安装 go 环境

根据自己的系统版本下载安装包,进行 go 的环境的安装。下载地址:

https://www.golangtc.com/download

环境变量配置

对 go 的环境变量进行配置,有些安装是默认配置,有些需要手动的配置环境变量。

Go 环境验证,在控制台中输入 go version, 如显示以下内容,则表示输入成功。

下载安装 Eclipse

安装环境:

  • ​ Java VM version 8 or later.
  • ​ Eclipse 4.6 (Neon) or later.
  • ​ CDT 9.0 or later (this will be installed or updated automatically as part of the steps below).

下载安装 eclipse,下载地址如下:

http://archive.eclipse.org/eclipse/downloads/drops4/R-4.6- 201606061100/#PlatformRuntime

安装配置 mingw64

MinGW,是 Minimalist GNUfor Windows 的缩写。它是一个可自由使用和自由发布的 Windows 特定头文件和使用 GNU 工具集导入库的集合,允许你在 GNU/Linux Windows 平台生成本地的 Windows 程序而不需要第三方 C 运行时(C Runtime)库。

下 载MingW

Mingw 的官网是 http://www.mingw.org/ ,也可以通过 http://mingw-w64.org/doku.php 下载, 根据你的系统选择相应版本。

安装配置

双击进入系统配置页面

根据机器选择相应型号。

配置成功后的页面如下:

配置环境变量,首先配置 C_INCLUDE_PATH

C:\Program Files\mingw-w64\x86_64-7.2-win32-seh-rt_v5-rev0\mingw64\include

配置 path 路径

C:\Program Files\mingw-w64\x86_64-7.2.0-win32-seh-rt_v5-rev0\mingw64\bin

结果验证

在控制台中如输入 gcc -v ,如返回 mingw 信息,则表示配置环境安装成功。

Go 插件安装

下载后的 eclipse 没有配置 go 的环境,需要手动配置开发系统环境.

离线安装

因为 GFC 的封闭,我们需要先到https://github.com/GoClipse/goclipse.github.io/archive/master.zip 下载 GoClipse 的安装包,并解压。然后打开 Eclipse(确保已经包含 CDT),Help –> Install New Software…,然后选择 Add 按钮,输入解压后的 Release 目录,选择 GoClipse,然后一直 next 就安装成功了。安装完成后需要重启 Eclipse。

在线安装

打开:help>Install New Software ,安装完成后,需要重启 Eclipse

Go 插件配置

打开:Window>Preferences>Go。

运行环境

首先配置 Go 的运行环境和工作空间,如下图所示。

编辑环境

此部分最快的方式就是直接下载。

go-ethereum 配置

自行到 https://github.com/ethereum/go-ethereum 下载最新版。

目录配置

请严格按照如下格式进行配置目录: src –> github.com->Ethereum->go-ethereum.

配置完成后,再次添加环境变量。

编译运行

在/go-ethereum/src/github.com/ethereum/go-ethereum/cmd/geth/main.go 中 run as, Go

applicantion. 则可以看到如下的信息输出。

引用

http://www.jianshu.com/p/d26553e7c253

http://blog.csdn.net/u013474104/article/details/52350403

感谢HPB团队整理。

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握手,在运行通信的整个过程中,需要进行如下判断:

加密握手协议流程

加密握手细节描述

关键步骤补充:

感谢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团队整理。