Substrate Developer Hub

Substrate Developer Hub

  • 教程
  • 知识库
  • 进阶菜谱
  • API 文档
  • Languages icon简体中文
    • English
    • 协助翻译

›进阶

开始

  • 总览
  • 安装
  • 在 Windows 系统开始
  • 词汇表

学习 Substrate

  • Extrinsics
  • 交易池
  • 账户摘要
  • 会话密钥
  • 交易权重
  • 链下功能

Runtime

  • Runtime 总览
  • Runtime 的基本类型
  • FRAME
  • Pallets
  • Runtime宏
  • Runtime 元数据
  • Runtime 存储
  • Runtime 来源
  • Runtime 执行流程
  • Runtime事件
  • Runtime 错误
  • 交易费用
  • 链下工作机
  • 调试
  • Runtime 测试
  • 链上随机生成
  • Runtime 升级

智能合约

  • 总览
  • ink! 智能合约
  • ink! 概念
  • ink! 开发
  • EVM 模块
  • ink! 常问问题

整合

  • Polkadot-JS
  • 客户端库
  • 链规范
  • Subkey 工具
  • 内存分析

进阶

  • SCALE 编解码器
  • 共识机制
  • 区块导入过程
  • 执行器
  • 密码学
  • 存储
  • SS58 地址格式

贡献

  • 协助翻译
Translate

存储

Substrate 使用简单的键值存储数据,该存储的实现背后是经修改过,有数据库支撑的 Merkle 树。 所有 更高层的存储抽象 都是建立在这个简单的键值存储之上。

键值数据库

Substrate 用 RocksDB 实现了它的存储数据库,RocksDB 是一个可快速并作持久储存的键值存储。 它还支持一个试验中的 Parity DB。

该数据库用于 Substrate 所有需要持久化存储的组件,例如:

  • Substrate 客户端
  • Substrate 轻客户端
  • 链下工作机

Trie 抽象

使用简单的键值存储的一个好处是,你能够很容易地在它上面抽象出存储结构。

Substrate 使用来自 paritytech/trie 的 Base-16 修改后的 Merkle Patricia 树 ("trie"),来提供一个 trie 结构。其内容可以修改并且其根哈希可以有效地重新计算。

Tries 结构允许高效存储和历史区块的状态共享。 Trie 根是 trie 内数据的表征,也就是说两个具有不同数据的 tire 一定会具有不同的根。 因此,两个区块链节点可以通过比较它们的 trie 根来轻松验证它们是否具有相同的状态。

访问 trie 数据的费用非常高。 每次读取操作都需要 O(log N) 时间,其中 N 是存储在 trie 中的数据的数量。 为了减轻查询费用,我们使用了键值缓存。

所有 trie 节点都存储在数据库中,部分 trie 状态可以被删剪掉,即当键值对超出非存档节点的删剪范围时,键值对会从存储空间中被删除掉。 出于性能原因,我们没用 引用计数法。

状态树

Substrate 链都有一个主状态树,叫 State trie,而它的根哈希存储于每个区块头中。 这用于可容易验证区块链的状态,并为轻客户端可作验证证据打下基础。

这个 trie 只存储主链 (canonical chain) 中的内容,而不存储分叉链 (forks) 的。 There is a separate state_db layer that maintains the trie state with references counted in memory for all that is non-canonical.

子状态树

Substrate 还提供了一个 API 来生成一个有着根哈希的新子状态树 (Child Trie),而这根哈希可供 runtime 使用。

子状态树与主状态树基本相同, 唯一区别是子状态树的根是作为节点在主状态树中储存并更新的,而不是作为区块头。 由于他们的头是主状态树的一部分,因此要验证包含子状态树的完整状态仍然是很容易的。

当你希望拥有独立的状态树和根哈希、并能对该树中的特定内容作验证时,子状态树是很有用的。 因为一个状态树的子部分并没有根哈希,具有这方面的要求就需要用到子状态树来满足。

查询存储

Substrate 区块链对外开放了远端过程调用 (RPC) 服务,该服务可用于查询 runtime 的存储。 当您使用 Substrate RPC 访问一个存储项时,您只需要提供与该存储项关联的 键。 Substrate 的 Runtime 存储 APIs 开放了数种存储项类型;请继续阅读来了解如何为不同类型的存储项计算其存储键。

存储键

要计算一个简单的 存储值 的键,取包含存储值的模块名称的 TwoX 128 hash 哈希值,并在后面附上存储值名称本身的 TwoX 128 哈希值。 For example, the Sudo pallet exposes a Storage Value item named Key:

twox_128("Sudo")                   = "0x5c0d1176a568c1f92944340dbfed9e9c"
twox_128("Key")                    = "0x530ebca703c85910e7164cb7d1c9e47b"
twox_128("Sudo") + twox_128("Key") = "0x5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b"

如果 Alice 账号是 sudo 用户,读取 Sudo 模块的 Key 存储值的 RPC 请求和响应可表示为:

state_getStorage("0x5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b") = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"

在这种情况下,返回的值 ("0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") 是Alice的 SCALE -编码账户 ID (5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY)。

您可能已经注意到,非加密的 的 TwoX 128 哈希算法被用来生成这个存储值的键。 是因为在这里我们不需要付出加密哈希函数带来的性能成本,因为这哈希函数的输入 (即模块和存储项的名称) 是由 Runtime 开发人员决定,而不是潜在恶意用户来决定的。

存储映射键

和存储值一样,存储映射 的键是由包含该映射模块的名称的 TwoX 128 哈希值,再加上存储映射本身名称的 TwoX 128 哈希值。 要从映射中检索一个值,只需将映射在存储的键,再加上该映射里的键的哈希值即可。 对于有两个键的映射 (双映射存储),将第一个映射键的哈希值和第二个映射键的哈希值附加到双映射存储的键上。 像存储值一样,Substrate 使用 TwoX 128 哈希算法来处理模块和存储映射的名称。但是当要确定映射中值的哈希键时,需要确保使用正确的 哈希算法 (即在 decl_storage 宏 中声明的算法)。

下面是一个例子,演示从名为 "Balances " 模块中查询名为 FreeBalance 的存储映射,来查询 Alice 的账户余额。 这个例子中, FreeBalance 映射使用的是 透明的 Blake2 128 Concat 哈希算法:

twox_128("Balances")                                             = "0xc2261276cc9d1f8598ea4b6a74b15c2f"
twox_128("FreeBalance")                                          = "0x6482b9ade7bc6657aaca787ba1add3b4"
scale_encode("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"

blake2_128_concat("0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") = "0xde1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"

state_getStorage("0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") = "0x0000a0dec5adc9353600000000000000"

存储查询返回的值 (上面例子中的 "0x0000a0dec5adc9353600000000000000") 是 Alice 账户余额的 SCALE-编码值 (本例中的 "1000000000000000000000")。 请注意,在对 Alice 的账户 ID 进行哈希之前,它必须是 SCALE 编码的。 也请注意,blake2_128_concat 函数的输出由 32 个十六进制字符组成,后面跟着函数的输入参数。 这是因为Blake2 128 Concat 是一种 透明的哈希算法。 尽管上面的例子可能会让这个特性显得多余,但当我们需要遍历一个映射中的所有键时 (而不只是检索与单个键相关的值),其作用就变得明显。 能遍历一个映射中的所有键,是一个让 开发者 接受去用它,而实现 UI 时又常常要用到的基本需求:首先,用户会看到映射中的所有键,然后用户可选择他们感兴趣的键,并同时在映射中查询该键的更多细节。 下面是另一个例子,使用同样的存储映射(一个名为 FreeBalances 的映射,在 "Balances" 模块中使用 Blake2 128 Concat 哈希算法),该例将演示使用 Substrate RPC 通过 state_getKeys 函数查询某个存储映射所有的键。

twox_128("Balances")                                      = "0xc2261276cc9d1f8598ea4b6a74b15c2f"
twox_128("FreeBalance")                                   = "0x6482b9ade7bc6657aaca787ba1add3b4"

state_getKeys("0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4") = [
    "0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
    "0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b432a5935f6edc617ae178fef9eb1e211fbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f",
    ...
]

Substrate RPC 的 state_getKeys 函数返回的序列中的每个值,都可作为参数直接传入 RPC state_getStorage 函数来使用。 事实上,上面例子列表中的第一个值,等于再前一个例子中 state_getStorage 查询所使用的输入 (用于查找 Alice 余额的那个)。 因为这些键所属的映射使用了透明的哈希算法生成其键,所以可确定序列中与第二个值相关联的账户。 请注意序列中的每个值都是以相同的 64 个字符的十六进制值开头;这是因为每个值都代表了同一个映射中的一个键,而这个映射是通过连接两个 TwoX 128 哈希值来确定的,每个哈希值都是 128 位或 32 位的十六进制字符。 丢弃序列中第二个值的该部分内容后,你会得到 0x32a5935f6edc617ae178fef9eb1e211fbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f。

你在前面的例子中理解到,这值代表了某个 SCALE- 编码账户 ID 的 Blake2 128 Concat 哈希值。 这 Blake 128 Concat 哈希算法包括将哈希算法的输入追加 (连缀) 到其 Blake 128 哈希中。 这意味着 Blake2 128 Concat 哈希的前128位 (或 32 个十六进制字符) 代表 Blake2 128 哈希,而其余部分代表传递给 Blake2 128 哈希算法的值。 在这例子中,去掉代表 Blake2 128 哈希的前 32 个十六进制字符 (即 0x32a5935f6edc617ae178fef9eb1e211f) 后,剩下的是十六进制值 0xbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f,就是一个 SCALE 编码的账户 ID。 对这个值进行解码,得到的结果是 5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY,这就是大家熟悉的 Alice_Stash 的账号 ID。

Runtime 存储接口

Substrate's FRAME Support crate provides utilities for generating unique, deterministic keys for your runtime's storage items. 这些存储项都放置在 状态树 中,并且可以 通过键来查询。

后续步骤

进一步学习

  • 学习如何在 Substrate Runtime 模块中添加 存储项 。

参考文档

  • Visit the reference docs for paritytech/trie 的文档。
← 密码学SS58 地址格式 →
  • 键值数据库
  • Trie 抽象
    • 状态树
    • 子状态树
  • 查询存储
    • 存储键
    • 存储映射键
  • Runtime 存储接口
  • 后续步骤
    • 进一步学习
    • 参考文档
Substrate Developer Hub
开发者中心
教程知识库进阶菜谱API 文档
社区
社区主页通讯Substrate 技术聊天室Substrate 研讨会Stack Overflow推特聚会活动
更多
Substrate Builders 计划BlogSubstrate GitHub开发者中心 GitHub隐私政策使用条款Cookie 设置
Copyright © 2021 Parity Technologies