Substrate: del runtime al consenso propio
Artículo 9 de 20
45%
completado al llegar aquí
Sistemas distribuidosNota de investigación14 min de lectura
Actualizar sin fork: set_code, versiones y migraciones
Cómo una cadena Substrate cambia su lógica escribiendo un Wasm nuevo en :code, qué comprueba set_code, por qué hay que subir spec_version y cómo se prueban las migraciones.
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.
El runtime en Wasm: nodo y runtime hablan por dos puertas
Dónde vive el runtime de Substrate (la clave :code del estado) y cómo conversa con el nodo: host functions de sp_io y runtime APIs de sp_api.
Blockchain15 min de lectura→FRAME: pallets que se ensamblan en un runtime
Cómo FRAME arma un runtime de Substrate con pallets: Config, storage, llamadas, eventos y errores unidos con construct_runtime, y en qué difiere de un contrato.
Blockchain14 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.
Blockchain16 min de lectura→El estado: clave-valor, trie radix-16 y raíz de estado
Cómo guarda Substrate el estado como bytes opacos, cómo FRAME arma las claves y cómo el trie radix-16 permite probar un valor con unos pocos nodos.
Blockchain13 min de lectura→
Un equipo cambia el tipo de un item de storage: los montos de un pallet pasan de u64 a u128 para admitir activos con más decimales. Compila, los tests unitarios pasan y la gobernanza aprueba la actualización. En el bloque siguiente, la primera llamada que lee un monto antiguo intenta decodificar ocho bytes como si fueran dieciséis y falla. El nodo no tiene la culpa: guarda bytes y nunca supo qué significaban. Faltó una migración que convirtiera los valores viejos al formato nuevo.
Este capítulo recorre cómo una cadena Substrate (Polkadot SDK) cambia su lógica sin que los operadores reinstalen el nodo, qué controles tiene ese proceso y dónde falla en la práctica.
Brújula de lectura
Actor, evidencia y estado
Actor
Un origen con permiso, normalmente la gobernanzaSolo el origen Root, o lo que la cadena configure como tal, puede llamar a set_code.
Evidencia
Evento CodeUpdated y digest RuntimeEnvironmentUpdatedEl bloque que cambia el código lo deja escrito en eventos y en la cabecera.
Estado
:code, spec_version y LastRuntimeUpgradeEl Wasm vive en el estado; la versión decide si corren las migraciones.
Por qué una cadena Substrate puede cambiar sin fork
La documentación del SDK parte de la definición de siempre: la lógica de una cadena es estado más función de transición. Antes de Substrate, cambiar esa función obligaba a los operadores a descargar software nuevo y reiniciar, es decir, a un fork. Las cadenas Substrate no lo necesitan y actualizan el runtime con un proceso llamado runtime upgrade. [1] Eso es posible porque el runtime vive en el propio estado.
Ya viste las dos piezas. En el runtime en Wasm, el blob se guarda bajo la clave :code. En el estado, el nodo trata todo el estado como bytes opacos. Cambiar la lógica es, entonces, escribir otro valor bajo :code: una transición de estado más, sujeta a las mismas reglas de consenso que una transferencia.
La especificación fija la obligación del lado del nodo: el ejecutor debe cargar siempre el runtime más reciente del storage, tomando el del bloque padre al importar y el del mejor bloque al producir. [2] Por eso todos los nodos cambian de runtime en el mismo bloque sin coordinarse: el cambio lo dicta el estado, no la fecha en que cada operador actualiza.
set_code: quién puede y qué se comprueba
La documentación lo resume así: un origen con los permisos necesarios, normalmente vía gobernanza, cambia el storage :code, en general llamando a set_code con el blob nuevo y programando la llamada con pallet_scheduler. [1] En una red de desarrollo ese origen suele ser una cuenta sudo; en producción, un referéndum. [3]
El código de frame_system muestra qué pasa dentro: [4]
// Fragmento de substrate/frame/system/src/lib.rs (polkadot-sdk)
#[pallet::weight((T::SystemWeightInfo::set_code(), DispatchClass::Operational))]
pub fn set_code(origin: OriginFor<T>, code: Vec<u8>) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
Self::can_set_code(&code, true).into_result()?;
T::OnSetCode::set_code(code)?;
// consume the rest of the block to prevent further transactions
Ok(Some(T::BlockWeights::get().max_block).into())
}Cuatro detalles merecen nombre:
ensure_root: solo el origen Root. Qué cuenta, colectivo o referéndum se convierte en Root lo decide cada runtime.can_set_code: lee la versión del Wasm nuevo y exige el mismospec_namey unaspec_versionmayor. Si no, falla conInvalidSpecNameoSpecVersionNeedsToIncrease.- Clase
Operational: puede usar el bloque entero, como viste en pesos y comisiones. - Peso real igual al máximo del bloque: la llamada consume todo lo que queda para que no entren más transacciones detrás de ella.
Hay variantes. set_code_without_checks omite la comprobación de versión; el propio código advierte que, con una versión que no sube, las migraciones no se ejecutarán. Y existe una ruta en dos fases: authorize_upgrade autoriza solo el hash del código, y después cualquiera puede subir el blob con apply_authorized_upgrade. Sirve cuando mover el código entero por gobernanza o por un puente es incómodo.
RuntimeVersion: el número que todos miran
Cada runtime declara una RuntimeVersion. Los campos que importan aquí son spec_name, que identifica la cadena, y spec_version, que identifica la versión de la lógica. Olvidar subir spec_version es, según la guía de actualización, un error común: la cadena no reconoce el runtime como nuevo. [3] Con set_code la llamada directamente falla; con la variante sin comprobaciones, el código cambia pero las migraciones no corren.
La versión también la usan los clientes. En extrinsics viste que CheckSpecVersion mete spec_version en lo que se firma. Una actualización invalida las firmas pendientes, y una billetera debe volver a leer la versión y la metadata cuando la cadena cambia de runtime.
Del bloque que cambia el código al primero que lo usa
El orden de los hechos en el tiempo es lo que más confunde. La figura lo recorre con el camino clásico, en el que set_code escribe directamente en :code.
Una actualización del runtime, bloque a bloque
Cómo leer esta figura
Dos columnas de bloque, N y N+1, sobre una franja superior que representa al nodo con el mismo binario en ambos bloques. En el bloque N, ejecutado con el runtime v100: un extrinsic set_code con origen Root; can_set_code comprueba spec_name igual y spec_version mayor; se escribe :code; se emiten el evento CodeUpdated y el digest RuntimeEnvironmentUpdated; la llamada consume el resto del peso del bloque. Una flecha va del estado final de N al inicio de N+1 con la etiqueta el nodo carga :code del estado padre. En el bloque N+1, ejecutado con v101: initialize_block compara LastRuntimeUpgrade con la versión actual; como subió, corre on_runtime_upgrade con las migraciones; actualiza LastRuntimeUpgrade; luego on_initialize, inherentes y transacciones. Una nota indica que en runtimes con system_version 3 o mayor el Wasm espera en :pending_code y se aplica unos bloques después.
En el bloque N el runtime viejo ejecuta set_code, escribe el Wasm nuevo y deja dos rastros: el evento CodeUpdated y un ítem RuntimeEnvironmentUpdated en el digest de la cabecera, que avisa a quien sigue la cadena que el entorno cambió. En el bloque N+1 el nodo toma :code del estado del padre, de modo que ese bloque ya corre con la lógica nueva.
Lo primero que hace el runtime nuevo es comprobar si hubo actualización. frame_executive compara el valor guardado LastRuntimeUpgrade con la versión actual; si spec_version subió o spec_name cambió, ejecuta on_runtime_upgrade antes que on_initialize y guarda la versión nueva. [5] Ese es el momento en que corren las migraciones.
Hay una variante reciente. En runtimes cuya system_version es 3 o mayor, frame_system no escribe en :code de inmediato: guarda el blob en :pending_code y lo copia a :code al finalizar un bloque posterior, con un contador de bloques. [4] Cuántos bloques exactos pasan entre set_code y el primer bloque con el código nuevo en esa variante es un dato a validar en una red local con la versión del SDK que uses. Las parachains, además, difieren la aplicación hasta que la relay chain la acepta; se verá en el capítulo de Cumulus.
Migraciones: adaptar los datos a la lógica nueva
Una migración es lógica autocontenida que corre después de una actualización. El uso típico es pasar el storage de un pallet de un formato a otro, como en el caso del principio, pero también puede limpiar items viejos o llamar a funciones de otros pallets. [1]
Migración de un bloque o de varios
La advertencia más útil de la documentación es la de los tests: que los unitarios pasen no significa que la migración sea segura. Antes de desplegar hay que comprobar que no entra en pánico, que no toca tantas claves como para producir un PoV excesivo y que no tarda demasiado. La herramienta es try-runtime-cli con su subcomando on-runtime-upgrade, que ejecuta la migración contra el estado real de la cadena; la documentación dice que los desarrolladores deben correrlo siempre, idealmente en CI. Chopsticks es otra opción para el mismo fin.
Ni siquiera eso basta frente a un adversario. try-runtime-cli mide con el estado de hoy, y alguien puede inflar ese estado antes del bloque de la migración. Por eso la regla es no recorrer en una migración de un bloque storage que se pueda agrandar rápido o barato; si no hay alternativa, usar una de varios bloques.
El límite: cuándo sí hay que actualizar el nodo
"Sin fork" tiene una condición. El runtime llama al nodo mediante host functions, y el nodo debe implementar todas las que el runtime necesite. Como un nodo quiere poder re-ejecutar bloques viejos, debe mantener para siempre el soporte de las host functions antiguas, y agregar una nueva es un compromiso grande. [6]
La consecuencia operativa: si el runtime nuevo usa una host function que el nodo no tiene, primero hay que distribuir un binario de nodo que la implemente y esperar a que lo adopten los validadores; después, actualizar el runtime. Mientras el cambio quede dentro del runtime, ningún operador tiene que hacer nada. Lo mismo vale para el consenso, que vive en el nodo: cambiar de algoritmo de consenso no es un runtime upgrade.
Qué cambia respecto de Ethereum
En Ethereum, cambiar las reglas del protocolo es un hard fork: cada equipo de cliente publica una versión y los operadores actualizan antes de una altura acordada. En Substrate la lógica de aplicación y buena parte del protocolo de la cadena viven en el runtime, así que el mismo cambio se decide en la cadena y se aplica en un bloque concreto.
El paralelo más cercano no está en el protocolo de Ethereum sino en sus contratos. Los contratos actualizables con proxies resuelven el mismo problema en otra capa: separan el código del almacenamiento y apuntan a una implementación nueva. Los riesgos coinciden: quién tiene la llave de la actualización y qué pasa si el diseño del almacenamiento cambia sin migrar.
La pregunta de quién autoriza es de gobernanza, no de tecnología. La gobernanza en la cadena analiza cómo una votación se convierte en autoridad para cambiar reglas, y qué demoras protegen a los usuarios que no están de acuerdo.
Decisiones antes de la primera actualización en producción
Antes de llamar a set_code fuera de una red de pruebas
0 de 6 revisados
El siguiente capítulo, offchain workers, muestra una ventaja lateral de que la lógica viaje en el Wasm: el código que corre fuera de la cadena se actualiza junto con el runtime.
Bibliografía académica
- [1]
Parity Technologies, "Runtime Upgrades," polkadot_sdk_docs, paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Actualización sin fork, set_code por un origen con permiso, migraciones single-block y multi-block, try-runtime-cli y riesgo de PoV manipulable.
- [2]
Web3 Foundation, "States and Transitions," Polkadot Protocol Specification, w3f/polkadot-spec, GitHub repository, accedido 2026-11-13.
El ejecutor debe cargar siempre el runtime más reciente del storage, según el bloque padre al importar; blobs Wasm comprimidos con zstd.
- [3]
Polkadot Developers, "Runtime Upgrades," polkadot-developers/polkadot-docs, GitHub repository (fuente de docs.polkadot.com), accedido 2026-11-13.
Tutorial de actualización con sudo y setCode; advertencia sobre no subir spec_version; en producción se usa gobernanza.
- [4]
Parity Technologies, "frame/system/src/lib.rs," paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
set_code y set_code_without_checks, can_set_code (spec_name igual, spec_version mayor), authorize_upgrade, :pending_code, evento CodeUpdated y digest RuntimeEnvironmentUpdated, LastRuntimeUpgrade.
- [5]
Parity Technologies, "frame/executive/src/lib.rs," paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
initialize_block ejecuta on_runtime_upgrade si runtime_upgraded() compara LastRuntimeUpgrade con la versión actual; diagrama de ejecución de bloque.
- [6]
Parity Technologies, "WASM Meta Protocol," polkadot_sdk_docs, paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Host functions y runtime APIs; el nodo debe mantener las host functions viejas; agregar una nueva es un compromiso que en Polkadot exige un RFC.
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.
Extrinsics: transacciones firmadas, sin firmar e inherentes
Transacciones firmadas, sin firmar e inherentes: los bytes de un extrinsic versión 5, el payload que se firma y las extensiones que validan génesis, mortalidad, nonce, versión y comisión.
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.