本文作者:shenstone.eth[1]
我们将从下面的图片开始。不要被吓倒,在本文结束时,你会明白这一切到底是如何结合在一起的。这代表了以太坊的架构和以太坊链中包含的数据。
与其将图表作为一个整体来看,我们不如逐块分析。现在,让我们把重点放在 "区块头 N"和它包含的字段上。
区块头包含了一个以太坊区块的关键信息。下面是 "区块头 N "片段,以及它的数据字段。看一下 etherscan 上的这个区块14698834[2],看看你是否能找到图中的这些成员。
该区块头包含以下成员。
让我们看看这些成员如何与 Geth 客户端代码库中的内容相对应。我们先看block.go[3]中定义的 "Header "结构,它表示一个块的头。
我们可以看到,代码库中所述的值与我们的概念图相匹配。我们的目标是要如何从区块头找到我们合约的 storage 存储的位置。
要做到这一点,我们需要关注块头的 "State Root"字段,该字段以红色标示。
"State Root"的作用类似于merkle root[4],因为它是一个依赖于它中间所有数据的哈希值。如果任何数据发生变化,根哈希值也会发生变化。
在 "State Root"下面的数据结构是一个 Merkle Patric Trie(MPT),它为网络上的每个以太坊账户存储一个键值对,其中 key 是一个以太坊地址,value 是以太坊账户对象。
实际上,key 是以太坊地址的哈希值,value 是 RLP 编码的以太坊账户,但是我们现在可以忽略这一点。
下面是 "以太坊架构 "图的一部分,表示 State Root 下的 MPT。
Merkle Patricia Trie 是一个非三态的数据结构,所以我们不会在这篇文章中深入研究它。我们可以继续抽象化地址到以太坊账户的键值映射模型。
如果你对 Merkle Patricia Trie 感兴趣,我建议你看看这篇优秀的介绍文章[5]。
接下来让我们细究一下以太坊地址所映射到的以太坊账户值。
以太坊账户是以太坊地址的共识代表,它由 4 部分构成
从以太坊架构图的部分片段里可以看到这些内容
我们再看 Geth 的代码,找到相关的代码'state_account.go[6]',之前提及的以太坊账户结构被定义为‘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[10]代码和 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 客户端层面实现的有了了解。它们如何与状态和存储对象互动,以及更新存储槽与更广泛的以太坊 "世界状态 "的关系。
这很难,但你做到了。我猜这篇文章给你留下了比你开始之前更多的问题,但这也是加密货币的乐趣之一。
继续磨练吧,伙计。
原文:https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy-5a5?s=r
[1]
shenstone.eth: https://learnblockchain.cn/people/2033
[2]
14698834: https://etherscan.io/block/14698834
[3]
block.go: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/types/block.go#L70
[4]
merkle root: https://www.investopedia.com/terms/m/merkle-root-cryptocurrency.asp#:~:text=A%20Merkle%20root%20is%20a,whole%2C%20undamaged%2C%20and%20unaltered.
[5]
文章: https://medium.com/shyft-network-media/understanding-trie-databases-in-ethereum-9f03d2c3325d
[6]
state_account.go: https://github.com/ethereum/go-ethereum/blob/b1e72f7ea998ad662166bcf23705ca59cf81e925/core/types/state_account.go#L27
[7]
StateDB结构: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/statedb.go#L64
[8]
stateObject结构: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L66
[9]
StateAccount结构: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/types/state_account.go#L29
[10]
statedb.go: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/statedb.go
[11]
createObject函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/statedb.go#L575
[12]
newObject函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L102
[13]
instruction.go文件: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/vm/instructions.go#L524
[14]
SetState函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/statedb.go#L414
[15]
SetState函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L245
[16]
journal结构: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/journal.go#L38
[17]
setState函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L283
[18]
stateObject结构: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L66
[19]
类型Storage: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L41
[20]
类型Hash: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/common/types.go#L49
[21]
HashLength: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/common/types.go#L36
[22]
instructions.go 文件: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/vm/instructions.go#L516
[23]
GetState函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/statedb.go#L308
[24]
GetState函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L172
[25]
GetCommitedState函数: https://github.com/ethereum/go-ethereum/blob/d4d288e3f1cebb183fce9137829a76ddf7c6d12a/core/state/state_object.go#L187