肖臻区块链b站网课笔记——19-26
视频链接:【北京大学肖臻老师《区块链技术与应用》公开课】 https://www.bilibili.com/video/BV1Vt411X7JF?p=6&;;;share_source=copy_web&vd_source=33abd457d9415a4317112e8206e5360e
【笔记的内容参考了评论区里其他同学的笔记,标注了自以为的重点,其实这部分我还很多看不懂,还是要另外仔细学】
19-ETH-挖矿算法
一、工作量证明
1.比特币系统的工作量证明
对于基于工作量证明的系统来说,挖矿是保障区块链安全的重要手段。
Bounty(赏金),Bug Bounty:有的公司悬赏来找软件中的漏洞。比特币的挖矿算法是一个天然的Bug Bounty,如果你能找到里面的漏洞,或者是某一个挖矿的捷径就能取得很大的利益。但是到目前为止还没有人发现,所以比特币的挖矿算法总的来说是比较成功的。
2.比特币系统挖矿算法的问题
比特币的挖矿算法有一个饱受争议的问题:挖矿设备的专业化,只能用专门的设备,专用的ASIC芯片来挖矿,那么很多人认为这种做法与去中心化的理念和比特币的设计初衷相违背。
中本聪最早的一篇论文,提出One CPU,one vote,理想状况下,应该让普通老百姓也能参与挖矿过程,这样也更安全,因为算力分散之后,有恶意的攻击者想要聚集到51%的算力发动攻击,难度就会大得多。所以比特币之后出现的加密货币包括以太坊设计Mining Puzzle的时候,就想要做到ASIC Resistance。
3.如何设计出对ASIC芯片不友好的Mining Puzzle(挖矿谜题)
一个常用的做法就是增加Mining Puzzle对内存访问的需求(memory hard mining puzzle)。同样的价格买一个ASIC矿机和买一个普通的计算机,这个ASIC矿机的计算能力是普通计算机的几千倍,但是内存访问方面的性能差距远没有这么大,所以能设计出一个对内存要求很高的Puzzle,就能起到遏制ASIC芯片的作用。
4.LiteCoin
LiteCoin(莱特币),曾经是市值仅次于比特币的第二大加密货币,其Puzzle是基于Scrypt(一个对内存要求很高的哈希函数,以前用于计算机安全领域,跟密码相关)。其具体设计思想是,开一个很大的数组,然后按照顺序填充一些伪随机数,比如说有一个种子节点,seed的值通过一些运算,算出一个数来,填在第一个位置,然后后面每个位置都是前一个位置的值取哈希得到的,如图。

伪随机数是说取哈希值后的值是不知道的,不可能真的用随机数,不然就没法验证。填充完后,里面的数有前后依赖关系,是从第一个数依次算出来的,需要求解这个puzzle的时候,按照伪随机数的顺序从数组当中读取一些数,每次读取的位置跟前一个数相关,如图。

(1)LiteCoin的好处
如果这个数组开的足够大的时候,挖矿的矿工就是Memory Hard,因为如果不保存数组,那么挖矿的计算复杂度会大幅度上升,需要从头计算。所以要想高效地挖矿,这个内存区域是需要保存的,有的矿工可能保存一部分内存区的内容,如,只保留数组中奇数位置的元素,这样数组可以少一半,用到偶数位置的数的话,要根据另外一半算一下,计算复杂度会提高一点,但是内存量可以减小一半,叫做time-memory trade off。
该设计的核心思想:不能仅仅进行运算,增加其对内存的访问,从而实现对ASIC芯片不友好,普通计算机能参与的。
(2)LiteCoin的缺点
LiteCoin的优点是对于矿工挖矿的时候是memory hard,缺点是对轻节点来说也是memory hard。设计puzzle的一个原则:difficult to solve, but easy to verify。这个问题在于验证这个puzzle需要的内存区域跟求解需要的区域几乎是一样,轻节点验证的时候也需要保存数组,否则计算复杂度也会大幅度提高。对于scrypt早期计算机的安全领域,不存在轻节点验证这个问题,但对于区块链来说是不行的。
(3)LiteCoin的实践
莱特币在真正使用的时候,这个内存区域不敢设置的太大,比如说设一个1G的数组,这对于计算机来说是不大的,但是如果是一个手机上的app,1G的内存可能就太大了。因为这个原因,实际莱特币在使用的时候,这个数组只有128Ks,连1M都不到,就是为了照顾轻节点。
当初莱特币在发行的时候,目标不仅仅是ASIC resistance,还是GPU resistance,就是挖矿最好连GPU都不要用,都用普通的CPU挖矿就行了。结果后来就出现GPU挖矿的,再后来就出现用ASIC芯片挖矿的,实践证明莱特币要求的128k内存不足以对ASIC芯片的生产和设计带来实际上的障碍。
莱特币跟比特币的另一个区别是莱特币的出块速度是比特币的4倍,他的出块间隔是2min30s,而不是10min,除此之外,这两种加密货币基本上是一样的。
二、以太坊的工作量证明
1.Memory hard mining puzzle
以太坊也是用一种Memory hard mining puzzle,具体设计上,跟莱特币有很大的不同。 以太坊用的是两个数据集,一大一小,小的是16M cache,大的数据集是一个1G 的dataset(DAG),这1G的数据集是从16M的cache生成出来的。
为什么设计成一大一小的两个数据集呢?
就是为了便于验证。轻节点只要保存16M cache就行了,只有需要挖矿的矿工才需要保存1G的dataset。
2.基本思想
16MB的小Cache的生成方式跟LiteCoin中数组的生成方式是比较类似的。
通过Seed进行一些运算获得第一个数,后每个数字都通过前一个位置的值取哈希获得。
(不同):
莱特币:直接从数组中按照伪随机顺序读取一些数据进行运算
以太坊:先生成一个更大的数组(注:以太坊中这两个数组大小并不固定,因为考虑到计算机内存不断增大,因此该两个数组需要定期增大)
大的DAG生成方式:
如第一次读取A位置数据,对当前哈希值更新迭代算出下一次读取位置B,再进行哈希值更新迭代计算出C位置元素。如此来回迭代读取256次,最终算出一个数作为DAG中第一个元素,如此类推,DAG中每个元素生成方式都依次类推。
以太坊挖矿过程:【太坊设计的挖矿算法(Ethash)】
根据区块block header和其中的nonce值计算一个初始哈希,根据其映射到某个初始位置A,读取A位置的数及其相邻的后一个位置A’上的数,根据该两个数进行运算,算得下一个位置B,读取B和B’位置上的数,依次类推,迭代读取64次,共读取128个数。最后算出一个哈希值来,跟挖矿难度的目标域值比较是不是符合难度要求,若不符合就重新更换Nonce,因为换了Nonce之后,第一次算的那个哈希值就变了,然后重复以上操作直到最终计算哈希值符合难度要求或当前区块已经被挖出。
每次读取大数据集中两个相邻位置的哈希值,这两个哈希值其实并没有什么联系,他们虽然位置相邻,但是生成的过程是独立的,每个都是由前面那个16M的cache中的256个数生成的,而且256个数的位置是按照伪随机数的顺序产生的,这个是构造大数据集的一个特点,每个元素独立生成,这才给轻节点的验证提供了方便,所以每次读取的相邻两个位置的哈希值是没有什么联系的。
3.ethash算法伪代码
(1)生成16M cache。
def mkcache(cache_size,seed):
o = [hash(seed)]
for i in range(1,cache_size):
o.append(hash(o[-1]))
return o
cache中每个元素都是64个字节的哈希值,生成的方法与莱特币类似(第一个元素是Seed的哈希,后面每个元素是前一个的哈希),哈希的内容每隔3万个区块会变化一次,Seed每隔3万个区块会发生变化,然后重新生成cache中的内容,同时cache的容量要增加原始大小的1/128,也就是16M的1/128=128K。
(2)通过cache来生成dataset中的第i个元素。
def calc_ dataset item(cache,i):
cache size = cache.size
mix hash(cache[i % cache_size] ^ i)
for j in range(256):
cache_ index = get_ int_ from_item(mix)
mix = make_item(mix,cache[cache_index % cache_size])
return hash(mix)
先通过cache中的第i % cache size个元素生成初始的mix。因为两个不同的dataset元素可能对应向一个cache中的元素为了保证每个初始的mix都不同,注意到i也参与了哈希计算。
【自己定义的两个函数】,get_int_from_item函数:用当前算出来的哈希值求出下一个要读取的位置,make_item函数:用cache中这个位置的数和当前的哈希值计算出下一个哈希值,这样迭代256轮,最后得到一个64字节的哈希值,作为大数据集中的第 i 个元素。
(3)此函数不断调用calc_dataset_item函数来依次生成dataset中全部full_size个元素。
def calc_dataset(full_size, cache);
return [calc_dataset_item(cache,i) for i in range(full_size)]
(4)矿工用来挖矿的函数。
def hashimoto_full(header,nonce,full_size,dataset):
mix = hash(header,nonce)
for i in range(64) :
dataset_index = get_int_from item(mix) % full_size
mix = make_item(mix,dataset[dataset_index])
mix = make_item(mix, dataset[dataset_index + 1])
return hash(mix)
矿工用来挖矿的函数,有四个参数:
header:当前要生成的区块的块头,以太坊和比特币的挖矿只用到块头的信息【轻节点只下载块头,就可以验证这个区块是否符合挖矿的难度要求】
nonce:当前尝试的nonce值
full_size:大数据集中元素的个数,每3万个区块会增加一次,增加原始大小的1/128也就是1G的1/128=8M
Dataset:前面生成的大数据集
(5)轻节点用来验证的函数。
def hashimoto light(header,nonce,full_size,cache):
mix = hash(header, nonce)
for i in range(64):
dataset_index = get_int _from item(mix) % full_size
mix = make_item(mix,calc_ dataset _item(cache,dataset_index))
mix = make_item(mix,calc_dataset_item(cache,dataset_index + 1))
return hash(mix)
header:这个区块的块头【矿工挖矿函数里是要生成的区块的块头】
nonce:包含在这个块头里的nonce,是发布这个区块的矿工选好的
轻节点的任务是验证这个nonce是否符合要求,验证用的是16M的cache,也就是最后的参数cache。full_size仍是大数据集的元素个数,验证的过程也是64轮循环,看上去与挖矿的过程类似。
每次从当前的哈希值算出要读取的元素的位置【指在在大数据集中的位置】,但是轻节点并没有这个大数据集,所以要从cache中生成大数据集中这个位置的元素,我们前面说过大数据集中每个元素都可以独立生成出来。
为何验证只需保存cache,而矿工需要保存大数组DAG?由于矿工需要验证非常多的nonce,如果每次都要从16M的cache中重新生成的话,效率太低了。而且这里面有大量的重复计算【随机选取的dataset的元素中有很多是重复的,可能是之前尝试别的nonce时用过的】。所以,矿工采取以空间换时间的策略,把整个dataset保存下来。轻节点由于只验证一个nonce,验证的时候就直接生成要用到的dataset中的元素就行了。
(6)矿工挖矿的主循环
def mine(full size,dataset,header,target):
nonce = random.randint(0,2**64)
while hashimoto_full(header,nonce,full_size,dataset) > target:
nonce = (nonce + 1) % 2**64
return nonce
这个函数,其实是不断尝试nonce的过程,这里的target就是挖矿的难度目标,跟比特币类似,也是可以动态调整的,nonce的可能取值是从0-2的64次方,对每个nonce用前面讲的那个函数算出一个哈希值,看看是不是小于难度目标,如果不行的话,就再试下一个nonce。
4.实际效果
目前为止,以太坊挖矿主要还是以GPU为主,用ASIC矿机的很少,所以从这一点来说,他比莱特币来说要成功,起到了ASIC Resistance的作用,这个跟以太坊的挖矿算法需要的大内存是很有关系的,这个挖矿算法就是ethash。矿工挖矿需要1G的内存,跟莱特币的128K比差别非常大,即使是16M的cache跟128K比差距也很大。而且这个大小还会定期增长。
以太坊没有出现ASIC矿机还有另一个原因,以太坊从很早就计划要从工作量证明转移到权益证明,即PoW(Proof of Work)→PoS(Proof of Stake)。所谓的权益证明,就是按照所占的权益进行投票来形成共识,权益证明是不挖矿,就类似于股份公司按照股票多少来进行投票。这对于ASIC矿机的厂商来说是个威胁,因为ASIC芯片的研发周期很长,且研发的成本也很高,将来以太坊转入权益证明之后,投入的研发费用就白费了。其实到目前为止,以太坊是基于工作量证明,以太坊很早就说要转入权益证明,但是转移的时间点一再往后推迟,到现在也没转过来,但是他不停的宣称要这么做,所以要想达到ASIC Resistance一个简单的办法就是不断地吓唬大家:大家注意哦,下面要搞权益证明就不挖矿了,所以你就不要设计ASIC矿机了,你设计出来到时候也没用了,因为要设计一年嘛,一年以后,我们就不挖矿了。
5.预挖矿(pre-mining)
以太坊中采用了预挖矿:在当初发行货币的时候,预留一部分货币给以太坊的开发者。
比特币就没有采用pre-mining的模式,所有的比特币都是挖出来的,只不过早期的挖矿的难度低。与pre-mining相关的一个概念叫pre-sale(就是把pre-mining预留的那些币通过出售的方法来换取一些资产用于加密货币的开发工作),有点类似于众筹。
20-ETH难度调整
为了维持出块时间在十分钟左右,比特币每隔2016个区块会调整一下挖矿难度。以太坊是每个区块都有可能调整挖矿难度,调整的方法也比较复杂。
1.难度调整公式

H是指当前这个区块,Hi是这个区块的序号,D(H)是这个区块当前的难度。
max括号里的是第一部分【基础部分】,为了维持出块时间大概在十五秒左右,∈是第二部分【难度炸弹】,为了向权益证明过渡。


难度炸弹这部分的取值,是从指数形式增长的。以太坊刚刚上线不久的时候,区块号都比较小,难度炸弹这部分算出来的值是很小的,基本上可以忽略不计,所以难度调整主要还是由基础部分(系统中的出块时间)来决定的。随着区块号越来越大,难度炸弹的威力开始显现。
本来担心大家不愿意转,现在变成了想转也没法转。以太坊最后决定计算难度炸弹的时候,把区块号回退三百万个区块,即把真实的区块号减去三百万,算出一个假的区块号,然后再算难度炸弹,给权益证明的上限争取了一些时间。

大概是370万个区块左右,难度炸弹的威力开始指数上升,到上面这个尖峰(就是以太坊决定回调这个难度炸弹的时候),减了三百万个区块,这个难度炸弹的取值一下就掉下来了,看上去好像是个平的直线,其实也是在增长。
二、以太坊发展的四个阶段
我们目前处于第三个阶段中的拜占庭阶段,难度炸弹回调就是在拜占庭阶段进行的。

以太坊系统在难度回调的同时,把出块奖励从5ETH降到了3ETH(如果不调的话,对于回调之前的矿工是不公平的),而且要维护总供应量的稳定,挖矿变得容易,就应该相应将出块奖励减少一些。比特币当中每隔一段时间出块奖励减半,以太坊中是没有的,这次是一次性的。
三、代码实现
1.Byzantium阶段,挖矿难度调整的代码
输入是父区块的时间戳和难度,计算出当前挖的这个区块的难度。代码中的BigTime就是当前区块的时间戳,bigParentTime就是父区块的时间戳。

2.计算基础部分的难度调整

第一行就是把当前时间戳减去父区块的时间戳算出出块时间,然后第二行除以9向下取整。判断一下是不是有叔父区块,有的话,是用2减去前面这个数x,没有的话用1减去前面这个数x,然后接下来跟负的99相比,接下来算的是难度调整的力度,父区块的难度除以这个DifficultyBoundDivisor【实际上就是2048】,然后跟前面算出的系数相乘,加到父区块的难度上面去,基础部分的难度调整有一个下限,难度再小也不能小于那个D0,这个MinimumDifficulty就是那个D0=131072。
3.难度炸弹的计算

为什么不是减去3000000,而是2999999?因为这里判断的父区块号,而公式中是根据当前区块来算的。
四、以太坊实际统计统计
1.以太坊中的难度统计
到尖峰的位置就是以太坊决定回滚难度炸弹,回滚三百万个区块,挖矿难度一下就掉下来了,目前以太坊的挖矿难度基本上是区域稳定的。

2.出块时间统计
出块时间稳定在15s左右。

3.两个区块
difficulty为当前区块难度,total difficulty为当前区块链上所有区块难度相加。
最长合法链等同于最难合法链(难度最大合法链)。每个区块的难度,反应的是挖出这个区块所需要的工作量。一般来说,靠后的区块挖出来需要的工作量比较大。

21-权益证明
比特币和以太坊目前都是基于工作量的证明,这种共识机制的一大典型缺点就是浪费电。
以太坊平均每个交易能耗远远低于比特币,由于比特币系统中,出块时间过长导致的。
系统给予出块奖励的目的是激励矿工参与区块链系统维护,进行记账,而挖矿本质上是看矿工投入资金来决定的(投入资金买设备->设备决定算力->算力比例决定收益)。
那么,为什么不直接拼“钱”呢?为什么不大家都将钱投入到系统开发和维护中,而根据投入钱的多少来进行收益分配呢?这就是权益证明的基本思想。
权益证明
一般来说,采用权益证明的货币,会先预留一些货币给开发者,开发者也会出售一些货币换取开发所需要的资金,在系统进入稳定状态后,每个人都按照持有货币的数量进行投票。
优点:
①省去挖矿过程,避免了因此产生的能耗和对环境影响,减少了温室气体的排放。
②发动攻击的资源只能从加密货币系统内部得到【维护区块链安全的资源形成闭环】。POW中维护其安全的资源需要通过现实中流通的货币购买矿机等设备进去区块链的,攻击者只需要外部聚集足够资金就可以攻击成功(小型币种很容易被攻击,也就是在摇篮里就扼杀掉)。
权益证明和工作量证明混合模型:
POS与POW并不互斥。有的加密货币采用的是一种混合模型。持有的币越多,挖矿的难度就越小,根据持有的这个币的权益降低调整你的挖矿难度(实际并不能这么简单设置,因为会导致“旱的旱死,涝的涝死”,需要添加一定限制)。所以,有的加密货币要求你投入的币会被锁定一段时间,不能重复使用,这种情况叫做Proof of Deposit。
权益证明这么好,为什么实际中并未得到大规模应用呢?
仍存在很多挑战,如“双边下注”:

区块链系统产生了分叉,采用权益证明的方法就是所有持币者对这两个区块投入币进行投票,从而决定哪一个区块成为最长合法链上的区块。假如有一个人,在A和B同时进行了下注。最终A区块胜出,那么他能够获得A区块相应收益,而在B区块进行投票放入的“筹码”也会被退还,这也就导致其每次都能获得收益。
一个人可以拥有多个账户,无法强迫一个人一次只能投向一个区块。越有钱的人,通过“双边下注”得到的收益也就越多。
以太坊拟采用的权益证明
以太坊中,准备采用的权益证明协议为Casper the Friendly Finality Gadget(FFG),该协议在过渡阶段是要和POW结合使用的。为工作量证明提供Finality,Finality是一种最终的状态,包含在Finality中的交易不会被取消,单纯基于工作量证明的交易是有可能被回滚的。
在比特币系统中,为了防范分叉攻击,一个交易在其获得6次确认(其后跟着6个区块)后认为该区块安全。但实际上,这种安全只是概率意义上的安全,仍然可能会被拥有强大算力的用户在其前面发动分叉攻击进行回滚。
Casper协议引入一个概念:Validator(验证者),一个用户想要成为Validator,需要上交一笔“保证金”,这笔保证金会被系统锁定。Validator的职责是推动系统达成共识,投票决定哪一条链成为最长合法链,投票权重取决于保证金数目。
混用的时候还是有人挖矿的,挖矿每挖出100个区块就作为一个epoch,然后决定能不能成为Finality,要进行一些投票,第一轮投票是Prepare Message,然后第二轮是Commit Message,Casper规定每一轮投票都要得到2/3的验证者才能通过(按照保证金的金额大小来算)。实际系统当中不再区分这两个Message,而且把这个epoch从原来的100个区块减少到50个区块,变成了每50个区块就是一个epoch,每个epoch只用一轮投票的就行了,这一轮投票对于上一个epoch来说是个Commit Message,对于下一个来说是一个Prepare Message,要连续两轮投票,两个epoch都得到2/3以上的多数,才算有效。

矿工挖矿会获得出块奖励,而验证者也会得到相应奖励。当然,为了防止验证者的不良行为,规定其被发现时要受到处罚。如“行政不作为”,扣掉部分保证金;如“乱作为”,没收全部保证金。没收的保证金被销毁,从而减少系统中货币总量。验证者存在“任期”,在任期结束后,进入“等待期”,在此期间等待其他节点检举揭发是否存在不良行为,若通过等待期,则可以取回保证金并获得一定投票奖励。
思考
(1)Casper 协议可以给挖矿完成一个区块链的某一种状态做一个检查点,做一个check point,那这个check point是不是绝对安全的?通过这个验证者投票达成的Finality有没有可能被推翻?
包含在Finality里的交易是不会被推翻的。单纯是有恶意的矿工,无论他算力有多强,如果没有验证者作为同伙是不可能推翻的。攻击成功的情况:有大量【至少1/3(该协议规定超过2/3才有效)】的验证者两边下注。
(2)以太坊是要逐步从工作量证明过渡到权益证明。随着时间的推移,挖矿得到的奖励是越来越少的,权益证明得到的奖励是越来越多的,最后达到完全不用挖矿的境界。以太坊为什么从一开始就不用权益证明呢?
权益证明仍然存在缺陷,但工作量证明已经得到了事实检验,该机制较为成熟。
目前,EOS加密货币,即“柚子”,2018年上线,就是采用权益证明的共识机制,其采用的是DPOS:Delegated Proof of Stake。该协议核心思想是通过投票选21个超级节点,再由超级节点产生区块。但目前,权益证明仍然处于探索阶段。
22-ETH-智能合约
一、什么是智能合约
1.智能合约的本质是运行在区块链上的一段代码,代码的逻辑定义了智能合约的内容。
2.智能合约的账户保存了合约当前的运行状态
(1)balance:当前余额;
(2)nonce:交易次数;
(3)code:合约代码;
(4)storage:存储,数据结构一是一颗MPT;
3.Solidity是智能合约最常用的语言,语法上与JavaScript很接近。
1.智能合约的创建
智能合约由一个外部账户发起一个转账交易,转给0x0这个地址,然后把这个要发布合约的代码放到data域里面。合约的代码要编译成bytecode,然后在EVM上运行。JVM,Java Virtual Machine,目的是增强可移植性。EVM类似这种设计思想,通过加一层虚拟机,对智能合约的运行提供一个一致性平台。EVM有时叫做Worldwide Computer(全世界的一个计算机),EVM的寻址空间非常大,为256位,unsigned int就是256位。
比特币设计理念是简单,脚本语言的功能很有限,如:不支持循环。以太坊要提供一个图灵完备的编程模型,Turing-complete Programming Mode。问题:当一个全节点收到一个对智能合约的调用,怎么知道这个调用执行起来会不会导致死循环?
没有办法,这实际上是一个Halting Problem停机问题,是不可解的。不是NPC的,NPC的问题可解,但没有多项式时间的解法。从理论上可以证明不存在这样一个算法,能够对任意给定的输入程序判断出这个程序是否会停机。这时我们会把这个问题推给发起交易的那个账户,以太坊引入了汽油费机制,即发起对智能合约的调用时需要支付相应的汽油费。
二、智能合约的代码结构

1.Solidity语言
Solidity是面向对象的编程语言,定义了很多状态变量。Solidity是强类型语言,address类型是Solidity语言所特有的。
上述代码段中的event事件,是用来记录日志的。事件①HighestBidIncreased,拍卖的最高出价增加了,记录最新高价的参数(address bidder),金额是amount;事件②是Pay2Beneficiary,参数是赢得拍卖的人的地址及最后出价amount。
Solidity语言跟其他普通编程语言相比,有一些特别之处。如:mapping,mapping是一个哈希表,保存了从地址到unit的一个映射。Solidity语言中哈希表不支持遍历,如果想遍历,需要记录哈希表中有哪些元素,用bidders数组记录下来,Solidity语言中的数组可以是固定长度的,也可以是动态改变长度的。上述代码是一个动态改变长度的数组,如果要在数组里增加一个元素,就用push操作,即bidders.push(bidder):新增加一个出价人在数组的末尾;想知道这个数组有多少个元素,可以用bidders.length。
Solidity语言中定义构造函数有两种方法,构造函数只能有一个。一种方法就是像c++构造函数一样,定一个与contract同名的函数,这个函数可以有参数,但是不能有返回值。实际上新版本Solidity语言更推荐用这里使用的方法:用一个constructor来定义一个构造函数,这个构造函数只有在合约创建的时候会被调用一次。
最后是三个成员函数,三个函数都是public,说明其他账户可以调用这些函数。
2.bid函数
在bid函数中,有一个payable(另外两个函数都没有),以太坊中规定如果这个合约账户要能接收外部转账的话,那么必须标注成payable。
拍卖规则是,调用bid函数时要把拍卖的出价100个以太币也发送过去,存储到这个合约里,锁定到拍卖结束。避免有人凭空出价。所以bid函数要有能够接收外部转账的能力,所以才标注一个payable。withdraw函数就没有payable,拍卖结束了,没赢得拍卖的人可以调用withdraw把锁定在智能合约里的以太币取回来。
3.fallback()函数
function()public [payable]{……}
这个函数没有参数、返回值、函数名,是个匿名函数,fallback这个关键字也没有出现在这个函数名里。调用这个合约的时候,A调用B这个合约,要在转账交易的data域说明你调用的是B当中的哪个函数。若没说调用哪个函数,或者调用的函数不存在,就调用这个fallback()函数。
如果fallback()函数需要有接收转账的能力的话,也需要写成是payable,一般情况下,都是写上payable的,如果合约账户没有任何函数标识为payable,那么这个合约没有任何能力接受外部的转账。
fallback()函数不是必须定义的。
fallback()函数和payable都是在合约定义的时候写的,我给你转账时候不用写payable,也不用写fallback()。
转账金额可以是0,但汽油费是要给的,转账金额是给收款人的,汽油费是给发布这个区块的矿工的,如果汽油费不给的话,矿工不会把你这个交易打包发布到区块链。
汽油费
当一个全节点收到一个对智能合约的调用,先按照最大汽油费收取,从其账户一次性扣除,再根据实际执行情况,多退少补(汽油费不够会引发回滚,而非简单的补齐)。
以太坊中存在gaslimit,通过收取汽油费保障系统中不会存在对资源消耗特别大的调用。但与比特币不同,比特币直接通过限制区块大小1MB保障对网络资源压力不会过大。而以太坊中,每个矿工都可以以前一个区块中gaslimt为基数,进行上调或下调1/1024,从而,通过绝大多数区块不断上下调整,保证得到一个较为理想化的gaslimt值。最终整个系统的gaslimt就是所有矿工希望的平均值。
为什么要引入汽油费?
在比特币系统中,交易较简单【仅转账】,可通过交易的字节数衡量出交易所需要消耗的资源多少。以太坊中引入了智能合约,而智能合约逻辑很复杂,其字节数与消耗资源数并无关联。在block header中包含了gaslimit,其并非将所有交易的消耗汽油费相加,而是该区块中所有交易能够消耗的资源的上限。
错误处理:
(1)交易执行完之后,发现汽油费没有达到当初的GasLimit,那么多余的汽油费会被退回到这个账户里。若交易执行到一半,GasLimit已经用尽,该合约的执行要退回到开始执行之前的状态,这是一种错误处理,这个时候已经消耗掉的汽油费是不退的。否则会有恶意的节点发动delayous service attack,发布一个计算量很大的合约,然后不停的调这个合约,每次调的时候给的汽油费都不够,最后汽油费还会退回来,那么对这个节点来说没有什么损失,但矿工白白浪费了很多资源。
(2)assert语句和require语句,都是用来判断某种条件,如果条件不满足的话,就会导致抛出异常。assert语句一般来说是用于判断某种内部条件,require语句一般用于判断某种外部条件,如判断函数的输入是否符合要求。
function bid() public payable {
//对于能接收以太币的函数,关键字payable是必须的。
//拍卖尚未结束
require(now <= auctionEnd);
}
当前时间now<=拍卖的结束时间auctionEnd,则继续执行,否则,会抛出异常。第三个语句是revert(),无条件的抛出异常,如果执行到revert语句,那么会自动回滚。早期版本里用throw语句,新版本建议改用revert这个语句。solidity当中没有这种try-catch这种结构,有的编程语言像Java(用户自己可以定义出现问题后怎么办,他有这种try-catch)。
嵌套调用是否发生回滚,取决于调用方式。一个合约向一个合约账户直接转账,因为fallback函数的存在,仍有可能会引发嵌套调用。
二、外部账户如何调用智能合约
调用智能合约其实跟转账是类似的。如A发起一个交易转账给B,如果B是一个普通的账户,那么这就是一个普通的转账交易,与比特币转账交易是一样的。如果B是一个合约账户的话,那么这个转账实际上是发起一次对B这个合约的调用,那么具体是调用合约中的哪个函数,是在数据域data域中说明的,如图。

三、一个合约如何调用另一个合约中的函数
1.直接调用

A这个合约就只是写成log,event定义事件LogCallFoo,emit LogCallFoo():用emit这个操作来调用这个事件,作用就是写一个log,对于程序的运行逻辑是没有影响的。
B合约中,函数参数是一个地址(A合约的地址),然后把这个地址转换成A这个合约的一个实例,然后调用其中的foo这个函数。
以太坊中规定一个交易只有外部账户才能够发起,合约账户不能自己主动发起一个交易。这个例子当中需要有一个外部账户调用了合约B当中的这个callAFooDirectly函数,然后这个函数再调用合约A当中的foo函数。
错误处理:直接调用的方式,一方产生异常会导致另一方也进行回滚操作。如果调用的合约在执行过程中出现错误,那么会导致发起调用的合约也跟着一起回滚,如果在直接调用方法中A在执行过程出现异常,B这个合约也跟着一起出错。
2.使用address类型的call()函数
funcsing:要调用函数的签名,然后后面跟的是调用的参数。该方法与直接调用方法相比,区别是对于错误处理的不同。address.call()这种形式,如果在调用过程中,被调用的合约抛出异常,那么这个call函数会返回false,表明这个调用是失败的,但发起调用的这个函数不会抛出异常,可以继续执行。

3.代理调用 delegatecall()
与address.call()方法基本上是一样的,一个主要的区别是delegatecall不需要切换到被调用的合约的环境中去执行,而是在当前合约环境中执行就可以了,比如就用当前账户的账户余额存储之类的,如图。

一、思考【挖矿与智能约合执行】
1.假设某个全节点要打包一些交易到一个区块里,其中有一些是对智能合约的调用,那么这个全节点应该先执行完智能合约再挖矿,还是先挖矿获得记账权再执行这些智能合约?
在区块链中,如果有一笔转账交易发布上去,需要所有的全节点都执行的,因为所有的全节点要同步状态,都要在本地执行这个转账交易。比特币系统也一样,比特币发布一个交易到区块链上,也需要所有全节点都得执行这个转账交易,以便更新UTXO。
全节点收到一个对合约的调用的时候,要一次性的先把这个调用,可能花掉的最大汽油费从发起这个调用的账户上扣掉。以太坊系统中存在三棵树,即状态树、交易树和收据树,这三棵树都是全节点在本地维护的数据结构,状态树记录了每个账户的状态包括账户余额。
汽油费是怎么扣的?全节点收到调用时,从本地维护的数据结构中将账户余额减掉就可以,若余额不够,则这个交易不能执行,一次性要按GasLimit把余额减掉。
智能合约执行过程中任何对状态的修改都是在改※本地※的数据结构,只有在合约执行完了,而且发布到区块链上之后,本地的修改才会变成外部可见的,才会变成区块链上的共识。以太坊存在很多全节点,每个全节点都在本地做这个事情,执行的智能合约可能不完全一样(因为收到的交易可能执行不完全一样),如果某个全节点发布一个区块,收到这个区块之后,其他节点本地执行的就扔掉了,要将这个新发布区块里的交易再执行一遍,更新本地的三棵树。如果本来已经执行一遍了,但没有挖到矿,发布新区块了还得执行一遍,因为其他节点组装的在本地候选区块中包含的交易跟刚发布的那个交易中区块里包含的交易不一定完全一样,至少给出块奖励的那个地方肯定不一样,所以没有办法,都是得要重新执行一遍。
以太坊挖矿是尝试各种Nonce找到一个符合要求的,计算哈希的时候要用到Block Header的内容:Root,TxHash,ReceiptHash,是那三棵树的根哈希值。所以得先执行完这个区块中的所有交易包括智能合约的交易,这样才能更新这三棵树,这样才能知道这三个根哈希值,这样这个Block Header的内容才能确定然后才能尝试各个Nonce。
假设一个矿工费了半天劲执行这些智能合约,消耗了本地的很多资源,最后我挖矿没挖到,那该矿工能得到什么补偿,能得到汽油费吗?
设置汽油费的目的是对于矿工执行这些智能合约所消耗的这些资源的一种补偿,但是这种补偿只有挖到矿的矿工才能得到,其他的矿工相当于陪跑。
2.会不会有的矿工不给汽油费,就不验证?
为保障区块链的安全,要求所有全节点要独立验证发布的区块的合法性。
跳过验证这个步骤,以后就没法再挖矿了。因为验证的时候是要把区块的交易再执行一遍,更新本地的三棵树。如果不去验证的话,本地三棵树的内容没有办法更新(本地的这些状态就不对了,算出的根哈希值发布出去之后别人认为是错的)。
为什么要执行才能更新状态?因为发布的区块里没有这三棵树的内容【不能将状态树的整个状态发布到区块链上,多且重复】。
在一个矿池中,存在验证阶段的“抄作业”情况,也就是全节点负责统一验证,其他矿工就负责相信全结点的验证情况。就是说,全节点分配给矿工的只是Pullze的内容,Pullze是根据区块链更新得到的,矿工则不需要考虑这部分内容。
3.发布到区块链上的交易是否都是成功执行的?如果智能合约执行过程中出现了错误,要不要也发布到区块链上去?
执行发生错误的交易也要发布到区块链上去,否则汽油费扣不掉,光是在本地的数据结构上把他的账户扣了汽油费,是没用的,拿不到钱,你得把区块发布上去之后形成共识扣掉的汽油费才能成为你账户上的钱。所以发布到区块链上的交易不一定都是成功执行的,而且要告诉大家为什么扣汽油费,而且别人得验证一遍,也要把这个交易执行完一遍,看扣的是不是对的。
Receipt数据结构
前面说过那三棵树,每个交易执行完后形成一个收据,这个是这个收据的内容,Status这个域就是告诉你交易执行的情况如何,如图。

4.智能合约是不是支持多线程?
Solidity不支持多线程,他根本没有支持多线程的语句,原因是以太坊是一个交易驱动的状态机,这个状态机必须是完全确定性的,即给定一个智能合约。所有全节点都得执行同一组操作到达同一个状态,要进行验证,若状态不确定,三棵树的根哈希值根本对不上。
多线程的问题是什么?多个核对内存访问顺序不同的话,执行结果有可能是不确定的。除了多线程之外,其他可能造成执行结果不确定的操作,智能合约也都不支持,最简单的会导致执行结果不确定的操作:产生随机数。所以以太坊的智能合约没有办法产生真正意义下的随机数,他用的是一些伪随机数,否则,又会出现前面的问题,每个全节点执行完一遍得到的结果都不一样。
其不支持多线程,所以无法通过系统调用获得系统信息,因为每个全节点环境并非完全一样。因此只能通过固定的结构获取。下图分别为为其可以获得的区块链信息和调用信息。


msg.sender是发起调用的人,tx.origin是交易的发起者,不一样。如有个外部账户A调用了一个合约C1,C1中有一个函数f1,f1又调用另外一个合约,里面的函数是f2。那么对于函数f2来说,msg.sender是这个合约,当前msg call这个调用,是这个合约发起的,而tx.origin是A这个账户,因为整个交易的发起者是A账户。
· msg.data就是数据域,在里面写了调用哪些函数和这些函数的参数取值。msg.sig是msg.data的前四个字节,就是函数标志符(调用的是哪个函数)。now和block.timestamp是当前区块的时间戳,智能合约里没有办法获得很精确的时间,只能获得跟当前区块信息的一些时间。
以太坊地址类型
第一个,以wei为单位的地址类型的余额中,uint256并不是指其包含一个类型为uint256的参数,而是指该变量本身为uint256类型的变量。
address.balance指的是address这个账户的余额
address.transfer(12345),当前合约向address地址中转入12345Wei。后面的函数同理。

在以太坊中,转账有以下三种方法:
transfer在转账失败后会导致连锁性回滚,抛出异常;而send转账失败会返回false,不会导致连锁性回滚。call的方式本意是用于发动函数调用,但是也可以进行转账。
前两者在调用时,只发生2300wei的汽油费,这点汽油费很少,只能写一个log,而call的方式则是将自己还剩下的所有汽油费全部发送过去(合约调用合约时常用call,没用完的汽油费会退回)。例如A合约调用B合约,而A不知道B要调用哪些合约,为了防止汽油费不足导致交易失败,A将自己所有汽油费发给B来减少失败可能性。

拍卖例子:
(1)主函数
pragma solidity ^0.4.21;
contract SimpleAuction {
address public beneficiary; //拍卖受益人
uint public auctionEnd; //结束时间
address public highestBidder; //当前的最高出价人
mapping( address => uint) bids; //所有竞拍者的出价
address[] bidders; //所有竞拍者
//需要记录的事件
event HighestBidIncreased(address bidder,uint amount);
event -Pay2Beneficiary( address - winner , uint amount);
//以受益者地址 `_beneficiary` 的名义,创建拍卖,拍卖时间为 `_biddingTime` 秒。
constructor(uint _biddingTime,address _beneficiary
)public {
beneficiary = _beneficiary;
auctionEnd = now + biddingTime;
}
(2)拍卖用的函数——bid函数
//对拍卖进行出价,随交易一起发送的ether与之前已经发送的ether的和为本次出价。
function bid()public payable { //能接收以太币的函数,关键字payable是必须的。
require(now <= auctionEnd); //拍卖尚未结束
//如果出价不够高,本次出价无效,直接报错返回
require(bids[msg.sender]+msg.value > bids[highestBidder]);
//如果此人之前未出价,则加入到竞拍者列表中
if (!(bids[msg.sender] == uint(0))){
bidders.push(msg.sender);
}
//本次出价比当前最高价高,取代之
highestBidder = msg.sender;
bids[msg.sender] += msg.value;
emit HighestBidIncreased(msg.sender,bids[msg.sender]);
}
要竞拍就发起一个交易调用这个拍卖中合约的bid函数,这个bid函数没有参数,竞拍的时候出的价格其实是在msg.value写的。逻辑是:①查拍卖是否结束,结束了还出价则抛出异常;②查该账户上一次的出价加上你当前发的以太币是否为最高出价(bids是个哈希表,Solidity中哈希表的特点是,如果你要查询的那个键值不存在,那么他返回默认值就是0,所以如果没有出过价,第一部分就是0);③如果是第一次拍卖,把拍卖者的信息放到bidders数组里(Solidity哈希表不支持遍历,要遍历哈希表的话,要保存一下它包含哪些元素,然后记录一下新的最高出价人是谁,写一些日志之类的)。
(3)拍卖用的函数——拍卖结束的函数
//结束拍卖,把最高的出价发送给受益人,并把未中标的出价者的钱返还
function auctionEnd( ) public {
//拍卖已截止
require(now > auctionEnd); //该函数未被调用过
require(!ended) ;
//把最高的出价发送给受益人
beneficiary.transfer(bids[highestBidder]);
for (uint i = 0;i<bidders.length;i++){
address bidder = bidders[i];
if(bidder == highestBidder) continue;
bidder.transfer(bids[bidder]);
}
ended = true;
emit AuctionEnded(highestBidder, bids[highestBidder]);
}
①查拍卖是否已结束,如果拍卖还没有结束,调用这个函数就是非法的,会抛出异常;②判断这个函数是不是已被调过,如果已经被调过了,就不用再调一遍了;如果没有,首先把这个金额给这个beneficiary,beneficiary.transfer是当前这个合约把这个金额给这个beneficiary转过去,最高出价人的钱是给受益人了;③那些剩下的没有竞拍成功的用一个循环,把这个金额退回给这个bidder,然后标明一下,这个函数已经执行完了写一个log。
(4)上面智能合约存在的问题
智能合约是不可篡改的,有bug也没法改。
auctionEnd这个函数必须要某个人调用才能执行,这也是Solidity语言跟其他编程语言不同的一个地方,没法自动执行。可能是拍卖的受益人beneficiary,也可能是参与竞拍没有成功的人去调用,如果两个人都调用auctionEnd,矿工在执行的时候把第一个调用执行完了,第二个就不再执行了(因为第一个执行完之后,ended就是true了,没有并发执行)。
假设有一个人通过这样的一个合约账户参与竞拍,会有什么结果?
pragma solidity ^0.4.21;
import "./simpleAuctionV1.sol";
contract hackV1 {
function hack_bid(address addr) payable public {
simpleAuctionV1 sa = simpleAuctionV1(addr);
sa.bid.value(msg.value)();
}
}
函数hack_bid,参数是拍卖合约的地址,然后把它转成这个拍卖合约的一个实例,然后调用拍卖合约用的bid函数,把这个钱发送过去。这是一个合约账户,合约账户不能自己发起交易,所以实际上得有一个黑客从他自己的外部账户发起一个交易,调用这份合约账户的hack_bid函数,然后这个函数再去调用拍卖合约的bid函数,把他自己收到的转过来的钱(这个黑客外部账户转过来的钱再)转给这个拍卖合约中的bid函数,就参与拍卖了。
这个auctionEnd,这个合约参与拍卖没有问题,最后拍卖结束退款的时候会有什么问题?如图红框里的循环退款,退到合约账户上的钱会有什么情况,退到黑客合约账户上的钱会有什么情况?

黑客外部账户对这个合约来说是不可见的,拍卖合约能看到的只是这个黑客的合约,转账的时候没有调用任何函数,调用fallback函数,而这个合约没有定义fallback函数,所以会调用失败,抛出异常。transfer函数会引起连锁式的回滚,就会导致这个转账操作是失败的,会收不到钱。
如有20个人参与竞拍,黑客合约排在第10个,最高出价人排在第16个,一开始执行时先把钱转给受益人了(转账实际上是你这个矿工或是全节点执行到beneficiary.transfer时把相应账户的余额进行了调整)。这个合约当中无论是排在黑客合约前面还是后面,都是在改本地数据结构,只不过排在后面的bidder根本没有机会来得及执行,然后整个都回滚了,所以排在前面的这些转账并没有执行,就是改本地结构。如果都顺利执行完了,发布出去之后,别的矿工也把这个auctionEnd重头到尾执行一遍,也改它本地的数据结构跟你的能对得上就叫形成共识了,而不是说没有一个转账交易的语句是产生一个新的交易写到区块链上,所以都收不到钱,没有任何一个人能收到钱。
发起这个攻击的有可能是故意捣乱,写这样一个程序让大家都拿不到钱;也可能是这个人不懂,他就忘了写fallback函数了,那出现这种情况怎么办呢?
现在的问题是你已经把钱投进去,锁里面了,怎么把它取出来的问题,出现这种情况是没有办法的。智能合约设计的不好的话,有可能把以太币永久的锁起来。以前有用智能合约锁仓的,如要开发一个新的加密货币,然后pre-mining先预留一部分币给开发者,这些币都打到一个智能合约账户锁三年,三年以后这些币才能卖,这样做为了大家一开始能集中精力开发加密货币。智能合约锁仓是个常用的操作,在发布一个智能合约之前一定要多次测试。
如何解决?
那能不能在这个智能合约里留一个后门,用来修复bug,比如给合约的创建者超级用户的权利。这样做的前提是所有人都要信任这个系统管理员。
解决重入攻击
网上竞拍第二版:由投标者自己取回出价
//使用withdraw模式
function withdraw( ) public returns (bool) {
//竞拍成功者需要把钱给受益人,当前地址有钱可取,账户余额是否为正
require(now > auctionEnd); //拍卖已截止
require(msg.sender != highestBidder);
require(bids[msg.sender] > 0);
uint amount = bids[msg.sender]; //账户余额
if(msg.sender.call.value(amount)()) { //把账户余额转给msg.sender
bids[msg.sender] = 0; //把账户余额清成0
return true;
}
return false;
}
//结束拍卖,把最高的出价发送给受益人
event Pay2Beneficiary( address winner,uint amount);
function pay2Beneficiary ()public returns(bool) {
require(now > auctionEnd); //拍卖已截止
require(bids[highestBidder] > 0); //有钱可以支付,最高出价的金额大于零
uint amount = bids[highestBidder];
bids[highestBidder] = 0;
emit Pay2Beneficiary(highestBidder,bids[highestBidder]);
if(!beneficiary.call.value(amount)()){
bids[highestBidder] = amount;
return false;
}
return true;
}
(1)存在的问题
重入攻击,如果有黑客写了一个如下方程序会怎么样?
pragma solidity ^0.4.21;
import "./simpleAuctionv2.sol";
contract HackV2 {
uint stack = 0;
function hack_bid( address addr) payable public {
simpleAuctionv2 sa = simpleAuctionv2(addr);
sa.bid.value(msg.value)();
}
function hack_withdraw(address addr) public payable{
SimpleAuctionv2(addr).withdraw();
}
function() public payable{
stack += 2;
//当前调用的剩余汽油,msg.gas还有6000个单位以上,调用栈的深度不超过500
if (msg.sender.balance >= msg.value && msg.gas > 6000 && stack < 500{
SimpleAuctionV2(msg.sender).withdraw();
}
}
}
这个hack_bid跟前面的那个黑客合约hack_bid合约是一样的,通过调用拍卖bid函数参与竞拍,hack_withdraw就在拍卖结束的时候调用withdraw函数,把钱取回来,这两个看上去好像都没有问题,问题在于fallback函数,他又把钱取了一遍。
在hack_withdraw调用withdraw函数的时候,执行到“if(msg.sender.call.value(amount)())”会向黑客合约转账,这个msg.sender就是黑客的合约,把当初出价的金额转给他,而在这个合约中,又调用了拍卖函数的withdraw函数“SimpleAuctionV2(msg.sender).withdraw();”,又去取钱,fallback函数这里的msg.sender就是这个拍卖合约,因为是拍卖合约把这个钱转给这个合约的,这个左边的拍卖合约执行到if那里,再给他转一次钱,注意这个清零的操作,把黑客合约账户清零的操作,只有在转账交易完成之后,才会进行,而前面if这个转账的语句已经陷入到了跟黑客合约当中的递归调用当中,根本执行不到下面这个清零操作,所以最后的结果就是这个黑客一开始出价的时候给出了一个价格,拍卖结束之后,就按照这个价格不停地从这个智能合约中去取钱,第一次取得是他自己的出价,后面取得就是别人的钱了。
递归重复取钱,持续到什么时候会结束?有三种情况:
①这个拍卖合约上的余额不够,不足以支持转账的语句
②汽油费不够
③调用栈溢出了,所以黑客合约的fallback函数判断一下这个拍卖合约的余额还足以支持转账,当前调用的剩余汽油,msg.gas还有6000个单位以上,调用栈的深度不超过500。那么就再发起一轮攻击,那怎么办呢?
(2)如何处理
最简单的就是先清零再转账,就是Pay2Beneficiary的这种写法,把highestBidder的账户余额清成零了(在bids哈希表里面的余额已经清成0了),然后再转账,转账如果不成功的话,再把余额恢复。实际上是对于可能跟其他合约发生交互的情况的一种经典的编程模式,就先要判断条件,然后改变条件,最后再跟别的合约发生交互。
另一种修改方式:

不用call.value的形式转账,对比一下修改前后的两段代码(绿框的部分),把清零的位置提前了(先清零再转账);转账的时候用的是sender(transfer也可),sender和transfer一个共同的特点就是转账的时候发送过去的汽油费只有2300个单位,这个不足以让接收的那个合约再发起一个新的调用,只够写一个log而已。

智能合约中安全漏洞的例子:The DAO(2016年)、美链(2018年04月)。
23-ETH-TheDAO
一、TheDao
1.提出背景
重入攻击在现实中真的发生过,造成了以太坊的分裂,可以说以太坊的历史都被它改写。
比特币实现了去中心化的货币,以太坊实现了去中心化的合约,既然去中心化这么好,为什么不把所有的都改成去中心化,所以有人提出一个口号:decentralized everthing,DAO(Decentralized Automous Organization,去中心化的自治组织)就是在这个背景下产生的。
在区块链上,DAO这个组织就是建立在代码基础上的,组织的规章制度是写在代码里的,通过区块链的共识协议维护这种规章制度的正常执行。
2.The DAO的工作原理
在2016年出现了一个致力于众筹投资的DAO——The DAO,投资的钱是在区块链上众筹得到的,其本质是一个运行在以太坊上的智能合约。如果想要参与The DAO,可以把以太币发给这个智能合约,然后换回The DAO的代币。需要决定投资哪个项目的时候是大家投票决定的,手里的代币越多,投票权中就越大。投资后如果有了收益,也是按照智能合约中的规章制度进行收益分配的。
The DAO工作原理有点像DAC(Decentralized Automous Corporation,去中心化的自治公司)。区别是:DAC是处于盈利目的的,DAO可以是处于非盈利性目的。虽然是Corporation,但是现实中不具有公司应有的法人地位,也就是董事长之类的职务。
The DAO只存活了3个月,问题在哪呢?假如投资者需要用钱了,想把以前投资的以太币换回来,在The DAO的基金里,以拆分的方法实现。
3.拆分
拆分过程,就是split DAO(拆分DAO),这个拆分的方法并不仅仅是取回收益,还是建立子资金(chlid DAO)的方法。如果有一小部分人和其他人的投资理念不一样,这种情况下,这一小部分人可以用拆分的方法成立自己的子资金,拆分的时候手中的代币是要被收回的,换成相应数量的以太币,然后就可以投想投的项目。极端情况下,单个投资者成立一个子资金,在子资金里就能把所有的钱投给自己,这是投资者取回投资和收益的唯一途经。
拆分的时候有7天的讨论期,拆分之后有28天锁定期,给了以太坊一个缓冲期。拆分的理念没有错,而且可以说是民主制度的进一步体现,问题出现在splitDao实现上,如图,他是先转账后扣除总资金,然后把账户清零,正确的操作顺序是先清零后转账,黑客就用这个漏洞进行重入攻击。
4.暴露问题

5.讨论阶段
针对这样的重入攻击,以太坊社区进行了激烈的讨论,社区讨论的补救措施大致分为两类:
(1)回滚交易。成立的子基金有28天的锁定期,所以黑客暂时还没有办法把钱取走,还有时间可以采取补救措施。通过回滚交易,不让黑客得逞,以此保护广大投资者的利益。(如果出了问题就回滚,就不是去中心化的);
(2)不需要采取补救措施。因为黑客没有做错,没有违反法律。
以太坊有一部分人认为,不应该回滚交易,因为区块链最重要的特性是不可篡改性。出问题的只是以太坊上的一个应用而已,The DAO是以太坊上的一个智能合约,以太坊没有问题。以太坊有那么多的智能合约,如果每个合约出了问题都回滚的话那不就乱套了。
6.补救措施
Vitalik Buterin团队认为因为该事件影响非常大,The DAO又占据了超过10%的以太币,too big to fail,所以还是决定回滚了交易。如果就是一个小的智能合约出了问题,或者转账转错了,以太坊社区是不管的,开发团队也是不管的。
如果使用分叉攻击,从黑客最开始重入攻击的区块开始分叉,是不行的,因为还存在一些其他交易,如果要回滚必须精确定位到黑客盗取以太币的交易,其他发布的正常交易不能受到影响,这是采取补救措施的一个原则。
具体的补救措施:
以太坊团队对此制定两步走战略:①锁定黑客账户;②清退The DAO基金上的这些钱。
(1)软分叉补救
“凡是跟The DAO这个基金上的账户相关的,不允许做任何交易”。这里形成的是软分叉(临时性分叉),因为增加这个规则之后,新矿工挖出的区块旧矿工是认可的,但是旧矿工挖出的区块,新矿工有可能不认可。
Bug:不予执行的非法交易不用收取汽油费。以太坊发布的这个软件升级,没有收取汽油费(检查到地址错误的时候没有收汽油费)。导致网上有大量的Denial of Service,非法交易进行攻击,于是后来就很多人恢复了原来的版本。
(2)硬分叉补救
通过软件升级的方法,把The DAO账户上的所有资金,强行转到新的智能合约上去。这个新的智能合约只有一个功能:退钱,把代币退回成以太币。升级的软件里规定了强制执行的具体日期:升级了软件的矿工,挖到第192W个区块的时候,自动执行转账交易,不用合不合法签名。这是在升级的软件里写死的规则,旧矿工是不会认可这些区块的,因为没有合法签名,属于非法交易,所以这是硬分叉。
(3)后续
硬分叉之后,旧的那条链并没有消亡,还是有一部分矿工留在上面继续挖,只不过算力大幅度下降了,不到原来的1/10,但是相应的挖矿难度也降低了。过了一段时间,有一些交易所开始上市交易旧链上的以太币。以太币ETH,硬分叉后新链继承了这个符号,仍叫ETH,旧链则为ETC(Ethereum Classic)。
在旧链上挖矿的矿工有些是为了投机,也有一些是为了信仰,他们坚持着纯粹的去中心化理念,认为旧链才是正宗的以太坊,那些搞硬分叉的是在搞修正主义。一开始大家认为ETC的前途非常迷茫,但是一直到现在,仍然是新链和旧链并存。旧链和新链并存,这会导致重放攻击,在新链上的也可以在旧链上执行,旧链上的合法交易在新链上也可以执行,于是增加一个chainID区分它们。
24-ETH-反思
1. 智能合约真的智能吗?
智能合约没有用到任何人工智能的技术,所以有人认为应该将其称为自动合约:按照写好的代码,自动执行某些操作,并不智能,写好之后不能修改。
2. 不可篡改性实际上是一把双刃剑。
(1)优点:增加了合约的公信力,所有人只能按照合约中的规则来,没人能够篡改规则。
(2)缺点:如果规则有漏洞,想要修补漏洞或者软件升级都是很困难的。
(3)缺点:已经发现了系统漏洞,有人进行恶意攻击了,想要冻结账户终止交易也是很困难的。
个人的私钥泄露,想要冻结账户需要软分叉(发行一个软件的更新并设置凡是跟这个账户相关的交易都是不合法的),但是对于个人账户没有办法进行软分叉,只能尽快把账户剩下的钱转到安全的账户。
智能合约一旦发布到区块链上,没有办法阻止对它的调用。要阻止的话就要软分叉,唯一的办法是用黑客的方法把钱转到另一个安全的合约,再用安全的智能合约将来把钱退还给大家。
2. 没有什么是真的不可篡改的。
分叉攻击中,如果有人从本来已经写入区块链的内容的前面开始分叉,可能会导致后面的交易被回滚。The DAO的盗币事件中,开发团队强行修改数据使得交易恢复被攻击之前的账户状态,所以没有什么是绝对不可篡改的。
代码开源的双刃剑
去中心化的系统像如区块链一般都是开源的,也就是透明的,因为必须要让所有的节点都执行同样的内容才能达成共识。开源的一个好处就是增加合约的公信力,接受群众的监督。
理论上,代码开源,任何人想看都可以去看,好像更安全,但实际上是真正有时间看代码的人少之又少,也不一定能看得懂。
三、去中心化
1.What does decentralized mean?
以太坊的硬分叉是以太坊的开发团队说了算的吗?不是,以太坊的团队升级软件之后,也是90%绝大多数的矿工用行动支持了硬分叉,而剩下的一小部分虽然没有支持,但是也依然在旧链上继续挖矿,以太坊团队也没有办法强制所有人都升级软件。去中心化并不是全自动化,不是说不能修改已经制定的规则,而是修改规则要用去中心化的方式进行。
2.分叉
分叉正好是去中心化系统的体现,因为只有去中心化系统,用户才可以选择分叉,中心化系统只能选择继续或者放弃。存在分叉的现象恰恰是民主的体现,比如系统私自增多以太币供给量,使得以太币贬值,矿工就可以选择分叉继续维护原来的以太币。
3.Decentralized ≠ Distributed
一个去中心化的系统一定是分布式的,如果这个系统只运行在一台计算机上,显然不能叫去中心化;但是分布式系统不一定是去中心化的,即使这个系统运行在成千上万的计算机上,如果计算机都是由同一个组织管辖的。在一个分布式的平台上可以运行一个中心化的应用,也可以运行一个去中心化的应用。
比特币和以太坊都是交易驱动的状态机,State Machine,特点是让系统中几千台机器重复做同一组操作,付出很大的代价来维护状态的一致性,这个并不是分布式系统常用的工作模式,大多数的分布式系统是让每台机器做不同的事情,然后再把各台机器的工作结果汇总起来,目的是比单机速度快。
状态机的目的不是为了比一台计算机的处理速度快,而是为了容错。
智能合约是编写控制逻辑的,只有那些互不信任的实体之间建立共识的操作才需要写在智能合约里。大规模存储和计算不适用,又慢又贵,因为要耗汽油费,云服务更好。如果需要大规模计算服务,可以使用亚马逊的云服务平台。
25-ETH-美链
美链(Beauty Chain)是一个部署在以太坊上的智能合约,有自己的代币BEC。
IPO,Initial Public Offering(首次公开募股)
ICO,Initial Coin Offering(首次代币发行)
这些发行的代币没有自己的区块链,而是以智能合约的形式运行在以太坊的EVM平台上。发行代币的智能合约对应的是以太坊状态树中的一个节点。这个节点有他自己的账户余额,相当于这个智能合约一共有多少个以太币,也就是这个发行代币的智能合约的总资产是多少个以太币,然后在这个合约里每个账户上有多少个代币是作为存储树的一个变量存储在智能合约的账户里的。
代币的发行、转账、销毁都是通过调用智能合约中的函数来实现的。跟以太坊上的以太币不太一样,它不需要通过挖矿来维护一个底层的基础链,像以太坊上每个账户有多少以太币是直接保存在状态树上的变量。以太坊中两个账户转账是通过发布一个交易到区块链上,这个交易会打包到将要发布的区块里,而在代币中如果想要转账的话,实际上是智能合约里两个账户之间转账,通过调用智能合约的函数就可以完成。
每个代币可以制定自己的发行规则,如某个代币可以是1ETH=100代币,如果从外部账户给智能合约发送1ETH,智能合约就可以给相应的代币账户发送100代币。每个代币账户上有多少代币的信息是维护在发行这个智能合约的存储树上的。
以太坊平台的出现,为各种代币的发展提供了很多方便。ERC 20(Ethereum Request for Comments)是以太坊上发行代币的一个标准,规范了所有发行代币的合约应该实现的功能和遵循的接口。美链中有一个叫batchTransfer的函数, 它的功能是向多个接收者发送代币,然后把这些代币从调用者的帐户上扣除。
2.batchTransfer的实现
3.batchTransfer的问题
uint256 amount =uint256(cnt)*_value;
如果value的值很大话可能会产生溢出,发生溢出之后,amount会变为一个很小的值;这样的话减去的是很小的值,给每个账户增加的是很大的value,相当于系统凭空多发行了很多的代币。
4.攻击细节

【0】是_receivers数组在参数列表中的位置,即从第64个byte开始,【2】;【2】先指明数组长度为2,【3】和【4】表明两个接受者的地址第1号参数是给每个接收者转账的金额,通过这样的参数计算出来的amount恰好溢出为0。
5.发生攻击的实际情况

6.攻击结果
攻击在2018年4月22日发生,攻击发生后,代币的币值断崖式暴跌。

发生这个事件之后,发行该代币的交易所马上暂停了提币功能,防止黑客卷款潜逃,两天之后,就决定将交易回滚了。
7.反思
在进行数学运算的时候,一定要考虑到溢出的可能性。Solidity实际上有一个检测溢出的库——SafeMath库,里面提供的操作运算都会自动检测有没有溢出。SafeMath库里对乘法的运算大致如下:

如果出现溢出的话,这里的assert就不会成立,会抛出异常。由于Solidity里面都是256位的整数,所以这里不会存在精度损失而导致的误差。而且观察batchTransfer的代码可以发现,代码中的减法调用的是sub库,加法调用的是add库,实际上加法和减法都有溢出检查,只有乘法不小心,酿成悲剧。
26-总结
质疑区块链的原因:
(1)区块链的概念被滥用
①保险公司的理赔问题
国外有人提出将保险理赔业务放在区块链上,如果把保险理赔业务用比特币系统实现的话,等待6个确认区块(也就是大约1h的时间),理赔速度和理赔效率会快很多。区块链本身并不能解决人工审核方面的问题。
②防伪溯源问题
如将有机蔬菜的生产到销售的全过程都写在区块链上,利用区块链的不可篡改性,使区块链生产销售的全过程透明。主要问题是区块链不能自己输入数据,区块链技术不可篡改也没有办法检测出哪些数据是不真实的。
③信任机制相关的问题
在互不信任的实体之间建立共识,有些人认为这是一个伪命题。在现实世界中,“中心化”和“去中心化”并不是黑白分明的。
(2)不可篡改性的相关问题
如果发生转账出错的情况,是不能撤销的,这种质疑是存在问题的。日常生活中使用的退款交易并不是说将原先的转账交易撤销掉,而是发起一笔新的转账交易,实现转账金额的退回。用比特币支付实际上是可以达到同样效果的,这个跟区块链的不可篡改性是无关的。
(3)法律监管和保护相关问题
区块链目前处于缺乏法律监管的状态,同样意味着没有法律保护。比特币本就不应该用于跟已有的支付方式进行竞争,加密货币应该用在已有的支付方式解决不是很好的领域,如跨国转账等。货币的支付方式可以和信息传播的方式融合在一起。下一代互联网可能是价值交换网络,支付渠道和信息渠道相互融合,使得价值获得和信息获得一样方便。
(4)支付方式的效率相关问题
比特币和以太坊的能耗都是非常大的,比现有的支付方式耗费的能源大很多。
①加密货币本来就不是用来和已有的支付方式竞争;
②区块链的发展以及共识协议的改进,一些新的加密货币已经在支付效率上已经是大大提高了;
③评价支付的效率要放在当时的历史背景之下比较。