北京大学肖臻老师《区块链技术与应用》公开课笔记:以太坊原理:智能合约

Posted 邋遢的流浪剑客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了北京大学肖臻老师《区块链技术与应用》公开课笔记:以太坊原理:智能合约相关的知识,希望对你有一定的参考价值。

9、ETH-智能合约

智能合约是以太坊的精髓,也是以太坊和比特币一个最大的区别

1)、什么是智能合约

智能合约的本质是运行在区块链上的一段代码,代码的逻辑定义了智能合约的内容

智能合约的账户保存了合约当前的运行状态

  • balance:当前余额
  • nonce:交易次数
  • code:合约代码
  • storage:存储,数据结构是一棵MPT

Solidity是智能合约最常用的语言,语法上与javascript很接近

2)、智能合约的代码结构

Solidity是面向对象的编程语言,这里的contract类似于C++当中的类class,这里的contract定义了很多状态变量,Solidity是强类型语言,这里的类型跟普通的编程语言像C++之类的是比较接近的,比如说uint(unsigned int)是无符号的整数,address类型是Solidity语言所特有的

接下来是两个event事件,作用是用来记录日志的

第一个事件是HighestBidIncreased,拍卖的最高出价增加了,上图是一个网上拍卖的例子,如果有人出现新的最高价,记录一下参数是address bidder,金额是amount,第二个事件是Pay2Beneficiary,参数是赢得拍卖的人的地址winner以及他最后的出价amount

Solidity语言跟别的普通编程语言相比有一些特别之处:

比如mapping,mapping是一个哈希表,保存了从地址到unit的一个映射。Solidity语言中哈希表不支持遍历,如果想遍历哈希表里的所有元素,需要自己想办法记录哈希表中有哪些元素,这里是用bidders数组来记录的。Solidity语言中的数组可以是固定长度的,也可以是动态改变长度的,这里是一个动态改变长度的数组。如果想在数组里增加一个元素,就用push操作,bidders.push(bidder),新增加一个出价人在数组的末尾,要想知道这个数组有多少个元素,可以用bidders.length,如果是固定长度的数组的话,就要写明数组的长度,比如说address[1024],这个就是长度为1024的数组

再往下是构造函数,构造函数只能有一个,Solidity语言中定义构造函数有两种方法

  • 一种方法就是像C++构造函数一样,定一个与contract同名的函数,这个函数可以有参数,但是不能有返回值
  • 新版本Solidity语言更推荐用这个例子的方法,就用一个constructor来定义一个构造函数,这个构造函数只有在合约创建的时候会被调用一次

接下来是三个成员函数,三个函数都是public,说明其他账户可以调用这些函数

3)、账户调用

1)外部账户如何调用智能合约?

调用智能合约其实跟转账是类似的,比如说A发起一个交易转账给B:

  • 如果B是一个普通的账户,那么这就是一个普通的转账交易,就跟比特币当中的转账交易时一样的
  • 如果B是一个合约账户的话,那么这个转账实际上是发起一次对B这个合约的调用,那么具体是调用合约中的哪个函数呢,是在data域(数据域)说明的

上图这个例子中,sender address是发起这个调用的账户的地址,to contract address是被调用的合约的地址,调用的函数是txdata,如果函数是有参数的话,那么参数的取值也是在data域里说明的,上面看的网上拍卖的例子中,三个成员函数都没有参数,但是有的成员函数是可以有参数的

中间那一行是调用的参数,value是说发起调用的时候转过去多少钱,这里是0,这个调用的目的仅仅是为了调用它的函数,并不是真的要转帐,所以value=0,gas used是这个交易花了多少汽油费,gas price是单位汽油的价格,gas limit是这个交易我最多原意支付多少汽油费

2)一个合约如何调用另一个合约中的函数?

方法一:直接调用

上图这个例子中,有A和B两个合约:

A这个合约就只是写log,event定义事件LogCallFoo,emit LogCallFoo()是用emit这个操作来调用这个事件,emit语句的作用就是写一个log,对于程序的运行逻辑是没有影响的

B这个合约,callAFooDirectly这个函数参数是一个地址,就是A这个合约的地址,然后就这个语句把这个地址转换成A这个合约的一个实例,然后调用其中的foo这个函数

以太坊中规定一个交易只有外部账户才能够发起,合约账户不能自己主动发起一个交易。所以这个例子中需要有一个外部账户调用了合约B当中的这个callAFooDirectly函数,然后这个函数再调用合约A当中的foo函数

方法二:使用address类型的call()函数

address类型的call()函数,第一个参数是要调用函数的签名,然后后面跟的是调用的参数

这种调用的方法跟上一个调用的方法相比,一个区别是对于错误处理的不同,直接调用时,如果你调用了那个合约在执行过程中出现错误,那么会导致发起调用的这个合约也跟着一起回滚,在直接调用的例子中如果A在执行过程出现什么异常,会导致B这个合约也跟着一起出错

而这种address.call()这种形式如果在调用过程中,被调用的合约抛出异常,那么这个call函数会返回false,表明这个调用是失败的,但是发起调用的这个函数并不会抛出异常,而是可以继续执行

方法三:代理调用delegatecall()

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

4)、payable

上图中,bid函数有一个payable,另外两个函数都没有。以太坊中规定如果这个合约账户要能接收外部转账的话,那么必须标注成payable

这个例子中bid函数是什么意思?

这是一个网上拍卖的合约,bid函数是用来进行竞拍出价的,比如说你要参与拍卖,你说你出100个以太币,那么就调用合约当中的bid函数。拍卖规则是调用bid函数时要把拍卖的出价100个以太币也发送过去,存储到这个合约里,锁定到拍卖结束,避免有人凭空出价,所以这个bid函数要有能够接收外部转账的能力,才标注一个payable

第二个withdraw函数没有payable,withdraw是拍卖结束了,出价最高的那个人赢得了拍卖,其他人没有拍到想要的东西,可以调用withdraw把自己当初出的价钱,就是原来bid的时候锁定在智能合约里的以太币再取回来,因为这个的目的不是为了真的转账,不是要把钱转给智能合约,而仅仅是调用withdraw函数把当初锁定在智能合约里的那一部分钱取回来,所以没必要标注payable

上图转账交易的例子,value=0,这个交易就属于并没有真的把钱转出去,所以to contract address这个函数就不用定义成payable

以太坊中凡是要接收外部转账的函数,都必须标识为payable,否则你给这个函数转出钱的话,会引发错误处理,会抛出异常,如果你不需要接收外部转账你就不用标识为payable

5)、fallback()函数

有一个特殊的函数叫fallback()函数,这个函数既没有参数也没有返回值,而且也没有函数名是个匿名函数,这个fallback关键字也没有出现在这个函数名里

调用合约的时候,A调用B这个合约,然后要在转账交易的data域说明你调用的是B当中的哪个函数,如果A给合约B转账了一笔钱,没有说明调用的是哪个函数,它的data域是空的,那怎么办呢?那么这个时候缺省的就是调用这个fallback()函数,为什么叫fallback()函数,因为没有别的函数可调了,就调它

还有一种情况是你要调的函数不存在,在那个data域里,你说要调这个函数,而实际这个合约当中没有这个函数,那怎么办呢?也是调用这个fallback()函数。这就是为什么这个函数没有参数也没有返回值,因为它没法提供参数

对于fallback()函数来说,也可能需要标注payable关键字,如果fallback()函数需要有接收转账的能力的话,也需要写成是payable,一般情况下,都是写上payable的,如果合约账户没有任何函数标识为payable,包括fallback()函数函数也没有标识成payable,那么这个合约没有任何能力接受外部的转账。如果这个合约没有fallback()函数或者是有fallback()函数但是没有写payable,那么其他人往这个合约里转一笔钱,别的都不说,data域是空的就会引发异常

fallback()函数不是必须定义的,合约里可以没有fallback()函数,如果没有fallback()函数的话,出现前面说的几种情况,就会抛出异常。另外只有合约账户才有这些东西,外部账户跟这个都没有关系,外部账户都没有代码

还有一点,转账金额可以是0,但是汽油费是要给的,这是两码事,转账金额是给收款人的,汽油费是给发布这个区块的矿工的,如果汽油费不给的话,矿工不会把你这个交易打包发布到区块链

6)、智能合约的创建和运行

智能合约是怎么创建的呢?是由一个外部账户发起一个转账交易,转给0x0这个地址,然后把这个要发布合约的代码放到data域里面。你要创建一个合约,要发起一个转账交易,给0这个地址转账,转账的金额都是0,因为你实际上不是真的想转帐,只是想发布一个智能合约,发布的这个智能合约的代码放到数据域就行了

合约的代码写完之后都是要编译成bytecode,然后运行在EVM上。EVM是类似于JVM的设计思想,通过加一层虚拟机,对智能合约的运行提供一个一致性的平台,所以EVM有时叫做Worldwide Computer(全世界的一个计算机),EVM的寻址空间是非常大,是256位,像前面讲的unsigned int就是256位

7)、汽油费(gas fee)

比特币和以太坊这两种区块链的编程模型,设计理念是有很大差别的

比特币设计理念是简单,脚本语言的功能很有限,比如说不支持循环

而以太坊是要提供一个图灵完备的编程模型(Turing-complete Programming Model),很多功能在比特币平台上实现起来很困难,甚至是根本实现不了,而到以太坊平台上呢,实现起来就很容易,当然,这样也带来一个问题,出现死循环怎么办,当一个全节点收到一个对智能合约的调用,怎么知道这个调用执行起来会不会导致死循环?有什么办法吗?

没有办法,这实际上是一个Halting Problem(停机问题),停机问题是不可解的,从理论上可以证明不存在这样一个算法,能够对任意给定的输入程序判断出这个程序是否会停机。那怎么办呢?办法就是把这个问题推给发起交易的那个账户,以太坊引入了汽油费机制,发起一个对智能合约的调用要支付相应的汽油费

上图中间是一个交易的数据结构:

  • AccountNonce就是这个交易的序号,用于防止replay attack
  • Price和GasLimit就是跟汽油费相关的,GasLimit是这个交易原意支付的最大汽油量,Price是单位汽油的价格,两个乘在一起就是这个交易可能消耗的最大汽油费
  • Recipient就是收款人的地址,转账交易转给谁的收款人地址
  • Amount是转账金额,把Amount这么多钱转给Recipient,也可以看到交易当中的汽油费跟转账金额是分开的
  • Payload就是前面说的data域,用于存放调用的是合约中的哪一个函数,函数的参数取值是什么,都在Payload里面

当一个全节点收到一个对智能合约的调用的时候,先按照调用过程中给出的GasLimit算出可能花掉的最大汽油费,然后一次性的把这个汽油费从这个发起调用的账户上扣掉,然后再根据实际执行的情况,算出实际花了多少钱,如果汽油费不够的会引起回滚

不同的指令消耗的汽油费是不一样的。一些简单的指令,比如说加法减法消耗的汽油费是很少的,复杂的指令消耗的汽油费就比较多,比如说取哈希,这个运算一条指令就可以完成,但是汽油费就比较贵,除了计算量之外,需要存储状态的指令消耗的汽油费也是比较大的,那么相比之下,如果仅仅是为了读取公共数据,那么那些指令可以是免费的

8)、错误处理

以太坊中的交易执行起来具有原子性,一个交易要么全部执行,要么完全不执行,不会只执行一部分,这个交易既包含普通的转账交易,也包含对智能合约的调用,所以如果在执行智能合约的过程当中,出现任何错误,会导致整个交易的执行回滚,退回到开始执行的之前的状态,就好像这个交易完全没有执行过

那么什么情况下会出现错误呢?

一种情况就是刚才说的汽油费,如果这个交易执行完之后,没有达到当初的GasLimit,那么多余的汽油费会被退回到这个账户里,一开始的时候是按照最大的GasLimit把汽油费扣掉了,如果最后运行完了,还有剩下来的,实际上是用的多少汽油收多少钱,剩的可以退回去。相反,如果执行到一半,GasLimit已经都用完了,那么这个时候这个合约的执行要退回到开始执行之前的状态,这就是一种错误处理,而且这个时候已经消耗掉的汽油费是不退的

为什么要这么设计呢?执行的状态要回滚,但已经耗掉的汽油费是不退的

因为要么的话就会有恶意的节点可能会发动delays service attack,可能他发布一个计算量很大的合约,然后不停的调这个合约,每次调的时候给的汽油费都不够,反正最后汽油费还会退回来,那么对攻击者来说没有什么损失,但是对矿工来说是白白浪费了很多的资源,这就是为什么说,汽油费不够的话,执行到一半会回滚,花掉的汽油费是不退的

除了这种汽油费不够的情况,还有一种情况是引起错误处理的,比如说assert语句和require语句,这两个语句都是用来判断某种条件,如果条件不满足的话,就会导致抛出异常

assert语句一般用于判断某种内部条件,有点像C语言中的assert是一样的,require语句一般用于判断某种外部条件,比如说判断函数的输入是否符合要求。上图中给出了一个简单的例子,bid这个竞拍的函数判断一下,当前的时间now<=拍卖的结束时间auctionEnd,如果符合条件继续执行,如果不符合的话,拍卖都已经结束了,你还在出价,这个时候就会抛出异常

第三个语句是revert,revert是无条件的抛出异常,如果执行到revert语句,那么自动的就会导致回滚,早期的版本里用的是throw语句,新版本Solidity里建议改用revert这个语句

Solidity当中没有这种try-catch这种结构,有的编程语言像Java,用户自己可以定义出现问题后怎么办,有这种try-catch,Solidity里没有这种结构

9)、嵌套调用

智能合约出现错误会导致回滚,那么如果是嵌套调用,一个智能合约调用另外一个智能合约,那么被调用的这个智能合约出现错误,是不是会导致发起调用的智能合约,也跟着一起回滚呢?所谓的叫连锁式回滚

不一定,这个取决于调用这个智能合约的方式。如果是直接调用的话,会出现连锁式的回滚,整个交易都会回滚,如果调用的方式是用比如说call这种方式,就不会引起连锁式回滚,只会使当前的调用失败返回一个false的返回值

有些情况下,从表面上看你并没有调用任何一个函数,比如说,你就是往一个账户里转账,但是这个账户是合约账户的话,转账这个操作本身就有可能触发对函数的调用,因为有fallback()函数,这就是一种嵌套调用,一个合约往另一个合约里转账,就有可能调用这个合约里的fallback函数

10)、Block Header中的GasLimit和GasUsed

Block Header中的GasLimit和GasUsed也是跟汽油费相关的,Block Header里面的GasUsed是这个区块里所有交易所消耗的汽油费加在一起

发布区块需要消耗一定的资源,这个消耗的资源要不要有一个限制,比特币当中对于发布的的区块也是有一个限制的,大小的限制,最多不能超过1M,因为发布的区块如果没有任何限制,有的矿工可能把特别多的交易全部打包到一个区块里面然后发布出去,那么这个超大的区块在区块链上会消耗很多资源,所以它规定每个区块最多不能超过1M,比特币交易是比较简单的,基本上可以用交易的字节数来衡量出这个交易消耗的资源有多少,但以太坊中如果这么规定是不行的,因为以太坊中智能合约的逻辑很复杂,有的交易可能从字节数上看是很小的,但它消耗的资源可能很大,比如它可能调用别的合约之类的,所以要根据交易的具体操作来收费,这就是汽油费

Block Header里面的GasLimit是这个区块里所有交易能够消耗的汽油的一个上限,不是说把区块里每个交易的GasLimit加在一起,如果那样的话,就等于没有限制了,因为每个交易的GasLimit是发布这个交易的账户自己定的,定多少是自己说了算,但是这个区块中的所有交易,实际能够消耗的汽油是有一个上限的,不能无限的消耗,否则你也可能发布一个对资源消耗很大的区块,对整个系统的运行是没有好处的

GasLimit跟比特币的区别:

比特币限制资源是按照大小来限制的,而且这个1M的上限是固定了的,是写死在协议里面的,有些人认为1M太小了,而且有的分叉币的产生就是为了提高这个上限

以太坊中也有一个上限,这个GasLimit,但是每个矿工在发布区块的时候可以对GasLimit进行微调,可以在上一个GasLimit的基础上上调或者下调 1 1024 \\frac11024 10241。如果出现像比特币那种情况,大家都觉得这个GasLimit设置的太小了,那轮到你发布区块的时候可以增加 1 1024 \\frac11024 10241,1/1024听起来很小,以太坊的出块速度很快,十几秒就是一个新的区块,所以的话,如果大家都觉得当前的GasLimit太小,那么很快就可以翻一番。当然,也可能下调,有矿工认为GasLimit太大了需要下调,所以这种机制实际上求出的GasLimit,是所有矿工认为比较合理的GasLimit的一个平均值,有的矿工认为要上调,有的矿工认为要下调,那么每个矿工在获得记账权之后就按照自己的意愿进行这种上调或者下调的微调,所以最后整个系统的GasLimit就趋向于所有矿工的一个平均意见

11)、一些问题

问题1:某个全节点要打包一些交易到一个区块里面,这些交易里有一些是对智能合约的调用,那么这个全节点应该先把这个智能合约都执行完之后再去挖矿呢,还是说先挖矿获得了记账权然后再执行这些智能合约?

区块链里有一笔转账交易发布上去的话,本来就是需要所有的全节点都执行的,这不是一种浪费也不是一种出问题了,就是所有的全节点要同步状态,大家都要在本地执行这个转账交易,如果一个全节点不执行那就出问题了,那他的状态跟别人的状态是不一样的,比特币也是一样的,比特币发布一个交易到区块链上,也是要所有的全节点都得执行这个转账交易,要不然怎么更新UTXO啊

先往回退一步,不回答这个问题,在全节点收到一个对合约的调用的时候,要一次性的先把这个调用可能花掉的最大汽油费从发起这个调用的账户上扣掉,这个具体是怎么操作的?

状态树、交易树和收据树,这三棵树都是全节点在本地维护的数据结构,状态树记录了每个账户的状态包括账户余额,所以扣汽油费的时候实际怎么扣的?全节点收到调用的时候,从本地维护的数据结构里把账户的余额减掉就行了,如果余额不够的话,这个交易就不能执行,一次性要按GasLimit把他这个余额减掉,执行完之后如果有剩的,再把他的余额再加回去一点

智能合约执行过程中任何对状态的修改都是在改本地的数据结构,只有在合约执行完了,而且发布到区块链上之后,本地的修改才会变成外部可见的,才会变成区块链上的共识。有很多全节点,每个全节点都在本地做这个事情,执行的智能合约可能不完全一样,因为根据你收到的交易可能执行不完全一样,如果某个全节点发布一个区块,我收到这个区块之后,我本地执行的就扔掉了,我把收到这个区块里的交易再执行一遍,更新我本地的三棵树。如果我本来已经执行一遍了,我没有挖到矿,那个人发过来我又得执行一遍,我得执行两遍多浪费啊,问题是你不这样还能怎么办,你本地那个候选区块中包含的交易跟他发布的那个交易不一定完全一样,至少有一个肯定不一样,给出块奖励的那个肯定不一样,他不会给你,别的交易也不一定就一样,所以这个没有办法,都是得要重新执行一遍

以太坊挖矿其实也是尝试各种nonce找到一个符合要求的,计算哈希的时候要用到什么?要用到这个Block Header的内容,Block Header的内容这个Root、TxHash、ReceiptHash,是那三棵树的根哈希值,所以要先执行完这个区块中的所有交易包括智能合约的交易,这样才能更新这三棵树,这样才能知道这三个根哈希值,这样这个Block Header的内容才能确定,然后才能尝试各个nonce

问题2:假设我是一个矿工我费了半天劲执行这些智能合约,消耗了我本地的好多资源,最后我挖矿没挖到怎么办,因为挖矿是竞争,很多矿工竞争,记账权被别人抢先了,那我能得到什么补偿,我能得到汽油费吗?

汽油费是没有的,因为汽油费是给获得记账权发布区块的那个矿工,那我能得到啥补偿?以太坊中没有任何补偿,他得不到汽油费也得不到任何补偿,不仅如此,他还要把别人发布的区块里的交易在本地执行一遍,以太坊中规定要验证发布区块的正确性,每个全节点要独立验证,那怎么验证呢?别人发布一个交易区块,你把那个区块里的所有交易在本地执行完一遍,更新三棵树的内容,算出根哈希值,再跟他发布的那个根哈希值比较一下看是不是一致,所有这些都是免费的,没有人给你补偿。所以呢,这种机制下,挖矿慢的矿工就特别吃亏,本来汽油费的设置的目的是对于矿工执行这些智能合约所消耗的这些资源的一种补偿,但是这种补偿只有挖到矿的矿工才能得到,其他的矿工等于是陪太子读书

问题3:会不会有的矿工你不给我汽油费,那我就不验证?比如说我挖半天没有挖到矿,你发布一个区块,按照协议我要验证一下你这个区块的正确性,我验证他有啥好处,你又不给我汽油费,我验证他干嘛,我就认为你是正确的不就行了吗,我就接着挖,会不会有矿工想不通?

先说一下,如果这样做会导致什么后果,最直接的后果是危害区块链的安全,区块链的安全是是怎么保证的,就是要求所有的全节点要独立验证发布的区块的合法性,这样少数有恶意的节点没法篡改区块链上的内容。如果某个矿工想不通,不给钱我就不验证了,这样的风气蔓延开来就会危及区块链的安全

会不会有这样的情况?如果他跳过验证这个步骤,他以后就没法再挖矿了,因为你验证的时候是要把区块的交易再执行一遍,更新本地的那三棵树,如果不去验证的话,本地三棵树的内容没有办法更新,以后再发布区块你怎么发布,你本地的这些状态就不对了,你算出的根哈希值发布出去之后别人认为是错的。没有办法跳过验证这个步骤

为什么要执行才能更新状态?因为发布的区块里没有这三棵树的内容,只是块头里有三个根哈希值,这三棵树的账户状态具体是什么余额什么内容,发布出来是没有的,不能把状态树的整个状态发布到区块链上,那太多了,而且很多是重复的,状态都不改了,所以不会跳过验证这个步骤,以太坊的安全还是有保证的

问题4:发布到区块链上的交易是不是都是成功执行的?如果智能合约执行过程中出现了错误,要不要也发布到区块链上去?

执行发生错误的交易也要发布到区块链上去,否则汽油费扣不掉,光是在本地的数据结构上把他的账户扣了汽油费,是没用的,你拿不到钱,你得把区块发布上去之后形成共识,扣掉的汽油费才能成为你账户上的钱,所以发布到区块链上的交易不一定都是成功执行的。要告诉大家为什么扣汽油费,而且别人得验证一遍,也要把这个交易执行完一遍,看你扣的是不是对的

那怎么知道一个交易是不是执行成功了呢,前面说过那三棵树,每个交易执行完后形成一个收据,上图是这个收据的内容,Status这个域就是告诉你交易执行的情况是怎么样的

问题5:智能合约是不是支持多线程,现在多核处理器很普遍,一个计算器有十几核,几十个核,都是正常的,那么智能合约支不支持多核并行处理?

Solidity不支持多线程,它根本没有支持多线程的语句,原因是以太坊是一个交易驱动的状态机,这个状态机必须是完全确定性的,就是给定一个智能合约,面对同一组输入,产生的输出或者说转移到的下一个状态必须是完全确定的

为什么要求这个?因为所有的全节点都得执行同一组操作到达同一个状态,要验证,如果状态不确定的话,那三棵树得根哈希值根本对不上,必须完全确定才行

多线程的问题在于什么?多个核对内存访问顺序不同的话,执行结果有可能是不确定的,除了多线程之外,其他可能造成执行结果不确定的操作也都不支持,最直接最简单的会导致执行结果不确定的操作:产生随机数,这个操作就是不确定性的,而且这个操作必须得是不确定的,所以以太坊的智能合约没有办法产生真正意义下的随机数,可以用一些伪随机数,不能是真的随机数,否则的话,又会出现前面的问题,每个全节点执行完一遍得到的结果都不一样

13)、智能合约可以获得的信息

1)区块信息

智能合约的执行必须是确定性的,这也就导致了智能合约不能像通用的编程语言那样通过系统调用来得到一些环境信息,因为每个全节点的执行环境不是完全一样的,所以它只有通过一些固定的一些变量的值能够得到一些状态信息,上图就是智能合约能够得到的区块链的一些信息

2)调用信息

上图是智能合约可以获得的调用信息:

  • msg.sender是发起这个调用的人是谁,这个跟最后一个tx.origin交易的发起者是不一样的。比如说有一个外部账户A调用了一个合约叫 C 1 C_1 C1 C 1 C_1 C1当中有一个函数 f 1 f_1 f1 f 1 f_1 f1又调用另外一个合约 C 2 C_2 C2,里面的函数 f 2 f_2 f2,那么对这个 f 2 f_2 f2函数来说,msg.sender C 1 C_1 C1这个合约,因为当前这个调用,是 C 1 C_1 C1这个合约发起的,但是tx.origin是A这个账户,因为整个交易的发起者是A这个账户
  • msg.gas是当前调用还剩下多少汽油费,这个决定了我还能做哪些操作,包括你还想调用别的合约前提是还有足够的汽油费剩下来
  • msg.data就是所谓的叫数据域,在里面写了调用哪些函数和这些函数的参数取值
  • msg.sigmsg.data的前四个字节,就是函数标志符调用的是哪个函数
  • now是当前区块的时间戳,跟区块信息中的block.timestamp是一个意思,就是智能合约里没有办法获得很精确的时间,只能获得跟当前区块信息的一些时间

14)、地址类型

上图中第一个是个成员变量,剩下的都是成员函数。成员变量就是账户的余额balance,unit256是这个成员变量的类型,是以Wei为单位的,是个很小的单位

下面这些成员函数的话,有一点要注意的,这些成员函数的语义跟我们直观上的理解不是很一样,跟第一个成员变量balance也不太一样

addr.balance是address这个地址上他的账户他的余额,那addr.transfer(12345)是什么意思呢?感觉像是addr这个账户往外转了12345个Wei,是不是这个意思?如果是这个意思的话,问题在于他只有一个参数,他只有转账的金额,没有说转给谁,所以addr.transfer(unit amount)是什么意思呢?并不是说addr这个账户往外转了多少钱,而是当前这个合约往addr这个地址里转入多少钱,这个addr是转入的地址不是转出的地址,转出的地址是哪一个?比如说这是个智能合约C,里面有一个函数f,它包含这条语句addr.transfer(12345),意思是说C这个合约的账上往这个addr地址里转入12345这么多的钱

addr.call也是一样的语句,并不是说addr这个合约账户发起了一个调用,调哪个别的合约账户,而是说当前这个合约发起一个调用,调得是addr这个合约

delegatecall区别就是说不需要切换到被调用的函数的环境中,就用当前合约的余额,当前合约的存储这些状态去运行就可以了

问题:我向一个帐户转账说这个账户没有定fallback函数会引起错误,会不会连锁回滚?

这取决于你怎么转账的,转账有三种方法,上图中这三种形式都可以发送ETH

区别是这个transfer和send,这两个是专门为了转账的函数,区别在于transfer会导致连锁性回滚,类似于你直接调用那个函数直接调用的方法是一样的,失败的时候抛出异常,而send返回一个false,不会导致连锁式回滚。call其实也是可以转账的,call.value(unit256 amount)(),最后一个参数如果不用调用函数可以是空的。区别在于transfer和send是专门用来转账的,call的话本意是发动函数调用,但是也可以用来转账,call也不会引起连锁式回滚,失败时返回false

另外一个区别是transfer和send在发起调用的时候,只给了一点儿的汽油,是2300个单位,非常少的,那么收到这个转账的合约基本上干不了别的事,写一个log就行了,别的事都干不了,而call是把当前这个调用剩下的所有的汽油都发过去,比如说call所在的合约本身被调用的时候,可能还剩8000个汽油,然后去调别的合约的时候如果是用call这种方法去转账,就把剩多少汽油都发过去了

15)、拍卖的例子

回到一开始讲的拍卖的例子,拍卖有一个受益人beneficiary,比如说你有一个古董要拍卖,那么这个受益人就是你;auctionEnd是整个拍卖的结束时间;highestBidder是最高出价人

拍卖的规则:

在拍卖结束之前,每个人都可以去出价,去竞拍,竞拍的时候为了保证诚信,要把竞拍的价格相应的以太币发过去,比如你出价100个以太币,那么你竞拍的时候要把100个以太币发到这个智能合约里,它就会锁在这里面直到拍卖结束,拍卖的规则不允许中途退出,我去竞拍发了100个以太币,过一会儿我后悔了想把钱要回来,这个不行。拍卖结束的时侯出价最高的那个人highestBidder,他投出去的钱会给这个受益人beneficiary,当然你也要想办法把这个古董给最高出价人,其他没有拍卖成功的人可以把当初投进去的钱再取回来

竞拍是可以多次出价的,比如说我出个价钱,100个以太币,然后呢,另外一个人出价110个以太币,我再出价120个以太币,这个时候我只要补差价就行了,就把我这一次的出价跟上一次的出价差额发到智能合约里,我上次投标的时候已经发了100个以太币,这次只要再发20个以太币就行了。出价要有效的话,必须比最高出价还要高,比如说当前的最高出价是100个以太币,我去竞拍,我投80个以太币,这个是无效的,等于是非法的拍卖

constructor会记录下收益人是谁,结束时间是什么时候,这个构造函数,在合约创建的时候,把这两个就记下来了

上图是拍卖用的两个函数,左边的bid函数是竞拍时候用的,你要竞拍你就发起一个交易调用这个拍卖合约中的bid函数,这个bid函数有一个奇怪的地方,它没有参数,感觉上你竞拍的时候你不需要告诉对方你出的价格是多少吗?它其实是在msg.value这个地方写的,这个是发起调用的时候,转账转过去的以太币数目,以Wei为单位的转账金额,这个的逻辑是:

首先查一下当前的拍卖还没有结束,如果拍卖结束了,你还出价会抛出异常,然后查一下你上一次的出价加上你当前发过去的以太币大于最高出价,如果你以前没有出价过会怎么样?这个bids是个哈希表,Solidity中哈希表的特点是,如果你要查询的那个键值不存在,那么它返回默认值就是0,所以如果没有出过价,第一部分就是0,然后呢,第一次拍卖的时候把拍卖者的信息放到bidders数组里,原因是Solidity哈希表不支持遍历,要遍历哈希表的话,要保存一下它包含哪些元素,然后记录一下新的最高出价人是谁,写一些日志之类的

右边是拍卖结束的函数,首先查一下拍卖是不是已经结束了,如果拍卖还没有结束,有人调用这个函数,就是非法的会抛出异常,然后判断一下这个函数是不是已经被调过了,如果已经被调过了,就不用再调一遍了,首先把这个金额给这个beneficiary,beneficiary.transfer是当前这个合约把这个金额给这个beneficiary转过去,最高出价人的钱是给受益人了,然后那些剩下的没有竞拍成功的用一个循环,把这个金额退回给这个bidder,然后标明一下,这个函数已经执行完了写一个log

智能合约是怎么工作的?

你写完一个智能合约,你写一个拍卖程序要先把它发布到区块链上,往那个0地址发一笔转账交易,转账的金额是0,然后把智能合约的代码放到data域里面,汽油费是要交的,然后矿工把这个智能合约发布到区块链上之后会返回这个合约的地址,然后这个合约就在区块链上了,所有人都可以调用它

每次竞拍存在哪?

智能合约本身有一个合约账户,里面有一个状态信息,它的存储都是在一个MPT存着的

拍卖的流程:

比如你的外部账户要拍卖,你要发起一个交易,这个交易要调用这个bid函数,然后这个交易要调用这个bid函数要矿工写到区块链里。任何一个人出价参与这个竞拍,调用这个bid函数的操作都需要发布到区块链里

你要竞拍就是写一个Solidity程序,然后你发布一个交易把这个合约放到网上,那别人怎么知道你这个合约,你需要线下宣传,用别的方法宣传,区块链不负责给你做这个宣传,就像你的比特币地址别人怎么能知道,你自己去宣传

上图智能合约这么写的问题是什么?

写智能合约一定要小心因为智能合约是不可篡改的,说的好听点儿叫不可篡改,说的不好听点儿叫你没法改bug

auctionEnd这个函数必须要某个人调用才能执行,这个也是Solidity语言跟其他编程语言不同的一个地方,就是没有办法把它设置成拍卖结束了自动执行auctionEnd,可能是拍卖的受益人beneficiary去调用这个auctionEnd,也可能是参与竞拍没有成功的人去调用,总之得有一个人去调用。如果两个人都去调用auctionEnd,矿工在执行的时候把第一个调用执行完了,然后第二个再执行就执行不了了,因为第一个执行完之后,ended就是true了,没有并发执行

假设有一个人通过上图这样的一个合约账户参与竞拍,会有什么结果?

这个合约实际上就一个函数hack_bid,这个函数的参数是拍卖合约的地址,然后把它转成这个拍卖合约的一个实例,然后调用拍卖合约用的bid函数,把这个钱发送过去。这是一个合约账户,合约账户不能自己发起交易,所以实际上得有一个黑客从他自己的外部账户发起一个交易,调用这个合约账户的hack_bid函数,然后这个函数再去调用拍卖合约的bid函数,把这个黑客外部账户转过来的钱再转给这个拍卖合约中的bid函数,就参与拍卖了

这个合约参与拍卖没有问题,最后拍卖结束退款的时候会有什么问题?这个红框里循环退款,退到合约账户上的钱会有什么情况,退到黑客合约账户上的钱会有什么情况?

黑客外部账户对拍卖合约来说是不可见的,拍卖合约能看到的只是这个黑客的合约。转账的时候没有调用任何函数,那么当一个合约账户收到转账没有调用任何函数的时候应该调用fallback函数,而这个合约没有定义fallback函数,所以会调用失败,会抛出异常,这个transfer函数会引起连锁式的回滚,就会导致这个转账操作是失败的,所有人都收不到钱了

再具体点,比如有20个人参与竞拍了,这个黑客是排在第10个,最高出价人排在第16个,那么最后是有哪些收得到钱,哪些收不到钱?

这个转账实际上是全节点执行到beneficiary.transfer的时候把相应账户的余额进行了调整,所有的Solidity语句就是智能合约执行过程中的任何对状态的修改改的都是本地的状态,都是改的本地的数据结构。所以这个循环当中无论是排在黑客合约前面还是后面,都是在改本地数据结构,只不过排在后面的bidder根本没有机会来得及执行,然后整个都回滚了,就好像这个智能合约从来没有被执行过。所以排在前面的这些转账并没有执行,就是改本地结构,然后如果都顺利执行完了,发布出去之后,别的矿工也把这个auctionEnd重头到尾执行一遍,也改它本地的数据结构,跟你的能对得上就叫形成共识了,而不是说每有一个转账交易的语句是产生一个新的交易写到区块链上。所以都收不到钱,没有任何一个人能收到钱

发起这个攻击的有可能是故意捣乱,写这样一个程序让大家都拿不到钱,也可能是这个人不懂,他就忘了写fallback函数了,那出现这种情况怎么办呢?比如说你发布一个拍卖合约到区块链上,吸引很多人来拍卖,拍卖完之后发现有这样一个问题这个黑客合约,你怎么办?

现在的问题是你已经把钱投进去了,锁在里面了,你怎么把它取出来。答案是没有办法,出现这种情况没有办法了。Code is law,智能合约的规则是由代码逻辑决定的,而代码一旦发布到区块链上就改不了了,所谓的叫区块链的不可篡改性,这样的好处是没有人能够篡改规则,这样的坏处是规则中有漏洞你也改不了了

智能合约如果设计的不好的话,有可能把以太币永久的锁起来,谁也取不出来,所以在你发布一个智能合约之前一定要测试测试再测试,你可以在专门的那种测试的网上用假的以太币,做测试确认完全没有问题的情况下再发布

那我能不能在这个智能合约里留一个后门,用来修复bug,比如给合约的创建者超级用户的权利,在这个构造函数里加一个域叫owner,记录一下这个owner是谁,然后对这个owner的地址允许他做一些系统管理员的操作,比如可以任意转账,把钱转给哪个地址都行

那样的话,如果出现像这种bug,超级管理员就可以发挥作用,把锁进去的钱给转出来了,因为反正对他没有限制,他转给谁都行。但这样有可能出现卷款跑路的情况,这样做的前提是所有人都要信任这个超级用户,这个跟去中心化的理念是背道而驰的,也是绝大多数区块链的用户不能接受的

第二个版本,把前面那个auctionEnd拆成两个函数,左边是withdraw,右边是Pay2Beneficiary

withdraw函数这里就不用循环了,每个竞拍失败的人自己调用withdraw函数,把那一部分钱取回来。首先判断一下拍卖是不是结束了,然后看一看调用的那个人是不是最高出价者,如果是的话,不能把钱给他,因为要留着给那个拍卖的beneficiary,然后看一下这个人账户的余额是不是正的,amount是他的账户余额,把账户余额转给msg.sender,就是发起调用的这个人,然后把他账户余额清成0,免得他下次再来取一下钱

Pay2Beneficiary函数是说把最高出价给这个受益人,也是判断一下拍卖已经结束了,最高出价的金额大于零,下面再把它转过去

这样可以了吗?

还是有一个问题:重入攻击,如果有黑客写了上图右边这样一个程序会怎么样?

这个hack_bid跟前面的那个黑客合约hack_bid合约是一样的,通过调用拍卖bid函数参与竞拍,hack_withdraw就在拍卖结束的时候调用withdraw函数,把钱取回来,这两个看上去好像都没有问题

问题在于fallback函数,他又把钱取了一遍,左边是智能合约中的withdraw函数,hack_withdraw调用withdraw函数的时候,执行到左边第47行会向黑客合约转账,这个msg.sender就是黑客的合约,把它当初出价的金额转给他,而右边这个合约在干嘛?它又调用了拍卖函数的withdraw函数,又去取钱,fallback函数这里的msg.sender就是这个拍卖合约,因为是拍卖合约把这个钱转给黑客合约的,这个左边的拍卖合约执行到if那里,再给他转一次钱

注意这个清零的操作,把黑客合约账户清零的操作,只有在转账交易完成之后,才会进行,而第47行这个转账的语句已经陷入到了跟黑客合约当中的递归调用当中,根本执行不到下面这个清

区块链与比特币基础知识——北京大学肖臻老师《区块链技术与应用》公开课笔记

区块链技术与应用

北京大学肖臻老师《区块链技术与应用》公开课的一些笔记

1 比特币中的密码学原理

crypto-currency 加密货币

1 密码学中用到的哈希函数的几个特性:

1 collision resistance 抗碰撞性

​ 哈希碰撞的就是不同的输入有相同的输出而导致的冲突

​ 意思是没有什么高效的方法去人为的制造哈希碰撞

2 hiding 哈希函数的计算过程是单向的,不可逆的

​ 从哈希值是不能反推得到输入的(输入空间足够大的情况下)

1和2的性质可以用来做digital commitment,也叫digital equivalent of a sealed envelope

在网络中,公开的是由输入得到的哈希值,由于上面两个特性,所以无法反推得到输入值,就可以用输入值来进行验证

因为在实际操作中输入的空间不一定是无限大,所以需要给输入值X拼接一个随机数,然后整体进行取Hash

3 puzzle friendly 哈希值的计算是不可预测的,由输入看不出来输出值

​ 想要得到理想范围的Hash值,需要一个一个去试

所谓挖矿其实就是不断的去试验输入,来得到比给定target要小的Hash值,这需要大量的工作量,但是验证起来是很容易的,只需要求一次Hash值

2 签名

比特币的开户;只需要在本地创立一个public key和Private key对就可以(公钥和私钥)

公私钥的概念来自非对称的加密体系(asymmetric encryption algorithm)加密和解密用的分别是接收者的公钥和私钥,公钥是对所有人可见的

因为对称加密体系中公钥的分配与传输并不安全

发布时用到的时个人的私钥作为签名,在别人验证时使用的公钥进行验证

2 比特币中的数据结构

**1 哈希指针:**哈希指针除了保存地址之外,还要保存哈希值

区块链与普通链表的区别之一就是用哈希指针代替了普通的指针

区块a指向区块b 则区块a中的哈希值是对区块b的整体去哈希得到的

这也就会导致无论在区块链的哪个节点做了改动,都会导致其祖上节点Hash值的变化,这样就很容易检测到哪里做了改动

哈希指针无法应用与有环的链

2 Merkle tree

与普通的binary tree的区别就是用Hash指针代替了普通指针

每个叶子节点(data block)都是一个交易,他的父节点保存了他的Hash值

根节点的Hash值并没保存具体的交易 boack header你保存了根节点的Hash值

一个作用就是可以提供Merkle proof:就是从具体交易内容的叶子节点直到根节点,在这过程中,只要每一个Hash值都能对应上,就说明是正确的

3 协议

double spending attack 双花攻击 普通的数字货币可以无限复制 如果交易需要通过央行,那么则还是中心化的模式,区块链需要的是去中心化

如何以去中心化的方式来防范double spending attack

每一次交易既要写明去处,也要写明币的来源 这样在交易时可以验证

每一次转账信息包括 本人的公钥和目标转账人的公钥,这样目标人可以用自己的私钥解密。也可以通过公钥的Hash值去验证这个币的来源

比特币包括 Block header和Block body

Block header包括的是比特币的协议版本 version、指向前一个区块的指针 hash of previous block header 整个Merkle Tree的根哈希值 挖矿的目标预值 target 还有随机数nonce

全节点(full node):保存区块链的所有信息

轻节点(light node):只保存block header的信息

分布式共识(distributed consensus): Paxos协议能够保证一致性

比特币中的共识协议:Consensus in BitCoin

最长合法链:现有区块链接受的区块应满足最长合法链,不能插入到中间开出分支(分叉攻击)

当然区块链是可能出现分叉的,两个节点同事都捕获到了随机数,则都会其对进行记录

当然在之后的竞争中会有胜出者称为主链,而分叉会被丢弃

block reward:铸币交易是比特币中发行新币的唯一途径,而拿到记账权的节点可以有特殊权利进行铸币交易,刚开始每一次可以产生50btc,每过21w个就会减半(大概需要每四年减少一次)

4 比特币系统的实现

比特币采用的是基于账本的交易模式 记录了铸币交易转账交易,但没有显示的记录每个账户有多少钱

比特币系统的全节点要维护一个叫UTSO(Unspent Transaction Output)的数据结构(没花出去的钱,同一个交易可能有多个输出。有的保存在结构中,有的没有)

total input == total output 有时候不是完全相等,可能有一点给了记账的区块作为奖励

激励机制(交易费 transaction fee):

基于账户的模式(以太坊)

挖矿

每一个挖矿的过程都是在不断的尝试nonce ,每一个尝试的过程都能看作一个Bernoulli trial

Bernoulli trial(伯努利试验):随机试验只有两种结果 0或者1

Bernoulli process:a sequence of independent binary outcome 无记忆性(前面的实验对后面的结果没有影响)在任何时候挖到矿的概率都是一样的

因为出币的数量每21w(大约需要4年)就会减少一半,总数构成了一个几何序列21w * 50(1 + 1/2 + 1/4…) 大约2100w

风险:只能从概率上保证交易的安全性。需要六个确认来辨别一个节点为诚实节点

selfish mining 挖到先不发布,而是先积累,等到时候直接发布一条长链以此来破坏主链

当然这样需要极强的算力,且失败的风险极大

5 比特币的网络

应用层(application layer): 遵循BitCoin Block chain

网络层(network layer):遵循 P2P Overlay Network

所有节点都是平等的,没有超级节点

设计原则:simple robust but not effictive

消息传播在节点中采用flooding的方式

比特币要求区块的大小不超过1M

6 挖矿难度

对块头节点进行Hash计算,得到的值与t目标预值target进行比较(小于等于),target越小,目标难度越大。调整挖矿难度就是调整目标空间在整个输出空间中所占的比例

比特币使用的哈希算法是SHA-256,所以整个输出空间是2的256次方

挖矿难度与target成反比

总结

全节点

  • 一直在线
  • 在本地硬盘上维护完整的区块链信息
  • 在内存里维护UTXO集合,以便快速检验交易的正确性
  • 监听比特币网络上的交易信息,验证每个交易的合法性
  • 决定哪些交易会被打包到区块里
  • 监听别的矿工挖出来的区块,验证其合法性
  • 挖矿

轻节点

  • 不是一直在线
  • 不用保存整个区块链,只要保存每个区块的块头(差了大约1000倍)
  • 不用保存全部交易,只保存和自己有关的交易
  • 无法检验发多数交易的合法性,只能检验与自己相关的那些交易的合法性
  • 无法检测网上发布的区块的正确性
  • 可以验证挖矿的难度
  • 只能检测哪个是最长链,不知道哪个是最长合法链

7 分叉(分叉攻击)

state fork:(两个节点同时发布区块,对比特币当前的状态有意见而产生的分叉)

​ forking attack:分叉攻击

​ deliberate attack:认为故意造成的

protocd forkl(比特币协议不同造成的分叉)

​ hard fork:

​ block size有一定的限制 limit 1M

当大部分节点更新块大小限制,而少部分未更新时,会导致小块会把大块视为非法链,出现硬分叉,只要旧节点不更新,分叉就不会消失

必须所有的节点都更新了,分叉才会消失,否则会永久存在

​ soft fork:(软分叉的短链可能会消失,不会产生永久性的分叉)

只要拥有半数以上的节点更新了软件,分叉就会消失

  • 只能检测哪个是最长链,不知道哪个是最长合法链

8 匿名性

并未能实现真正的匿名,对外交互的时候可能暴露自己的信息

应用层

网络层的加密是很重要的

零知识证明:

指一方(证明者)向另一方(验证者)证明一个陈述时正确的,而无需透露除该陈述是正确的外的任何信息

同态隐藏:

1 如果下x,y不同,那么他们加密函数值E(x)、E(y)也不相同(不会出现碰撞)

2 给定加密函数值,很难反推得到x的值

3 给定E(x)、E(y)的值,可以很容易的计算得到关于x,y的加密函数值

同态加法:通过E(x)、E(y)计算得到E(x+y)的值

同态乘法:通过E(x)、E(y)计算得到E(xy)的值

扩展到多项式

零币和零钞:主要是为了实现匿名性

以上是关于北京大学肖臻老师《区块链技术与应用》公开课笔记:以太坊原理:智能合约的主要内容,如果未能解决你的问题,请参考以下文章

北京大学肖臻老师《区块链技术与应用》公开课笔记-BTC

北京大学肖臻老师《区块链技术与应用》公开课笔记-BTC

待更新北京大学肖臻老师《区块链技术与应用》公开课笔记04-BTC-协议

北京大学肖臻老师《区块链技术与应用》公开课笔记03-BTC-数据结构

北京大学肖臻老师《区块链技术与应用》公开课笔记1——课程简介

北京大学肖臻老师《区块链技术与应用》公开课-ETH