Substrate: del runtime al consenso propio
Artículo 8 de 20
40%
completado al llegar aquí
Sistemas distribuidosNota de investigación13 min de lectura
Pesos y comisiones: medir antes de ejecutar
Por qué Substrate no mide gas instrucción por instrucción, qué son ref_time y proof_size, cómo se escribe un benchmark del peor caso y cómo se calcula la comisión.
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.
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 publica un pallet de listas de distribución. La llamada notify_all recorre la lista de suscriptores de un canal y escribe un registro por cada uno. En pruebas, con diez suscriptores, el benchmark devolvió un peso pequeño y la comisión salió barata. Un mes después alguien crea un canal con cincuenta mil suscriptores y llama a notify_all. El autor del bloque incluye la transacción confiando en el peso declarado, la ejecución tarda mucho más de lo previsto y el bloque llega tarde a su turno.
El error no estuvo en la red ni en el consenso. Estuvo en medir el caso promedio en vez del peor. Este capítulo explica por qué Substrate (Polkadot SDK) necesita esa medición previa, qué mide exactamente y cómo de ahí sale lo que paga el usuario.
Brújula de lectura
Actor, evidencia y estado
Actor
Quien escribe el pallet y el autor del bloqueEl primero declara el peso con benchmarks; el segundo llena el bloque confiando en esa cifra.
Evidencia
Weight (ref_time, proof_size)El resultado del benchmark, declarado en la llamada antes de ejecutarla.
Estado
Saldo del emisor y multiplicador de comisiónLa comisión se retira antes de ejecutar; el multiplicador cambia al final de cada bloque.
Por qué no hay gas instrucción por instrucción
En la EVM cada instrucción tiene un costo fijo, la ejecución acumula gas y se detiene al llegar al límite. Eso se llama medición (metering). La documentación del SDK explica por qué el runtime de Substrate no funciona así: Wasm es una máquina de pila con un juego de instrucciones más complejo y tiempos de ejecución menos predecibles, y por eso el runtime en Wasm no se supone medido. [1]
La segunda razón es de confianza. La EVM ejecuta contratos ajenos, que pueden ser maliciosos; sin medición, un contrato podría consumir recursos sin límite. El runtime, en cambio, es código confiable escrito por el mismo equipo que opera la cadena. Si recuerdas el runtime en Wasm, el runtime es la función de transición de la propia cadena, no un programa que sube cualquiera.
Sin medición en vivo sigue haciendo falta una cota. El autor de un bloque tiene un tiempo limitado para armarlo y necesita saber, antes de ejecutar una transacción, si cabe. La documentación pone el ejemplo de un autor al que le quedan 20 ms: en un entorno medido no sabría qué transacción cabe; en uno con benchmarks mira la cota superior de cada una e incluye las que entran en el peor caso. [1]
Un peso con dos dimensiones
Esa cota es el peso (sp_weights::Weight). No es un número sino un par:
ref_time: el tiempo de ejecución en picosegundos sobre un hardware de referencia.proof_size: cuánta prueba de storage hace falta para volver a ejecutar ese código. Importa sobre todo cuando una parachain tiene que enviar su bloque a la relay chain para que lo validen.
La segunda dimensión conecta con el capítulo del estado. Quien re-ejecuta un bloque sin tener el estado completo necesita los nodos del trie de cada clave que el bloque leyó o escribió. Cada lectura de storage cuesta tiempo y además engorda esa prueba. Por eso un bloque puede llenarse por tiempo o por tamaño de prueba, lo que llegue primero.
Las lecturas y escrituras se cuentan por clave única: varias lecturas del mismo valor cuentan como una, porque después de la primera el valor queda en caché. [2]
El benchmark mide el peor caso
La regla más importante sobre benchmarks está escrita sin matices en la documentación: el código de benchmark debe capturar la ejecución del peor caso de cualquier bloque de código. [1] Es un análisis estático: se conoce la cota antes de ejecutar, en la fase que FRAME llama pre-dispatch.
Si una llamada recorre una lista, el benchmark debe usar el tamaño máximo de la lista, y el pallet debe imponer ese máximo. En el caso del principio, el arreglo tiene dos partes: acotar los suscriptores por canal y declarar un peso que dependa del tamaño, por ejemplo con un argumento que diga cuántos hay.
El flujo de trabajo tiene tres piezas. Cada pallet trae su propio código de benchmark, protegido detrás de la feature runtime-benchmarks. Se ejecuta con el subcomando benchmark del nodo o con frame-omni-bencher, que solo necesita el runtime. El resultado genera una implementación del trait WeightInfo que el pallet recibe por su Config y usa en el atributo #[pallet::weight] de cada llamada.
Cuando el peor caso es mucho más caro que el caso habitual, la llamada puede devolver su peso real al terminar y el sistema reembolsa la diferencia:
// Fragmento ilustrativo, simplificado de
// docs/sdk/src/reference_docs/frame_benchmarking_weight.rs (polkadot-sdk)
#[pallet::weight(T::WeightInfo::simple_transfer())] // peor caso, pre-dispatch
pub fn simple_transfer_3(origin: OriginFor<T>, dest: T::AccountId, amount: u32)
-> DispatchResultWithPostInfo
{
if destination_exists {
Ok(().into()) // camino barato: se cobra el peso declarado
} else {
let actual_weight = /* otro benchmark, no el del peor caso */;
Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes })
}
}El reembolso solo puede bajar el peso, nunca subirlo: si la llamada informa un peso real mayor que el declarado, FRAME registra un error en el log e ignora los componentes que exceden. [3] El peso declarado sigue siendo la promesa que el autor del bloque usó para decidir que la transacción cabía.
Cuánto cabe en un bloque
Cada bloque tiene un peso máximo, y no todo está disponible para los usuarios. FRAME separa las llamadas en tres clases de despacho: [2]
- Normal: las transacciones habituales. Solo pueden usar una fracción del bloque, configurada con
AvailableBlockRatio. - Operational: las que mantienen la red funcionando, como una actualización del runtime. Pueden usar el bloque entero, tienen prioridad máxima y no pagan comisión por longitud.
- Mandatory: los inherentes. Entran aunque el bloque supere su límite, porque es preferible un bloque pesado a no tener bloque. A cambio deben ser livianas y aparecer una sola vez por bloque.
La extensión CheckWeight, que vimos en extrinsics, es la que comprueba que una transacción cabe antes de despacharla. [4] Las extensiones WeightReclaim y, en parachains, StorageWeightReclaim devuelven al bloque el peso no usado tras ejecutar.
De peso a comisión
El peso dice cuánto cabe; la comisión dice cuánto se paga. El pallet transaction-payment define la fórmula: [5]
inclusion_fee = base_fee + length_fee + [targeted_fee_adjustment * weight_fee]
final_fee = inclusion_fee + tipbase_fee: el mínimo por existir, derivado de un peso base que cubre, por ejemplo, la verificación de la firma.length_fee: un precio por byte del extrinsic codificado.weight_fee: el peso convertido a moneda conWeightToFee.targeted_fee_adjustment: un multiplicador que sube o baja con la congestión.tip: una propina opcional que aumenta la prioridad en el pool.
La comisión se retira antes de ejecutar y se cobra aunque la llamada falle. Si el peso real resulta menor que el declarado, la comisión se corrige y se devuelve la diferencia. [2]
Vida de una transacción en pesos y comisiones
Cómo leer esta figura
Cinco pasos de izquierda a derecha. Uno: el benchmark del peor caso fija el peso declarado, un par ref_time y proof_size. Dos: CheckWeight comprueba que ese peso cabe en la parte del bloque disponible para su clase. Tres: ChargeTransactionPayment retira base_fee más length_fee más multiplicador por weight_fee más propina. Cuatro: la llamada se ejecuta y puede devolver un peso real menor. Cinco: se reembolsa la diferencia de comisión y se devuelve peso al bloque. Debajo, una flecha de retorno indica que al final del bloque el multiplicador se actualiza según lo lleno que quedó respecto del objetivo, en la dimensión limitante, y afecta a las transacciones del bloque siguiente.
El multiplicador que sigue a la congestión
La comisión por peso se multiplica por un factor que cambia de bloque en bloque. La implementación por defecto, TargetedFeeAdjustment, compara el peso del bloque anterior (s) con un peso objetivo (s'), normaliza la diferencia por el peso máximo (m) y actualiza: [5]
Si el bloque vino más lleno que el objetivo, diff es positivo y la comisión sube un poco; si vino más vacío, baja. La variable v controla la velocidad. Como el peso tiene dos dimensiones, el cálculo usa la dimensión limitante: la que esté más cerca de su máximo en proporción. Un bloque al 30 % en tiempo y al 25 % en prueba cuenta como lleno al 30 %.
El código trae un ejemplo numérico. Con 28 800 bloques por día, v igual a 0,00001 y un objetivo de 0,1875 del bloque, en congestión extrema total la comisión puede cambiar en torno a un 23 % por día. Es un ajuste lento a propósito: suaviza picos y hace que un ataque de saturación sostenido cueste cada vez más.
Las transacciones con necesidades especiales no se resuelven con la fórmula. Depósitos que se devuelven al liberar storage, fianzas que se pierden por mal comportamiento o límites fijos sobre una operación son herramientas aparte que cada pallet decide usar.
Qué cambia respecto de Ethereum
Si vienes de la transacción de Ethereum y el gas, el contraste es casi simétrico.
Dos modelos de costo
Medir durante o medir antes
Ethereum: gas
Cada instrucción suma gas mientras corre; al llegar al límite, la ejecución se detiene.
- Pensado para código ajeno y no confiable.
- El usuario fija un límite de gas y paga lo consumido.
Substrate: weight
Cada llamada declara su peor caso antes de correr.
- Pensado para código confiable de la propia cadena.
- Un benchmark mal escrito es un bug de seguridad, no un detalle de precio.
El ajuste por congestión también tiene un pariente. EIP-1559 mueve una base fee por bloque según lo lleno que vino el anterior. El multiplicador de Substrate persigue el mismo objetivo, pero es una pieza del runtime que cada cadena configura o reemplaza, no una regla del protocolo.
Un matiz para no confundirse: los contratos que corren sobre una cadena Substrate sí necesitan medición, porque son código ajeno. La documentación menciona que PolkaVM, una máquina virtual capaz de medir, podría volver innecesarios los benchmarks en el futuro. Los contratos quedan fuera del alcance de esta ruta.
Decisiones para quien escribe pallets
Antes de dar por bueno el peso de una llamada
0 de 5 revisados
El siguiente capítulo, actualizar sin fork, muestra una llamada que usa este sistema de una manera llamativa: la que cambia el runtime consume a propósito todo el peso que queda en el bloque.
Bibliografía académica
- [1]
Parity Technologies, "FRAME Benchmarking and Weights," polkadot_sdk_docs, paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Wasm sin metering, runtime como código confiable, benchmarking estático del peor caso, Weight con ref_time y proof_size, reembolso manual y frame-omni-bencher.
- [2]
Polkadot Developers, "Transactions Weights and Fees," polkadot-developers/polkadot-docs, GitHub repository (fuente de docs.polkadot.com), accedido 2026-11-13.
Fórmula de inclusion_fee y final_fee, retiro previo de la comisión, conteo de lecturas y escrituras, clases Normal, Operational y Mandatory, corrección post-dispatch.
- [3]
Parity Technologies, "frame/support/src/dispatch.rs (PostDispatchInfo::calc_actual_weight)," paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
El peso post-dispatch mayor que el pre-dispatch se ignora y se registra como error.
- [4]
Parity Technologies, "Transaction Extensions," polkadot_sdk_docs, paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
CheckWeight comprueba que la transacción cabe en el bloque; ChargeTransactionPayment cobra según el peso; WeightReclaim y StorageWeightReclaim reclaman peso no usado.
- [5]
Parity Technologies, "frame/transaction-payment/src/lib.rs," paritytech/polkadot-sdk, GitHub repository, accedido 2026-11-13.
Fórmula inclusion_fee y final_fee; TargetedFeeAdjustment con s, s', m, v; dimensión limitante; ejemplo de unos 23 % por día con congestión extrema.
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.