彻底理解solidity里的storage:Ethereum Architecture(以太坊架构),Block Header(区块头),State Root,Ethereum Account,Storage Root,StateDB -> stateObject -> StateAccount,初始化一个新的以太坊账户,SSTORE,SLOAD
原文:https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy-5a5?s=r 译者:Shenstone。 校对:Shenstone。 本文永久链接:https://learnblockchain.cn/article/4172
我们将从下面的图片开始。不要被吓倒,在本文结束时,你会明白这一切到底是如何结合在一起的。这代表了以太坊的架构和以太坊链中包含的数据。
与其将图表作为一个整体来看,我们不如逐块分析。现在,让我们把重点放在 "区块头N"和它包含的字段上。
区块头包含了一个以太坊区块的关键信息。下面是 "区块头N "片段,以及它的数据字段。看一下etherscan上的这个区块14698834,看看你是否能找到图中的这些成员。
该区块头包含以下成员。
让我们看看这些成员如何与 Geth 客户端代码库中的内容相对应。我们先看block.go中定义的 "Header "结构,它表示一个块的头。
我们可以看到,代码库中所述的值与我们的概念图相匹配。我们的目标是要如何从区块头找到我们合约的storage存储的位置。
要做到这一点,我们需要关注块头的 "State Root"字段,该字段以红色标示。
"State Root"的作用类似于merkle root,因为它是一个依赖于它中间所有数据的哈希值。如果任何数据发生变化,根哈希值也会发生变化。
在 "State Root"下面的数据结构是一个Merkle Patric Trie(MPT),它为网络上的每个以太坊账户存储一个键值对,其中key是一个以太坊地址,value是以太坊账户对象。
实际上,key是以太坊地址的哈希值,value是RLP编码的以太坊账户,但是我们现在可以忽略这一点。
下面是 "以太坊架构 "图的一部分,表示State Root下的MPT。
Merkle Patricia Trie是一个非三态的数据结构,所以我们不会在这篇文章中深入研究它。我们可以继续抽象化地址到以太坊账户的键值映射模型。
如果你对Merkle Patricia Trie感兴趣,我建议你看看这篇优秀的介绍文章。
接下来让我们细究一下以太坊地址所映射到的以太坊账户值。
以太坊账户是以太坊地址的共识代表,它由4部分构成
从以太坊架构图的部分片段里可以看到这些内容
我们再看Geth的代码,找到相关的代码'state_account.go',之前提及的以太坊账户结构被定义为‘StateAccount’。
我们可以看到代码里的结构成员一一对应我们的概念图。
接下来,我们需要深入学习以太坊账户里的"Storage Root"字段。
storage root跟state root一样,在它下面也是一棵Merkle Patricia trie。
区别在于这次key值是存储插槽(storage slots),而value值是插槽里的数据。
再次注意这里实际上会对value进行RLP编码,以及对key取hash
下图是以太坊架构图里代表'Storage Root’的MPT的部分。
像之前一样,'Storage Root'是默克尔根哈希,它会因为任一底层数据变化而变化。
合约storage的任何变化都会影响到 "Storage Root",进而影响到 "State Root",进而影响到 "Block Header"。
文章到这里,我们已经成功把你从一个以太坊区块深入到一个合约的存储空间。
文章的下一部分是对Geth代码库的深入探讨。我们将简要地看一下合约存储是如何初始化的,以及当调用SSTORE & SLOAD操作码时会发生什么。
这将帮助你从我们到目前为止所讨论的内容,回到你的 solidity 代码和底层存储操作码,建立起联系。
ummm,下面的内容涉及代码比较多,假定读者有基础的代码阅读理解能力
为了开始之后的内容,我们需要一个全新的合约。一个全新的合约意为着一个全新的状态账户(StateAccount)。
我们先介绍三个结构:
让我们看看这3个概念是如何相互关联的,以及它们与我们一直在讨论的内容有什么关系。
为了创建一个新的StateAccount,我们需要与statedb.go代码和StateDB结构交互。
StateDB有一个createObject函数,可以创建一个新的stateObject,并将一个空的StateAccount传给它。这实际上是创建一个空的"以太坊账户"。
下图详细说明了代码流程。
好了,我们有一个空的stateAccount,接下来我们要做什么?
我们想存储一些数据,为此我们需要使用SSTORE操作码。
在我们深入了解Geth中的SSTORE实现之前,让我们快速回忆SSTORE的作用。
它从堆栈中弹出两个值,首先是32字节的key,其次是32字节的value,并将该值存储在由key定义的指定存储槽中。 下面是SSTORE操作码的Geth代码流程,让我们看看它的作用。
让我们从代码中的dirtyStorage定义继续学习。
你可能已经注意到stateObject中的pendingStorage和originStorage就在dirtyStorage字段的上方。它们都是相关的,在最终确定过程中,dirtyStorage被复制到pendingStorage,而pendingStorage在 trie被更新时又被复制到originStorage。
在 trie 被更新后,StateAccount 的 "存储根 "也将在 StateDB 的 "提交 "中被更新。这将把新的状态写入底层的内存 trie 数据库中。
现在到了拼图的最后一块,SLOAD。
让我们再次快速回忆,SLOAD操作码做什么。
它从堆栈中弹出1个值,32字节的key,它代表存储槽,并返回存储在那里的32字节的value。
下面是SLOAD操作码的Geth代码流程,让我们看一下它的作用
一个交易可以多次操作一个存储槽,所以我们必须确保我们有最新的值。
让我们想象一下,在同一交易中,在同一存储槽的SLOAD之前,发生了一个SSTORE。在这种情况下,dirtyStorage将在SSTORE中被更新,在SLOAD中被返回。
到这里,你应该对SSTORE和SLOAD是如何在Geth客户端层面实现的有了了解。它们如何与状态和存储对象互动,以及更新存储槽与更广泛的以太坊 "世界状态 "的关系。
这很难,但你做到了。我猜这篇文章给你留下了比你开始之前更多的问题,但这也是加密货币的乐趣之一。
继续磨练吧,伙计。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!