Substrate 节点通过 chain_getBlock(<hash>) 返回一个 SignedBlock,其 block.extrinsics 数组存放的是紧凑编码的 SCALE 字节,而非已解码的调用。要解释这些字节,必须根据该区块所处 specVersion 的运行时元数据进行解码,因为 pallet_index 与 call_index 对只能通过元数据解析,而这些索引会随运行时升级而变动。事件不属于区块主体:它们是执行期间写入的 System.Events 存储项,可通过 state_getStorage 在某个区块哈希处读取,或由 polkadot.js API 呈现。区块订阅只能看到新区块,因此读取特定历史区块需要在其哈希处调用 chain_getBlock,而完整的历史状态则需要归档节点。
chain_getBlock 对特定区块实际返回什么
Substrate 通过两个 JSON-RPC 方法暴露区块数据。chain_getBlockHash 接受一个可选的区块号并返回哈希;chain_getBlockHash(null) 返回当前最佳(头部)哈希,而 chain_getBlockHash(<number>) 解析特定高度。chain_getBlock 随后接受该哈希并返回一个 SignedBlock 信封。权威结构记录在 Substrate JSON-RPC 规范和 polkadot.js API JSON-RPC 参考中。
该信封包含 block.header、block.extrinsics 和 justifications。头部携带 parentHash、number、stateRoot、extrinsicsRoot 和 digest(包括共识日志条目)。extrinsics 数组是关键部分:每个元素都是紧凑编码 SCALE 字节的十六进制字符串,而非已解码的调用。如果你的索引器将该十六进制当作 JSON 或具名调用来处理,就会错误解码每一个区块。
- chain_getBlockHash(null) → 当前最佳哈希;chain_getBlockHash(n) → 高度 n 处的哈希。
- chain_getBlock(hash) → { block: { header, extrinsics }, justifications }。
- block.extrinsics[i] 是 SCALE 十六进制;解码需要运行时元数据。
- justifications 携带 GRANDPA 最终性证明;参见 GRANDPA 证明与已最终确定的头部。
为什么外部交易是不透明字节,以及元数据如何解析它们
外部交易的第一个字节编码其版本和格式(例如,它是已签名、裸交易还是未签名)。之后,已签名外部交易包含签名者账户、签名以及 era/nonce/tip 字段,随后是调用本身。调用以 pallet_index 加 call_index 对的形式寻址。如果没有将这些小整数映射到 pallet 和调用名称的运行时元数据,它们就毫无意义。
这就是为什么 state_getMetadata 对于解码是必需的。元数据描述了一个 specVersion 下的每个 pallet、其调用、事件和存储项。由于运行时升级可能重新编号 pallet 和调用,索引器必须按 specVersion 为元数据建立键,并使用区块处的 state_getRuntimeVersion 来确定适用哪个元数据。历史元数据由归档 RPC 提供,或从元数据缓存中获取。参见 使用 state_getMetadata 读取运行时元数据。
- 字节 0:外部交易版本/格式。
- 已签名外部交易:账户、签名、era、nonce、tip,然后是调用。
- 调用 = pallet_index + call_index,只能通过元数据解析。
- 按 specVersion 为元数据建立键;索引会随升级而变动。
事件在哪里:System.Events,而非区块主体
事件不存储在 block.extrinsics 中。它们是 System.Events 存储项,由运行时在区块执行期间写入。你可以使用键 twox128('System') ++ twox128('Events') 和区块哈希,通过 state_getStorage 在特定区块读取它们,或者让 polkadot.js API 从区块中呈现它们。每个事件携带一个阶段:Initialization、ApplyExtrinsic(index) 或 Finalization。ApplyExtrinsic 阶段将事件链接到产生它的外部交易。
这种阶段关联是链上调用与其效果之间的区别。单个外部交易可以发出许多事件,而某些事件(如费用或国库存款)是由系统而非调用者发出的。不结合阶段读取事件,会让浏览器将效果归因到错误的外部交易。
- 事件是存储项,不属于 SignedBlock 主体。
- 通过 state_getStorage(twox128('System')++twox128('Events'), blockHash) 读取。
- 阶段:Initialization | ApplyExtrinsic(index) | Finalization。
- 使用阶段将事件映射回其来源外部交易。
使用 @polkadot/api 读取特定区块
polkadot.js API 封装了原始 RPC 并为你处理元数据解析。使用 api.rpc.chain.getBlock(hash) 获取 SignedBlock,然后使用 api.at(blockHash) 获取固定到该区块的上下文,用于查询和事件。api.query.system.events.at(blockHash) 返回该区块的事件,api.rpc.state.getStorage 在该哈希处读取特定存储键。
下面的示例按编号获取区块,解码已签名外部交易的签名者和 nonce,并读取该区块的事件。它可以在任何提供该区块的 Substrate 端点上运行;请替换端点和区块号。
const { ApiPromise, WsProvider } = require('@polkadot/api');
async function main() {
const api = await ApiPromise.create({ provider: new WsProvider('wss://your-endpoint') });
const blockNumber = 20000000;
const blockHash = await api.rpc.chain.getBlockHash(blockNumber);
const signedBlock = await api.rpc.chain.getBlock(blockHash);
console.log('hash', blockHash.toHex());
console.log('parent', signedBlock.block.header.parentHash.toHex());
console.log('extrinsics', signedBlock.block.extrinsics.length);
signedBlock.block.extrinsics.forEach((ex, i) => {
const { isSigned, signer, nonce, method } = ex;
console.log(i, isSigned ? signer.toString() : 'unsigned',
isSigned ? nonce.toString() : '-', method.section + '.' + method.method);
});
const apiAt = await api.at(blockHash);
const events = await apiAt.query.system.events();
events.forEach(({ phase, event }) => {
console.log(phase.toString(), event.section + '.' + event.method);
});
await api.disconnect();
}
main().catch(console.error);原始获取回退方案:chain_getBlock 加元数据解码
当你无法使用 API 封装时,直接获取 chain_getBlock 和 state_getMetadata,然后使用 @polkadot/types 根据元数据进行解码。元数据必须与区块处的 specVersion 匹配。在解码前,使用区块哈希处的 state_getRuntimeVersion 确认适用哪个元数据。
原始路径对于批量处理许多区块并希望避免逐区块 API 开销的索引器很有用。它还使元数据依赖变得明确,而这正是大多数浏览器遇到的故障点。
const { WsProvider } = require('@polkadot/api');
const { TypeRegistry } = require('@polkadot/types');
async function raw(provider, blockHash) {
const block = await provider.send('chain_getBlock', [blockHash]);
const metaHex = await provider.send('state_getMetadata', [blockHash]);
const version = await provider.send('state_getRuntimeVersion', [blockHash]);
const registry = new TypeRegistry();
registry.setMetadata(new (require('@polkadot/types').Metadata)(registry, metaHex));
const extrinsics = block.block.extrinsics.map((hex) =>
registry.createType('Extrinsic', hex, { version: version.specVersion }));
return { version, extrinsics };
}区块处的存储变化,以及为什么订阅不会回填
区块处的存储变化可以通过在该区块哈希处读取 state_getStorage 来观察,或者在节点支持的情况下使用 tracing。普通的区块订阅(chain_subscribeNewHeads 或 finalized heads)只能看到新区块,永远不会回填特定的历史区块。如果你需要过去的区块,必须在其哈希处调用 chain_getBlock。完整的历史状态需要归档节点;被修剪的全节点会对旧存储键返回 null。
这一区别对索引器很重要。订阅用于流式传输新区块;历史读取用于回填和审计。将它们混为一谈会产生空事件和 null 存储,看起来像解码错误,但实际上是数据可用性限制。参见 用于历史状态的 Polkadot 和 Substrate 归档节点。
- state_getStorage(key, blockHash) 在某个区块读取存储项。
- 订阅仅流式传输新区块;它们不会回填。
- 历史状态需要归档节点。
- 被修剪的全节点对旧键返回 null。
结果表:测量你自己端点的区块数据行为
端点行为因提供商和节点类型而异。针对你自己的端点填写下表,以记录它实际提供的内容。不要假设某个值;要测量它。对于特定提供商的数字,将其视为已记录 / 因提供商而异。
在最近区块和一个旧区块(例如来自先前运行时时代的区块)分别运行每项检查,以暴露修剪和元数据漂移。
- chain_getBlock 在最近哈希处 → 返回 SignedBlock?(是/否)
- chain_getBlock 在旧哈希处 → 返回 SignedBlock?(是/否)
- state_getStorage(System.Events) 在旧哈希处 → 返回数据?(是/否)
- state_getRuntimeVersion 在区块处 → specVersion 值
- state_getMetadata 在区块处 → 返回元数据?(是/否)
- 节点类型:归档还是全节点?(归档/全节点)
- 端点 URL 和提供商:(填写)
常见故障及其诊断方法
大多数区块数据错误可归为几类。“Unable to decode”或未知 pallet 索引几乎总是意味着元数据版本与区块的 specVersion 不匹配。空事件通常意味着你查询的是最新状态而非该区块。chain_getBlock 返回 null 意味着哈希未知或区块尚未最终确定。全节点上的被修剪状态对旧存储键返回 null。运行时升级索引漂移意味着同一个 pallet_index 在不同 specVersion 下映射到不同的 pallet。
通过检查区块处的 specVersion、确认节点类型,并验证你为每次读取都传入了区块哈希来进行诊断。如果你在提交外部交易并看到调度错误,那是另一条路径;参见 解码 Polkadot 外部交易提交错误。
- 解码错误 → 元数据/specVersion 不匹配。
- 空事件 → 查询的是最新状态,而非该区块。
- chain_getBlock 为 null → 哈希未知或未最终确定。
- 存储为 null → 全节点上的被修剪状态。
- 索引漂移 → 运行时升级改变了 pallet/调用索引。
限制与权衡
在特定区块读取区块数据并非没有约束。历史状态仅在归档节点上可用,即便如此,旧 specVersion 的元数据也必须获取或缓存。解码原始 SCALE 需要正确的元数据,因此索引器必须按 specVersion 存储元数据。Tracing 方法并非普遍支持,在公共端点上可能被禁用。
订阅对新区块很高效,但对回填无用。批量读取许多区块会增加负载大小,并可能触及速率限制,因此要规划分页和重试。关于端点选择和限制,参见 Polkadot RPC 端点与提供商(RPC Assistant) 和 RPC 定价。
- 历史状态需要归档节点。
- 元数据必须按 specVersion 建立键。
- Tracing 支持因节点而异。
- 批量读取可能触及速率限制。
故障排除清单
在假设存在解码错误之前,请先完成此清单。它将数据可用性问题与元数据问题以及查询错误区分开来。
如果某一步失败,请先修复它再继续;后续步骤依赖前面的步骤。
- 使用 chain_getBlockHash(number) 确认区块哈希。
- 获取 chain_getBlock(hash) 并验证 block.extrinsics 非空。
- 调用 state_getRuntimeVersion(hash) 并记录 specVersion。
- 获取 state_getMetadata(hash) 并确认它与 specVersion 匹配。
- 使用该元数据解码外部交易;检查 pallet_index/call_index。
- 在同一哈希处读取 System.Events;验证阶段。
- 对于旧区块,检查节点类型(归档还是全节点)。
- 如果为 null,尝试归档端点或其他提供商。
后续步骤与深入方向
一旦你能正确读取特定区块,下一步就是构建一个按 specVersion 为元数据建立键并从归档节点回填的管道。对于特定网络的端点,请从 Polkadot 开始。对于托管访问和历史状态,参见 API 服务 和 用于历史状态的 Polkadot 和 Substrate 归档节点。
关于 RPC 行为的更广泛背景,请浏览 OnFinality Learn 中心。如果你在比较提供商,Polkadot RPC 端点与提供商(RPC Assistant) 页面列出了选项,RPC 定价 涵盖了套餐限制。
- 在索引器中按 specVersion 为元数据建立键。
- 从归档节点回填,通过订阅流式传输新区块。
- 对照已知区块浏览器验证解码。
- 监控 specVersion 变化,及早发现索引漂移。