BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

了解 Geth 客户端:快照加速机制

本文为 Geth 客户端有问必答系列的第一篇文章,大家可以就 Geth 客户端的问题踊跃提问,我会每周用一篇小文章回答得票最高的问题。本周呼声最高的问题是:你能说说 flat 数据库结构与 legacy 结构的主要区别吗?

以太坊的状态

在深入了解加速结构(acceleration structure)之前,我们先回顾一下以太坊的 “状态” 概念、在涉及到不同层次的抽象时又是如何存储的。

以太坊有两种不同类型的状态:账户的集合;每一合约账户存储槽的集合。从 完全抽象的角度 来看,两种数据都是 键-值 对。账户集合把地址映射到该地址的 nonce、余额,等等。而一个合约的存储领域把任意的值(由该合约定义并使用)映射到某个值。

但糟糕的是,虽然把这些键值对存储成扁平数据(flat data)可以非常高效,但验证它们的正确性在计算上就会变得很难。每当对数据修改时,我们都要自下而上对所有数据做哈希运算。

为免去总是对整个数据库做哈希运算的需要,我们可以把数据库分割成连续的小片,然后建立出一种树状结构!最原始、最有用的数据就放在叶子节点上,然后树上每一个内部节点都是该节点以下内容的哈希值。如此一来,当我们要修改某些值时,就只需做对数次的哈希运算。这种数据结构其实有一个路人皆知的名字,就是 “默克尔树”。

但还没完,这种办法在计算复杂性上还是有所欠缺。默克尔树结构虽然在修改现有数据时非常高效,但是,如果插入数据和删除数据会更改底层小数据块的边界,那就会让所有已经算好的哈希值全都变为无效。

这时候,与其盲目地对数据库分组,我们可以使用键本身来组织数据、基于共同前缀将数据都安排到树状格式中!这样插入和删除操作都不会影响到所有节点,只会影响到从树根到叶子路径上的(对数个)节点。这种数据结构就叫 “帕特里夏树”。

把上面两种办法合在一起 —— 帕特里夏树的树状分层和默克尔树的哈希算法 —— 就是所谓的 “默克尔-帕特里夏树”,也是实践中用于代表以太坊状态的数据结构。无论是修改、插入、删除还是验证,都只有对数复杂度!唯一的小小例外是,有些键会在插入前做哈希运算(存入树中),以平衡整棵树(A tiny extra is that keys are hashed before insertion to balance the tries)。

以太坊的状态存储

上文解释了为什么以太坊要用默克尔帕特里夏树结构来存储其状态。遗憾的是,虽然所需操作的速度都很快,但每一种选择都有所牺牲。更新操作和验证操作的对数复杂性 意味着对 每一个单独的密钥 的读取和存储都是对数复杂的(logarithmic reads and logarithmic storage)。这是因为树状结构的每一个内部节点都要单独保存在硬盘上。

此时此刻,账户树的深度确切是多少我不知道,但在大约一年以前,账户状态就已填满了 7 层高的树。这就意味着,每一次树操作(例如读取余额、写入 nonce)都要触达至少 7~8 个内部节点,因此会做至少 7~8 次持久数据库访问(persistent database accesses)。LevelDB 组织数据时最多也是 7 层,所以还有一个额外的乘数。最终的结果是,单次 状态访问预计会放大为 25~50 次随机的 硬盘访问。你再乘上一个区块中的所有交易的所有状态读取和写入,你会得到一个 吓人 的数字。

[当然,所有客户端实现都在尽力降低开销。Geth 使用更大的内存区域来缓存树节点;还使用了内存内的修剪机制、避免将几个块之后就会删除的数据写入硬盘。不过这需要另外一篇文章才能讲清楚。]

可怕之处还在于,这个数字就是运行一个以太坊节点、保证能全时验证所有状态的成本。

我们能做得更好一点吗?

并不是所有访问都要一视同仁
以太坊的运行依赖于对状态的密码学证明。只要我们还想保持对所有数据的验证能力,就绕不开硬盘读写放大问题。也就是说,我们 —— 可以并且也事实上 —— 相信我们已经验证过的数据。

不断重复验证每一个状态物是没有意义的,但如果每次从硬盘中拉取数据都要验证一次的话,就是在做这样没有意义的事。默克尔帕特里夏树结构本质上是为写入操作设计的,但反过来就成了读取操作的负担。我们摆脱不了它,也无法让它瘦身,但 这绝不意味着 我们在每一个场合都必须使用它。

以太坊节点访问状态的场景可大致分为以下三类:

  • 在导入一个新区块的时候,EVM 代码的执行会产生或多或少基本平衡的状态读取和写入次数。不过,一个用于拒绝服务式攻击的区块可能会产生远多于写入操作的读取操作次数。
  • 当节点运营者检索状态的时候(例如调用 eth_call 及类似操作),EVM 代码执行仅产生读取操作(当然也可能有写入操作,但这些操作产生的数据最终会丢弃掉,不会持久化到硬盘里面)。
  • 当节点在同步区块链的时候,同步者会向远程节点请求状态,被请求者会将数据挖掘出来并通过网络传播给同步者。

基于上述访问模式,如果我们可以短路(short circuit)读取操作而不触及状态树,则许多节点操作都可以变得快 很多。这样甚至能开启一些新奇的访问模式(比如状态迭代),让原来因为太过昂贵而不可行的模式变为可能。

当然,还是不免有所牺牲。没有去掉树结构,任何新的加速结构都会带来额外的开销。问题只在于:额外的开销是否能带来足够多的好处,值得我们一试?

请循其本

我们已经开发出了神奇的默克尔帕特里夏树结构来解决我们所有的问题,现在,我们希望让读取操作能绕过它。那么,我们应该用什么样的加速结构来让读取操作重新变得快起来呢?显然,如果我们不需要树结构,那就大可以把伴随树结构而生的复杂性都丢在一边,我们可以直接回到原始状态。

如同在本文开头说到的那样,理论上的理想状态下 以太坊状态的数据存储方式应是简单键值对,没了默克尔帕特里夏树构成的限制,那就没有什么能阻止我们去实现这种理想方案了!

不久之前,Geth 引入了 snapshot(快照)加速结构(不是默认开启的)。一个快照就是给定一个区块处的以太坊状态的完整视图。抽象掉实现方面的细节,它就是把所有账户和合约存储槽堆放在一起,都由扁平的键值对来表示。

每当我们想要访问某个账户或者某个存储槽的时候,我们只需付出一次 LevelDB 的查询操作即可,而不用在每棵树上查询 7~8 次。理论上来说,更新快照也很简单,处理完一个区块后,我们只需为每个要更新的存储槽多做 1 次额外的 LevelDB 写入操作即可。

快照加速结构实际上将读取操作的计算复杂性从 O(log n) 降到了 O(1) (乘以 LevelDB 的开销),代价是将写入操作的计算复杂性从 O(log n) 变成了 O(1 + log n) (乘以 LevelDB 的开销),并将硬盘存储空间从 O(n log n) 增加到了 O(n + n log n)。

魔鬼藏在细节中

维持以太坊状态快照的可用性也不容易。只要区块还在一个接一个地产生,一个接一个地摞在最后一个区块上,那将最新变更合并到快照中的粗疏办法就能正常工作。但是,哪怕有微小的区块链重组(即便只有一个区块),快照机制就崩溃了,因为根本没有设计撤销操作。对扁平数据表示模式来说,持久化写入是单向的操作。而且让事情变得更糟糕的是,我们没办法访问更老的状态了(例如某些 dApp 需要 3 个区块以前的状态;或者 fast/snap 同步模式中要访问 64 个区块以前的状态)。

为了克服这些限制,Geth 客户端的快照由两部分组成:一部分持久化的硬盘层,是对旧区块(例如顶端区块前 128 个区块)处状态的完整快照;还有一棵内存内 diff 层组成的树,用于收集最新的写入操作。

处理新区块的时候,我们不会直接合并这些写入操作到硬盘层,而仅仅是创建一个新的、包含这些变更的内存内 diff 层。当内存内部的 diff 层积累到足够高的层数时,最底部的一个就开始合并更新并推到硬盘层。当需要读取一个状态物时,我们就从最顶端的 diff 层开始查找,一直往下,直至在 diff 层中或者在硬盘层中找到。

这种数据表示方法非常强大,解决了很多问题。因为内存内部的 diff 层组成了一棵树,所以 128 个区块以内的链重组只需取出属于父块的 diff 层,然后就此开始构建即可。需要较旧状态的 dApp 和远程同步者可以访问到最近 128 个最近的状态。开销变成了 128 次映射查找,但 128 次内存内的查找比起 8 次硬盘读取及 Level DB 的 4~5 倍放大要快上几个数量级。

当然,这里面还有很多很多的坑。就不讲太深了,简单列举就有下面这张清单:

  • Self-destruct (合约自毁操作)(以及删除操作)特别难以对付,因为它们需要短路 diff 层的沉降(descent)。
  • 如果出现了比持久硬盘层更深的链重组,那现在的快照就要完全废弃掉、重新生成。整套操作非常昂贵。
  • 在节点关机时,内存内的 diff 层需要持久化到日志并加载备份,不然重启之后快照就没用了。
  • 使用最底层的 diff 层作为一个累加器,仅在其超过一定的内存使用时才刷新到硬盘。这就允许跨区块对同一存储槽执行去重写入操作(deduping write)。
  • 要为硬盘层分配一个读取缓存,这样合约重复访问同一个古老的存储槽时硬盘才不会损坏。
  • 在内存内 diff 层中使用累积的布隆过滤器(bloom filter),以便快速检测出状态物有没有可能存在于 diff 层中,还是应该直接跳到硬盘中查找。
  • 不把原始数据(账户地址、合约存储键)设为键,而是以这些数据的哈希值为键,以保证快照的迭代顺序与默克尔帕特里夏树相同。
  • 生成持久化硬盘层的时间要比剪除状态树窗口的时间多得多,所以即使是生成器,也需要动态地追踪链的运行。

美丑并存

Geth 的快照加速结构将状态读取的复杂性降低了一个数量级。这就意味着基于读取操作的 DoS 攻击的发动难度上了一个数量级,而 eth_call 调用也快了一个数量级(假设 CPU 不存在瓶颈的话)。

快照还让对最近的块进行极速状态迭代成为可能。实际上这曾是我们开发快照机制的主要理由,因为我们可以此为基础创造新的 snap 同步算法。讲清楚它需要一篇全新的文章,但最近我们在 Rinkeby 测试网上的基准测试很能说明问题:

当然,这一切同样不是没有代价的。当初始同步完成之后,参与主网的节点需要 9~10 小时来建构初始快照(此后再维持其可用性),还需要额外的 15 GB 以上的硬盘。

那糟糕的部分是哪里呢?我们花了 6 个月时间才积累起足够的自信、发布了快照机制,而且现在它仍然不是默认功能,需要主动使用 --snapshot 标记来开启,而且还有一些围绕内存使用和崩溃恢复的打磨工作要做。

总而言之,对于这一提升,我们非常自豪。其中有巨大的工作量,而且是在黑暗中摸索、自己实现所有东西并祈祷它能工作。还有一个有趣的事情,第一个版本的快照同步(leaf sync)是在两年半以前写的,但一直都处于被阻塞的状态,因为我们缺乏必要的加速结构来驱动它。

结语

希望你能喜欢 Geth 客户端有问必答 的这一篇文章。我花了比自己所预想的多出一倍的时间,但我并不后悔,因为这个主题值得。下周见。

[又:我故意不在文章里留下 提问/投票 的网站,因为我确信这个活动只是暂时的,我不想留下一个没用的超链接,也不希望有人会在未来买下那个域名并托管恶意信息。你可以在我的 Twitter 中找到那个网站。]

(完)

原文链接: https://blog.ethereum.org/2020/07/17/ask-about-geth-snapshot-acceleration/
作者: Péter Szilágyi
翻译: 阿剑

本文由原作者授权 EthFans 翻译及再出版。

转载自:https://ethfans.org/posts/ask-about-geth-snapshot-acceleration

以太坊状态树架构解释

解释以太坊状态试图加深对以太坊区块链的了解。

介绍

这篇文章解释了以太坊状态树。复仇通常被称为“世态町ñ e”和使用原始数据存储到记录状态(账户)和交易。由于 state trie 是 Ethereum 的核心数据库,因此了解它以加深您对 Ethereum 的了解非常重要。我构建的内容是为了让你在逻辑上一步一步地理解。当我学习它时,很难深入理解,因为状态树有多种类型,并且每个状态树都彼此密切相关。我希望这篇文章可以帮助您轻松深入地了解 state try。本文按顺序涵盖以下主题。

  • Merkle Patricia Trie
  • World State Trie
  • Transaction Trie
  • Receipt Trie
  • Account Storage Trie

Merkle Patricia Trie(基数树/帕特里夏树/前缀树)

Trie,也称为Radix Trie、Patricia Trie或Prefix Tree,是一种查找公共前缀最快、实现简单、占用内存小的数据结构。由于以太坊使用 Merkle Tree 将哈希高效地存储在块中,因此使用 Trie 作为数据存储的核心数据结构。以太坊使用“Modified Merkel Patricia Trie”,它是由 Merkle Tree、Patricia Tree(Trie) 和一些改进发明的。修改后的 Merkle Patricia Trie 作为以太坊尝试接收树、世界状态树、账户存储树和交易树中的主要数据结构。

上图显示了 Merkel Patricia Trie 的结构。它主要由三种类型的节点组成:扩展节点、分支节点和叶节点。每个节点由其内容的 sha3 散列值决定,并将散列用作键。Go-ethereum 使用 levelDB,parity 使用 RocksDB 存储状态。如果您想了解更深入的内容,请参阅“修改后的 Merkle Patricia Trie — 以太坊如何保存状态”。

状态树结构

在开始解释每个状态树之前,让我解释一下以太坊状态树的整个架构。如前所述,状态树有四种类型:世界状态树、交易树、交易收据树和账户存储树。每个状态树都是用 Merkle Patricia Trie 构建的,只有根节点(状态树的顶部节点)存储在块中以备用存储。您可以在下图中看到整个架构。

如您所见,三个主要的状态尝试:世界状态树、交易树和接收树被存储在块中。并且,账户存储树(account storage contents trie)在世界状态树中构造叶节点。

世界状态树(State Trie,全局状态树)

世界状态树是地址和帐户状态之间的映射。它可以被视为一个全局状态,通过事务执行不断更新。以太坊网络是一个分散的计算机,状态树被认为是硬盘驱动器。所有关于账户的信息都存储在世界状态树中,您可以通过查询来检索信息。世界状态树与账户存储树关系密切,因为它有“storageRoot”字段,指向账户存储树中的根节点。

帐户存储树

帐户存储树是存储与帐户关联的数据的地方。这仅与合约账户相关,所有智能合约数据都作为 32 字节整数之间的映射保存在账户存储树中。
并且,帐户状态存储有关帐户的信息,例如帐户有多少以及从帐户发送了多少交易。它有四个字段:nonce、balance、storageRoot 和 codeHash。它是世界状态树中的叶节点。

事务树

交易树记录以太坊中的交易。交易在改变状态方面起着核心作用,因为以太坊是基于交易的“状态”机器。一旦交易记录在一个区块中,就不能永久更改以证明账户余额(世界状态)。由于 Transaction Trie 是使用 Modified Merkel Patricia Trie 构建的,因此唯一的根节点存储在块中。下面的灰色框描述了交易数据字段。如果您想了解更多详细信息,请参阅以太坊交易结构说明。

nonce:交易 nonce 是从给定地址发送的交易序列号。
Gas Price:您愿意支付的价格
Gas Limit:Gas Limit 是发送方愿意为交易支付的 ETH 数量的限制
Recipient:收件人是以太坊地址的目的地。
Value:值字段表示从发送者到接收者的以太币/wei 的数量。
Data:数据字段用于合同相关活动,例如合同的部署或执行。
v,r,s:该字段是原始 EOA 的 ECDSA 数字签名的组成部分。

交易收据树(Receipt Trie)

交易收据 Trie 记录交易的收据(结果)。收据是交易成功执行的结果。收据包括交易的哈希值、区块号、使用的gas 数量和合约地址等。这是交易收据的字段。

blockHash: String, 32 Bytes - 此交易所在区块的哈希值。
blockNumber: Number - 此交易所在的区块号。
transactionHash: String, 32 Bytes - 交易的哈希值。
transactionIndex: Number - 区块中交易索引位置的整数。
from: String, 20 Bytes - 发件人的地址。
to: String, 20 Bytes - 接收者的地址。如果是合约创建交易,则为 null。
cumulativeGasUsed: Number - 在区块中执行此交易时使用的总燃料量。
gasUsed: Number - 仅此特定交易使用的 gas 量。
contractAddress: String - 20 Bytes - 创建的合约地址,如果交易是合约创建,否则为 null。
logs:数组 - 此事务生成的日志对象数组。
status : String - '0x0' 表示交易失败,'0x1' 表示交易成功。

引用:https : //ethereum.stackexchange.com/questions/6531/structure-of-a-transaction-receipt

结论

文章解释了以太坊的主要状态尝试:Merkle Patricia Trie、世界状态Trie、交易Trie、收据Trie和账户存储Trie。由于以太坊是一个世界的“状态机”,它具有原始的机制来记录和管理状态与特里数据结构。世界状态树存储帐户状态,表示帐户有多少钱。交易树记录可以更新世界状态树的交易,并且不可变地存储在区块链中以证明活动历史。Receipt trie 代表交易的结果,可以对外查询。我希望这篇文章有助于加深您对以太坊的了解。

参考

深入了解以太坊的世界状态
以太坊解释:默克尔树、世界状态、交易等
了解以太坊中的 Trie 数据库
Transaction Trie 和 Receepts Trie 之间的关系
以太坊区块架构
以太坊中的数据结构| 第 1 集:递归长度前缀 (RLP) 编码/解码。
Modified Merkle Patricia Trie——以太坊如何拯救一个状态

原文

https://medium.com/@eiki1212/ethereum-state-trie-architecture-explained-a30237009d4e

Ethereum 中keccak和sha3的区别

keccak应用

在以太坊中,用keccak哈希算法来计算公钥的256位哈希,再截取这256位哈希的后160位哈希作为地址值。

keccak和sha3的区别

sha3由keccak标准化而来,在很多场合下Keccak和SHA3是同义词,但在2015年8月SHA3最终完成标准化时,NIST调整了填充算法:SHA3-256(M) = KECCAK [512] (M || 01, 256)。所以标准的NIST-SHA3就和keccak计算的结果不一样。
以太坊在开发的时候sha3还在标准化中,所以采用了keccak,所以Ethereum和Solidity智能合约代码中的SHA3是指Keccak256,而不是标准的NIST-SHA3,为了避免混淆,直接在合约代码中写成Keccak256是最清晰的

为何推出sha3

推出sha3不是因为sha2出现了漏洞,只是当时学术界对于sha1被成功碰撞的担忧,但目前基于NIST的建议,sha2和sha3都是属于可以安全商用的哈希算法,sha3相当于多了一种安全选择,比特币选用的就是sha2(SHA256)。

参考

https://ethereum.stackexchange.com/questions/550/which-cryptographic-hash-function-does-ethereum-use
https://www.cnblogs.com/HachikoT/p/12792362.html

以太坊核心存储结构分析

以太坊核心存储结构:Merkle-Patricia-Tree(前缀树与默克尔树的结合体),以太坊中的交易树、交易收据树、账户树以及合约存储树均使用该树索引。

该树分为三种类型节点:branch(分支,17个元素的元组)、extension(扩展,2个元素的元组)、leaf(叶子节点,2个元素的元组),因此为了区分 extension 与 leaf 节点,使用 key 的第一个 16 进制字符,其中 0000 与 0001 均代表扩展节点,0010 与 0011 均代表叶子节点,也就是说使用倒数第二位来区分 extension 与 leaf 节点。最后一位的 0、1 分别表示了该 key 原先为偶数个 16 进制字符与奇数个 16 进制字符,也就意味着为 0 时,需要填充另外的0000。

账户在以太坊中的存储

下面来看一下以太坊底层存储中是如何实现账户存储的:

目前以太坊中存在两个账户: 0xa3ac96fbe4b0dce5f6f89a715ca00934d68f6c37 0x0f5578914288da3b9a3f43ba41a2e6b4d3dd587a

通过使用编写的拉取以太坊底层存储的 python 脚本,拉取目前底层存储的账户数据(脚本的具体用法有所改变,详见这里):

其中前一项是以太坊在底层中真正存储的 key,与账户地址的对应关系如下:

也就是说,以太坊存储账户数据的时候,会将账户地址进行一次keccak256 哈希计算

此时,账户树的形状如下:

以太坊中会对节点使用 RLP 编码来存储在底层数据 leveldb 或 rocksdb 中,存储形式为 。所以在上图的槽中,slot 1 实际存储的是 sha3(rlp(leaf1))。

合约在以太坊中的存储

下面来看一下智能合约中的数据是如何存储在以太坊中的: 智能合约中的每一个状态变量都有一个 position,以太坊中每一个 storage slot 为 32 个字节,即 256 位,solidity 编译器会尽量将变量装到同一个 storage slot 中去,对于装不下的,会重新分配 storage slot,遇到 mapping、struct 等类型的变量时,编译器会自动重新分配 storage slot。

该代码存在于账户下,该合约的地址为 0xfe5eeb229738ab87753623a81a42656bcde30a67,contract address = sha3(rlp.encode([creator address, nonce]))

该账户在底层数据库中存储的 key 为

// geth console 环境中
> web3.sha3("0xfe5eeb229738ab87753623a81a42656bcde30a67", {encoding : "hex"})
0x886f7bfb7a4887d716ec4fbb06a8bf35fc1972d2962590248ffe6271e77ac7c1

// python
In [50]: '\xed\xa8}\x9d\xeb\xa5\xbb\xc6O\xa7\'B\xf5\x84"\xaa\xf4f\x9e\xaai)\xe2\xf2_\xa60D\x8a\x0c\x7fJ'.encode("hex")
Out[50]: 'eda87d9deba5bbc64fa72742f58422aaf4669eaa6929e2f25fa630448a0c7f4a'


我们来一一分析:

我们可以看到在相应的位置上分别存储了相应的值, 对于 mapping 来说,其中元素存储的position如下:

sha3(LeftPad32(key, 0), LeftPad32(map position, 0))

所以有:

由于 mapping 中 value 为一个 struct,size 大于 256 位,因此,在存储的位置按顺位加 1,如上图所示。 来看一下,动态数组在以太坊底层存储形式:

其中,在位置 5 存储动态数组的长度,然后以位置 sha3(5) 开始顺序存放数组元素。

--- update ---

mapping中的 key 也可以是 string 形式,假设 mapping(string => string),key = "11", value = "22", 则在 eth 底层中存储的 key 为:

// 3131 代表字符串 "11",后面的 32 个 0 代表 map 的 position
> web3.sha3("31310000000000000000000000000000000000000000000000000000000000000000", {encoding : "hex"})
"0x756ab6158180196289fbd030ff61972bf49c0e51dbf603d0dfaf6b1d3f0e49a6"

// 0x3232...04 是 bytes(string) 在底层的存储表达形式,后面的 04 代表字符串的长度为 2, 前面的 3232 代表真正存储的字符串 "22"
> eth.getStorageAt("0xf8a7e4fb488d5e0426012592c5d66e44dffa6cb7", "0x756ab6158180196289fbd030ff61972bf49c0e51dbf603d0dfaf6b1d3f0e49a6")
"0x3232000000000000000000000000000000000000000000000000000000000004"
key = "1111111111111111111111111111111111", len(key) = 34; value = "2222222222222222222222222222222", len(value) = 31
> web3.sha3("313131313131313131313131313131313131313131313131313131313131313131310000000000000000000000000000000000000000000000000000000000000000", {encoding : "hex"})
"0xeb5b36d98f0c746023b3b0e91319a7ee8cb743f75df9f8ce513c5120487cdac3"

// 最后的 3e 代表 31 个字节长
> eth.getStorageAt("0xf8a7e4fb488d5e0426012592c5d66e44dffa6cb7", "0xeb5b36d98f0c746023b3b0e91319a7ee8cb743f75df9f8ce513c5120487cdac3")
"0x323232323232323232323232323232323232323232323232323232323232323e"
key = "1111111111111111111111111111111111", len(key) = 34; value = "22222222222222222222222222222222", len(value) = 32
// 41 代表字符串长度,为了与小于32个字节的长度区分,这里加了 1,所以算长度时:(0x41-1)/2 = 32 个字节
> eth.getStorageAt("0xf8a7e4fb488d5e0426012592c5d66e44dffa6cb7", "0xeb5b36d98f0c746023b3b0e91319a7ee8cb743f75df9f8ce513c5120487cdac3")
"0x0000000000000000000000000000000000000000000000000000000000000041"

// 对该 value 对应的 key 再次进行哈希,用于存放真正字符串
> web3.sha3("0xeb5b36d98f0c746023b3b0e91319a7ee8cb743f75df9f8ce513c5120487cdac3", {encoding : "hex"})
"0x2b3b0a6d0771d1a8fa6f89276ead655b7a0684e2a22d9290bcf4f8944f05b504"

> eth.getStorageAt("0xf8a7e4fb488d5e0426012592c5d66e44dffa6cb7", "0x2b3b0a6d0771d1a8fa6f89276ead655b7a0684e2a22d9290bcf4f8944f05b504")
"0x3232323232323232323232323232323232323232323232323232323232323232"

转载自:https://ethereum.iethpay.com/ethereum-core-storage.html