跳到主要内容

以太坊存储和执行

在过去的几个轨道上,我们一直在编写智能合约,并简要地提到,以太坊智能合约在这个叫做以太坊虚拟机(EVM)的东西中运行。

我们还顺便简单地提到,EVM 能够运行某些 OPCODES,并处理堆栈或堆中存在的数据。如果你有正式的计算机科学背景,这对你来说可能是有意义的,但对其他人来说,这实际上意味着什么?

在这一层面,我们将深入挖掘 EVM 执行引擎,以及数据在整个事务过程中是如何存储、操作和运行的。

总结

在继续前进之前,让我们回顾一下我们在先前的轨道中所教授的一些东西。

回顾一下 Ethereum 作为一个基于交易的状态机工作。从某个状态 s1 开始,一个交易操作某些数据,将世界状态转移到某个状态 s2。

为了把事情组合在一起,交易被打包成块。一般来说,每个区块都会将世界状态从状态 s1 改变到 s2,而转换是根据区块内每个事务的状态变化来计算的。

当我们想到这些状态变化时,以太坊可以被认为是一个状态链。

但是,这个世界的状态是什么?

世界状态

以太坊的世界状态是地址和账户状态之间的映射。以太坊的每个地址都有它自己的状态,这可能是一个用户账户(EOA)或一个智能合约。

每个区块基本上都会操纵多个账户状态,从而操纵以太坊的整体世界状态。

帐户状态

好吧,那么世界状态是由各种账户状态组成的。什么是账户状态?

帐户状态包含一些常见的东西,如 nonce 和余额(以 ETH 为单位)。此外,智能合约还包含一个存储哈希和一个代码哈希。这两个哈希值作为独立状态树的引用,分别存储状态变量和智能合约的字节码。

回顾一下,在 Ethereum 有两种类型的账户。外部拥有的账户(如 Coinbase 钱包,Metamask 钱包等)和智能合约账户。

EOA 的由私钥控制,没有任何 EVM 代码。另一方面,合约账户包含 EVM 代码,由代码本身控制,没有与之相关的私钥。

交易的类型

在以太坊上主要有两种类型的交易。一种是创建新合约,另一种是发送消息。

发送消息在这里意味着进行交易,要么转移 ETH,要么调用智能合约上的功能。它们只是 EOA 可以发送的不同类型的信息。

当合约创建交易完成后,一个新的账户被添加到世界状态中。该交易带有要创建的合约的字节码和初始化代码(即构造函数调用)。

另一方面,对于所有其他交易,即消息调用,现有账户的状态在交易后被修改。

消息

以太坊的消息是在两个账户之间传递的。它们主要由两样东西组成 - 数据价值

数据是一组字节,表示需要进行的交易类型(转移 ETH、铸造 NFT、在 DAO 中投票等),价值是与交易一起转移的以太币价值。

由 EOA 进行的交易会向接收者账户发送一个 mesasge。合约账户也可以通过 EVM 代码向账户发送消息。

以太坊虚拟机

就像 Java 的 JVM,Javascript 和 Python 也有自己的运行时环境,以太坊智能合约的运行时环境是 EVM。

EVM 有一个基于堆栈的架构。对现代 CPU 架构进行了大规模的简化。

智能合约代码,或 EVM 代码,生活在 EVM 内的一个不可变的存储位置。

对于运行时的计算,即局部变量之类的,EVM 可以访问两个存储位置 - 堆栈和内存(即堆)。

EVM 还可以访问持久的世界状态,即账户状态的读写,例如,改变合约中的状态变量。

堆栈是一个简单的堆栈,支持 PUSH/POP 操作,每个堆栈元素是 256 比特(32 字节),最大深度为 1024 个元素。

内存(或堆)是一个线性内存结构,可以在运行时存储动态大小的数据,即字符串和动态数组。

帐户存储是世界状态的一部分,是持久性存储,即使在事务执行完毕后,所做的任何改变都会继续留在这里。

堆栈

堆栈是一种用于保存临时值的后进先出数据结构。可以把它想象成一摞盘子。你堆在上面的盘子,将是第一个被移除的。堆栈在整个计算机科学中被用来对固定大小的数据进行快速操作,EVM 也不例外。

EVM 的所有操作都在堆栈中运行。EVM 的堆栈支持对堆栈的前 16 个元素进行操作,但不支持更深的操作。其他的 1008 个堆栈元素可以用来存储操作数据,如要运行的 OPCODES 等。

有趣的是:在 Solidity 中,如果你编写的函数中声明的局部变量超过 16 个,你会得到一个编译错误。因为堆栈不能处理超过前 16 个元素的数据,拥有超过 16 个变量意味着在 EVM 中不能对其中一些变量进行操作。

内存

EVM 内存是一个线性寻址的内存,它可以在字节级寻址。你可以在内存中一次存储 8 比特(1 字节)或 256 比特(32 字节),但只能以 256 比特(32 字节)为单位从内存中读取。内存用于在实体中存储动态值,如可变长度数组、字符串等。

最初,所有内存位置的值都是零。然而,在事务执行过程中,这些值可以被更新和修改。

账户存储

持久账户存储是一个从 256 位密钥到 256 位数值的映射。持久存储中的所有位置最初也被定义为零(因此 Solidity 中整数的初始值为 0,布尔运算为假,字符串为空,等等)。

这些映射中的键通常被称为槽。智能合约中的每个状态变量都在账户存储中被分配了一个槽,按照它们被定义的顺序。

因此,对于一个看起来像这样的合约。

contract Sample {
uint256 first;
uint256 second;
address third;
}

第一个将拥有 0 号槽,第二个将拥有 1 号槽,第三个将拥有 2 号槽。

当我们开始学习 Solidity 中的 DELEGATECALL(.delegatecall())时,这个槽的概念将变得非常重要,在本轨道的后面。

执行模型

让我们看一下 EVM 中的高级执行模型。这张图一开始可能有点混乱,但读完这一节,你就会明白发生了什么。

EVM 包含一个程序计数器(PC)。PC,有时也被称为指令指针,是一个指向计算机在代码执行中的位置的数值。

如果你把 EVM 的代码看作是一个要运行的指令列表,PC 将指向需要运行的指令。最初,PC 指向零,即第一条指令。当该指令被运行后,PC 被更新为指向下一条指令,以此类推。

PC 所指向的指令对给定的数据执行某些操作。这些操作发生在堆栈中,堆栈可以从内存和账户存储中读/写值。

我以前用过这个比喻,我将再次使用它--把内存想象成你的 RAM,把账户存储想象成你的硬盘。堆栈(指令处理器)可以从 RAM 和硬盘中读/写数据,但只有对硬盘数据的修改在代码运行结束后才会继续存在,而内存则会被清除。

到目前为止,这与实际的 CPU 架构非常相似。对于那些有正式计算机科学背景的人来说,如果你在大学里上过硬件或计算机处理器课,你一定被教导过关于实际处理器如何工作的类似内容。EVM 的行为也非常类似。

但是,这里有一件特别的事情。EVM 还存储了一个计数器,用于计算有多少气体可用。EVM 执行的每个操作都要花费一定量的气体,只要有足够的气体来运行操作,EVM 就会继续执行操作。如果可用的气体低于继续运行所需的数量,整个执行将停止并导致交易失败。正如我们之前所讲,这样做是为了避免在 EVM 内出现无限循环,从而使以太坊网络陷入停顿。因此,对于复杂的交易,你需要支付更高的气体来支付执行成本。

执行过程中的气体

突出以上几点,你可以看到 EOA 在发送消息时,会向合约账户传递一定量的气体。EVM 代码运行并使用了一些气体。如果有剩余的气体,就会退还给 EOA。

然而,如果 EVM 代码耗尽气体,即没有提供足够的气体,执行将失败,交易将失败。在这种情况下,没有气体被退还,因为 EVM 仍然不得不执行所有这些操作,以弄清所提供的气体太少,所以气体被收取所做的工作。

总结

以太坊是一个复杂的软件。如果你能走到这一步,为你鼓掌。我希望这一关能帮助你解开一些关于以太坊存储如何工作的疑惑,以及 EVM 如何在运行时处理数据和执行交易。

我们可以更深入地研究在 EVM 中运行的 Assembly 和 OPCODES,但这值得我们自己写一篇(或更多)文章,因为这是一个巨大的话题。

参考资料