区块链学习笔记21——ETH智能合约

学习视频:北京大学肖臻老师《区块链技术与应用》
笔记参考:北京大学肖臻老师《区块链技术与应用》公开课系列笔记——目录导航页

智能合约简介

  • 智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容
  • 智能合约的账户保存了合约当前的运行状态
    • balance:当前余额
    • nonce:交易次数
    • code:合约代码
    • storage:存储,数据结构是一棵MPT
  • Solidty是智能合约最常用的语言,其语法上与JavaScript很接近

智能合约的代码结构
在这里插入图片描述

如何调用智能合约

调用智能合约与转账是类似的,比如A发起一个交易转账给B,如果B是一个普通账户那么这就是一个普通的转账交易,如果B是一个合约账户的话,那么这个交易实际上是发起一次对B这个合约的调用,具体调用的是哪个函数是在DATA域中说明的

在这里插入图片描述

event的作用就是写一个log,对程序的运行逻辑没有影响
一个交易只能外部账户发起,合约账户不能自己主动发起一个交易
下面例子实际上需要一个外部账户先调用合约B中的callAFooDirectly函数,这个函数再调用合约A中的foo函数
在这里插入图片描述
使用call()函数调用与直接调用的一个区别是错误处理的方式
直接调用如果在a.foo()执行出错,那么外部callAFooDirectly也会出错,本次调用全部回滚
使用call()函数调用,如果调用过程中被调用合约产生异常,会导致call()返回false,但发起调用的函数不会抛出异常,而是继续执行。
在这里插入图片描述在这里插入图片描述

关于之前函数中的payable
以太坊中规定,如果一个函数可以接收外部转账,则必须标记为payable。该例中背景为拍卖,bid()为出价,调用bid()函数的时候要把你的出价发送出去,存储到合约中锁定一直到拍卖结束。因此需要payable进行标记;withdraw()为其他未拍卖到的人将锁定在智能合约中的钱取出的函数,其不涉及转账,不需要把钱转给智能合约,而仅仅是把当初锁定的钱取回来,因此不需要payable进行标记。
在这里插入图片描述

fallback()函数
该函数主要是防止A向B转账,但没有在data域中说明要调用哪个函数或说明的要调用函数不存在,此时调用fallback()函数。
只有合约账户才有代码,因此这些只和合约账户有关。如果没有fallback(),在发生之前的情况后,就会直接抛出异常。
另:转账金额和汽油费是不同的。汽油费是为了让矿工打包该交易,而转账金额是单纯为了转账,其可以为0,但汽油费必须给
在这里插入图片描述

智能合约的创建和运行

在这里插入图片描述

汽油费

以太坊中功能很充足,提供图灵完备的编程模型,但这也导致一些问题,例如当一个全节点收到一个对智能合约的调用时怎么知晓执行其是否会导致死循环(比特币中根本不支持循环)。
事实上,无法预知其是否会导致死循环,实际上,该问题是一个停机问题,而停机问题不可解。因此,以太坊引入汽油费机制将该问题扔给了发起交易的账户。
汽油费实际上是对执行智能合约所消耗资源的补偿
在这里插入图片描述

当一个全节点收到一个对智能合约的调用,先按照最大汽油费收取,从发起调用的账户一次性扣除,再根据实际执行情况,多退少补(汽油费不够会引发回滚,而非简单的补齐)。

发布区块的汽油费
在这里插入图片描述
这里面的GasUsed是区块中包含的所有交易所消耗的汽油费的总和
但是GasLimit并不是区块中所有交易的GasLimit的总和
发布区块需要消耗一定的资源,我们需要对消耗的资源进行限制,如果不限制的话,有的矿工可能把特别多的交易打包到一个区块中,这个区块在区块链上会消耗很多的资源。比特币中规定每个区块的大小不能超过1MB,比特币系统比较简单基本可以通过交易的字节数来衡量其消耗的资源;而以太坊中智能合约的逻辑很复杂,所以我们要根据交易的具体操作来收费,这里的区块头中的GasLimit是区块中所有交易能消耗汽油的一个上限,不是把区块中所有的GasLimit加在一起(这样的话就没有限制了,因为每个交易的GasLimit是发布交易的账户自己定的)

比特币直接通过限制区块大小为1MB是固定的,无法修改。而以太坊中,每个矿工都可以以前一个区块中GasLimit为基数,进行上调或下调1/1024,从而,通过矿工不断地上下调整,最终得到的GasLimit是所有矿工希望的平均值。

错误处理

以太坊中交易具有原子性,要么全执行,要么全不执行,不会只执行一部分(包含智能合约)。
所以如果在执行智能合约的过程中出现错误,会导致整个交易回滚,退回到之前的状态,就像这个交易从未执行。
出现错误的情况:如果交易执行完后没有达到gas limit,那么多余的会退回;如果执行到一半gas limit都用完了,要退回到交易执行前的状态,而且已经消耗的汽油费是不会退回的,防止了恶意节点对全节点进行恶意调用。
在这里插入图片描述

嵌套调用

在这里插入图片描述
嵌套调用是否发生连锁式回滚,取决于调用方式,直接调用方式会引发连锁回滚,使用call()函数的话只会使当前调用失败返回一个false
一个合约向一个合约账户直接转账,因为fallback函数的存在,仍有可能会引发嵌套调用。

问答

Q:假设全节点要打包一些交易到区块中,其中存在某些交易是对智能合约的调用。全节点应该先执行智能合约再挖矿,还是先挖矿获得记账权后执行智能合约?

  • 观点1:先挖矿后执行智能合约。因为如果先执行智能合约,后挖矿,可能导致同一智能合约被不同节点执行多次,因此可能会导致一个转账操作被执行多次,即转账了好多次。

实际上,一个在区块链上的区块中的智能合约,其必然在系统中所有节点中都得到了执行,因为这样才能保证系统中所有节点从一个状态转入另一个状态,从而保证系统的一致性。
如果存在一个全节点没有执行该智能合约,那么该全节点的状态就和其他节点不一致,则该系统就没有保持状态一致。

  • 观点2:先挖矿后执行智能合约。因为执行智能合约要收取汽油费,如果多个人都执行,会收取很多份汽油费。

汽油费是怎么扣除的?
首先,之前在以太坊数据结构中介绍了以太坊中“三棵树”——状态树、交易树、收据树。这三棵树都位于全节点中,是全节点在本地维护的数据结构,记录了每个账户的状态等数据,所以该节点收到调用时,是在本地对该账户的余额减掉即可,如果余额不够就不执行,如果有剩下的再加回去即可。所以多个全节点每人扣一次,仅仅是每个全节点各自在本地扣一次。
也就是说,智能合约在执行过程中,修改的都是本地的数据结构,只有在该智能合约执行完被发布到区块链上之后,这个本地修改才是外部可见的,才会成为区块链上的共识。

  • 观点3:先执行智能合约后挖矿。

这个观点是正确的,在挖矿的时候要计算block header的哈希值,而block header中包含有三棵树的根哈希值。所以,只有执行完区块中的所有交易(包括智能合约交易)才能更新这三棵树,得到这三个根哈希值,这样block header的内容才能确定,然后才能尝试nonce进行挖矿。

Q:矿工先消耗了很多资源执行了这个智能合约,但是最后没有挖到矿怎么办?能得到什么补偿?

没有任何补偿,汽油费只给获得记账权发布区块的那个矿工,不仅如此,他还需要把别人发布的区块中的交易在本地执行一遍,验证它的正确性,每个全结点都要独立验证。

Q:会不会有的矿工因为没有汽油费而不去验证别人的区块(验证别人的区块还会消耗自己的资源)?

如果出现这种情况最直接的后果会危害区块链的安全(区块链安全的保证:所有的全结点独立验证发布区块的合法性,这样少数恶意结点才无法篡改区块链的内容),如果矿工跳过验证,那么他就无法更新本地的三棵树,以后再发布区块时别人也不会通过他所发布的区块

Q:发布到区块链上交易都是成功执行的吗?如果智能合约的执行出现错误,要不要也发布到区块上去?

要发布,要扣掉汽油费,只在本地扣掉的汽油费是没有用的,只有发布到区块上形成共识,才会成为你账户上的钱。

Q:智能合约支持多线程吗?

不支持,solidty根本就没有支持多线程的语句。因为以太坊本质为一个交易驱动的状态机,给定一个智能合约,面对同一组输入,必须转移到一个确定的状态。因为所有全结点都要执行同一组操作,到达同一个状态进行验证,如果状态不确定的话三个树的根哈希值根本对不上。但对于多线程来说,如果多个核对内存访问顺序不同的话,最终的结果可能不一致。
除了多线程外,其他可能造成结果不一致的操作也都不支持,如产生随机数。所以以太坊的智能合约没法产生真正意义下的随机数,都是伪随机数。同时也不能获得执行环境的信息。

Receipt数据结构

在这里插入图片描述

智能合约可以获得的信息

在这里插入图片描述
在这里插入图片描述
下图中A调用合约C1中f1函数,C1中f1函数调用C2合约的f2函数
对f2来说,C1是msg.sender;A是tx.origin
在这里插入图片描述

地址类型

在这里插入图片描述

转账方法

transfer和send专门用来转账:transfer会导致连锁式回滚;send不会导致连锁式回滚
call也可用来转账,不会导致连锁式回滚
区别在于transfer和send转账金额是很少的,call是把当前调用剩下的所有汽油全部发送过去
在这里插入图片描述

例子

拍卖规则:在拍卖的时候每个人都可以出价竞拍,同时要把出价的以太币发到智能合约中锁定,直到拍卖结束。拍卖结束后,出价最高的人会把他投出去的钱给受益人
,受益人也应该把拍卖物品想办法给最高出价人。其他没竞拍成功的人可以把投进去的钱才取回来。
竞拍可以多次出价,比如第一次出100个以太币,第二次出120个以太币,这时只需要补差价20个以太币即可,出价要想有效必须比当前的最高出价要高。
在这里插入图片描述
拍卖用到的两个函数
在这里插入图片描述

收款地址未定义fallback函数的问题:
竞拍合约退款时,用的是transfer方式,没有调用任何函数,这时会调用fallback函数,而该合约没有定义,所以竞拍合约的退款会抛出异常,引起连锁式回滚,中间执行过程更改的数据结构也会全部还原,所以整个auction函数执行失败,所有人都收不到转账。
在这里插入图片描述
解决方案:
设计由投标者自己取回出价的方式,首先判断拍卖是否截止,检查取回的地址是否为最高出价人,然后判断余额是否大于0,然后将账户余额转给调用合约的人,然后在合约中给对应出价人清0,如下所示
在这里插入图片描述

code is law
智能合约的规则由代码逻辑决定,由于区块链的不可篡改,发布到区块链上的所有合约都将无法修改,好处是没有人可以篡改规则,坏处是如果存在漏洞也无法修改,智能合约如果设计不好,有可能会造成以太币锁在里面,永远无法取出来。
所以智能合约必须经过严格的测试,可以在testnet上用假的以太币测试,确认完全没有问题再发布。
能否在智能合约中留一个后门,用来修改bug?
比如给合约的创建者超级用户的权利,这样做的前提是所有人都要信任这个超级用户,与去中心化的理念背道而驰。

重入攻击的问题
合约账户收到ETH时,通过addr.send()、addr.transfer()、addr.call.value()()三种方式都会触发addr里的fallback函数。
fallback()函数由用户自己编写,内部又调用一次withdraw函数,造成退款的递归调用,不停的从竞拍合约中取钱,直到余额不足、汽油费不足以及栈溢出。
如下所示:
在这里插入图片描述
解决方案
改为先将收款人清0,再转账,再次调用将没有金额用于转账;另外还可将转账方式改为send或transfer,汽油费只有2300,不足以让接受的合约再发起一个新的调用,只够写一个log,如下所示:
在这里插入图片描述

对于可能和其他合约发生交互的经典的编程模式:

  1. 判断条件
  2. 改变条件
  3. 与其他合约交互
Logo

为所有Web3兴趣爱好者提供学习成长、分享交流、生态实践、资源工具等服务,作为Anome Land原住民可不断优先享受各种福利,共同打造全球最大的Web3 UGC游戏平台。

更多推荐