彻底理解solidity里的storage

2022-11-07 09:53:10 浏览数 (1)

本文作者:shenstone.eth[1]

Ethereum Architecture(以太坊架构)

我们将从下面的图片开始。不要被吓倒,在本文结束时,你会明白这一切到底是如何结合在一起的。这代表了以太坊的架构和以太坊链中包含的数据。

与其将图表作为一个整体来看,我们不如逐块分析。现在,让我们把重点放在 "区块头 N"和它包含的字段上。


Block Header(区块头)

区块头包含了一个以太坊区块的关键信息。下面是 "区块头 N "片段,以及它的数据字段。看一下 etherscan 上的这个区块14698834[2],看看你是否能找到图中的这些成员。

该区块头包含以下成员。

  • Prev Hash - 父区块的 Keccak 哈希值
  • Nonce - 用于工作证明的计算
  • Timestamp - 时间戳
  • Uncles Hash - 叔块的 Keccak 哈希值
  • Benficiary - 受益人地址,矿工地址
  • Logs Bloom - 事件地址和事件 topic 的布隆滤波器
  • Difficult - 前一个区块的难度
  • Extra Data - 与该区块相关的 32 字节数据
  • Block Num - 祖先块数
  • Gas Limit -当前每个区块的 gas 使用限制值
  • Gas Used - 该区块中用于交易的 gas 消耗值
  • Mix Hash - 与 nonce 一起用来证明工作证明计算的 256 位值
  • State Root - 状态树的根哈希值
  • Transaction Root - 交易树的根哈希值
  • Receipt Root - 收据树的根哈希值

让我们看看这些成员如何与 Geth 客户端代码库中的内容相对应。我们先看block.go[3]中定义的 "Header "结构,它表示一个块的头。

我们可以看到,代码库中所述的值与我们的概念图相匹配。我们的目标是要如何从区块头找到我们合约的 storage 存储的位置。

要做到这一点,我们需要关注块头的 "State Root"字段,该字段以红色标示。


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]

接下来让我们细究一下以太坊地址所映射到的以太坊账户值。


Ethereum Account

以太坊账户是以太坊地址的共识代表,它由 4 部分构成

  • Nonce - 该账号的交易数量
  • Banlance - 账户余额(以 wei 为单位)
  • Code Hash - 合约字节码的 hash 值
  • Storage Root - storage trie 的根节点的 hash 值

从以太坊架构图的部分片段里可以看到这些内容

我们再看 Geth 的代码,找到相关的代码'state_account.go[6]',之前提及的以太坊账户结构被定义为‘StateAccount’。

我们可以看到代码里的结构成员一一对应我们的概念图。

接下来,我们需要深入学习以太坊账户里的"Storage Root"字段。


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,下面的内容涉及代码比较多,假定读者有基础的代码阅读理解能力


StateDB -> stateObject -> StateAccount

为了开始之后的内容,我们需要一个全新的合约。一个全新的合约意为着一个全新的状态账户(StateAccount)。

我们先介绍三个结构:

  • StateAccount:状态账户是以太坊账户的'consensus representation'。
  • stateObject:stateObject 代表一个正在被修改的 "Ethereum 账户"。
  • StateDB:以太坊协议内的 StateDB 结构是用来存储 Merkle trie 内的任何东西。它是检索合约和以太坊账户的查询接口。

让我们看看这 3 个概念是如何相互关联的,以及它们与我们一直在讨论的内容有什么关系。

  1. StateDB 结构[7],我们可以看到它有一个 stateObjects 字段,是地址到 stateObjects 的映射表(记得 "State Root"Merkle Patricia Trie 是以太坊地址到以太坊账户的映射,stateObject 是一个正在被修改的以太坊账户。)
  2. stateObject 结构[8],我们可以看到它有一个数据字段,属于 StateAccount 类型(记得在文章的前面,我们将 Ethereum 账户映射到 Geth 中的 StateAccount)。
  3. StateAccount 结构[9],我们已经学习了这个结构,它代表一个以太坊账户,Root 字段代表我们之前讨论的 "Storage Root"。在这个阶段,一些拼图的碎片开始拼凑起来。现在我们有了背景,可以看到一个新的 "以太坊账户"(StateAccount)是如何初始化的。

初始化一个新的以太坊账户

为了创建一个新的 StateAccount,我们需要与statedb.go[10]代码和 StateDB 结构交互。

StateDB 有一个 createObject 函数,可以创建一个新的 stateObject,并将一个空的 StateAccount 传给它。这实际上是创建一个空的"以太坊账户"。

下图详细说明了代码流程。

  1. StateDB 有一个createObject 函数[11],它接收一个 Ethereum 地址并返回一个 stateObject(记住一个 stateObject 代表一个正在修改的 Ethereum 账户。)
  2. createObject 函数调用newObject 函数[12],输入 stateDB、地址和一个空的 StateAccount(记住一个 StateAccount=以太坊账户),返回一个 stateObject。
  3. 在 newObject 函数的返回语句中,我们可以看到有许多与 stateObject 相关的字段,地址、数据、dirtyStorage 等。
  4. stateObject 的 data 字段映射到函数中的空 StateAccount 输入--注意在第 103-111 行 StateAccount 中的 nil 值被赋值。
  5. 创建的 stateObject 包含初始化的 StateAccount 作为数据字段被返回。

好了,我们有一个空的 stateAccount,接下来我们要做什么?

我们想存储一些数据,为此我们需要使用 SSTORE 操作码。


SSTORE

在我们深入了解 Geth 中的 SSTORE 实现之前,让我们快速回忆 SSTORE 的作用。

它从堆栈中弹出两个值,首先是 32 字节的 key,其次是 32 字节的 value,并将该值存储在由 key 定义的指定存储槽中。下面是 SSTORE 操作码的 Geth 代码流程,让我们看看它的作用。

  1. 我们从定义了所有 EVM 操作码的instruction.go 文件[13]开始。在这个文件中,我们找到了 "opSstore "函数。
  2. 传入该函数的范围变量包含合同上下文,如堆栈、内存等。我们从堆栈中弹出 2 个值,并标记为 loc(位置的缩写)和 val(值的缩写)。
  3. 然后,从堆栈中弹出的 2 个值以及合约地址一起被用作 StateDB 对象的SetState 函数[14]的输入。SetState 函数先用合约地址来检查该合约是否存在一个 stateObject,如果不存在,它将创建一个。然后,它在该 stateObject 上调用 SetState,传入 StateDB db、相应的 key 和 value 值。
  4. stateObject SetState 函数[15]对'fake storage'做了一些空值检查,然后检查 value 是否有变化,如果有变化,则通过 journal 结构记录变化。
  5. 如果你看一下关于journal 结构[16]的代码注释,你会发现 journal 是用来跟踪状态修改的,以便在出现执行异常或请求撤销的情况下可以恢复这些修改。
  6. 在 journal 结构被更新后,storageObject 的setState 函数[17]被调用,入参为 key 和 value。这将更新 storageObjects 的 dirtyStorage。好了,我们已经用 key 和 value 更新了 stateObject 的 dirtyStorage。这实际上意味着什么,它与我们到目前为止所学的一切有什么关系?

让我们从代码中的 dirtyStorage 定义继续学习。

  1. dirtyStorage 被定义在stateObject 结构[18]中,它属于 Storage 类型,被描述为 "在当前交易执行中被修改的存储条目"。
  2. 与 dirtyStorage 相对应的类型 Storage[19]是 common.Hash 到 common.Hash 的简单映射。
  3. 类型 Hash[20]只是一个长度为 HashLength 的数组。
  4. HashLength[21]是一个常数,定义为 32 这对你来说应该很熟悉,一个 32 字节的 key 映射到一个 32 字节的 value。这正是我们在 EVM 深度探讨的第三部分中从概念上看待合约 storage 存储空间的方式。

你可能已经注意到 stateObject 中的 pendingStorage 和 originStorage 就在 dirtyStorage 字段的上方。它们都是相关的,在最终确定过程中,dirtyStorage 被复制到 pendingStorage,而 pendingStorage 在 trie 被更新时又被复制到 originStorage。

在 trie 被更新后,StateAccount 的 "存储根 "也将在 StateDB 的 "提交 "中被更新。这将把新的状态写入底层的内存 trie 数据库中。

现在到了拼图的最后一块,SLOAD。


SLOAD

让我们再次快速回忆,SLOAD 操作码做什么。

它从堆栈中弹出 1 个值,32 字节的 key,它代表存储槽,并返回存储在那里的 32 字节的 value。

下面是 SLOAD 操作码的 Geth 代码流程,让我们看一下它的作用

  1. 我们再次从 instructions.go 文件[22]开始,在那里我们可以找到 "opSload "函数。我们使用 peek 从堆栈的顶部抓取 SLOAD 的位置(存储槽)。
  2. 我们调用 StateDB 上的GetState 函数[23],输入合约地址和 slot 位置。GetState 函数返回与该合约地址相关的 stateObject。如果返回的 stateObject 不是空值,则调用该 stateObject 上的 GetState 函数。
  3. 在 stateObject 上的GetState 函数[24]对 fakeStorage 进行了检查,然后对 dirtyStorage 进行检查。
  4. 如果 dirtyStorage 存在,返回 dirtyStorage 映射表中位置 key 相对应的值。(dirtyStorage 代表了合约的最新状态,这就是为什么我们试图首先返回它)
  5. 否则就调用GetCommitedState 函数[25],尝试在 storage trie 中查找该值。同样需要先检查 fakeStorage。
  6. 如果 pendingStorage 存在,返回 pendingStorage 映射表中位置 key 相对应的值。
  7. 如果上述方法都没有返回,就去找 originStorage,从那里检索并返回值。你会注意到,该函数试图先返回 dirtyStorage,然后是 pendingStorage,最后是 originStorage。这是有道理的,在执行过程中,dirtyStorage 是最新的存储映射,其次是 pending,然后是 originStorage。

一个交易可以多次操作一个存储槽,所以我们必须确保我们有最新的值。

让我们想象一下,在同一交易中,在同一存储槽的 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 Merkle root is a,whole, undamaged, and unaltered.

[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

0 人点赞