主页 > 下载imtoken官方网站 > 为了把傅恒和魏璎珞的爱情上链,作为技术小白看了几百行EVM代码,终于搞定了

为了把傅恒和魏璎珞的爱情上链,作为技术小白看了几百行EVM代码,终于搞定了

下载imtoken官方网站 2023-12-08 05:13:05

640?wx_fmt=jpeg

作者| Vasa TowardsBlockChain联合创始人

编译 | 国喜口

傅恒爱上了魏璎珞,我却因为爱而学了EVM! 因为小编要用以太坊来为他们的爱上链,而要学习以太坊,就必须了解以太坊虚拟机!

与以太坊相比,以太坊虚拟机犹如优质机芯成就精美腕表。 它的出现让以太坊如虎添翼,从众多区块链平台中脱颖而出,开启了“区块链2.0”的新时代。 . 因此,深入了解和熟练掌握以太坊虚拟机是以太坊开发者的基本功。

近日,MIT孵化初创公司TowardsBlockChain的联合创始人vasa对以太坊虚拟机的概念以及以太坊虚拟机的工作原理进行了深入透彻的分析。 在本文中,vasa 将带您了解以太坊虚拟机的核心细节,如何创建智能合约,消息调用函数(message call)如何工作,以及与数据管理相关的一切,如存储、内存、calldata还有一堆,满满的干货!

傅恒,放弃尔晴,这世上,难逃魏璎珞。

最近《延禧攻略》火了,傅恒和魏璎珞求不来的爱情也让很多人唏嘘不已。 那么傅恒为什么会爱上魏璎珞呢? 有网友说出了实话。

640?wx_fmt=jpeg

傅恒为什么喜欢魏璎珞? 原来是因为周星驰的《大话西游》中的副歌《一生挚爱》。

傅恒放下身份爱上魏璎珞,小编感动。

小编想把他们的爱情记录在区块链上。 之前看到有人把他们的爱情宣言永久记录在以太坊区块链上。 我迫不及待地想尝试一下,也想亲自尝试一下。 想要学习以太坊,首先要了解什么是以太坊虚拟机。

以太坊虚拟机透视

在通过代码示例了解以太坊虚拟机的工作机制之前,小编先用3张图给大家展示一下以太坊虚拟机在以太坊中的位置和组成。

下图展示了以太坊虚拟机(红框)在整个以太坊架构中的位置,节点之上,代码之下。

640?wx_fmt=jpeg

接下来,大家看到的是以太坊虚拟机架构,这是一个简单的基于栈的架构。

640?wx_fmt=jpeg

以太坊是一个简单的基于堆栈的架构

下图是以太坊的运行模型,描述了以太坊虚拟机的不同组件之间是如何交互的。

640?wx_fmt=jpeg

通过这3张图,相信你已经了解了以太坊虚拟机的“样貌”。 接下来,我将通过一些图表和示例代码,带大家一探以太坊虚拟机的内在奥秘。

以太坊智能合约

智能合约基础

要探索以太坊虚拟机的内在奥秘,首先需要了解什么是智能合约。 一般来说,以太坊智能合约是运行在以太坊虚拟机上的计算机程序。 以太坊虚拟机是一个完全隔离的环境,专为以太坊智能合约在容器沙箱中运行而设计。 这意味着在以太坊虚拟机中运行的每个智能合约都无法访问托管虚拟机的计算机上运行的网络基础设施、文件系统或其他进程。

以太坊网络中有两种账户:智能合约账户和外部拥有账户。 每个账户都由地址标识,所有账户共享同一个地址空间,即以太坊虚拟机接受长度为 160 位的地址。

640?wx_fmt=jpeg

以太坊网络中的账户由 160 位字符索引

每个账户包含四个部分:余额、随机数、字节码和存储数据(简称存储,下同)。 但是这两个帐户之间存在一些差异。 例如,外部拥有账户没有代码部分和存储部分,而智能合约账户的这两部分分别存储其字节码和整个状态树的 Merkle Tree 根哈希。 此外,外部拥有的账户有与之对应的私钥,而智能合约账户则没有。 除了智能合约账户中每笔以太坊交易的常规密码签名外,所有操作均由智能合约中的代码控制。

640?wx_fmt=jpeg

以太坊上链记录_以太坊的私有链_陀螺币上以太坊公链吗

由上图可知,外资账户由私钥控制,外资账户不包含EVM代码,而智能合约账户包含EVM代码,由代码控制。

创建智能合约

了解了什么是智能合约之后,接下来,通过代码示例教大家如何创建智能合约。

创建智能合约的操作是一个简单的交易,收件人地址为空,数据字段包含要创建的智能合约的编译字节码。 我们先来看这个例子,智能合约MyContract。

640?wx_fmt=png

打开一个Truffle控制台(Truffle Console,Truffle是目前最流行的以太坊开发框架),运行truffle develop命令进入开发者模式。 进入后按照以下说明部署智能合约MyContract:

640?wx_fmt=png

通过运行以下代码检查智能合约是否已成功部署:

640?wx_fmt=png

这么长的一串代码有什么作用呢?

将智能合约部署到以太坊区块链时,首先发生的事情是创建智能合约账户。

以太坊黄皮书上写到,新建智能合约账户的地址是对数据进行RLP(Recursive Length Prefix,递归长度前缀)编码得到的哈希值,只包含发送者和账户的随机数以及通过 Keccak 哈希计算。 最右边的 160 位。

可以看到,上面例子的构造函数中记录了智能合约的地址。 您可以通过检查 receipt.logs[0].data 是用 32 字节填充的智能合约地址和 receipt.logs[0].topics 是字符串 "Log(address)" 的 keccak-256 哈希来确认这一点。

640?wx_fmt=jpeg

当您在智能合约中调用函数时,后台会发生什么

接下来,交易中与智能合约一起发送的数据作为字节码执行。

此操作将初始化存储中的状态变量并确定正在创建的智能合约的主体。 此过程在智能合约的生命周期内仅执行一次。 初始化代码不会存储在智能合约中。 实际上,其执行的返回值,即字节码,会存储在智能合约中。 请记住,一旦创建了智能合约,任何人都无法更改其代码。

由于智能合约的初始化过程返回了需要存储的智能合约主体的代码,因此从构造函数逻辑上无法访问该代码。 看一下智能合约 Impossible 的代码:

640?wx_fmt=png

如果你尝试编译这个智能合约,你会得到一个警告,告诉你构造函数中引用了 this 指针,但它仍然会编译。

但是,如果您尝试部署一个新的智能合约实例,它将恢复,因为尝试运行未存储的代码是没有意义的。

另一方面,我们可以访问智能合约的地址,因为智能合约帐户存在,但它还没有任何代码。

同时,代码的执行可以生成其他事件,例如更改存储、创建更多帐户或进行进一步的消息调用。 例如下面的 AnotherContract 智能合约代码:

640?wx_fmt=png

猜猜当你在 Truffle 控制台中编译它时会发生什么。

640?wx_fmt=png

此外以太坊上链记录,还可以使用 CREATE 操作码创建智能合约,这是为 Solidity 语言新建的编译操作码。 两种智能合约创建方案具有相同的运行机制。

在深入解读了以上三段智能合约代码之后,接下来,我就带大家了解一下消息调用机制的工作原理。

消息调用机制

智能合约可以通过消息调用机制调用其他智能合约。

以太坊上链记录_陀螺币上以太坊公链吗_以太坊的私有链

每当智能合约需要调用另一个智能合约的功能时,它都会通过生成消息来实现。 每个消息调用都有一个发送者、接收者、有效载荷、要传输的以太币数量和一定数量的以太坊气体。

消息调用的深度限制在 1024 级以下。

640?wx_fmt=jpeg

消息调用由调用命令触发,请求和返回值由内存传递

Solidity语言为地址类型提供了原生的调用方法,其工作原理如下:

640?wx_fmt=png

这里gas是要发送的以太坊gas数量,address是要调用的智能合约地址,value是要转账的以太坊数量,单位是wei,data是要发送的payload。 请记住,value 和 gas 都是可选参数,但请注意,低级调用中的默认设置是发送发送方所有剩余的 gas。

640?wx_fmt=jpeg

油耗图表

每个智能合约都可以决定调用消息时发送的气体量。

由于每次消息调用都可能以 out-of-gas (OOG) 结束,为了避免安全问题,发送方至少应保留剩余 gas 的 1/64。

通过这种机制,发送方可以避免内部gas耗尽异常,保证智能合约在gas耗尽之前完成执行。

640?wx_fmt=jpeg

以太坊虚拟机异常执行图

看看下面的代码:智能合约调用者。

640?wx_fmt=png

智能合约 Caller 只有一个 fallback 函数,可以将每一个收到的消息调用重定向到 Implementation example。 每次收到消息调用时,此实例都会抛出一个 assert(false),这将消耗所有给定的固定燃料。 那么,这里的思路就是在调用执行前后记录Caller合约中的gas量。

打开 Truffle 控制台,看看会发生什么。

640?wx_fmt=png

如您所见,71495 大约是 4578955 的 1/64。这个例子清楚地表明可以处理内部调用的 out-of-gas 异常。

Solidity 还提供了以下允许使用内联汇编管理消息调用的操作码:

640?wx_fmt=png

其中g是要发送的fuel数量,a是要调用的地址,v是wei中要转账的以太币数量,in是insize字节的内存位置,这里存放调用的信息,out和outsize指示消息调用的返回数据在内存中的存储位置。

唯一的区别是使用内联汇编进行消息调用允许处理返回数据,而使用函数只返回1或0来表示调用是否失败。

640?wx_fmt=jpeg

以太坊虚拟机可以通过消息调用输入外部数据

以太坊虚拟机可以输出日志,也可以输出调用智能合约的返回值

以太坊虚拟机支持一种特殊的消息调用方式,称为delegatecall。 也就是说,除了内联汇编版本,Solidity语言还提供了内置的寻址方式。

它与底层消息调用的区别在于,目标代码是在消息调用智能合约中执行的,msg.sender和msg.value不会发生变化。

使用以下示例来说明 delegatecall 方法的工作原理。

以太坊上链记录_以太坊的私有链_陀螺币上以太坊公链吗

先看智能合约Greeter:

640?wx_fmt=png

使用上面的代码,智能合约 Greeter 只需声明一个 thanks 函数,该函数发布一个包含 msg.value 和 msg.sender 数据的事件。

通过在 Truffle 控制台中运行以下代码来尝试一下:

640?wx_fmt=png

通过上面的代码已经确认了它的功能,我们来看看智能合约钱包:

640?wx_fmt=png

这个智能合约只定义了一个回退函数,它通过 delegatecall 消息调用 Greeter#thanks 方法。

那么,在智能合约钱包中调用Greeter会发生什么?

640?wx_fmt=png

你可能已经注意到,上面的代码确认了 delegatecall 函数保留了 msg.value 和 msg.sender 的值。

这意味着智能合约可以在运行时从不同地址动态加载代码。 存储、当前地址和余额仍然指向调用智能合约,只是代码来自被调用智能合约。 这使得在 Solidity 语言中实现类似库的功能成为可能。

您还需要了解有关 delegatecalls 消息调用的这一点,如上所述,调用合约的存储由执行代码访问。 看看智能合约计算器:

640?wx_fmt=jpeg

智能合约Calculator只有两个功能:add和product。 计算器合约不知道如何执行加法或乘法。 必要时,它将这些调用分配给 Addition 和 Product 智能合约以供执行。 然而,所有这些智能合约共享相同的状态变量来存储每次计算的结果。

查看它如何使用以下代码:

640?wx_fmt=png

从上面的代码可以确认正在使用Calculator合约的存储。

此外,还可以确认正在执行的代码存储在两个智能合约Addition和Product中。

此外,delegatecall 有一个用于调用函数的 Solidity 语言汇编操作码版本。

通过以下几行 Delegator 智能合约代码,您可以学习如何使用它:

640?wx_fmt=png

您需要使用内联汇编来执行委托调用。

您会注意到没有值参数,因此 msg.value 不会改变。 也许你会有疑问,为什么要加载这个0x40地址呢? 什么是 calldatacopy 和 calldatasize? 您需要做的是在 Truffle 控制台上运行相同的命令来验证结果是否正确。

清楚地了解 delegatecall 消息调用的工作原理很重要。

每个触发的消息调用都将从当前智能合约发送,而不是被调用的智能合约。 此外,执行代码可以读取和写入调用智能合约的存储。 如果不注意这些细节,即使是一个小错误也可能导致数百万的损失以太坊上链记录,例如 The DAO 事件。

了解了以太坊智能合约的相关内容之后,接下来,我将带大家一起探索以太坊虚拟机的数据管理。

数据管理

以太坊虚拟机根据数据的内容以不同的方式管理不同类型的数据。

除了智能合约代码,至少可以区分四种主要数据类型:堆栈、调用数据、内存和存储。 接下来,依次分析它们。

陀螺币上以太坊公链吗_以太坊上链记录_以太坊的私有链

640?wx_fmt=jpeg

以太坊中的不同数据类型

堆栈

以太坊虚拟机是基于堆栈的机器,这意味着它不在寄存器上运行,而是在虚拟堆栈上运行。 堆栈的深度上限为 1024,堆栈条目的大小为 256 位。 事实上,以太坊虚拟机是一个256位的机器(方便Keccak256哈希计算和椭圆曲线计算)。 堆栈是大多数操作码存储其参数的地方。

640?wx_fmt=jpeg

所有操作都在栈上执行

通过PUSH/POP/COPY/SWAP等操作进行交互

以太坊虚拟机提供了一些操作码来直接在栈上操作。 这些包括:

通话数据

calldata 是一个只读的字节寻址空间,其中存储了事务或调用的数据参数。 与堆栈不同,要使用 calldata 数据,您必须准确指定字节偏移量和要读取的字节数。

以太坊虚拟机提供的用于操作调用数据的操作码包括:

Solidity 语言还提供了这些操作码的内联汇编版本,它们是 calldatasize、calldataload 和 calldatacopy。 calldatacopy 接受三个参数 (t, f, s):它将 s 个字节从位置 f 的 calldata 复制到位置 t 的内存。 此外,Solidity 语言允许您通过 msg.data 访问呼叫数据。

查看下面委托调用的内联汇编代码:

640?wx_fmt=png

为了将调用分配给地址 _impl,必须发送 msg.data。 由于 delegatecall 操作码对内存中的数据进行操作,因此您需要将调用数据复制到内存中。 这就是为什么使用 calldatacopy 将所有 calldata 复制到一个内存指针(注意参数 calldatasize 的使用)。

接下来使用 calldata 分析另一个实例。

640?wx_fmt=png

将内存指针存放在变量a中,将a后32字节的内容存放在b中。 然后使用 calldatacopy 将第一个参数存储在 a 中。

您会注意到它是从 calldata 的第 4 个位置开始复制的,而不是从它的开头复制的。 这是因为 CallData 的前 4 个字节包含被调用函数的签名,在示例中为 bytes4(keccak256("add(uint256,uint256)"))。 这是以太坊虚拟机用来识别哪个是被调用函数的原理。

然后,将第二个参数存入b,即复制calldata的最后32字节。 最后,只需从内存中加载它们并将两个值相加。

通过运行以下命令在 Truffle 控制台上自行测试:

640?wx_fmt=png

记忆

内存是一个非永久性的、可读可写的字节可寻址空间。 主要用于存储执行过程中的数据,多为内部函数传递参数。 由于内存是非持久性的,因此每个消息调用都以清除内存开始。 也就是说,内存中的所有位置都被初始化为零。 与 calldata 相比,内存可以按字节级别寻址,但一次只能读取 32 字节的字。

640?wx_fmt=jpeg

内存线性排列,可以按字节级别索引

通过 MSTORE/MSTORE/MLOAD 命令交互

内存中的所有位置都初始化为 0

当你向之前没有使用过的内存写入数据时,内存就“增加”了。 除了写入本身的成本外,这种增加还有成本,前 724 字节线性增加,此后呈二次方增加。

以太坊虚拟机提供了三种与内存区域交互的操作码:

以太坊的私有链_陀螺币上以太坊公链吗_以太坊上链记录

Solidity 语言还提供了这些操作码的内联汇编版本。

光看上面的内容就够了吗? 不! 关于内存,您还需要了解另一件重要的事情。

Solidity 总是在 0x40 位置存储一个空闲内存指针,指向内存中第一个未使用的字,这就是为什么你加载这个字来操作内联汇编。

由于前 64 字节的内存是为以太坊虚拟机保留的,因此可以确保该操作不会覆盖 Solidity 内部使用的内存。

例如,在上面给出的消息委托调用示例中,加载此指针以存储给定的调用数据并发送它。 这是因为内联汇编操作码委托调用需要从内存中获取其有效负载。

此外,如果您注意 Solidity 编译器输出的字节码,您会发现它们都以 0x6060604052 开头...即:

在汇编级使用内存时必须非常小心。 否则,您可能会覆盖已使用的内存空间。

贮存

存储是一个永久的、可读可写的字节寻址空间,每个智能合约都在其中存储其永久信息。 与内存不同,存储是一个只能由单词索引的持久区域。 它是一个 256 位到 256 位的键值映射。

智能合约既不能读取也不能写入除自身存储之外的任何智能合约的存储。 与内存一样,存储中的所有位置都被初始化为零。

将数据保存到存储是以太坊虚拟机中消耗最多 gas 的少数操作之一。

这种燃料成本并不总是相同的。 例如,将存储中的值从零更改为非零值需要 20,000 个单位的燃料,而存储相同的非零值或将非零值设置为零只需要 5,000 个单位的燃料。

以太坊虚拟机提供了两个用于操作存储的操作码:

Solidity 语言的内联汇编也支持这些操作码。

Solidity 会自动将智能合约中定义的每个状态变量映射到存储中的相应位置。 该策略非常简单,对于静态大小的变量,即除了映射和动态数组之外的所有变量都从位置 0 开始连续分布在存储中。

对于动态数组,position(p) 存储动态数组的长度,其数据将位于哈希计算结果 p(keccak256(p)) 的位置。 对于映射,此位置未使用,对应于键 k 的值将位于哈希计算 keccak256(k,p) 的结果位置。 哈希计算keccak256的参数(k和p)总是需要填充到32字节。

通过存储智能合约代码了解它是如何工作的:

640?wx_fmt=png

打开一个Truffle控制台测试其存储架构,首先编译创建一个新的智能合约实例:

640?wx_fmt=png

那么,可以确定地址0保存的是数字2,地址1保存的是智能合约的地址:

640?wx_fmt=png

检查存储位置 2 是否保存数组的长度,如下所示:

640?wx_fmt=png

最后检查存储位置3没有被使用,键值对map的值存储在上面提到的位置:

640?wx_fmt=png

通过上面的图解和详细的代码示例,你是否和我一样了解以太坊虚拟机? 接下来,为了将傅恒和魏璎珞的爱情上链,小编将继续学习以太坊!

最新热点文章:

640?wx_fmt=jpeg

大力戳↑↑↑加入区块链大本营读者群⑦

(群已满微信qk15732632926加群)