Substrate: del runtime al consenso propio

Artículo 19 de 20

95%

completado al llegar aquí

Sistemas distribuidosNota de investigación21 min de lectura

Anatomía del consenso en un nodo Substrate

El recorrido de un bloque por el nodo, leído en el código del Polkadot SDK: import queue, BlockImport, fork choice, justificaciones, finalidad y autoría.

Ruta Substrate: 0/20

Una mañana, un nodo de una red privada construida con Substrate (Polkadot SDK) deja de finalizar. Sigue importando bloques de Aura, la altura sube, pero el último bloque final no se mueve. En los logs aparece un aviso de GRANDPA sobre una justificación que no se pudo importar. El equipo de guardia tiene que responder rápido: ¿el problema está en la red, en la import queue, en la cadena de BlockImport o en el voter de GRANDPA? Para contestar hace falta un mapa del nodo.

Este capítulo dibuja ese mapa. Sigue un bloque desde que llega por red hasta que una notificación anuncia que es final, y otro desde que el nodo decide producirlo. En cada tramo nombra el tipo de Rust que lo implementa y el archivo donde leerlo. Es el capítulo que prepara el capstone: conectar un consenso propio exige saber qué pieza se reemplaza y cuál se conserva.

Brújula de lectura

Actor, evidencia y estado

Antes de avanzar, ubica quién actúa, qué prueba queda y qué parte del sistema cambia.

Actor

Tareas del nodo: import queue, slot worker, voter

Cada motor de consenso es un conjunto de tareas y envoltorios que el servicio del nodo cablea.

Evidencia

Sello en la cabecera y justificación aparte

El sello prueba quién produjo el bloque; la justificación prueba que es final.

Estado

Mejor bloque y último bloque finalizado

Dos punteros distintos en la base de datos del cliente, movidos por piezas distintas.

Preguntas guía

  • ¿Qué hace la import queue y en qué se diferencia de la cadena de BlockImport?
  • ¿Dónde se decide cuál es el mejor bloque y dónde cuál es el último final?
  • ¿Qué es exactamente una justificación en el código y por qué no va dentro del bloque?
  • ¿Qué tiene que coincidir entre el nodo y el runtime para que un motor funcione?

Conexiones dentro de la serie

Sin guía oficial: leer el código

Lo primero que conviene saber es incómodo. La documentación del SDK tiene un archivo docs/sdk/src/guides/changing_consensus.rs, pero su único contenido es el título # Changing Consensus. No hay un tutorial oficial paso a paso para cambiar el consenso de un nodo. Este capítulo trabaja, por tanto, desde el código fuente y la documentación en línea de los crates.

El inventario sí está documentado. La página de Substrate en el SDK lista "All of the consensus related crates provided by Substrate": Aura, BABE, GRANDPA, BEEFY, manual seal y prueba de trabajo. Todos se apoyan en dos crates comunes: sc-consensus (import queue, block import, LongestChain) y sp-consensus (Environment, Proposer, SelectChain).

La regla que más errores evita está en la documentación del omni node: "In any given substrate-based chain, both the node and the runtime will have their own opinion/information about what consensus engine is going to be used." La mayor parte del motor vive en el nodo, pero el runtime expone una runtime API propia del motor, implementada por su pallet. La conclusión del documento va en negrita: nodo y runtime "must have matching consensus components."

En la práctica, esa regla tiene dos caras. Si el nodo ejecuta Aura y el runtime no implementa AuraApi, el nodo no puede saber la duración del slot ni las autoridades. Si el runtime incluye pallet_grandpa pero el nodo no arranca el voter, la cadena produce bloques y nunca finaliza: exactamente el síntoma del escenario.

La puerta de entrada: import queue y Verifier

Todo bloque ajeno entra por la import queue. El código la define como "an intermediate and abstracted step between synchronization and import. Each mode of consensus will have its own requirements for block verification." Sync le entrega bloques como IncomingBlock, que pueden traer cabecera, cuerpo y justificaciones.

La pieza que cada motor aporta es el Verifier, un trait de un solo método: verificar los datos del bloque y "return the BlockImportParams to continue the block import process." El verificador no guarda nada: comprueba lo que se puede comprobar sin ejecutar el bloque y prepara los parámetros para el siguiente tramo.

El verificador de Aura es un buen ejemplo. Comprueba el sello de la cabecera contra la autoridad del slot y, si es válido, quita el sello de la cabecera, lo añade a post_digests y fija la estrategia de fork choice en LongestChain. Sus errores dicen qué falló: cabecera sin sello, sello inválido, firma inválida o autor del slot no encontrado.

¿Por qué quitar el sello? Porque el runtime no debe verlo. El código de BlockImportParams lo explica: los motores que alteran la cabecera con digests posteriores al runtime "should strip those off in the initial verification process and pass them via the post_digests field." Así la cabecera puede reejecutarse tal como salió del runtime, y el sello se vuelve a poner después.

Los digests tienen cuatro variantes relevantes en sp-runtime: PreRuntime (del motor al runtime, como el slot), Consensus (del runtime al motor, como un cambio de autoridades), Seal (solo del código nativo, nunca visto por el runtime) y RuntimeEnvironmentUpdated. Cada variante lleva un ConsensusEngineId de cuatro bytes que identifica al motor.

La cadena de BlockImport

Tras el verificador, los BlockImportParams recorren una cadena de objetos que implementan el trait BlockImport, con dos métodos: check_block e import_block. Cada eslabón envuelve al siguiente; el último es el propio cliente, que ejecuta el runtime y escribe en la base de datos.

Los campos de BlockImportParams son el contrato entre eslabones. Los que importan para el consenso:

  • justifications: "Justification(s) provided for this block from the outside."
  • finalized: si el bloque ya es final; true "implies instant finality".
  • fork_choice: la estrategia para decidir si es el nuevo mejor bloque.
  • intermediates: valores que un eslabón deja para otro; cada uno debe consumir los suyos, y el último rechaza la importación si queda alguno sin tratar.

El patrón de envoltorio es el que usa GRANDPA. Su crate lo exige sin matices: "All blocks imported (from network or consensus or otherwise) must pass through this wrapper, otherwise consensus is likely to break in unexpected ways." El envoltorio inspecciona cada bloque en busca de señales de cambio de autoridades. Si un bloque aplica un cambio y el conjunto actual ya no está vivo, el bloque debe traer una justificación válida.

La consecuencia práctica es una regla de cableado: el mismo objeto envuelto se usa en la import queue y en la autoría. Si el bloque propio se importara saltándose el envoltorio, GRANDPA no vería sus cambios de autoridades. BEEFY, en el nodo kitchensink, añade un eslabón más por encima del de GRANDPA.

Mejor bloque y bloque final: dos decisiones

El nodo mantiene dos punteros distintos. El mejor bloque se decide al importar, con ForkChoiceStrategy: LongestChain o Custom(bool), donde el booleano es una regla propia: "true indicates the new block should be the best block." Si nadie fija la estrategia, la importación falla: el campo None exige que un eslabón posterior lo complete.

El trait SelectChain responde una pregunta parecida desde fuera de la importación: entre las hojas de la cadena, cuál es la mejor para construir encima y cuál conviene intentar finalizar. Su documentación separa ambos usos: la estrategia "can be customized for the two use cases of authoring new blocks upon the best chain or which fork to finalize." El template usa LongestChain, y GRANDPA lo recibe para elegir hacia dónde votar.

El último bloque final no lo mueve la importación normal, sino una justificación. En sp-runtime, una justificación es una pareja (ConsensusEngineId, EncodedJustification): un identificador de motor de cuatro bytes y una prueba opaca. GRANDPA usa el identificador FRNK. Un bloque puede tener varias justificaciones de motores distintos.

¿Por qué no va dentro del bloque? La documentación lo explica: si hay varias pruebas válidas, incluirlas en el bloque permitiría cambiar su hash intercambiando justificaciones, "and thus fork the chain". Por eso viaja al lado del bloque, por sync, y se guarda aparte.

Justificaciones, finalidad y notificaciones

Una justificación puede llegar por dos caminos. El primero es el voter local: cuando GRANDPA concluye una ronda, finaliza el bloque y guarda su justificación. El segundo es la red: un par envía un bloque con justificación o la justificación sola. Para ese caso existe el trait JustificationImport, cuyo método import_justification debe "Import a Block justification and finalize the given block."

La import queue también puede pedir justificaciones. El trait Link reúne los "Hooks that the verification queue can use to influence the synchronization algorithm", entre ellos request_justification. El resultado de importar un bloque, ImportedAux, tiene banderas para ello: needs_justification y bad_justification. Así un envoltorio como el de GRANDPA dice "este bloque cambia autoridades, consigue su justificación".

Finalizar es una operación del cliente. El trait Finalizer documenta que finalizar un bloque "will implicitly finalize all blocks up to it and fire finality notifications." Si se pasa una justificación, se guarda con ese bloque; los antecesores quedan finales sin justificación propia.

Quien necesite reaccionar a la finalidad se suscribe. El cliente ofrece import_notification_stream y finality_notification_stream, con una advertencia que cualquier integración debe leer: la segunda "Not guaranteed to be fired for every finalized block." Cada FinalityNotification incluye la ruta de bloques finalizados implícitamente; un consumidor que solo mire el hash final perderá bloques intermedios.

Volviendo al escenario: un aviso de justificación no importada, con bloques que siguen entrando, apunta al tramo de JustificationImport o al voter, no a la import queue de bloques. Ese diagnóstico solo es posible con el mapa.

Autoría: slot worker, Environment y Proposer

Producir un bloque propio es el camino inverso. En Aura, una tarea de slot worker se despierta en cada slot, comprueba si la clave local es la autoridad del turno y, si lo es, pide un bloque. El crate de slots permite además frenar la autoría cuando la finalidad se atrasa: "An example strategy that back offs if the finalized head is lagging too much behind the tip is implemented by [BackoffAuthoringOnFinalizedHeadLagging]." El solochain template la deja desactivada.

El bloque lo arma un Proposer, creado por un Environment. sp-consensus describe al proposer como lógica que encapsula la creación de propuestas sobre un bloque padre, y añade: "Proposers are generic over bits of “consensus data” which are engine-specific." El proposer básico (sc-basic-authorship) toma transacciones del pool, ejecuta el runtime y devuelve el bloque con sus cambios de almacenamiento.

El motor añade lo suyo antes y después: el digest PreRuntime con el slot y, al final, el sello firmado. Después, el bloque sellado entra por la misma cadena de BlockImport que los bloques ajenos. Esa simetría es deliberada: el envoltorio de GRANDPA ve igual un bloque propio que uno de la red.

El cableado completo en el solochain template

Todo lo anterior se ve junto en un archivo: templates/solochain/node/src/service.rs. El fragmento siguiente lo resume; se han quitado tipos genéricos, telemetría y proveedores de inherentes.

rust
// Fragmento ilustrativo, simplificado de templates/solochain/node/src/service.rs (polkadot-sdk)
let select_chain = sc_consensus::LongestChain::new(backend.clone());
 
// 1. El envoltorio de GRANDPA y su enlace con el voter.
let (grandpa_block_import, grandpa_link) = sc_consensus_grandpa::block_import(
    client.clone(), GRANDPA_JUSTIFICATION_PERIOD, &client, select_chain.clone(), telemetry,
)?;
 
// 2. Import queue de Aura: verifica sellos e importa a través del envoltorio.
let import_queue = sc_consensus_aura::import_queue(ImportQueueParams {
    block_import: grandpa_block_import.clone(),
    justification_import: Some(Box::new(grandpa_block_import.clone())),
    client: client.clone(),
    /* inherentes (timestamp, slot), spawner, métricas */
})?;
 
// 3. Autoría con Aura (solo si el nodo es autoridad).
let aura = sc_consensus_aura::start_aura(StartAuraParams {
    block_import, proposer_factory, select_chain,
    justification_sync_link: sync_service.clone(),
    /* duración del slot, keystore, sync_oracle, ... */
})?;
 
// 4. Finalidad: el voter de GRANDPA como tarea esencial.
sc_consensus_grandpa::run_grandpa_voter(GrandpaParams { link: grandpa_link, network, /* ... */ })?;

Cuatro detalles merecen atención. El envoltorio de GRANDPA aparece dos veces en la import queue: como block_import y como justification_import. La autoría recibe el mismo block_import. El voter recibe el grandpa_link, que conecta su estado con el del envoltorio. Y la constante GRANDPA_JUSTIFICATION_PERIOD vale 512: cada cuántos bloques, como mínimo, se generan e importan justificaciones.

El servicio también registra el protocolo de notificaciones de GRANDPA en la red y un proveedor de pruebas para warp sync construido con el conjunto de autoridades de GRANDPA. Ese último detalle conecta con el capítulo de sincronización: warp sync depende de las justificaciones de los cambios de autoridades, y por eso el template instala un filtro de poda específico de GRANDPA.

Figura 1

El recorrido de un bloque por un nodo Substrate con Aura y GRANDPA

Fuentes:

[4][8][17]
El recorrido de un bloque por un nodo Substrate con Aura y GRANDPA
Cómo leer esta figura

Primera fila, importación: Red y sync con bloques y justificaciones, flecha a Import queue cuyo Verifier revisa el sello Aura, flecha al envoltorio GRANDPA, un BlockImport que vigila cambios de autoridades, y flecha al Cliente, que ejecuta el runtime, guarda y elige el mejor bloque. Segunda fila, autoría: Slot worker de Aura que pregunta si le toca el slot, flecha al Proposer que arma el bloque con el runtime, y una flecha que sube hasta el envoltorio GRANDPA con la etiqueta sella e importa. Tercera fila, finalidad: Voter GRANDPA con rondas por gossip, flecha a Justificación (FRNK, bytes), flecha a finalize_block, que guarda la justificación, y flecha a Notificación de finalidad. Una flecha discontinua baja del Cliente a la Notificación con la etiqueta el mismo cliente emite importación y finalidad.

Visual original de TrautsLab. Tres caminos en el nodo del solochain template: la importación de bloques ajenos, la autoría de bloques propios que entra por la misma puerta y la finalidad, que produce y guarda justificaciones y emite notificaciones.

Flujo visual

Qué reemplazar y qué conservar al cambiar un motor

  1. Producciónslot worker y Verifier del motor nuevo; Proposer básico se conserva
  2. Import queuenueva si cambia el sello; BasicQueue sirve casi siempre
  3. BlockImportenvoltorio propio si el motor vigila autoridades
  4. Finalidadvoter y JustificationImport propios, con su engine id
  5. Runtimepallet y runtime API del motor; el nodo debe coincidir
  6. Syncjustificaciones para warp sync y light clients

Qué cambia respecto de Ethereum

En Ethereum después de The Merge, la separación entre producir y ejecutar es una frontera entre procesos: un cliente de consenso y un cliente de ejecución que hablan por la Engine API, como explica el capítulo sobre ejecución y consenso. Cambiar el consenso de Ethereum es cambiar la especificación de toda la red.

En Substrate, la frontera está dentro de un mismo binario: el runtime ejecuta, y las tareas del nodo producen, importan y finalizan. Cambiar el consenso es cambiar qué tareas y envoltorios cablea el servicio, más el pallet y la runtime API del motor. La flexibilidad es mucho mayor, y también la responsabilidad: no hay una especificación externa que te diga si tu cableado es correcto.

La regla de fork choice también se reparte de otra forma. En Ethereum, Gasper combina LMD-GHOST y Casper FFG en una regla única de la especificación. En Substrate, la mejor cadena se decide al importar (ForkChoiceStrategy), la elección para autoría y finalidad la hace SelectChain, y la finalidad la decide un gadget aparte. Cada pieza es reemplazable por separado.

Laboratorio propuesto

Estado: propuesta. Este laboratorio todavía no está publicado en lab.trautslab.dev; describe lo que se construirá, en local, sin red pública ni fondos reales.

Objetivo. Seguir un bloque por el pipeline completo y ver qué piezas cambian al sustituir el motor de producción.

Pasos propuestos.

  1. Partir del solochain template con dos autoridades locales y arrancar los nodos con logs detallados de sync, import queue, Aura y GRANDPA (por ejemplo, con la opción -l y los objetivos de log de cada crate).
  2. Elegir un bloque y seguirlo: anuncio por red, verificación del sello, importación, nuevo mejor bloque, voto de GRANDPA y notificación de finalidad. Anotar la marca de tiempo de cada evento.
  3. Sustituir Aura por manual seal en el servicio del nodo, producir bloques con la llamada RPC engine_createBlock y anotar qué piezas del cableado cambian y cuáles se conservan.

Evidencia esperada. Una línea de tiempo por bloque con al menos cinco eventos, en el orden del diagrama, y una diferencia entre importación y finalidad del orden de varios bloques. En el paso 3, un servicio sin slot worker ni verificador de Aura, con la import queue de manual seal y el resto del cableado intacto. Los objetivos de log exactos y el comportamiento de GRANDPA con manual seal quedan a validar al construir el laboratorio.

Referencias

Bibliografía académica

Formato IEEENumerado para ingeniería, sistemas y protocolos.
  1. [1]

    Parity Technologies, "polkadot_sdk_docs: Changing Consensus (guía vacía)," polkadot-sdk, docs/sdk/src/guides/changing_consensus.rs, commit 544e829, 2026.

    source codeID: SDK-CHANGING-CONSENSUSAbrir fuente (abre en otra pestaña)

    El archivo solo contiene el título; no hay guía oficial paso a paso para cambiar el consenso.

  2. [2]

    Parity Technologies, "polkadot_sdk_docs: Substrate," polkadot-sdk, docs/sdk/src/polkadot_sdk/substrate.rs, commit 544e829, 2026.

    docsID: SDK-SUBSTRATE-DOCSAbrir fuente (abre en otra pestaña)

    Lista de crates de consenso y precedente de cadenas con componentes de nodo propios.

  3. [3]

    Parity Technologies, "polkadot_sdk_docs: (Omni) Node," polkadot-sdk, docs/sdk/src/reference_docs/omni_node.rs, commit 544e829, 2026.

    Nodo y runtime deben coincidir en el motor de consenso; runtime API del motor; opciones de autoría y finalidad.

  4. [4]

    Parity Technologies, "sc-consensus: import queue," polkadot-sdk, substrate/client/consensus/common/src/import_queue.rs, commit 544e829, 2026.

    source codeID: SDK-IMPORT-QUEUEAbrir fuente (abre en otra pestaña)

    ImportQueue, ImportQueueService, Verifier, IncomingBlock con justificaciones y Link con request_justification.

  5. [5]

    Parity Technologies, "sc-consensus-aura: import queue," polkadot-sdk, substrate/client/consensus/aura/src/import_queue.rs, commit 544e829, 2026.

    source codeID: SDK-AURA-IMPORT-QUEUEAbrir fuente (abre en otra pestaña)

    Verificador de Aura: errores de sello, paso del sello a post_digests y fork_choice LongestChain.

  6. [6]

    Parity Technologies, "sc-consensus: block import," polkadot-sdk, substrate/client/consensus/common/src/block_import.rs, commit 544e829, 2026.

    source codeID: SDK-BLOCK-IMPORTAbrir fuente (abre en otra pestaña)

    BlockImportParams (post_digests, justifications, finalized, fork_choice, intermediates), ImportedAux, BlockImport, JustificationImport, JustificationSyncLink.

  7. [7]

    Parity Technologies, "sp-runtime: generic digest," polkadot-sdk, substrate/primitives/runtime/src/generic/digest.rs, commit 544e829, 2026.

    Variantes de DigestItem: PreRuntime, Consensus, Seal, RuntimeEnvironmentUpdated.

  8. [8]

    Parity Technologies, "sc-consensus-grandpa," polkadot-sdk, substrate/client/consensus/grandpa/src/lib.rs, commit 544e829, 2026.

    source codeID: SDK-GRANDPAAbrir fuente (abre en otra pestaña)

    Envoltorio de block import obligatorio, LinkHalf, run_grandpa_voter, cambios de conjunto de autoridades; crate inestable.

  9. [9]

    Parity Technologies, "sc-consensus-grandpa: import," polkadot-sdk, substrate/client/consensus/grandpa/src/import.rs, commit 544e829, 2026.

    source codeID: SDK-GRANDPA-IMPORTAbrir fuente (abre en otra pestaña)

    GrandpaBlockImport: escaneo de cambios de autoridades e importación de justificaciones con finalización.

  10. [10]

    Parity Technologies, "Kitchensink node service," polkadot-sdk, substrate/bin/node/cli/src/service.rs, commit 544e829, 2026.

    source codeID: SDK-KITCHENSINK-SERVICEAbrir fuente (abre en otra pestaña)

    Servicio con BABE, GRANDPA y BEEFY: cadena de importación con BeefyBlockImport sobre GRANDPA.

  11. [11]

    Parity Technologies, "sp-consensus: SelectChain," polkadot-sdk, substrate/primitives/consensus/common/src/select_chain.rs, commit 544e829, 2026.

    source codeID: SDK-SELECT-CHAINAbrir fuente (abre en otra pestaña)

    SelectChain: leaves, best_chain y finality_target; distinción entre autoría y finalización.

  12. [12]

    Parity Technologies, "sp-runtime: Justification y Justifications," polkadot-sdk, substrate/primitives/runtime/src/lib.rs, commit 544e829, 2026.

    source codeID: SDK-SP-RUNTIMEAbrir fuente (abre en otra pestaña)

    Justification como (ConsensusEngineId, EncodedJustification); por qué viaja fuera del bloque; varias por bloque.

  13. [13]

    Parity Technologies, "sc-client-api: backend (Finalizer)," polkadot-sdk, substrate/client/api/src/backend.rs, commit 544e829, 2026.

    source codeID: SDK-CLIENT-BACKENDAbrir fuente (abre en otra pestaña)

    Finalizer: apply_finality y finalize_block, justificación guardada con el bloque final, notificaciones de finalidad.

  14. [14]

    Parity Technologies, "sc-client-api: client," polkadot-sdk, substrate/client/api/src/client.rs, commit 544e829, 2026.

    source codeID: SDK-CLIENT-APIAbrir fuente (abre en otra pestaña)

    Flujos de notificaciones de importación y de finalidad; FinalityNotification con ruta de bloques finalizados implícitamente.

  15. [15]

    Parity Technologies, "sc-consensus-slots," polkadot-sdk, substrate/client/consensus/slots/src/lib.rs, commit 544e829, 2026.

    Slots, SlotWorker y estrategia de back-off cuando la finalidad se atrasa.

  16. [16]

    Parity Technologies, "sp-consensus: Environment, Proposer y BlockOrigin," polkadot-sdk, substrate/primitives/consensus/common/src/lib.rs, commit 544e829, 2026.

    source codeID: SDK-SP-CONSENSUSAbrir fuente (abre en otra pestaña)

    Environment crea proposers; Proposer genérico sobre datos de consenso; BlockOrigin incluye WarpSync y GapSync; crate inestable.

  17. [17]

    Parity Technologies, "Solochain template: node service," polkadot-sdk, templates/solochain/node/src/service.rs, commit 544e829, 2026.

    source codeID: SDK-SOLOCHAIN-SERVICEAbrir fuente (abre en otra pestaña)

    Cableado de Aura y GRANDPA: block_import de GRANDPA, import queue de Aura con justification_import, warp sync, start_aura y run_grandpa_voter.

Fuentes primarias, papers seminales y especificaciones técnicas citadas en esta investigación.

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.

Sigue leyendo

Ideas conectadas con este artículo

Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.

Los comentarios se cargan al final de la lectura para no meter peso antes de tiempo.