像 Polkadot 这样的基于 Substrate 的链暴露了一个可升级的运行时,其 API 表面由版本化的元数据描述。state_getMetadata RPC 返回当前最佳区块或特定区块哈希的元数据,使您能够正确解码历史外部调用和存储。您必须将元数据固定到目标区块,并比较 spec_version 或元数据哈希以检测升级,否则会破坏客户端的解码逻辑。
直接回答:元数据是链的 API 契约,并且它会变化
如果您与基于 Substrate 的链(如 Polkadot)集成,运行时元数据是链 API 的权威描述:每个 pallet、可调用的外部函数、存储项、事件、错误和常量。此元数据是版本化的(目前 v14 和 v15 常见),并且每当链上运行时升级时都会更改。state_getMetadata RPC 方法返回此元数据,您可以传递一个可选的区块哈希来检索该确切区块的元数据。未能将元数据固定到目标区块是运行时升级后静默解码失败的根本原因。
实用规则:始终查询您正在解码的区块的元数据,并按运行时版本(spec_version)缓存。比较请求之间的 spec_version 或元数据哈希以检测升级。本文解释了机制,展示了如何查询任何区块的元数据,并提供了故障排除清单。
运行时元数据如何工作:可升级运行时的版本化模式
Substrate (FRAME) 链的运行时是存储在链上的 WebAssembly blob,可通过治理或 sudo 升级。每次运行时升级都可能改变 pallet 集合、外部函数、存储键、事件和错误。运行时元数据是该 API 表面的机器可读描述,使用 SCALE 编解码器编码。元数据格式本身是版本化的:v14 是当前广泛支持的格式,v15 添加了运行时 API 信息并支持异步支持时代。该版本与运行时的 spec_version、transaction_version 和 apex 规范名称相关联,这些在升级时会更改。
基于旧元数据构建的客户端在升级后可能会错误解码外部函数或存储,因为类型定义和索引可能已移动。例如,指向 Balances.transfer 的调用索引现在可能指向不同的外部函数。Polkadot-SDK 文档 和 polkadot.js.org 是元数据格式和运行时版本的权威主要来源。
RPC 表面包括 state_getMetadata(带有可选的 at 区块哈希)、state_call 以调用运行时 API(如 Metadata_metadata_at_version),以及 state_runtimeVersion(或 chain_getRuntimeVersion)以获取当前 spec_version。这些方法的可用性取决于节点构建和运行时版本,由链的运行时状态分发。
在特定区块查询元数据:工作流程
要解码历史外部函数或存储值,您需要在该外部函数所在区块有效的元数据。工作流程是:1) 获取感兴趣的区块哈希(例如,从交易或区块号)。2) 使用该哈希作为 at 参数调用 state_getMetadata。3) 解码返回的 SCALE 编码元数据以确定其版本(v14、v15 等)并提取类型注册表。4) 使用该注册表解码外部函数或存储键。
对于最新区块,您可以省略 at 参数,但请注意节点返回其当前最佳区块的元数据,该区块可能在请求之间变化。要检测两次请求之间的升级,请比较 state_runtimeVersion 中的 spec_version 或元数据哈希(例如,使用 state_call 调用 Metadata_metadata_at_version 并进行哈希)。
polkadot.js 公开了版本感知的元数据和注册表:@polkadot/api 自动查询元数据,并在检测到运行时升级时更新其注册表(通过 api.runtimeVersion)。但是,对于历史解码,您必须使用特定区块哈希手动创建 Api 实例,或使用更底层的库以正确的元数据进行解码。
可运行示例:获取并比较最新和历史区块的元数据
以下 bash 脚本使用 curl 查询 Polkadot RPC 端点(替换为您自己的端点,例如来自 OnFinality 的 API 服务)。它获取最新区块哈希,然后为最新区块和特定旧区块哈希调用 state_getMetadata(您可以替换为已知的历史哈希)。它使用 state_call 调用 Metadata_metadata_at_version 和 state_runtimeVersion 提取元数据版本和 spec_version。
预期输出显示两个区块的元数据版本和 spec_version。如果它们不同,则在这些区块之间发生了运行时升级。用您自己的测量结果填写下面的结果表。
#!/bin/bash
# Replace with your endpoint
ENDPOINT="https://rpc.polkadot.io"
# Get latest block hash
LATEST_HASH=$(curl -s -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"chain_getBlockHash","params":[],"id":1}' $ENDPOINT | jq -r '.result')
echo "Latest block hash: $LATEST_HASH"
# Fetch metadata at latest
curl -s -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"method\":\"state_getMetadata\",\"params\":[\"$LATEST_HASH\"],\"id\":2}" $ENDPOINT | jq -r '.result' | xxd -r -p | head -c 10 | od -An -t u1
echo ""
# Get runtime version at latest
curl -s -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"state_getRuntimeVersion","params":[],"id":3}' $ENDPOINT | jq '.result.specVersion'
# Replace with an older block hash (e.g., from a known block number)
OLD_HASH="0x..."
# Fetch metadata at old block
curl -s -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"method\":\"state_getMetadata\",\"params\":[\"$OLD_HASH\"],\"id\":4}" $ENDPOINT | jq -r '.result' | xxd -r -p | head -c 10 | od -An -t u1
echo ""
# Get runtime version at old block (using state_call)
curl -s -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"method\":\"state_call\",\"params\":[\"Core_version\",\"0x\",\"$OLD_HASH\"],\"id\":5}" $ENDPOINT | jq '.result'结果表:填写您的测量结果
运行上述脚本并记录每个区块的元数据版本和 spec_version。元数据版本是 SCALE 编码元数据的第一个字节(例如,v14 为 14,v15 为 15)。spec_version 是运行时版本调用返回的数字。
- 最新区块哈希:[填写]
- 最新区块的元数据版本:[填写]
- 最新区块的 spec_version:[填写]
- 旧区块哈希:[填写]
- 旧区块的元数据版本:[填写]
- 旧区块的 spec_version:[填写]
- spec_version 是否改变?[是/否]
故障排除清单:常见失败和修复
在查询区块元数据时,您可能会遇到几个问题。使用此清单诊断并修复它们。
如果您遇到“无法转换”错误或类型定义漂移,您可能正在使用最新元数据解码历史外部函数。修复:在目标区块获取元数据并使用该注册表。
如果升级后存储键不匹配,则 pallet 前缀或哈希器可能已更改。修复:比较两个区块的元数据以识别更改。
如果存档节点无法为深度修剪的区块提供元数据,可能是因为该节点不是存档节点或运行时状态不可用。修复:使用专用的存档节点(请参阅 通过 RPC 查询 Polkadot 历史状态)。
如果 state_getMetadata 对旧区块返回错误,则该区块可能早于元数据版本引入的时间,或者节点没有该状态。修复:验证区块在节点的修剪窗口内,或使用存档端点。
如果您看到 state_getMetadata 和 state_runtimeVersion 之间的不匹配,请记住后者返回当前运行时版本,不一定是您查询区块的版本。如果可用,始终将区块哈希传递给这两种方法。
按区块查询元数据的局限性和权衡
在特定区块查询元数据功能强大,但也有局限性。首先,并非所有节点都是存档节点;全节点可能会修剪历史状态,使旧区块的元数据不可用。其次,元数据格式本身会演变,因此您必须在解码器中处理多个版本(v14、v15、未来)。第三,RPC 方法的可用性因节点构建和运行时版本而异;例如,state_call 调用 Metadata_metadata_at_version 在较旧的运行时上可能不存在。
在性能方面,为每个区块获取元数据效率低下。相反,按运行时版本缓存元数据,并仅在 spec_version 更改时刷新。这就是 polkadot.js 处理运行时升级的方式:它监听 spec_version 更改并惰性更新其注册表。
最后,元数据描述了运行时 API,但不包括实际逻辑。对于解码错误,您可能需要将元数据与运行时版本和外部函数格式版本(例如,签名扩展)结合使用。
后续步骤:加深您的 Substrate 集成知识
既然您了解了元数据和运行时版本,请探索相关主题以加强您的集成。有关 Polkadot RPC 方法的更广泛概述,请参阅 Polkadot RPC 指南。要了解最终性如何影响区块可用性,请阅读 Polkadot 最终性和最终化头。
如果您处理历史数据,通过 RPC 查询 Polkadot 历史状态 的指南是必不可少的。有关处理外部函数错误,请参阅 解码 Polkadot 外部函数调度错误。对于高效的实时数据,请查看 Polkadot WebSocket RPC 深入指南。
对于生产使用,请考虑使用可靠的 RPC 提供商,如 OnFinality 的 API 服务,并提供适合您需求的 RPC 定价。在部署到主网之前,始终在测试网上测试您的元数据处理。