Substrate: del runtime al consenso propio
Artículo 6 de 20
30%
completado al llegar aquí
Sistemas distribuidosNota de investigación16 min de lectura
Extrinsics: transacciones firmadas, sin firmar e inherentes
Qué entra a un bloque Substrate desde fuera del runtime, cómo se codifica un extrinsic versión 5 y qué comprueban las transaction extensions antes de ejecutarlo.
Ruta Substrate: 0/20
Prerequisitos recomendados
Si alguno de estos conceptos todavía está medio nebuloso, empieza por aquí. Te ahorra tropiezos y un par de cejas fruncidas.
Un equipo integra su billetera con una cadena construida con Substrate (Polkadot SDK). Durante semanas todo funciona. Un martes la cadena aplica una actualización del runtime y, desde ese bloque, cada transferencia que la billetera tenía firmada y en cola vuelve rechazada con un error de prueba inválida. Nadie tocó las claves ni el código de firma. Lo que cambió fue un número que el usuario nunca ve: la versión del runtime, que forma parte de lo que se firma.
Este capítulo explica por qué. Para entenderlo hay que saber qué es un extrinsic, cómo se convierte en bytes, qué parte de esos bytes se firma y qué comprobaciones corre el runtime antes de aceptarlo en el pool o en un bloque.
Brújula de lectura
Actor, evidencia y estado
Actor
Usuario, autor del bloque o nodoEl usuario firma transacciones; el autor del bloque pone los inherentes; un nodo puede enviar transacciones sin firmar.
Evidencia
Bytes SCALE del extrinsicVersión, firma opcional, datos de las extensiones y la llamada; el runtime los valida, el nodo solo los transporta.
Estado
Nonce, saldo y storage de los palletsCada extrinsic aplicado sube el nonce, cobra la comisión y ejecuta una llamada despachable.
Qué es un extrinsic y por qué no se llama transacción
El glosario del Polkadot SDK define el término sin rodeos: un extrinsic es un dato que se origina fuera del runtime, se incluye en un bloque y provoca alguna acción. Incluye las transacciones de los usuarios y también los inherentes que el constructor del bloque coloca en él. [1]
La palabra existe porque "transacción" se queda corta. En Ethereum todo lo que entra a un bloque es una transacción firmada por una cuenta. En Substrate también entran datos que nadie firma, como la marca de tiempo del bloque. Si recuerdas el capítulo de FRAME, cada uno de esos datos termina llamando a una función despachable de algún pallet.
Hay tres familias, y conviene fijarlas desde el principio: [2]
- Firmadas. Una cuenta las autoriza con su clave y normalmente pagan comisión. Una transferencia de saldo es el caso típico.
- Sin firmar. Llegan sin firma ni comisión. Como nada las encarece, el pallet que las acepta debe escribir su propia validación para no abrir la puerta al spam.
- Inherentes. Las inserta quien produce el bloque, no circulan por la red como mensajes sueltos y llevan datos que el bloque necesita, como el tiempo.
La especificación del protocolo lo dice desde el lado del nodo: para el host cada extrinsic es solo un blob codificado en SCALE, y su contenido lo define y lo interpreta el runtime. [3] El nodo transporta y ordena; solo el runtime sabe qué significa cada byte.
Los bytes de un extrinsic versión 5
La mayoría de los runtimes usa el tipo sp_runtime::generic::UncheckedExtrinsic. Su documentación describe la codificación de la versión de formato 5 como la concatenación de cuatro piezas: [4]
extrinsic_bytes = concat(
compact_encoded_length, // longitud del resto, en SCALE compacto
version_and_extrinsic_type, // 1 byte: tipo (2 bits) + versión (6 bits)
maybe_extension_data, // dirección, firma y extensiones, según el tipo
call_data // índice de pallet, índice de llamada, argumentos
)El primer byte útil dice qué clase de extrinsic viene. Los dos bits más significativos marcan el tipo y los seis restantes la versión, hoy 5:
- Bare (
0b00, byte0b0000_0101): no lleva datos de extensión; después del byte viene directamente la llamada. Así viajan los inherentes y las transacciones sin firmar al estilo clásico. - Signed (
0b10, byte0b1000_0101): lleva la dirección del emisor, la firma y los bytes de las extensiones. - General (
0b01, byte0b0100_0101): no trae una firma fija; lleva extensiones, y alguna de ellas es la que autoriza el origen.
La llamada se codifica con dos bytes de índice y los argumentos. El primero identifica el pallet en el runtime y el segundo la función dentro del pallet. Por eso un cliente no puede construir llamadas sin leer la metadata de la cadena: los índices los fija cada runtime, y la metadata publica cuáles son, junto con las extensiones que exige y en qué orden.
Anatomía de un extrinsic firmado versión 5
Cómo leer esta figura
Fila superior de cajas, de izquierda a derecha: longitud compacta; byte de versión y tipo (0b1000_0101 para firmado); dirección del emisor; firma; extra de las extensiones (era, nonce, propina); y call_data con índice de pallet, índice de llamada y argumentos. Debajo, a la izquierda, un recuadro recuerda los dos bits altos del primer byte: bare 00, signed 10, general 01; otro, en rojo, advierte que subir spec_version invalida firmas hechas antes. A la derecha, la caja del signed payload es blake2_256 de la concatenación de call_data, extra e implicit: flechas desde call_data y extra bajan hasta ella y una flecha sube desde ella hasta la firma, rotulada se firma con la clave. Bajo el payload, un recuadro discontinuo, implicit, lista spec_version, transaction_version, hash de génesis y hash del bloque de nacimiento: datos que no viajan en el extrinsic y que el runtime reconstruye.
Un detalle de la propia documentación merece cuidado: un párrafo antiguo todavía describe el byte de una transacción sin firmar con la versión 4. El resto del texto, y el formato vigente, usan la versión 5. Si un decodificador viejo falla con extrinsics nuevos, ese cambio de versión es lo primero que hay que revisar.
El payload que se firma
La firma no cubre los bytes del extrinsic tal como viajan. Cubre un signed payload construido así: [4]
signed_payload = blake2_256(
concat(call_data, transaction_extensions_extra, transaction_extensions_implicit)
)La pieza nueva es implicit. Cada extensión puede aportar datos que no viajan en el extrinsic pero que el runtime reconstruye desde su propio estado al verificar la firma. CheckSpecVersion aporta la spec_version del runtime activo; CheckGenesis, el hash del bloque génesis; CheckMortality, el hash del bloque desde el que cuenta la vida de la transacción. [6]
Ahí está la respuesta al caso del principio. La billetera firmó con spec_version 100. Tras la actualización, el runtime reconstruye el payload con 101, el hash cambia y la firma deja de corresponder. No hay error de red ni de claves: es una protección diseñada. Una transacción firmada para un runtime no puede ejecutarse por accidente con otro.
La misma idea explica por qué una transacción de una red de pruebas no se puede reenviar en la red principal: el hash de génesis entra en la firma aunque no viaje en los bytes.
Transaction extensions: los controles previos
Las transaction extensions son, en palabras de la documentación, la forma en que cada cadena extiende el formato básico del extrinsic con datos propios que el runtime comprueba. [5] La documentación antigua las llama signed extensions; el nombre cambió cuando dejaron de depender de que hubiera una firma.
FRAME trae un juego estándar. Cada una responde una pregunta concreta:
Flujo visual
Qué comprueba cada extensión antes de despachar la llamada
CheckNonce merece un párrafo propio. Si el nonce de la transacción es menor que el de la cuenta, la transacción es vieja y se rechaza como stale. Si es mayor, no es inválida: es future y espera en el pool a que lleguen las que faltan. [7] Para ordenar, la extensión publica etiquetas: cada transacción provee el par (cuenta, nonce) y requiere el par anterior. El pool usa esas etiquetas para saber qué está listo.
La mortalidad cierra otro hueco. Una transacción mortal vale desde un bloque de referencia durante un número de bloques; si no entra a tiempo, sale del pool. Una inmortal no caduca, y eso es peligroso: si la cuenta se vacía, se elimina y se vuelve a crear con nonce 0, alguien podría repetir transacciones viejas que vuelven a ser válidas. [2] Por la misma razón, el hash de una transacción no la identifica de forma única: la identidad correcta es el bloque más el índice dentro del bloque.
Del pool al bloque: validar no es ejecutar
Cuando un extrinsic llega a un nodo, el nodo llama a la runtime API validate_transaction. En FRAME esa función decodifica, comprueba la firma, corre la fase de validación de las extensiones y devuelve prioridad y etiquetas. Lo que escriba en el storage durante esa comprobación se descarta. [8] Validar es preguntar "¿podría entrar?"; ejecutar es aplicarlo de verdad dentro de un bloque.
La misma función rechaza cualquier extrinsic de clase Mandatory que llegue desde fuera. Esa clase queda reservada a los inherentes, que no deben poder colarse por el pool.
Un inherente sigue otro camino. El nodo reúne los datos inherentes (por ejemplo, su reloj) y se los pasa al runtime con BlockBuilder_inherent_extrinsics; el runtime devuelve los extrinsics que hay que poner y el autor los coloca al principio del bloque. [3] Nadie firma la marca de tiempo. La protección es otra: los demás nodos, al importar el bloque, comprueban que el valor esté dentro de un margen aceptable. Si no lo está, el bloque es inválido.
Qué protege a cada una
Tres maneras de entrar a un bloque
Firmada
Una cuenta autoriza con su clave y paga.
- Protección: firma, nonce y comisión.
- Circula por la red y espera en el pool.
Sin firmar
Llega sin firma ni comisión.
- Protección: lógica propia del pallet, que debe limitar frecuencia y origen.
- Sin esa lógica, es una invitación al spam.
Inherente
La pone el autor del bloque.
- Protección: los demás nodos verifican el valor al importar el bloque.
- No pasa por el pool ni se difunde suelta.
Las transacciones sin firmar son la familia delicada. El ejemplo clásico es un latido de un validador que demuestra que sigue vivo: solo los validadores pueden enviarlo, así que el pallet comprueba esa condición y no cobra. Hoy el formato general y el atributo #[pallet::authorize] permiten expresar esa autorización como una extensión más, en vez de una excepción. El capítulo de offchain workers vuelve sobre este patrón, porque es la vía por la que un worker reporta resultados.
Qué cambia respecto de Ethereum
Si vienes de la transacción de Ethereum por dentro, la tentación es buscar el campo equivalente para cada cosa. Algunos existen; otros cambian de lugar.
- Chain ID frente a génesis. Ethereum mete el
chainIden la transacción firmada. Substrate mete el hash de génesis en los datos implícitos: no viaja, pero se firma. - Una sola forma frente a extensiones. El tipo de transacción de Ethereum fija sus campos por EIP. En Substrate cada runtime elige su tupla de extensiones, y la metadata la publica.
- La versión del software entra en la firma. Ethereum no firma la versión del cliente. Substrate firma
spec_version, y por eso una actualización del runtime invalida firmas pendientes. - Datos sin firma. Ethereum no tiene inherentes: la marca de tiempo va en la cabecera. Substrate la pasa por un extrinsic que el runtime valida como cualquier otro.
La firma también autoriza cosas distintas. En cuentas, claves y direcciones la dirección se deriva de la clave y la firma recupera al emisor. En Substrate la dirección viaja explícita en el extrinsic firmado, y el tipo de firma lo elige el runtime; en Polkadot es MultiSignature, que admite varios esquemas. [4]
Decisiones para quien diseña la cadena y quien construye el cliente
Antes de aceptar extrinsics en producción
0 de 5 revisados
Queda una pieza abierta: CheckWeight y ChargeTransactionPayment mencionan un peso y una comisión que todavía no explicamos. Antes de llegar ahí hace falta saber dónde escribe un extrinsic. El siguiente capítulo, el estado: clave-valor, trie radix-16 y raíz de estado, muestra cómo se guarda ese storage y cómo se prueba un valor sin bajar todo. Después, pesos y comisiones cierra el círculo. El capítulo anterior, tu primer nodo local, sirve para ver todo esto en un nodo que corre en tu máquina.
Bibliografía académica
- [1]
Parity Technologies, "Glossary," polkadot_sdk_docs, paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Definición de extrinsic: dato originado fuera del runtime, incluido en un bloque, que provoca una acción.
- [2]
Polkadot Developers, "Transactions," polkadot-developers/polkadot-docs, GitHub repository (fuente de docs.polkadot.com), accedido 2026-11-13.
Transacciones firmadas, sin firmar e inherentes; pool con colas ready y future; mortalidad; identificar un extrinsic por bloque e índice.
- [3]
Web3 Foundation, "States and Transitions," Polkadot Protocol Specification, w3f/polkadot-spec, GitHub repository, accedido 2026-11-13.
Extrinsics como blobs SCALE opacos para el host, pool de transacciones e inherentes generados con BlockBuilder_inherent_extrinsics.
- [4]
Parity Technologies, "Constructing and Signing Extrinsics," polkadot_sdk_docs, paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Codificación de UncheckedExtrinsic versión 5: longitud compacta, byte de versión y tipo, datos de extensiones, call_data y signed payload con blake2_256.
- [5]
Parity Technologies, "Transaction Extensions," polkadot_sdk_docs, paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Lista de extensiones de FRAME: CheckGenesis, CheckMortality, CheckNonZeroSender, CheckNonce, CheckSpecVersion, CheckTxVersion, CheckWeight, ChargeTransactionPayment y otras.
- [6]
Parity Technologies, "frame/system/src/extensions/check_spec_version.rs," paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
CheckSpecVersion declara type Implicit = u32 y devuelve la spec_version del runtime activo.
- [7]
Parity Technologies, "frame/system/src/extensions/check_nonce.rs," paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Validación del nonce: Stale si es menor, Future si es mayor, y etiquetas provides y requires por (cuenta, nonce).
- [8]
Parity Technologies, "frame/executive/src/lib.rs," paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
validate_transaction comprueba sin efectos y rechaza la clase Mandatory; el diagrama de ejecución pone los inherentes antes de las transacciones.
Rutas vecinas
Si este tema te abrió hambre, sigue por aquí.
Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.
Series relacionadas
HotStuff: de cero a estado del arte
Ruta progresiva para entender consenso BFT moderno: quórums, sincronía parcial, locks, taxonomía one/two/three-chain, QC/highQC, multipipeline, view-change, responsiveness, AggQC, Fast-HotStuff, sync y frontera DAG-BFT.
Ethereum: de historia a labs
Serie progresiva para entender Ethereum desde el whitepaper, forks y The Merge hasta EVM, Solidity, proof-of-stake, rollups, account abstraction, DeFi, observabilidad y laboratorios reproducibles.
Ruta práctica: activos digitales en la operación
Nueve módulos, de menor a mayor dependencia, para decidir cómo usar custodia, wallets, llaves, firmas multifirma y MPC, AML/KYT y stablecoins con Fireblocks, BitGo, Kaleido, Circle y Tether.
Labs para convertir lectura en práctica
Prueba de trabajo y reajuste de dificultad
Laboratorio determinista para medir el costo de la prueba de trabajo y ver cómo el reajuste por altura de bloque estabiliza el tiempo entre bloques.
Consensus benchmark
Banco reproducible para comparar consenso BFT y documentar los trade-offs de HotStuff-2.
Ethereum local: Anvil, wallet y primera transacción
Laboratorio base para levantar una red local, crear cuentas, firmar transacciones y observar receipts sin gastar ETH real.
RPC y Cast: leer Ethereum con lupa
Ejercicios con JSON-RPC y Cast para consultar bloques, nonces, gas, receipts, logs y errores reproducibles.
Sigue leyendo
Ideas conectadas con este artículo
Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.
Qué es Substrate hoy: el Polkadot SDK por dentro
Substrate ya no es un repositorio aparte: es el framework base del Polkadot SDK. Un mapa de nombres, piezas y crates para empezar sin perderse.
FRAME: pallets que se ensamblan en un runtime
Un pallet encapsula una responsabilidad; el runtime es la suma de pallets configurados. Anatomía de un pallet, cómo se ensamblan y por qué un pallet no es un contrato inteligente.
El estado: clave-valor, trie radix-16 y raíz de estado
El nodo guarda bytes que no entiende; el runtime los interpreta. Un trie radix-16 los resume en una raíz que va en la cabecera y de la que salen las pruebas de almacenamiento.
Pesos y comisiones: medir antes de ejecutar
El runtime no se mide mientras corre: se mide antes, con benchmarks del peor caso. El peso tiene dos dimensiones y la comisión suma base, longitud, peso ajustado por congestión y propina.