本文主要是介绍为将傅恒与魏璎珞的爱情上链,作为技术小白的我读了EVM上百行代码,终于搞定了...,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
作者 | Vasa TowardsBlockChain 联合创始人
编译 | kou、Guoxi
傅恒爱上了魏璎珞,我却因为他们的爱情学习了以太坊虚拟机!因为小编想要用在以太坊上将他们的爱情上链,学习以太坊,就要了解以太坊虚拟机!
以太坊虚拟机相对于以太坊就好比一枚优质的机芯成就一款精致的手表,它的出现让以太坊如虎添翼,从众多区块链平台中脱颖而出,开启了“区块链2.0”新时代。因此,深刻理解并熟练掌握以太坊虚拟机是以太坊开发者的基本功。
近日,MIT孵化初创公司TowardsBlockChain的联合创始人vasa对以太坊虚拟机概念以及以太坊虚拟机工作原理做了深入且透彻分析。在本文中,vasa将带你探讨以太坊虚拟机的核心细节,介绍如何创建智能合约,消息调用函数(message call)如何工作,以及与数据管理,比如存储,内存,calldata和堆栈相关的所有内容,满满的都是干货!
傅恒,放弃尔晴,这世间,难逃魏璎珞。
延禧攻略最近大火,傅恒和魏璎珞求而不得的爱情也令很多人觉得惋惜。那么傅恒到底为什么爱上魏璎珞呢?有网友真相了。
傅恒为什么爱魏璎珞?原来是因为周星驰《大话西游》中的副歌《一生所爱》。
傅恒放下身份,爱上魏璎珞,小编被感动了。
小编想把他们的爱情记录在区块链中,之前就看到过有人将爱情宣言永久记录在了以太坊区块链上,小编跃跃欲试,想要亲自试一下。想要学习以太坊,就要先了解什么是以太坊虚拟机。
透视以太坊虚拟机
在通过代码示例了解以太坊虚拟机工作机制之前,小编先通过3张图,带你了解以太坊虚拟机在以太坊中的位置以及组成部分。
下面这张图表展示了以太坊虚拟机在整个以太坊架构中的位置(红色框框),位于节点之上,代码之下。
接下来,你看到的是以太坊虚拟机架构,它是一个基于堆栈的简单架构。
以太坊是一个基于堆栈的简单架构
接下来这张图,是以太坊的运行模型,它描述了以太坊虚拟机的不同组成部分之间如何互相作用。
通过这3张图表,相信你已了解以太坊虚拟机的“外表”,接下来,通过一些图件和实例代码带你探索以太坊虚拟机的内部奥秘。
以太坊智能合约
智能合约基础
要想探索以太坊虚拟机的内部奥秘,首先你需要了解什么是智能合约,通俗的讲,以太坊智能合约就是在以太坊虚拟机上运行的计算机程序。以太坊虚拟机是为以太坊智能合约在容器沙箱中运行而设计的一个完全隔离的环境。这意味着在以太坊虚拟机中运行的每个智能合约都无法访问托管虚拟机的计算机上运行的网络架构,文件系统或其他进程。
以太坊网络中有两种帐户:智能合约账户和外部拥有帐户。每个帐户都由一个地址来标识,所有的帐户共享相同的地址空间,即以太坊虚拟机接受长度为160位的地址。
以太坊网络中的账户由一个160位的字符索引
每个帐户中都包含余额,随机数,字节码和存储的数据(简称存储,下同)四个部分。但这两种账户之间存在一些差异。例如,外部拥有帐户并没有代码部分和存储部分,而智能合约帐户的这两个部分分别存储它们的字节码和整个状态树的默克尔树( )根哈希。此外,外部拥有帐户具有与其相应的私钥,而智能合约帐户却没有。智能合约帐户除了对每笔以太坊交易进行常规的密码学签名之外,所有的操作都由智能合约中的代码控制。
通过上图,你会了解到,外部拥有账户由私钥控制,且外部拥有账户中不包含以太坊虚拟机代码,而智能合约账户含有以太坊虚拟机代码,并由代码控制。
创建智能合约
在了解什么是智能合约后,接下来,通过代码实例教你如何创建智能合约。
创建智能合约的操作就是一笔简单的交易,在这笔交易中接收者地址为空,数据字段包含要创建的智能合约的编译字节码。 先看这个实例, 智能合约MyContract。
打开一个Truffle控制台(Truffle Console,Truffle是目前最流行的以太坊开发框架),运行truffle develop命令进入到开发者模式。进入后,按照以下指令部署智能合约MyContract:
通过运行以下代码检查智能合约是否已成功部署:
这么一长串代码做了什么事情呢?
在将智能合约部署到以太坊区块链时,发生的第一件事是创建了智能合约账户。
以太坊黄皮书上写到,新创建智能合约帐户的地址为仅包含发件人和帐户随机数的数据经RLP(Recursive Length Prefix,递归长度前缀)编码并经Keccak哈希计算得到的哈希值的最右边160位。
正如你所看到的,上面示例的构造函数中记录了智能合约的地址。你可以通过检查receipt.logs [0] .data是否是填充32字节的智能合约地址以及receipt.logs [0] .topics是否是字符串“Log(address)”的keccak-256哈希值来确认。
当你调用了一个智能合约中函数时,后台发生的操作
接下来,在交易中与智能合约一起发送的数据将被作为字节码执行。
这个操作将初始化存储中的状态变量,并确定正在创建的智能合约的正文。此过程在智能合约的生命周期内仅执行一次。初始化代码并不会被存储在智能合约中,实际上它执行的返回值也就是字节码才会被存在智能合约中。切记,智能合约一旦被创建,任何人都无法更改其代码。
由于智能合约的初始化过程会返回需要存储的智能合约正文的代码,因此从构造函数的逻辑上讲,代码是不可访问的。看一下智能合约Impossible代码:
如果你尝试编译这个智能合约,你将收到一条警告,告诉你在构造函数中引用了this指针,但它还是会编译。
但是,如果你尝试部署一个新的智能合约实例,它将还原,这是因为尝试运行尚未存储的代码是没有意义的。
另一方面,我们能够访问智能合约的地址,因为智能合约帐户是存在的,但它里面还没有任何代码。
同时,代码的执行可以会产生其他事件,例如更改存储,创建更多帐户或进行进一步的消息调用。比如,下面这一段AnotherContract智能合约代码:
猜一下在Truffle控制台中编译它会发生什么。
此外,还可以使用CREATE操作码来创建智能合约,这是Solidity语言新构造的编译操作码。两种智能合约创建方案有着相同的运行机制。
在深入解读以上三段智能合约代码之后,接下来,带你了解消息调用机制的工作原理。
消息调用机制
智能合约可以通过消息调用机制调用其他智能合约。
每当智能合约需要调用另一个智能合约的函数时,它都会通过生成一个消息调用。每个消息调用都有发送者,接收者,有效载荷,以太币传输数量和一定量的以太坊燃料。
消息调用的深度被限制为小于1024级。
消息调用由调用命令触发,请求和返回值由内存传递
Solidity语言为地址类型提供了一个本地调用方法,其工作方式如下:
这里的gas就是要发送的以太坊燃料数量,address是要调用的智能合约地址,value是要传输的以太币数量,以wei为单位,data是要发送的有效载荷。请记住,value和gas都是可选参数,但要小心的是,在低级别调用中默认是将发送者剩余燃料全部发送。
燃料开支图
每个智能合约都可以决定在消息调用时发送的燃料数量。
由于每次消息调用都可以以燃料耗尽(out-of-gas,OOG)结束,为了避免安全问题,发送方要至少保留剩余燃料的1/64。
通过这种机制,发送者可以避免出现内部燃料耗尽异常,确保在耗尽燃料之前完成智能合约的执行。
以太坊虚拟机异常执行图
看一下下面这段代码:智能合约Caller。
智能合约Caller只有一个回退函数,可以将每个接收到的消息调用重定向到Implementation示例上,这个实例只是在每接收到一个消息调用时抛出一个assert(false),这将消耗完所有给定的燃料。然后,这里的想法是记录实施调用之前和之后Caller合约中燃料的数量。
打开一个Truffle控制台,看看会发生些什么。
如你所见,71495大约是4578955的1/64。这个例子清楚地表明了可以处理内部调用的燃料耗尽异常。
Solidity还提供以下操作码,允许使用内联汇编(inline assembly)管理消息调用:
其中g是要发送的燃料量,a是要调用的地址,v是传输的以wei为单位的以太币数量,in表示insize字节的内存位置,被调用的信息就存储在这里,out和outsize表明消息调用的返回数据在内存中存储的位置。
唯一的区别就是使用内联汇编进行消息调用允许处理返回数据,而如果使用函数只返回1或0来代表调用是否失败。
以太坊虚拟机可以通过消息调用来输入外部数据
以太坊虚拟机可以输出日志,也可以给调用智能合约输出返回值
以太坊虚拟机支持名为delegatecall的一个特殊的消息调用方法。也就是说除了内联汇编版本之外,Solidity语言还提供了内建地址方法。
它与低级消息调用的区别在于目标代码在消息调用智能合约中执行,并且msg.sender和msg.value不会更改。
通过下面实例来解读delegatecall方法的工作方式。
首先看一下智能合约Greeter:
通过上述代码,智能合约Greeter简单地声明了一个thanks函数,该函数发布一个包含msg.value和msg.sender数据的事件。
通过在Truffle控制台中运行以下代码来尝试此方法:
通过上面的代码已确认了它的功能,接下来再看一下智能合约Wallet:
这个智能合约仅定义了一个回退函数,该函数通过delegatecall消息调用执行Greeter#thanks方法。
那么,在智能合约Wallet中调用Greeter时会发生什么?
你可能已经注意到,上面的代码中可以确认delegatecall函数保留了msg.value和msg.sender的值。
这意味着智能合约可以在运行时从不同的地址动态加载代码。存储,当前地址和余额仍然指向调用智能合约,只有代码来自被调智能合约。这样使得可以在Solidity语言中实现类似程序库(library)功能。
关于delegatecalls消息调用,你还需要留意这一点,如上所述,调用合约的存储是由执行代码访问的。看一下智能合约Calculator:
智能合约Calculator只有两个功能:add和product。Calculator合约并不知道如何执行加法或乘法,需要时它将这些调用分别指派给Addition和Product两个智能合约来执行。但是,所有这些智能合约共享相同的状态变量用以存储每次计算的结果。
通过以下代码,看它是如何工作的:
通过上面的代码中可以确认正在使用Calculator合约的存储。
除此之外,还可以确认正在执行的代码存储在Addition和Product两个智能合约中。
另外,对于调用函数,delegatecall有一个Solidity语言汇编操作码版本。
通过下面的几行Delegator智能合约代码,你就可以学会如何去使用它:
你需要使用内联汇编来执行delegatecall。
你会注意到,这里没有值参数,因此msg.value并不会改变。也许你会有疑问,为什么要加载这个0x40地址? calldatacopy和calldatasize是什么?你需要做的是在Truffle控制台上运行相同的命令来验证结果是否正确。
清楚了解delegatecall消息调用的工作方式十分重要。
每个触发的消息调用都将从当前智能合约发送,而不是被调用的智能合约。此外,执行代码可以读取和写入调用智能合约的存储。如果没有留意这些细节,即使是很小的错误也可能导致数百万的损失,比如,The DAO事件。
在了解以太坊智能合约的相关内容后,接下来,带你探索以太坊虚拟机的数据管理。
数据管理
以太坊虚拟机根据数据的内容,以不同的方式来管理不同类型的数据。
除了智能合约代码,至少可以区分出四种主要类型的数据:堆栈,calldata,内存和存储。接下来依次对它们做出分析。
以太坊中的不同数据类型
堆栈
以太坊虚拟机是一个基于堆栈的机器,这意味着它不在寄存器上运行,而是在虚拟堆栈上运行。堆栈的深度上限为1024,堆栈项的大小为256位。事实上,以太坊虚拟机是一个256位的机器(这有利于Keccak256哈希计算和椭圆曲线计算)。堆栈是大多数操作码存储其参数的地方。
所有的操作都是在堆栈上执行的
通过PUSH/POP/COPY/SWAP等操作来交互
以太坊虚拟机提供了许多操作码来直接对堆栈进行操作。其中包括:
POP 从堆栈中删除项目。
PUSHn 将以下n个字节的项目放在堆栈中,n的取值为1到32。
DUPn 复制第n个堆栈项目,n的取值为1到32。
SWAPn 交换第1和第n个堆栈项目,n的取值从1到32。
Calldata
calldata是一个只读的字节编址空间,其中保存交易或调用的数据参数。与堆栈不同,要使用calldata数据,你必须准确指出字节偏移量和要读取的字节数。
以太坊虚拟机提供的用于操作calldata的操作码包括:
CALLDATASIZE 指出交易数据的大小。
CALLDATALOAD 将32个字节的交易数据加载到堆栈中。
CALLDATACOPY 将多个字节的交易数据复制到内存中。
Solidity语言还提供这些操作码的内联汇编版本,它们分别是calldatasize,calldataload和calldatacopy。calldatacopy需要三个参数(t,f,s):它会将位置为f的calldata中的s个字节复制到位置为t的内存中。此外,Solidity语言允许你通过msg.data访问calldata。
看一下下面这段delegatecall的内联汇编代码:
为了将调用指派给_impl这个地址,必须发送msg.data。由于delegatecall操作码与内存中的数据一起操作,你需要将calldata复制到内存中。这就是使用calldatacopy将所有calldata复制到一个内存指针的原因(注意使用到了参数calldatasize)。
接下来使用calldata来分析另一实例。
将该内存指针存储在变量a中,并在b中存储a之后32字节的内容。然后使用calldatacopy将第一个参数存储在a中。
你会注意到正在从calldata中的第4个位置而不是从它的开头复制它。这是因为calldata的前4个字节包含被调函数的签名,在示例中为bytes4(keccak256(“add(uint256,uint256)”))。这是以太坊虚拟机用来识别哪一个是被调函数的原理。
然后,将第二个参数存储在b中,即复制calldata的后面的32个字节。最后,只需要从内存加载它们,并把两个值相加。
运行以下命令在Truffle控制台上自行测试:
内存
内存是一个非永久性、可读写字节编址空间。它主要用于在执行期间存储数据,大部分情况下是将参数传递给内部函数。由于内存是非永久型的,每个消息调用都要从清空了的内存开始。即内存中所有位置被初始化为零。与calldata相比,内存可以在字节级别进行寻址,但一次只能读取32字节的字。
内存以线性排列,可以被字节级别的索引
通过MSTORE/MSTORE/MLOAD指令来交互
内存中的所有位置都被初始化为0
当你往内存中写入以前没有使用过的数据时,内存就“增加”了。除了写入本身的成本之外,这种增加也有成本,它在前724字节时线性增加,之后以二次方增加。
以太坊虚拟机提供三个操作码用于与内存区域的交互:
MLOAD 将一个字从内存加载到堆栈中。
MSTORE 将一个字保存到内存中。
MSTORE8 将一个字节保存到内存中。
Solidity语言还提供这些操作码的内联汇编版本。
只看上面内容就可以了吗?不!你还需要了解关于内存的另一个关键事项。
Solidity总是在0x40位置存储着一个空闲的内存指针,指向内存中第一个未使用的字,这就是你加载这个字来操作内联汇编的原因。
由于内存最前面的64字节是为以太坊虚拟机保留的,因此可以确保操作不会覆盖Solidity内部使用的内存。
例如,在上面给出的消息调用delegatecall示例中,加载这个指针用以存储给定的calldata并发送它。这是因为内联汇编操作码delegatecall需要从内存中获取其有效载荷。
另外,如果你注意Solidity编译器输出的字节码,你会发现它们都以0x6060604052 ...开头,即:
PUSH1:以太坊虚拟机操作码为0x60
0x60:空闲的内存指针
PUSH1:以太坊虚拟机操作码为0x60
0x40:空闲内存指针的内存位置
MSTORE:以太坊虚拟机操作码是0x52
在汇编级别使用内存时必须非常小心。否则,你可能会覆盖已使用的内存空间。
存储
存储是一个永久的、可读写字节编址空间,它是每个智能合约存储其永久信息的地方。与内存不同,存储是一个永久性区域,只能通过字来索引。它是256位到256位的键值映射。
一个智能合约除了本身的存储外,既不能读取也不能写入其他任何智能合约的存储,和内存一样,存储中所有位置都被初始化为零。
将数据保存到存储中是以太坊虚拟机耗费燃料值最高的几个操作之一。
这笔燃料费用并不总是相同的。就比如将存储中一个值从零修改为非零值需要20000单位的燃料,而存储同样的非零值或将这个非零值设置为零时只需要5000单位。
以太坊虚拟机提供两个操作存储的操作码:
SLOAD: 将存储中的字加载到堆栈中。
SSTORE: 将一个字保存到存储中。
Solidity语言的内联汇编也支持这些操作码。
Solidity会将智能合约中每个已定义的状态变量自动映射到存储中的相应位置。策略非常简单,对于静态大小的变量,也就是除了映射和动态数组之外的所有变量在从位置0开始的存储中连续分布。
对于动态数组,位置(p)存储动态数组的长度,其数据将位于由哈希计算p(keccak256(p))产生的位置。对于映射,该位置并未使用,并且对应于键k的值将位于哈希计算keccak256(k,p)产生的位置。哈希计算keccak256(k和p)的参数始终需要填充为32个字节。
通过Storage智能合约代码了解它是如何工作的:
打开一个Truffle控制台来测试它的存储架构,首先编译并创建一个新的智能合约实例:
然后,可以确定地址0保存数字2,地址1保存智能合约的地址:
检查存储位置2是否保存数组的长度,如下所示:
最后,检查存储位置3是否未使用,并且键值对映射的值是否保存在上文中所说的位置:
通过以上图表和详细的代码实例,你是否像小编一样很好地理解以太坊虚拟机?接下来,为了将傅恒与魏璎珞的爱情上链,小编会继续学习以太坊!
最新热文:
币价涨涨跌跌,程序员竟如此佛系... | 他说
为什么区块链开发者工资这么高?看看他们需要掌握多少东西就知道了
性能赶超EOS?一文带你深挖并发执行模型
Coinbase 交易所背后,究竟是一支怎样的团队?
大力戳↑↑↑ 加入区块链大本营读者⑦号群
(群满加微信 qk15732632926 入群)
内容转载请联系微信:qk15732632926
商务合作请联系微信:fengyan-1101
这篇关于为将傅恒与魏璎珞的爱情上链,作为技术小白的我读了EVM上百行代码,终于搞定了...的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!