Los nodos de Substrate devuelven un bloque a través de chain_getBlock(<hash>) como un SignedBlock cuyo array block.extrinsics contiene bytes SCALE codificados de forma compacta, no llamadas decodificadas. Para interpretar esos bytes debes decodificarlos con los metadatos del runtime para la specVersion activa en ese bloque, porque los pares pallet_index y call_index solo se resuelven mediante metadatos y esos índices cambian entre actualizaciones del runtime. Los eventos no forman parte del cuerpo del bloque: son el ítem de almacenamiento System.Events escrito durante la ejecución y legible en un hash de bloque mediante state_getStorage o expuesto por la API de polkadot.js. Una suscripción a bloques solo ve bloques nuevos, por lo que leer un bloque histórico específico requiere chain_getBlock en su hash, y el estado histórico completo requiere un nodo de archivo.
Qué devuelve realmente chain_getBlock para un bloque específico
Substrate expone los datos de bloque a través de dos métodos JSON-RPC. chain_getBlockHash toma un número de bloque opcional y devuelve el hash; chain_getBlockHash(null) devuelve el hash actual mejor (cabeza), mientras que chain_getBlockHash(<number>) resuelve una altura específica. chain_getBlock luego toma ese hash y devuelve un sobre SignedBlock. La forma autoritativa está documentada en la especificación JSON-RPC de Substrate y en la referencia JSON-RPC de la API de polkadot.js.
El sobre contiene block.header, block.extrinsics y justifications. El encabezado lleva parentHash, number, stateRoot, extrinsicsRoot y digest (incluidas las entradas de registro de consenso). El array de extrinsics es la parte importante: cada elemento es una cadena hexadecimal de bytes SCALE codificados de forma compacta, no una llamada decodificada. Si tu indexador trata ese hex como JSON o como una llamada con nombre, decodificará mal cada bloque.
- chain_getBlockHash(null) → hash mejor actual; chain_getBlockHash(n) → hash en la altura n.
- chain_getBlock(hash) → { block: { header, extrinsics }, justifications }.
- block.extrinsics[i] es hex SCALE; la decodificación requiere metadatos del runtime.
- justifications llevan pruebas de finalidad GRANDPA; consulta Justificaciones GRANDPA y la cabeza finalizada.
Por qué los extrinsics son bytes opacos y cómo los metadatos los resuelven
El primer byte de un extrinsic codifica su versión y formato (por ejemplo, si está firmado, es bare o no está firmado). Después de eso, un extrinsic firmado contiene la cuenta del firmante, una firma y los campos era/nonce/tip, seguidos de la llamada en sí. La llamada se direcciona como un par pallet_index más call_index. Esos pequeños enteros no tienen sentido sin los metadatos del runtime que los asignan a nombres de pallet y call.
Por eso state_getMetadata es obligatorio para decodificar. Los metadatos describen cada pallet, sus llamadas, sus eventos y sus ítems de almacenamiento para una specVersion. Debido a que las actualizaciones del runtime pueden renumerar pallets y llamadas, un indexador debe indexar los metadatos por specVersion y usar state_getRuntimeVersion en el bloque para saber qué metadatos aplican. Los metadatos históricos se sirven mediante RPC de archivo o se obtienen de una caché de metadatos. Consulta Leer metadatos del runtime con state_getMetadata.
- Byte 0: versión/formato del extrinsic.
- Extrinsics firmados: cuenta, firma, era, nonce, tip y luego la llamada.
- Llamada = pallet_index + call_index, resoluble solo mediante metadatos.
- Indexa los metadatos por specVersion; los índices cambian entre actualizaciones.
Dónde viven los eventos: System.Events, no el cuerpo del bloque
Los eventos no se almacenan en block.extrinsics. Son el ítem de almacenamiento System.Events, escrito por el runtime durante la ejecución del bloque. Los lees en un bloque específico con state_getStorage usando la clave twox128('System') ++ twox128('Events') y el hash del bloque, o dejas que la API de polkadot.js los exponga desde el bloque. Cada evento lleva una fase: Initialization, ApplyExtrinsic(index) o Finalization. La fase ApplyExtrinsic vincula un evento con el extrinsic que lo produjo.
Este vínculo de fase es la diferencia entre la llamada en cadena y sus efectos. Un solo extrinsic puede emitir muchos eventos, y algunos eventos (como comisiones o depósitos a tesorería) son emitidos por el sistema en lugar del llamador. Leer eventos sin su fase hará que un explorador atribuya efectos al extrinsic equivocado.
- Los eventos son un ítem de almacenamiento, no parte del cuerpo del SignedBlock.
- Se leen mediante state_getStorage(twox128('System')++twox128('Events'), blockHash).
- Fase: Initialization | ApplyExtrinsic(index) | Finalization.
- Usa la fase para mapear eventos de vuelta al extrinsic de origen.
Leer un bloque específico con @polkadot/api
La API de polkadot.js envuelve el RPC sin procesar y maneja la resolución de metadatos por ti. Usa api.rpc.chain.getBlock(hash) para obtener el SignedBlock, luego api.at(blockHash) para obtener un contexto fijado a ese bloque para consultas y eventos. api.query.system.events.at(blockHash) devuelve los eventos de ese bloque, y api.rpc.state.getStorage lee una clave de almacenamiento específica en ese hash.
El ejemplo a continuación obtiene un bloque por número, decodifica el firmante y el nonce de un extrinsic firmado, y lee los eventos en ese bloque. Es ejecutable contra cualquier endpoint de Substrate que sirva el bloque; reemplaza el endpoint y el número de bloque.
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);Alternativa de obtención sin procesar: chain_getBlock más decodificación de metadatos
Cuando no puedes usar el envoltorio de la API, obtén chain_getBlock y state_getMetadata directamente, luego decodifica con @polkadot/types contra los metadatos. Los metadatos deben coincidir con la specVersion en el bloque. Usa state_getRuntimeVersion en el hash del bloque para confirmar qué metadatos aplican antes de decodificar.
La ruta sin procesar es útil para indexadores que procesan muchos bloques en lote y quieren evitar la sobrecarga de la API por bloque. También hace explícita la dependencia de metadatos, que es exactamente el punto de fallo que más encuentran los exploradores.
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 };
}Cambios de almacenamiento en un bloque y por qué las suscripciones no rellenan el historial
Los cambios de almacenamiento en un bloque se observan leyendo state_getStorage en ese hash de bloque o usando tracing donde el nodo lo soporte. Una suscripción simple a bloques (chain_subscribeNewHeads o cabezas finalizadas) solo ve bloques nuevos y nunca rellena un bloque histórico específico. Si necesitas un bloque pasado, debes llamar a chain_getBlock en su hash. El estado histórico completo requiere un nodo de archivo; un nodo completo podado devolverá null para claves de almacenamiento antiguas.
Esta distinción importa para los indexadores. Las suscripciones son para transmitir bloques nuevos; las lecturas históricas son para rellenar y auditar. Confundirlas produce eventos vacíos y almacenamiento null, que parecen errores de decodificación pero en realidad son límites de disponibilidad de datos. Consulta Nodos de archivo de Polkadot y Substrate para estado histórico.
- state_getStorage(key, blockHash) lee un ítem de almacenamiento en un bloque.
- Las suscripciones transmiten solo bloques nuevos; no rellenan el historial.
- El estado histórico requiere un nodo de archivo.
- Los nodos completos podados devuelven null para claves antiguas.
Tabla de resultados: mide el comportamiento de datos de bloque de tu propio endpoint
El comportamiento del endpoint varía según el proveedor y el tipo de nodo. Completa la tabla a continuación con tu propio endpoint para documentar lo que realmente sirve. No asumas un valor; mídelo. Para números específicos de proveedores, trátalos como documentados / varía según el proveedor.
Ejecuta cada verificación en un bloque reciente y en un bloque antiguo (por ejemplo, uno de una era de runtime anterior) para exponer la poda y la deriva de metadatos.
- chain_getBlock en un hash reciente → ¿devuelve SignedBlock? (sí/no)
- chain_getBlock en un hash antiguo → ¿devuelve SignedBlock? (sí/no)
- state_getStorage(System.Events) en un hash antiguo → ¿devuelve datos? (sí/no)
- state_getRuntimeVersion en el bloque → valor de specVersion
- state_getMetadata en el bloque → ¿devuelve metadatos? (sí/no)
- Tipo de nodo: ¿archivo o completo? (archivo/completo)
- URL del endpoint y proveedor: (completar)
Fallos comunes y cómo diagnosticarlos
La mayoría de los errores de datos de bloque caen en unas pocas categorías. 'Unable to decode' o un índice de pallet desconocido casi siempre significa que la versión de metadatos no coincide con la specVersion del bloque. Los eventos vacíos normalmente significan que consultaste el estado más reciente en lugar del bloque. Que chain_getBlock devuelva null significa que el hash es desconocido o que el bloque aún no está finalizado. El estado podado en un nodo completo devuelve null para claves de almacenamiento antiguas. La deriva de índices por actualización del runtime significa que el mismo pallet_index se asigna a pallets diferentes entre specVersions.
Diagnostica verificando la specVersion en el bloque, confirmando el tipo de nodo y comprobando que pasaste el hash del bloque a cada lectura. Si estás enviando extrinsics y ves errores de dispatch, esa es otra ruta; consulta Decodificar errores de envío de extrinsics de Polkadot.
- Error de decodificación → desajuste de metadatos/specVersion.
- Eventos vacíos → consultaste lo más reciente, no el bloque.
- chain_getBlock null → hash desconocido o no finalizado.
- Almacenamiento null → estado podado en un nodo completo.
- Deriva de índices → una actualización del runtime cambió los índices de pallet/call.
Limitaciones y compensaciones
Leer datos de bloque en un bloque específico no está exento de restricciones. El estado histórico solo está disponible en nodos de archivo y, aun así, los metadatos de specVersions antiguas deben obtenerse o almacenarse en caché. Decodificar SCALE sin procesar requiere los metadatos correctos, por lo que un indexador debe almacenar metadatos por specVersion. Los métodos de tracing no son universalmente compatibles y pueden estar deshabilitados en endpoints públicos.
Las suscripciones son eficientes para bloques nuevos pero inútiles para rellenar el historial. Las lecturas por lotes de muchos bloques aumentan el tamaño de la carga útil y pueden alcanzar límites de tasa, así que planifica paginación y reintentos. Para la selección de endpoints y límites, consulta Endpoints y proveedores RPC de Polkadot (RPC Assistant) y Precios de RPC.
- Se requiere un nodo de archivo para el estado histórico.
- Los metadatos deben indexarse por specVersion.
- El soporte de tracing varía según el nodo.
- Las lecturas por lotes pueden alcanzar límites de tasa.
Lista de verificación para solución de problemas
Recorre esta lista de verificación antes de asumir un error de decodificación. Separa los problemas de disponibilidad de datos de los problemas de metadatos y de los errores de consulta.
Si un paso falla, corrígelo antes de continuar; los pasos posteriores dependen de los anteriores.
- Confirma el hash del bloque con chain_getBlockHash(number).
- Obtén chain_getBlock(hash) y verifica que block.extrinsics no esté vacío.
- Llama a state_getRuntimeVersion(hash) y registra specVersion.
- Obtén state_getMetadata(hash) y confirma que coincide con specVersion.
- Decodifica los extrinsics con esos metadatos; verifica pallet_index/call_index.
- Lee System.Events en el mismo hash; verifica las fases.
- Comprueba el tipo de nodo (archivo vs completo) para bloques antiguos.
- Si es null, prueba un endpoint de archivo o un proveedor diferente.
Próximos pasos y dónde profundizar
Una vez que puedas leer un bloque específico correctamente, el siguiente paso es construir un pipeline que indexe los metadatos por specVersion y rellene el historial desde un nodo de archivo. Para endpoints específicos de red, comienza con Polkadot. Para acceso gestionado y estado histórico, consulta el servicio de API y Nodos de archivo de Polkadot y Substrate para estado histórico.
Para un contexto más amplio sobre el comportamiento de RPC, explora el centro de aprendizaje de OnFinality. Si estás comparando proveedores, la página Endpoints y proveedores RPC de Polkadot (RPC Assistant) enumera opciones, y Precios de RPC cubre los límites de los planes.
- Indexa los metadatos por specVersion en tu indexador.
- Rellena el historial desde un nodo de archivo, transmite bloques nuevos por suscripción.
- Valida la decodificación contra un explorador de bloques conocido.
- Monitorea los cambios de specVersion para detectar la deriva de índices a tiempo.