1 比特币
1.1 密码学原理
- 加密货币(crypto-currency)用到的密码学技术是哈希和签名。
- 加密货币用到的哈希函数叫做加密哈希函数(cryptographic hash function), 它有三个特性:
- collision resistance:抗碰撞性,即输入空间足够大时,给定一个 $x$ ,没有一种比穷举更高效的方法使我们找到一个 $y\neq x$ 满足 $hash(x)=hash(y)$ 。有此性质可以防篡改,因为几乎不可能人为篡改信息后仍保持哈希值不变。抗碰撞性是不可证明的,只能通过经验大概判断。
- hiding:藏匿性,即输入空间足够大时,给定一个哈希值 $hash(x)$ ,没有一种比穷举更高效的方法使我们反推出对应的 $x$ 。结合抗碰撞性与藏匿性可以实现数字承诺(digital commitment),即 seal a value in an envelope and open the envelope later。
- puzzle friendly:任何人无法得知哈希函数的映射规则,无法根据给定的哈希值要求构造输入。这是比特币对哈希函数的要求,比特币是一块指定的哈希值空间,如果要寻找对应这一块空间的输入,除了穷举挖矿没有捷径,因此挖矿的过程能够比较公正地作为工作量证明(proof of work)。由于哈希函数的单向性,寻找输入只能穷举,但验证输入的合法性很简单,因此挖出的比特币能够被所有人验证。
- 比特币的哈希函数是SHA-256,SHA是 Secure Hash Algorithm,满足上述三个性质。
- 比特币用到的第二个密码学技术是数字签名,涉及到比特币的账户管理。比特币采用去中心化的账户管理,没有类似银行的中心机构,每个用户都能自主开户,开户过程就是在本地创建一个公钥私钥对,一个公钥私钥对就代表了一个账户。
- 公钥私钥是非对称加密算法中的概念。对称加密算法中,加密和解密过程使用的是同一个密钥,能加密的人必然也能解密,所以需要保证密钥分发的安全性。而非对称加密算法不需要考虑这一点,加密用公钥,解密用私钥,每个用户只公开自己的公钥,使得所有人都能用自己的公钥加密,但只有用户自己能用私钥解密,从而防止了信息泄露。
- 比特币系统本身是不加密的,所有信息都是公开的,公钥私钥主要是用来实现数字签名。每个用户的公钥分发给所有人,私钥保存在本地,进行比特币交易时,用自己的私钥进行加密,其他用户使用该用户的公钥进行解密即可验证交易人的身份。在此过程中,所有人都能解密,而只有签名用户能加密,同样保证了数字签名的安全性和可靠性,也就防止了假冒身份进行交易。
1.2 数据结构
- 比特币使用的一个数据结构是区块链,区块链是一个个区块构成的链表,与一般链表的区别是使用哈希指针进行链接。哈希指针(hash pointer)同时保存了下一个区块的地址和下一个区块的哈希值。
- 区块链中,最初的区块(next=null)称为创世纪块(genesis block),头节点区块称为最近区块(most recent block)。每个区块包含指向前一个区块的哈希指针,系统中存储着一个独立的哈希指针指向最近区块。
- 使用哈希指针可以实现防篡改日志(tamper-evident-log),当一个区块内容被篡改后,哈希值改变,导致原本指向该区块的哈希指针无法匹配,系统为了保持链接会自动修改指向该区块的那个区块中的哈希指针,由于计算哈希值时也包含了区块中的哈希指针,所以会导致传递地逆着区块链接的方向修改所有区块中指针的哈希值,最终修改到指向最近区块的指针,因此任一区块的改变都会导致指向最近区块的指针的改变,所以只看一个指针就可以方便地判断有无篡改。此外,当用户不小心删除了本地的一个区块,需要别人重新发送一份时,可以根据指向丢失区块的哈希指针的哈希值判断收到的区块是否是正确的。
- 比特币使用的另一个数据结构是梅克尔树(Merkle tree),梅克尔树的叶节点是存储数据的数据块,非叶节点存储的是其所有子节点的哈希值,把当前节点的所有子节点的哈希值拼接后再算一次哈希,将这个哈希值保存在当前节点的父节点中,如此自底向上计算哈希,最后归结到根节点,根节点中的所有哈希值拼接后再算一次哈希,得到的就是最终的根哈希值。与区块链类似,梅克尔树也具有防篡改日志的特性,对任一数据块的篡改最终都会导致根哈希值的变化。
- 由于区块链的冗余备份,要求所有节点都保存全部的数据文件,而随着区块链上的交易越来越多,数据同步的代价越来越高,只有少数大存储节点有能力保存全部数据,为了不让区块链演变成中心化结构,出现了全节点(full node)和轻节点(light node)的概念,全节点区块包含块头(header)和块身(body),轻节点只有块头。梅克尔树的每个数据块都代表一个交易(transaction),梅克尔树的根哈希值存储在块头,其余结构存储在块身,因此全节点存储了整个梅克尔树,而轻节点只存储了根哈希值。
- 梅克尔树提供了梅克尔证明(Merkle proofs)的机制,使轻节点用户能够验证一个交易是否被写入了区块链中。梅克尔树一个叶节点到根节点的路径叫做一个梅克尔证明,用户根据已有的交易,向全节点请求该交易对应的梅克尔证明路径中自己无法求出的哈希值(因为非叶节点中的哈希值还涉及了其他交易,发送路径中用到的哈希值比发送所有叶节点的数据量小多了),用户结合交易信息的哈希值和全节点提供的哈希值,就能逐层计算出最终的根哈希值,将计算得到的根哈希值和自己轻节点中存储的根哈希值对比,就能验证交易是否已经写入区块链。
- 由于哈希函数的抗碰撞性,全节点几乎不可能恶意提供一个错误的梅克尔证明,使得轻节点用户根据一个被篡改的交易能够算出正确的根哈希值,所以梅克尔证明的机制是安全的。
- 哈希指针只能用在无环的结构中,因为在环上会导致无穷尽的哈希值的循环影响。
1.3 协议
- 公钥私钥的加密只能保证数字货币交易的安全性,但容易实施双花攻击(double spending attack),因为数字货币的本质就是签了名的文件,不能即时标记使用次数,所以用户可以把数字货币复制多份重复使用。一种可行的解决方案是设立一个中央银行,数字货币的发行和交易必须经过央行的认证,央行给每个数字货币一个唯一的编号,验证交易时就可以根据编号查询该货币是否被使用过。但是这是中心化结构的实现方案,比特币系统的初衷是要去中心化的。
- 去中心化的比特币系统要解决两个问题。一个是由谁发行比特币,这个问题与挖矿有关,后面再讲。另一个是防止双花攻击,解决方法就是全体用户维护一个区块链,用于记录和跟踪交易日志,所以区块链也被叫做公共账本。比特币的交易一方面要根据签名验证交易人的身份,同时也要验证比特币的来源,因为双花攻击就相当于发行货币、凭空创造货币,只要确定比特币来源可靠就能保证交易的有效性。
- 假设A向B转账,在转账之前,A需要知道B的比特币账户地址,用户地址虽然不是保密的,但由于比特币系统没有查询用户地址的方法,所以需要通过别的渠道获得。同时,B要知道A的公钥,来验证比特币中A的签名,B收到一个带签名的交易和一个公钥,结合二者来验证交易的有效性,如果没有区块链的交易日志,就能允许一个第三者C同时篡改签名和公钥,让B成功验证交易后以为在和A交易但实际上走的别人的账户,一旦有了交易日志,就能知道比特币的当前持有人,甚至能追溯到发行该比特币的用户,所以对交易人公钥的篡改不会被区块链接受。(好像是这个意思,没太看懂)
- 区块身中记录的是交易列表(transaction list)。区块头中记录的是一些宏观信息,如比特币的协议版本(Bitcoin version),指向前一区块的指针(hash of previous block header),梅克尔树的根哈希值(Merkle root hash),挖矿的难度目标阈值(difficulty target),时间戳(timestamp),以及随机数(nonce)。哈希指针是在区块头之间链接的,指针中的哈希值也只是计算整个区块头的哈希值,不计算区块身,因为梅克尔树已经能保证交易的可靠性了,区块身就不用管了。
- 区块链要满足分布式共识(distributed comsensus),也就是每个用户本地的区块链数据要保持一致性,涉及到分布式系统的一些理论,如FLP、CAP。共识机制解决并保证每一笔交易在所有记帐节点上的一致性和正确性问题。区块链的共识机制不能依靠用户投票,因为区块链不能确定谁有投票权,也就是不能知道哪些用户是值得信任的,在区块链系统上创建账户太简单了,只需要生成公钥私钥对就够了,如果给所有用户同样的投票权,就可能会有某个用户恶意生成大量账户来操纵投票结果,这种攻击称为女巫攻击(sybil attack,水军攻击~)。
- 区块链的共识机制是依据算力进行投票。挖矿的过程就是创建区块的过程,有效的哈希值只是哈希函数输出空间上很小的一个区域,矿工先构造一个区块头,然后穷举随机数nonce使得 $hash(block\ header)\leq difficulty\ target$ ,找到满足条件的随机数时,该矿工就有了记账权,也就是能往区块链中添加该区块。但仅仅是新区块内容有效也不一定被区块链接受,因为区块链的共识最终目的是保证比特币不停的在工作量最大(通常也是难度最多区块数最多)的区块链上运转,保证工作量最大的区块链成为唯一权威的公共总帐本,如果新区块不是添加在最长有效链的末尾,而是额外插在最长有效链的某个节点上导致产生了无效分支链,那么就不会被区块链接受。
- 51%攻击就是利用新分支来篡改区块链,当攻击者掌握了全网超过50%的算力时,自己私下不断创建新区块却不发布到最长有效链上,当这条隐身的区块链长度超过了最长有效链时,攻击者将整条链发布,就能直接替代掉原来的最长有效链,抹除掉原来主链上的记录。
- 正常情况下也会出现分支,当两个矿工几乎同时发布了新区块到主链末尾,就会产生两个分支。对于这种等长的临时分支,系统会同时保留并认可,直到有用户在某个分支下添加新的区块造成两个分支不等长,此时系统就会放弃短的分支,意味着短分支的计算工作全都白干了。
- 因此共识机制就是有记账权的矿工们在区块链上添加区块,维护最长有效链,所谓的投票就是矿工选择分支进行扩展,只有有记账权的矿工才能投票,所以女巫攻击就不可行了,因为普通用户只有验证权没有记账权。之所以要拼算力竞争记账权,是因为添加新区块后添加者有权力在新区块内发行一定数量的比特币,作为挖矿的报酬,这种特殊交易称为铸币交易(coinbase transaction),铸币交易是发行新比特币的唯一途径。比特币数量上限是2100万个,最初的新区块内可发行50个比特币,而后每创建21万个区块后,新区块内可发行的比特币数量减半,大概是每四年减半一次(通过调整挖矿难度,比特币协议规定了系统平均出块时间是10分钟左右),目前已经减到12.5个,这种规则是为了抑制比特币的产出,但是随着比特币的升值,发布新区块的收益其实是越来越高的(不考虑挖矿的支出)。
1.4 实现
- 比特币系统采用基于交易的账本模式(transaction-based ledger),区块里记录着铸币交易和转账交易,但不显式记录每个账户的余额,查询余额需要根据交易日志推算。
- 区块链的全节点维护着一个名为UTXO(Unspent Transaction Output)的数据结构,记录尚未花费的所有交易的输出,一个交易可以有多个输出,即同时转账给多人,也可以有多个输入,即多人同时转账给同一人。UTXO中的索引是交易的哈希值和输出的序号,根据这两个值可以检索具体某个交易的某个输出。交易完成前查询UTXO可以防止双花攻击,因为已花费的比特币记录会从UTXO中删除,确保只会花一次。一个交易既消耗UTXO中的记录,又会产生新的UTXO记录。
- 通常交易的总输入等于总输出,但有时总输入会大于总输出,因为把自己的交易存储在别人的区块中要付给区块发布者少许交易费(transaction fee)。这是为了高效利用区块,激励区块发布者把别人的交易记录存储在自己的区块中。但相比于微薄的交易费,目前挖矿的动力主要还是为了十几个币的出块奖励,等到出块奖励衰减到微乎其微时,挖矿的动力就转为了交易费。
- 与基于交易的账本模式对应的是基于账户的账本模式(account-based ledger),系统会显式记录每个账户的余额,以太坊用的就是这种模式。
- 由于当前挖矿人数太多,竞争激烈,挖矿难度已经被调整得非常高。区块头的nonce只是32位的无符号整数,有时穷举完32位的所有整数也无法找到满足难度要求的数,这时就需要调整区块头中其他可改字段的值,扩大搜索空间。通常修改的是梅克尔树的根哈希值,这个字段可改是因为铸币交易记录中的coinbase字段是自定义的,铸币用户写什么都无所谓,修改这个字段会导致梅克尔树根哈希值的改变。所以挖矿过程其实是两层循环,先遍历coinbase字段,再遍历nonce字段,搜索空间大大增加。
- 挖矿过程中,每一次试数都是一次伯努利试验(Bernoulli trial: a random experiment with binary outcome),两种结果是成功和失败,成功的概率远远小于失败的概率。多次试数就构成了一个伯努利过程(Bernoulli process: a sequence of independent Bernoulli trials),伯努利过程的一个性质是无记忆性(memoryless),意味着每次试数都是独立的、不受之前结果影响的。这个伯努利过程可以用泊松过程(Poisson process)来近似,因为泊松过程就是无记忆性的,就算已经有10分钟没有挖出新的区块,接下来挖到新区块的概率也不会因此而上升。无记忆性能够保证挖矿的公平性,因为防止了算力强的矿工滚雪球。
- 由于四年减半,比特币的增量是一个等比数列,比特币的总量就是 $21万\times 50\times (1+1/2+1/4…)=2100万$ 。
- 挖矿一定程度上保证了比特币的安全,他不假定所有用户是诚实的,而是假定所有算力掌握在诚实的矿工手中。但这种安全性只是概率上的保证,体现了投票的可靠性,表明所有用户能够大概率地维护正确的最长有效链。此外,如果一个区块节点拒绝记录某个合法的交易,也总能等到有一个诚实的节点愿意记录该交易。
- 基于这种信任,有一种六次确认的防篡改判断。当用户A向B发起交易后,可以利用自己的算力立即创建一条略长的新分支,暂时取代当前的主链来欺骗B,而B可以选择等待一段时间,由于A无法对抗全体用户的算力,一段时间后原来的主链会自然而然地被重新维护起来,通常是在交易所在的区块后边被扩展了六个新区块后,可以认为该交易是可靠的,代表有六个其他用户认可了该交易,10分钟创建一个新区块,所以等待时间大约是一个小时。
- 虽然有了六次确认,零次确认还是比较普遍的。一方面因为创建新分支的节点会迟于在主链上添加节点,根据时间的先后,新分支有很大概率不会被区块链接受。另一方面交易的完成本身就需要一段时间,这就相当于六次确认的等待时间了。
1.5 网络
- 比特币系统的区块链运行在应用层,下面的网络层是一个P2P覆盖网络(P2P Overlay Network),网络中所有的节点都是平等的,没有所谓的super node或master node。节点之间使用TCP通信,有利于穿透防火墙。
- 比特币的设计原则是简单鲁棒而非高效,网络中的信息采用flooding的方式传播,每个节点收到信息后传播给所有邻居节点,邻居节点的选择是随机的,不会考虑拓扑结构上的远近,比如中国的一个节点邻居可能在美国,这样提高了鲁棒性但牺牲了效率。
- 每个节点要维护一个等待上链的交易的集合,通过查询集合可以保证相同的交易信息只对邻居发送一次。
- 比特币系统限制了区块的大小是1MB,之所以设计得这么小是因为比特币网络这种低效的信息传播方式非常耗带宽。
- 比特币网络的信息传播属于best effort(尽力而为),由于网络传输存在延迟,同时有些节点转发信息可能不合规范,最后可能会导致有的节点收不到信息,收到信息的顺序不一样,或者收到不合法的信息。这也是去中心化系统普遍面临的问题。
1.6 挖矿难度
- 挖矿难度 difficulty 和目标阈值 target成反比。随着挖矿人数的增加和硬件算力的提升,出块时间会越来越小,出块速度远远小于新区块信息在节点间的传播和同步。用户们不能及时扩展新区块结尾的主链,依然以旧节点为链尾进行扩展,会更容易造成区块链分叉,也更容易受到51% attack,因为不能保持大部分诚实的矿工维护同一条主链。因此,出块时间不是越短越好,相反,挖矿变得容易就表示人们挖矿的热情下降了,比特币的处境反而变得艰难了。
- 但是比特币协议把出块时间规定成10分钟也没有被证明是最优的,比如以太坊就把出块时间定为15秒,并设计了一个额外的ghost协议来解决区块链分叉的问题。
- 比特币协议规定每隔2016个区块(大概两个星期)调整一次目标阈值(挖矿难度),更新 target 的公式是:expected time就是2016X10分钟,actual time是最近的2016个区块的产生时间。可见,实际出块时间越短,target变得越小,挖矿难度也就越高。但是为了控制target的变动幅度,actual time最小是半个星期,最大是8个星期,也就是把target的变动限制在4倍,实际出块时间太大或太小都会按照4倍的边界值计算,这大概是考虑到区块链可能出意外。
1.7 挖矿
- 全节点:
- 一直在线
- 在本地硬盘上维护完整的区块链信息
- 在内存里维护UTXO集合,以便快速检验交易的正确性
- 监听比特币网络上的交易信息,验证每个交易的合法性
- 决定哪些交易会被打包到区块里
- 监听别的矿工挖出来的区块,验证其合法性
- 选择分叉,决定主链
- 轻节点:
- 不是一直在线
- 不保存整个区块链,只保存每个区块的区块头
- 不保存全部交易,只保存与自己相关的交易
- 无法检验大多数交易的合法性,只能检验与自己相关的交易的合法性
- 无法检测网上发布的区块的正确性,因为无法检验其包含的交易的合法性
- 可以验证挖矿的难度
- 只能检测哪个是最长链,不能检测哪个是最长合法链,合法性需要验证全部交易
- 比特币的安全性是由密码学原理和共识机制两方面所保证的。
- cpu挖矿浪费资源,因为只会用到很少的内存和指令部件。gpu挖矿同样闲置了浮点计算的部件,因为挖矿只涉及整数运算,而且目前的挖矿难度已经超过了gpu的算力范围。目前都用ASIC(Application Specific Integrated Circuit)芯片挖矿,是一种专门为挖矿设计的芯片,除了挖矿什么也干不了。可见,挖矿设备的演化是一个通用到专用的过程。但是因为ASIC芯片更新换代很快,过时以后也干不了别的事只能废弃,有人认为这违背了比特币去中心化的初衷,所以有的数字货币会设计成抗ASIC芯片,限制ASIC芯片的使用,鼓励人们用通用芯片挖矿。
- 比特币的发展导致了矿池的出现,矿主招揽多个矿工共同挖矿,矿工只负责用芯片计算哈希,节点的维护由矿主负责,得到的收益在整个矿池内按劳分配。矿工在矿池挖矿,降低了维护和管理设备的成本,收益相比于自己单干也更加稳定,是从小概率赚大钱变成了大概率赚小钱。但是矿池的出现也导致了矿主的算力垄断,使得51% attack更容易实施,因为记账权不再由全体矿工掌握,而是掌握在少数矿主的手里。
1.8 比特币脚本
- 略
1.9 分叉
- 区块链分叉包括两种情况。第一种是 state fork,是指用户对当前主链的确定产生分歧,比如两人几乎同时发布新区块,再如51% attack,由于这种分叉是人为的,所以也叫 deliberate fork。第二种是 protocol fork,当比特币协议改变时需要进行软件升级,但在去中心化的系统中无法保证所有节点都能及时完成升级,就会导致产生分叉。根据协议修改内容的不同,又分为硬分叉(hard fork)和软分叉(soft fork)。
- 硬分叉是指在比特币协议中增加了新特性,没有及时更新协议的节点就会拒绝接受符合新特性的区块。比如对区块大小限制的修改,尽管区块不是越大越好,但另一方面因为出块时间是固定的,单个区块越大越能承载更多的交易,也就提升了系统处理新交易的速度。当把区块大小上限从1MB调整到4MB后,没有更新协议的节点就会拒绝大于1MB的新区块,仍然接着扩展上一个1MB大小的区块,从而产生了分叉。无论主链有多长,旧协议的节点始终认为它是非法的,所以只要这些节点不更新协议,分叉就会一直保留。如果这些节点一直保守地不更新协议,就会导致社区分裂,两拨用户分道扬镳,独立运营,为了在分开后不混淆两条链上交易,防止重放攻击,要给每条链一个 chainID,但是原来的一个账户会复制成每个分支上都有一个独立的账户,不知道会怎么处理。
- 软分叉是指在比特币协议中增加了限制,某些原来合法的区块现在变得不合法了。比如把区块大小上限从1MB调整到0.5MB后,按照新协议挖出的所有区块会被旧协议接受,但按照旧协议挖出的大于0.5MB的区块不会被新协议接受,从而产生分叉。由于旧协议对两条链都能接受,所以只要大部分算力的用户使用了新协议,软分叉上的用户最终都会回到主链上,分叉上的区块就等于白挖了。所以软分叉是一种临时的分叉,只要挖出大于0.5MB的区块就会产生分叉,随着主链变得更长,分叉就会被放弃。
- 简单来说,硬分叉就是新协议(多数)向前兼容,少数不认可多数,多数认可少数,结果就是两拨人各自独立运营,分叉上的工作都保留了下来。软分叉是新协议(多数)不向前兼容,少数认可多数,但多数不认可少数,结果就是分分合合,分叉上的工作都白干了。
1.10 课堂问答
- 转账只是在区块链上记录交易信息,转账对象没必要保持在线。
- 交易的接收账户可能是全节点也不知道的,因为开通新账户只是在本地创建公钥私钥,不需要通知任何节点,新账户第一次收到钱后才会被节点记录。
- 私钥丢失了以后,账户就成了死户,钱就找不回了,因为没有一个中心化的交易所替用户管理,但是即使有交易所也未必是可信的。
- 私钥泄露了或发现有可疑交易,应该创建一个新账户,尽快把钱转走。私钥是账户的全部凭证,创建了就不能做修改,所有知道私钥的人都能合法操作账户的钱。
- 转账写错了地址,只能自认倒霉,因为用户不能修改已发布的交易。如果认识对方,可以联系对方能不能退回。如果转给了一个不存在的账户,钱就等于丢了,直到有人创建了该账户,他就直接白捡了这些钱。
- 交易费不需要指定接收的矿工,任何挖到区块的矿工,只要愿意打包某个交易,就能得到交易的交易费,所以设置交易费的时候不需要也不能够指定接收人。
1.11 比特币的匿名性
- 比特币系统中的匿名不是真正的匿名,而是一种伪匿名(Pseudo-anonymity),因为交易信息中心记录了使用者的数字签名,可以根据交易日志追踪某个账户的历史活动,但是用户的比特币账户和现实身份是无关的,一个用户也可以创建多个独立不相干的比特币账户。真正匿名的货币是现实中的纸币,货币的使用与使用者的身份毫无关系,只不过相比于数字货币,不便于管理。
- 一旦比特币账户与现实世界发生交互,比如用比特币购买现实中的商品,就会导致账户与显示身份的关联,破坏匿名性,因为大数据分析可以在现实交易和比特币交易之间寻找联系。
- 比特币系统运行在应用层,底层的网络层可以通过保护ip的方式来实现匿名性,比如洋葱网络的多路径转发,每一个转发节点都只能知道上一个节点的位置,但无法知道整个发送路径以及原发送者的地址,如此就保护了发送者的ip。应用层实现匿名性的一种方法是 coin mixing,把多个用户的比特币混在一起,一个账户每次操作的比特币不再是原来保存的币,而是从多个用户的币池里随机抽取得到的,但是 coin mixing 实现起来比较复杂,风险也比较大,因为这样一个去中心化的系统中很难建立一个绝对可信的 coin mixing 服务。
- 零知识证明(zero knowledge proof)是指证明者向验证者证明一个陈述是正确的,而无需透露除该陈述是正确的外的任何信息。零知识证明的数学基础是同态隐藏,同态隐藏有三个性质:
- 如果 x,y 不同,那么它们的加密函数值 E(x) 和 E(y) 也不相同。(和哈希函数不同,是完全无碰撞的)
- 给定 E(x) 的值,很难反推出 x 的值。
- 给定 E(x) 和 E(y) 的值,很容易计算出某些关于 x,y 的加密函数值。可以通过 E(x) 和 E(y) 计算出 同态加法 E(x+y) 和 同态乘法 E(xy) 的值,进而可以扩展到任意多项式。
- 比如A要向B证明他知道一组 x 和 y 的值使得 x+y=7 ,且不能让B知道 x 和 y 的具体数值。方法是,A向B发送 E(x) 和 E(y) 的值,B使用 E(x) 和 E(y) 计算出 E(x+y) 的值,然后B计算 E(7) 的值,如果 E(x+y) 和 E(7) 相等,说明A确实知道这样一组 x 和 y 。
- 零知识证明的一个应用是盲签。在 1.3 一节中提到过,防止双花攻击的一种中心化实现方法是建立央行,央行给每个比特币唯一的编号,通过编号记录交易和持有人,但这里的央行对交易和用户进行了关联,可以检索出用户的交易日志,匿名性比较差。而在盲签的实现方法中,给比特币提供编号的是用户而不是央行,交易发送方 A 设定一个编号 x ,然后将 E(x) 发送给央行,央行用自己的私钥对 E(x) 加密得到 C(E(x)) 再返回给 A 。之后 A 把 x 和 C(E(x)) 发送给交易接收方 B ,B 基于 x 计算出 E(x) ,同时使用央行的公钥解密出另一个 E(x) ,如果两个 E(x) 相等,说明交易的比特币是可靠的,因为既验证了 A 设置的编号,又验证了央行的签名。盲签的匿名性在于,央行在签名时不知道具体的比特币编号,也就无法追踪交易双方的账户。
- 比特币系统提供了伪匿名性,但无法消除关联性(交易日志)。由此,零币和零钞的设计动机就是在协议层基于密码学原理实现完全的匿名性。
- 零币(zerocoin)系统中存在基础币和零币,通过基础币和零币的来回转换,消除旧地址和新地址的关联,因为已经混淆了币的来源,原理类似于上面提到的 coin mixing。
- 零钞(zerocash)系统使用 zk-SNARKs 协议,不依赖基础币,区块链中只记录交易的存在性和矿工用来验证系统正常运行所需要的关键属性的证明,区块链上既不显示交易地址也不显示交易金额,所有交易通过零知识验证的方式进行。
- 像零币和零钞这种强匿名性的加密货币并没有成为主流,原因有三:一是因为系统为了实现强匿名性而牺牲了效率,二是对强匿名性有需求的用户并不是很多,三是因为系统只实现了区块链内部的强匿名性,在与现实世界交互时依然存在隐患。
1.12 反思
- 哈希指针在具体的实现中是只有哈希没有指针,哈希与区块的对应关系存储在一个 key-value 数据库中,常用的是 levelDB,区块链所谓的链表结构实际上是一个 key-value 数据表。
- 如果多人共用一个账户,把私钥截断成几段分开保存是不安全的。首先只要一个人丢失了他的那部分,所有人的钱都没了。再者,只有部分私钥也能降低暴力破解的难度,比特币账户的私钥有256位,假设有一半的私钥,穷举空间就从 $2^{256}$ 减少到了 $2^{128}$ ,所以知道一半私钥,破解难度就降低了 $2^{128}$ 倍,而不是简单地降低一半。更安全的方法是使用多重签名,对账户的每个使用者都生成独立的256位私钥。
- 分布式共识已经被科学家证明是不可能的了,所以比特币系统实际上并没有取得真正的分布式共识。真正的共识一旦形成就不能再修改,而区块链中可以通过分叉攻击实现回滚,否定当前的主链。
- 比特币的总量是一定的,所以比特币本身是稀缺品。稀缺品不适合作为货币,因为随着社会财富的增加,比特币会越来越值钱,拥有比特币的人会越来越富,没有比特币的人越来越难追上别人,贫富差距会失衡。目前比特币的涨幅已经超过了房价。
- 不需要担心量子计算对加密货币的破坏。首先,量子计算机距离投入使用还有很长的时间。其次,一旦量子计算得到应用,传统金融业的加密体系会比比特币系统更脆弱,比特币系统应用了非对称加密和不可逆的哈希函数,即使是量子计算机也很难短时间内暴力破解,所以该担心的应该是网银支付宝这类平台。
2 以太坊
2.1 概述
- 比特币简称 BTC,货币的最小单位是 satoshi(中本聪的“聪”)。以太坊简称 ETH,其中的货币称为以太(Ether),货币的最小单位是 wei(戴伟的“伟”)。
- 比特币被称为区块链1.0,以太坊被称为区块链2.0,以太坊针对比特币系统设计中的一些问题进行了改善,例如:
- 出块时间降到十几秒,为适应新的出块时间采用了基于ghost协议的共识机制。
- 采用了新的 mining puzzle ,比特币的 mining puzzle 比拼的是穷举哈希值的算力,导致的结果就是挖矿设备的专业化,很多人认为这违背了去中心化的理念,而以太坊的 mining puzzle 对内存(或显存)的需求很大,称为 memory hard mining puzzle ,一定程度上限制了 ASIC 芯片的使用。
- 用权益证明(proof of stake, POS)取代工作量证明(proof of work, POW),新区块不再通过耗费资源挖矿产生,而是有币的人共同投票选举产生。POW是竞争思维,而POS是合作思维。
- 支持智能合约(smart contract)。比特币是去中心化的货币系统(decentralized currency),使货币摆脱了政府司法的管制,完全由密码学原理和共识机制维护运行。以太坊受其启发设计了去中心化的合约(decentralized contract),使合约的实现和判定也摆脱政府管制,用技术手段取代司法手段。去中心化的好处是完全统一规则,不同的国家、地域都遵循同一套共识机制,使得跨国活动更方便。
2.2 账户
- 比特币采用基于交易的账本,不显式记录账户余额,余额只能通过UTXO推算得到,这种模式的好处是隐私性比较好,但是使用不方便,比如 A 有10个比特币,向 B 转账3个比特币,就需要显式地把多出来的7个币转回给自己,不然就成了交易费,这是因为不记录账户余额,所有的比特币流动都需要显式地用交易来记录,没有指定去向的币就会被当做交易输入输出的差额,也就是交易费。
- 以太坊采用基于账户的记账模式,更接近现实中银行账户的概念,没有参与交易以太币会被当做余额存在原账户。交易中的每个以太币只需要知道来自哪个账户就够了,不需要像比特币一样支持回溯币的来源。
- 以太坊的好处是能够天然防御双花攻击,因为交易可以回滚,但账户扣了钱是不能回滚的,花两次就是扣两次钱。然而以太坊可能会受到重放攻击(replay attack)。双花攻击是花钱的人不诚实,通过回滚交易使一份钱可以花两次,而重放攻击是收钱的人不诚实,通过重复广播交易使得花钱的人被扣两次钱。比特币不需要专门防御重放攻击,因为比特币系统是可以追溯币的来源的,UTXO的记录已经能够保证一份钱不会花两次,可以同时防御双花攻击和重放攻击。以太坊防止重放攻击的方法是在交易信息中记录该账户交易的次数(nonce,比特币里的nonce指的是随机数,这里则相当于计数器),nonce 每次加一,当做交易的ID,因为交易有签名保护,所以交易ID是无法篡改的。
- 以太坊账户分为外部账户(externally owned account)和合约账户(smart contract account)。外部账户像比特币账户一样通过公钥私钥控制,状态包括余额(balance)和交易次数(nonce)。合约账户不是用公钥私钥控制,状态包括 balance、nonce、code 和 storage,外部账户发起交易时可以调用合约账户,合约账户也可以调用另一个合约账户,但合约账户不能主动发起交易。
2.3 状态树
- 以太坊使用的账户地址是160bits,一般表示成40位16进制的数。虽然地址格式和比特币账户不一样,但原理都是对公钥取哈希。
- 比特币系统的每个区块中都会构建一个 Merkle tree,叶节点仅包含本区块内的交易, Merkle tree 生成后就不可更改了。所以每新增一个区块都会构建一个 Merkle tree,但是叶节点不会超过4000个交易。
- 而以太坊是基于账户的记账,Merkle tree 的叶节点是账户而不是交易,为了确保所有全节点对根哈希值达成共识,也为了提供 Merkle proof,所有账户必须全部参与 Merkle tree 的构建,无论它们的余额有没有发生变动。但是实际上发生改变的账户只有很少一部分,所以这种方案又费时间,效率又低,所以不可能每新增一个区块就重新构建一次 Merkle tree。
- 实际上,用和比特币系统里一样的 Merkle tree 本身就是不行的,因为每个账户作为一个叶节点的话,叶节点会非常多,查找和更新的效率很低。叶节点也必须进行排序,因为如果不排序,每个全节点构建出的 Merkle tree 就很可能是不同的。比特币系统中不用考虑排序是因为 Merkle tree 是由有当前区块记账权的用户决定的,其他用户只需要认同就够了,而全体账户状态不可能打包进单独一个区块,所以全体账户的 Merkle tree 不能由一个人决定,需要全体的共识。即使对叶节点进行排序也是不可行的,因为如果有人创建了新账户,插入新节点的代价非常高,可能大部分 Merkle tree 的非叶节点都要改变,跟重新构建 Merkle tree 几乎没区别。
- 以太坊使用的数据结构是 MPT(Merkle Patricia Tree),是一种融合Merkle tree 和 trie tree 得到的结构。前缀树的特性会带来以下优点:
- 由于账户是十六进制的,所以树中一个节点最多只有17个子节点(0~f,外加一个结束标志符)。
- 以太坊账户地址都是40位的,在前缀树中的查找效率比较平均。
- 原始的 Merkle tree 使用哈希值索引,有很小的概率出现哈希碰撞,但前缀树永远不可能出现索引碰撞。
- 只要给定的输入(全体账户集合)相同,即使顺序打乱,得到的树也是相同的。
- 插入和更新是局部性的,不会影响无关节点。
- patricia tree 是对 trie tree 的改进,进行了路径压缩,因此又叫做压缩前缀树。如果一颗子树是一条链,也就是每个节点只有一个子节点,就把它压缩成一个节点,节点内容就是这条链代表的字符串(将一串 branch node 替换为一个 extension node)。不过当插入新节点时,压缩的节点可能需要展开。在键值分布稀疏的情况下,压缩的效果比较好,也就是路径很长但分支很少,以太坊账户160bits,现存的账户数量与整个状态空间 $2^{160}$ 相比是相当稀疏的,所以理论上 MPT 的压缩效率非常高。实际上,以太坊地址设定成160位就是为了稀疏性,使用长地址是这样一个去中心化系统防止地址碰撞的唯一方法。
- MPT 的根哈希值可以防止账户篡改,提供的 Merkle proof 可以让轻节点验证账户余额。此外 Merkle proof 也可以证明某个账户是不存在的,因为 MPT 和 sorted Merkle tree 都是有序树,都可以定位到不存在的账户应该在的位置,然后把对应的 Merkle proof 发给轻节点,轻节点验证失败从而得知账户不存在。
- 因此以太坊中也是每新增一个区块就构建一次 MPT,但区别在于树中的大部分节点都是共享自前一区块的 MPT,只修改新区块影响到的账户的分支。之所以要对每个区块保存一个 MPT,而不是只维护一个 MPT,是为了保留历史记录,当区块链出现分叉时会重新选择主链,被淘汰的分叉就需要根据历史的 MPT 将账户数据进行回滚,回滚到分叉点后再沿着主链更新主链的 MPT。虽然交易可以通过推算交易信息来回滚,不太需要历史状态,但以太坊的智能合约很复杂,必须依赖历史状态才能回滚。
- MPT的非叶节点存储的是账户地址的前缀,也就是账户的索引,而 MPT 的叶节点存储的是账户完整地址和账户状态的 key-value 对,这里的账户状态 value 是基于 RLP 编码规则进行序列化后的结果,因为账户的状态最终都能表示成可嵌套的字节数组,所以用 RLP 编码比较方便。
2.4 交易树和收据树
- 除了状态树,以太坊还采用了交易树和收据树,这三个树的根哈希值都保存在块头中,它们的结构都是 MPT,可能是因为三种树用同一个结构便于管理代码。区别在于:
- 状态树的索引是账户地址,交易树和收据树的索引是交易在区块中的序号,该序号由发布区块的节点决定。
- 状态树包含了系统中所有账户的信息,而交易树和收据树只包含当前区块的交易信息。
- 状态树共享了前一区块 MPT 的节点,但每个区块的交易树和收据树都是独立的。
- 交易树和收据树可以提供 Merkle proof。交易树的 Merkle proof 用来证明交易在区块中的存在性和合法性。收据树的 Merkle proof 用来验证交易的结果。
- 以太坊采用了布隆过滤器,布隆过滤器是把一个大的集合映射到一个稠密的向量上,将元素是否存在于集合中的查询操作的时间复杂度,从线性降低到常数,但缺陷在于可能出现 false positive ,而且布隆过滤器只支持添加元素和查找元素,不支持删除元素的操作。
- 以太坊的区块在块头里存储了其内部所有交易归结成的布隆过滤器,当要在整个区块链中查找某个交易时,先用各个块头里的布隆过滤器判断,判断存在后再从对应区块的交易树和收据树中再次查找,因为布隆过滤器是不准的。这种使用了布隆过滤器的二次查找的机制的好处在于,提高了查找效率,布隆过滤器只有 false positive 而没有 false negative ,所以能够帮助过滤大部分无关的区块。此外,这使得只存储了块头的轻节点也能进行初步查询,缩小区块范围后再向全节点请求 Merkle proof ,大大提高了查找效率。
- 以太坊的运行过程是一个交易驱动的状态机(transaction-driven state machine),状态机的每一个状态都是一棵状态树,存储了全部账户的状态,通过进行交易,使得状态机转移到下一个状态。比特币也可以看作是一个交易驱动的状态机,只不过其中的状态是 UTXO 。以太坊和比特币的状态机都是确定性状态机,因为发生交易时,每个节点都要进行状态转移,所以状态转移必须是确定性的,从而保证各个节点的一致性。
- 以太坊和比特币一样,交易的收款账户可能是全节点没记录的,因为创建新账户不需要通知任何节点,只有新账户第一次收到钱时才需要作为新节点插入到状态树中。
- 以太坊的状态树之所以要包含全部账户,是为了方便查找。假如只包含与当前区块内交易相关的账户,要查找不在其中的账户的信息,就需要回溯到前面的区块去查找,如果一个账户很久没有进行过交易,查询余额就需要回溯非常长的距离。如果查询的账户是新建的没有进行过交易的账户,就要一直查到创世纪块,而在存储了全部账户的状态树中查询就很方便,只要这一棵树上查不到就说明是没进行过交易的账户。
2.5 GHOST
- 区块链底层的 P2P 网络采用拓扑结构无关的无脑 flooding 来传播信息,新发布的区块传到各个节点可能需要十几秒,比特币的出块时间设成10分钟是绰绰有余的,只有几乎同时发布的两个新区块才可能造成临时性的分叉。然而,以太坊的出块时间降低到了十几秒,发布间隔长达数秒的两个区块也有可能导致临时分叉,所以以太坊中这种临时性分叉成为了常态。比特币中分叉概率比较低,所以规定竞争失败的分叉中的区块和铸币奖励直接作废是可以接受的,但如果以太坊也这么规定对矿工就太不友好了。
- 以太坊采用了GHOST协议解决该问题,给予竞争失败的分叉一定的安慰奖励。比特币中竞争失败的分叉中的区块都叫做 orphan block 或 stale block ,以太坊换了一个不那么悲观的名字,叫做 uncle block 。发布区块的人事先不知道自己的区块是否会成为 uncle block ,当再有新区块选定了某个分支进行扩展时,其余分支的区块才确定是 uncle block 。主链上的侄子区块(和 uncle block 同层次的下一个区块)可以选择包含至多两个叔叔区块,每包含一个叔叔区块就额外获得 $\frac{1}{32}$ 的出块奖励,同时被包含的叔叔区块的发布者获得 $\frac{7}{8}$ 的出块奖励,所以这是个双赢的协议,能够鼓励区块链中出现分叉时及时合并。
- 由于网络传输的延迟,侄子区块发布时可能不知道叔叔区块的存在,或者侄子区块仅仅出于自私,选择牺牲自己的 $\frac{1}{32}$ 来不让对方得到 $\frac{7}{8}$ ,结果都是侄子区块不选择包含叔叔区块。此时,协议规定侄子区块后续的区块还可以选择是否包含叔叔区块,因为以太坊不考虑论资排辈,所以分叉后的叔叔区块对于后续主链上的区块来说都是叔叔区块,只不过每隔了一代,叔叔区块的收益就减少 $\frac{1}{8}$ ,直到减少到 $\frac{2}{8}$ ,再远的叔叔区块就没有奖励了,而侄子区块的 $\frac{1}{32}$ 的奖励是恒定的,所以七代以内的叔侄都是双赢的。这个机制抑制了侄子区块损人不利己的自私行为,就算当前的侄子区块不想双赢,剩下的六个侄子里也总会有愿意包含叔叔区块的(没有绝对垄断的情况下),损人不利己就变成单纯的不利己了。同时,收益递减也鼓励了分叉后叔叔区块尽早同意被包含,因为越晚被包含奖励越少。所以该协议对叔侄两方都有尽早合并的鼓励效果。
- 这种机制只适用于临时性分叉,同时侄子愿意包含,叔叔愿意被包含。而对于协议冲突导致的硬分叉和软分叉是不适用的,因为会有一方认为另一方的区块不合法,因此拒绝合并。
- 以太坊的以太币数量没有上限,所以不需要像比特币一样,每隔一段时间就把出块奖励减半,人为制造稀缺性,所以比特币会不断升值,而以太币是必定会通胀的。
- 让侄子区块包含叔叔区块,仅仅是为了安慰其竞争失败,所以不执行叔叔区块内的交易,因为其中的交易可能与主链上的交易冲突。所以侄子区块只检查叔叔区块的区块头是否合法,不会检查其中的交易是否合法。因此,叔叔区块只有安慰奖励和增加主链比重的作用,block body里存不了任何有用的信息。
- 只有分叉的第一个区块才叫做叔叔区块,才能被奖励,而叔叔区块后面扩展的所有区块都会被直接丢弃。如果接收分叉的全部区块,就会降低分叉攻击的代价,因为就算分叉攻击失败了,整条分叉也会被主链全部接收,攻击者能得到不少的奖励。
2.6 挖矿算法
- 比特币 POW 的挖矿算法很成功,至今没有被曝出严重的漏洞,但是令人诟病的一点是导致了挖矿设备的专业化。如果挖矿设备没有专业化,用 CPU、GPU、ASIC 挖矿的效率没有如此悬殊,那么挖矿会更加普及,51% attack 也会更加困难。所以以太坊挖矿算法的目标就是 ASIC resistance ,主要思路是增加挖矿算法对内存的需求,要求保留计算的中间结果,也就是 memory hard mining puzzle ,因为 ASIC 使用内存的效率与其他芯片差距不大。
- Litecoin 就是使用了 memory hard mining puzzle ,但是缺点在于求解 puzzle 和验证 puzzle 所需的内存都是相当大的。设计 mining puzzle 的原则是 “ difficule to solve, but easy to verify ” ,所以对内存的消耗应该仅在求解阶段。
- 以太坊的 mining puzzle 基于两个数据集,一个 16MB 的 cache 和一个 1GB 的 dataset(也叫DAG),dataset 是基于 cache 生成的,但是随着计算机内存的不断变大,两个数据集的大小也在不断增加。之所以使用一大一小两个数据集是为了便于验证,轻节点只需要存储 cache ,只有挖矿的矿工才会使用到大的 dataset 。
- cache 的构造方法是先确定一个随机数 seed ,取哈希后加入 cache ,之后每一个新元素都是 cache 中的元素再次取哈希得到的结果。每隔30000个区块重新生成一个 seed ,根据新的 seed 重新生成新的 cache 。cache初始大小 16MB,每隔30000个区块后,新的 cache 比旧的 cache 要大初始大小的 $\frac{1}{128}$ ,也就是 128KB 。
- dataset 中的每个元素也是通过基于哈希的方法从 cache 中取值计算出的结果,初始大小 1GB ,同样每隔30000个区块后重新生成一次,新的 dataset 比旧的 dataset 要大初始大小的 $\frac{1}{128}$ ,也就是 8MB 。
- 矿工挖矿的过程就是使用区块头、nonce 和 dataset 计算出一个值,再与 target 比较,因为 dataset 要放在内存中便于访问,所以内存消耗比较大。而轻节点的验证过程,是使用区块头、nonce 和基于 cache 临时计算出的 dataset 中的元素计算出一个值,再与 target 比较,因为验证不赶时间,所以也就不需要存储以直接访问 dataset ,所以内存消耗比较小。
- 以太坊为了实现 ASIC resistance ,一方面采用 memory hard mining puzzle ,另一方面一直声称要从 POW 转向 POS ,对 ASIC 厂商也起到了威慑作用。但是 ASIC resistance 也不是没有缺点的,一旦挖矿设备通用化,发动攻击的成本会变低,因为囤积大量 ASIC 芯片除了发动攻击什么也干不了,但囤积 CPU 和 GPU 除了挖矿还能干别的工作,虽然攻击难度不好说,但是攻击的风险和成本确实下降了。
2.7 难度调整
- 比特币每隔2016个区块调整一次挖矿难度,而以太坊每个区块都有可能调整难度,难度调整公式是:
- $D(H)$ 是本区块的难度,$H_i$ 是本区块的序号。难度的更新包括基础部分(max)和难度炸弹 $\epsilon$ 两部分。
- $P(H)_{H_d}$ 是本区块的父区块(前一区块)的难度,$x\times\zeta_2$ 用于自调节出块难度,维持稳定的出块速度。
- 通过使用 max 函数给难度设定了一个下届 $D_0=131072$ ,挖矿难度永远不会低于这个值。
- 在自调节的部分中,
- $x$ 是调整的单位,所以难度调整的最小单位是父区块难度的 $\frac{1}{2048}$。
- $\zeta_2$ 是调整的系数,$y$ 和父区块的叔叔区块有关,如果父区块包含了他的叔叔区块,$y$ 就等于2,否则 $y$ 就等于1。之所以考虑叔块是因为当父块包含了叔块时,父块和叔块的发布者都会得到奖励,以太币的供应量会比单个区块多,所以需要适当提高难度以保持货币发行量稳定。
- 难度系数的下界定为-99,主要是为了应对黑客攻击或其他意想不到的黑天鹅事件。
- $H s$ 是本区块的时间戳,$P(H) {H_s}$ 是父区块的时间戳,单位都是秒,二者相减就是本区块的出块时间。因为调整的目标是把出块时间稳定在15秒左右,所以以 $y=1$ 为例,假如本区块出块时间是1~8秒,结果是 $\zeta _2=1$ ,表示难度要增加一个单位,假如本区块出块时间是9~17秒,结果是 $\zeta _2=0$ ,表示难度不变,假如本区块出块时间是18~26秒,结果是 $\zeta _2=-1$ ,表示难度要下调一个单位。以此类推。
- 难度炸弹的定义是:
- 因为以太坊计划从 POW 转向 POS ,然而将来采用 POS 的共识机制后挖矿难度会急剧下降,之前矿工花高价购买的矿机就没用了,所以一部分矿工可能会联合起来抵制,拒绝转向 POS 。因此为了不让社区分裂,需要设置难度炸弹,让难度高到矿机也无能为力,那些矿工就不得不接受 POS 了。
- 难度炸弹是一个让难度呈指数型增长的因子,指数增长的特点是开始增长缓慢,到了后期增长越来越剧烈。最初设置难度炸弹时没有第二个公式,之所以把参数设置成100000,是为了让将来开始转向 POS 时,难度炸弹恰好开始显露威力。然而人算不如天算,转向 POS 的时间被一再延后,此时绝对不能让难度炸弹提前爆炸,否则出块时间会远远低于十几秒,所以就出现了用 $H’_i$ 取代 $H_i$ 的做法,把 $H_i$ 回调30万个区块,相当于强行把难度炸弹爆炸的时间点延后30万个区块。
- 以太坊的发展分成四个阶段:Frontier、Homestead、Metropolis 和 Serenity。 Metropolis 又分成 Byzantium 和 Constantinople 两个阶段。我们现在正处于 Byzantium 阶段,难度炸弹的回调也是发生在这个阶段。
2.8 权益证明
- 目前比特币和以太坊采用的共识机制是 POW ,缺点是费电,比如比特币系统平均处理一个交易要费大约1000度电,以太坊平均处理一个交易要费大约67度电,以太坊因为挖矿时间短,所以平均下来的能耗比比特币要低,但是仍然远远高于现实中银行的能耗。
- POW 烧钱主要是因为矿工一方面要买设备竞争算力,另一方面设备运行要耗费电力,但这一过程本质上就是经济实力的比拼。POS 的主要思想是,既然从头到尾都是在拼钱,不如省去烧钱的阶段,直接比较钱多钱少当做竞争结果,从间接拼钱变成了直接拼钱,省下来的钱还可以投入以太坊的建设中,何乐而不为。
- 采用 POS 的加密货币,一般要在发行前预留一部分货币给开发者,或者出售一部分货币换取开发资金。货币发行后,每个用户的投票权比重就取决于他持有的货币数量。
- 对于采用 POW 的加密货币,维护其安全的资源不是一个闭环,例如比特币的用户可以使用比特币系统外的资产来购入挖矿设备,当算力超过全体用户的50%时就可以发动攻击,虽然比特币的抗攻击性比较好,但对于一些用户不多的小币种就很容易被扼杀在摇篮里。相反,在采用 POS 的加密货币系统里,用户在系统外的资产不会直接影响到系统内的投票权,如果想要发动51% attack,必须要在系统内获得50%以上的该货币资产,这与在系统外购买挖矿设备不同,在系统内大量购入货币会导致货币剧烈升值,一定程度上起到了抵制恶意收购的作用,所以相当于系统内部自发地维护安全,所以对于采用 POS 的加密货币,维护其安全的资源是一个闭环。
- POW 和 POS 不是互斥的,有的货币就采用了混合策略,使用户挖矿的难度与用户持有的资产相关,但是难点在于防止用户贫富差距越来越大。
- 早期 POS 的设计面临的一个问题是 nothing at stake,也就是两边下注。在挖矿过程中同时扩展两个分叉是费力不讨好的,所以一般只选择一个分叉扩展,而在 POS 中,给两个分叉都投票是很容易的。
- 以太坊准备采用的 POS 协议称为 Casper the Friendly Finality Gadget(FFG),在过渡阶段也是要和 POW 混合使用的,为 POW 提供 finality 。finality 是一种状态,该状态下的交易不会被回滚取消,就算是分叉攻击也不能将其回滚,相比之下,单纯的 POW 是缺乏这种 finality 状态的保证的。
- 为了实现 finality 状态,Casper 协议引入了验证者(validator)的角色,成为验证者需要投入一定数量的以太币作为保证金。验证者的作用是投票决定最长合法链(主链),投票比重取决于保证金的比重。每挖出100个区块就作为一个 epoch ,由验证者投票决定是否成为 finality ,投票过程类似于数据库的 two-phase commit ,投票分成两轮,每一轮都有超过 $\frac{2}{3}$ 的验证者同意才能通过。但是实际应用时会对原始协议进行优化,比如精简到只有一轮投票, epoch 的大小也会减小到50个区块。验证者履行职责会得到奖励,反之被发现有不良行为就会被扣除一部分保证金,扣除保证金是直接销毁,相当于减少系统总资产。
- 因此,只有验证者有权利投票决定主链,两边下注会被视为不良行为,所以也就解决的 nothing at stake 的问题。
- 目前之所以还没有采用 POS 是因为还不成熟,而 POW 则是经过时间检验的成熟机制,所以目前的主流还是采用 POW 。EOS(柚子币)就是一种采用 POS 的加密货币,不过目前仍然处于不断调试改 bug 的过程中。
2.9 智能合约
- 智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容。智能合约的账户保存了合约当前的运行状态,包括余额(balance)、交易次数(nonce)、合约代码(code)和存储(storage,数据结构是一棵 MPT)。智能合约代码最常用的语言是 Solidity,语法和 JavaScript 接近。
- 智能合约的代码写完后,要编译成 bytecode 。
- 智能合约的创建是由一个外部账户向0x0地址发起交易,转账金额是0,但是要支付汽油费(交易费),合约的代码放在交易的 data 域里。
- 智能合约运行在 EVM(Ethereum Virtual Machine) 上,目的是提供可移植性或平台无关性。EVM 的寻址空间是256位,比当前通用PC的64位要高很多倍。
- 智能合约是一种 Truing-complete Programming Model ,可以实现很多比特币系统无法实现的功能。但也衍生出很多问题,比如交易可能引发死循环,系统对死循环束手无策,因为死循环属于停机问题(Halting Problem),是不可解的,不像 NPC 一样虽然很难但是可解。这类停机问题的处理方法是把问题推给发起交易的用户,系统会向发起交易的用户收取汽油费,在执行合约的过程中动态计算汽油费,执行合约需要的指令越多越复杂,汽油费就越贵,所以死循环的交易会让发起者承担巨额费用。
- 以太坊中的交易具有原子性,一旦遇到异常,整个交易全部回滚,不会只执行部分交易,所以也就不需要 try-catch 这种处理异常的语句,直接无脑回滚就完事了。
- 比特币系统对区块占用资源的限制方法是限制区块大小,区块大小的上限是写死在协议里的,因为比特币的交易很简单,交易信息的字节数越多消耗的资源就越多。而以太坊不能这么限制,因为以太坊的交易很复杂,字节数很少的交易可能会消耗大量的资源。以太坊通过限制整个区块的汽油费上限(gaslimit)来限制资源,因为交易执行过程中的任何操作都会耗费汽油费,设置 gaslimit 就是限制了操作的数量和复杂度上限,而且与比特币不同的是,每个区块的发布者都可以根据前一个区块的 gaslimit 来微调本区块的 gaslimit ,调整幅度是 $\frac{1}{1024}$ ,幅度这么小是因为以太坊出块速度太快,要防止 gaslimit 滚雪球速度过高。
- 对于一个区块,是先执行交易内容再挖矿,因为交易内容决定了 block header 中的三棵树的根哈希值,只有先算出三个根哈希值,才能对 block header 取哈希,尝试 nonce 值来挖矿。
- 执行失败的交易也可能会发布到区块中,因为矿工只有发布才能收到汽油费。判断交易是否成功就靠三棵树中的收据树,每个交易执行完后都生成一个收据,收据中包含了交易的状态。
- 以太坊不支持并行和并发,因为交易驱动的状态机必须是确定性的。同理,以太坊也不能生成真正的随机数,只能生成伪随机数,否则每个节点执行的结果就会不一样。
- 智能合约的代码是不能更改的,所以只要最初的代码有 bug,就会成为永久的漏洞,最后只能弃用该合约。可见,不可篡改性是一把双刃剑,一方面保证了合约的公信力,另一方面也给漏洞处理增添了困难。
- Solidity 语言的内容略~
2.10 TheDAO
- 略
2.11 反思
- 智能合约并不智能,只是一种按照写死的代码执行的自动化合同。
- 区块链的不可篡改性不是绝对的,写死在链上的东西至少还可以被硬分叉软分叉取消,只不过修改意见很难被通过罢了,所以没有什么是真正不可篡改的,不能盲目迷信。
- 比特币的脚本语言表达能力简陋,所以支持的功能很少。智能合约的 Solidity 语言图灵完备、表达能力很强,但是容易出现安全漏洞。所以应该寻找一种表达能力适中的编程语言。
- 智能合约不会因为公开性而有多安全,因为有时间读代码检查安全性的人就不多,其中有能力检查出漏洞的人就更少了。
- 去中心化的理念在于用户自己拥有决定权,只有用户决定做什么或支持什么,而不能是社区强迫用户做什么。所以用户可以选择分叉单干,也可以拒绝更新新协议,只不过后果要自己承担。
- 区块链不适合分布式计算,因为区块链的设计都是为了保证去中心化和共识机制,系统整体的运行效率其实是相当低的,想做分布式计算还是要用主流的云计算平台。
- 去中心化不一定比中心化更好。去中心化系统中的决策都是靠公投决定的,少数服从多数,多数人的决定不一定是最好的。其次,技术手段不一定比司法手段更安全、更公平,现实世界的法治体系是经过时间检验一路完善下来的结果,相比之下,区块链技术并不十分成熟。
2.12 美链
- 略