🇪🇸 La naturaleza abstracta de la capa de consenso de Cardano

:es: Traducción al español de “The abstract nature of the Cardano consensus layer”

Publicado por Edsko de Vries en el blog de IOHK el 27 de Mayo de 2020


Esta nueva serie de posteos técnicos de los ingenieros de IOHK considera las elecciones que se están haciendo

Mientras nos dirigimos a Shelley en la red principal de Cardano, queremos mantenerlos al tanto de las últimas noticias. También queremos destacar el incansable trabajo que se está llevando a cabo entre bastidores, y el equipo de ingenieros responsables.

En esta serie de blogs técnicos ocasionales de Developer Deep Dive, abrimos la palabra a los ingenieros de IOHK. En los próximos meses, nuestros desarrolladores de Haskell ofrecerán un sincero vistazo a los elementos centrales de la plataforma y los protocolos de Cardano, y compartirán ideas sobre las elecciones que se han hecho.

Aquí, en el primero de la serie, consideramos el uso de la abstracción en la capa de consenso.

Introducción

La capa de consenso de Cardano tiene dos responsabilidades importantes:

  • Dirige el protocolo de consenso de la blockchain. En el contexto de una blockchain, el consenso, es decir, la “mayoría de la opinión”, significa que todos los involucrados en la gestión de la blockchain están de acuerdo en lo que es la verdadera cadena. Esto significa que la capa de consenso se encarga de adoptar los bloques, elegir entre las cadenas competidoras, si las hay, y decidir cuándo producir bloques propios.

  • Es responsable de mantener todo el estado requerido para tomar estas decisiones. Para decidir si adoptar o no un bloque, el protocolo necesita validar ese bloque con respecto al estado del libro contable. Si decide cambiar a una cadena diferente (una púa diferente de una bifurcación en la cadena), debe mantener suficiente historia para poder reconstruir el estado del libro contable en esa cadena. Para poder producir bloques, debe mantener un mempool de transacciones para ser insertado en esos bloques.

La capa de consenso media entre la capa de red que está debajo de ella, que se ocupa de cuestiones como los protocolos de comunicación y la selección de pares, y la capa de libro contable que está encima de ella, que especifica cómo es el estado del libro contable y cómo debe actualizarse con cada nuevo bloque. La capa del libro contable es completamente sin estado, y consiste exclusivamente en funciones puras. A su vez, la capa de consenso no necesita conocer la naturaleza exacta del estado del libro contable, ni tampoco el contenido de los bloques (aparte de algunos campos de encabezamiento necesarios para ejecutar el protocolo de consenso).

Se hace un uso extensivo de la abstracción en la capa de consenso. Esto es importante por muchas razones:

  • Permite a los programadores inyectar fallos al realizar las pruebas. Por ejemplo, abstraemos el sistema de archivos subyacente, y lo usamos para probar la capa de almacenamiento mientras simulamos todo tipo de fallos de disco. De forma similar, hacemos abstracciones a lo largo del tiempo, y usamos esto para comprobar lo que le ocurre a un nodo cuando el reloj del sistema de un usuario salta hacia delante o hacia atrás.

  • Nos permite instanciar la capa de consenso con muchos tipos diferentes de libros contables. Actualmente lo usamos para ejecutar la capa de consenso con el libro contable Byron (el libro que está actualmente en la blockchain de Cardano), así como el libro contable Shelley para la próxima red de pruebas de Shelley Haskell. Además, lo usamos para ejecutar la capa de consenso con varios tipos de libros contables diseñados específicamente para pruebas, normalmente mucho más simples que los libros contables “reales”, para que podamos centrar nuestras pruebas en la capa de consenso en sí misma.

  • Mejora la composición, permitiéndonos construir componentes más grandes a partir de componentes más pequeños. Por ejemplo, el libro contable de pruebas Shelley sólo contiene el libro contable Shelley, pero una vez que Shelley se libere, la cadena real contendrá el libro contable Byron hasta el punto de bifurcación dura, y el libro contable Shelley a partir de entonces. Esto significa que necesitamos una capa de libro contable que cambie entre dos libros contables en un punto específico. En lugar de definir un nuevo libro contable, podemos definir un combinador de bifurcación dura que implemente precisamente esta, y sólo esta, funcionalidad. Esto mejora la reutilización del código (no necesitaremos reimplementar la funcionalidad de la bifurcación dura cuando llegue la próxima bifurcación dura), así como la separación de las preocupaciones: el desarrollo y las pruebas del combinador de bifurcación dura no dependen de las especificaciones de los libros contables entre los que cambia.

  • En estrecha relación con el último punto, el uso de la abstracción mejora la comprobabilidad. Podemos definir combinadores que definen variaciones menores de los protocolos de consenso que nos permiten centrarnos en aspectos específicos. Por ejemplo, tenemos un combinador que toma un protocolo de consenso existente y anula sólo la comprobación de si debemos producir un bloque. Podemos entonces utilizarlo para generar escenarios de prueba en los que muchos nodos produzcan un bloque en una franja determinada o, a la inversa, ningún nodo en absoluto, y verificar que la capa de consenso hace lo correcto en tales circunstancias. Sin un combinador que anule este aspecto de la capa de consenso, se dejaría al azar si surgen tales escenarios. Aunque podemos controlar el “azar” en los ensayos (eligiendo una semilla de partida concreta para el generador de números seudorreactivos), sería imposible establecer escenarios específicos, o incluso reducir automáticamente los casos de ensayo fallidos a un caso de ensayo mínimo. Con estos combinadores de consenso de prueba, la prueba puede literalmente utilizar un programa que especifique qué nodos deben producir bloques en qué franjas. Podemos entonces generar tales programas al azar, ejecutarlos, y si hay un fallo, reducirlos a un caso de prueba mínimo que todavía desencadene el fallo. Un programa de fallos tan mínimo es mucho más fácil de depurar, comprender y trabajar con él que una simple elección de semillas aleatorias iniciales que hace que… algo… suceda en… algún momento.

Para lograr todo esto, sin embargo, la capa de consenso necesita hacer uso de algunos tipos sofisticados de Haskell. En esta entrada del blog desarrollaremos un “mini protocolo de consenso”, explicando exactamente cómo resumimos varios aspectos, y cómo podemos hacer uso de esas abstracciones para definir varios combinadores. El resto de esta entrada del blog asume que el lector está familiarizado con Haskell.

Preliminares

La capa de consenso de Cardano está diseñada para apoyar la familia de protocolos de consenso de Ouroboros, todos los cuales hacen una suposición fundamental de que el tiempo se divide en franjas, donde la blockchain puede tener como máximo un bloque por franja. En este artículo, definimos que un número de franja es sólo un Int:

También necesitaremos un modelo mínimo de encriptación de clave pública, específicamente

La única extensión de lenguaje que necesitaremos en esta entrada del blog es TypeFamilies, aunque la implementación real utiliza mucho más. Si quieres seguir, descarga el código fuente completo.

Protocolo de consenso

Como se mencionó al principio de este posteo, el protocolo de consenso tiene tres responsabilidades principales:

  1. Comprobación del líder (¿debemos producir un bloque?)

  2. Selección de la cadena

  3. Verificación de bloque

El protocolo pretende ser independiente de una elección concreta de bloque, así como de una elección concreta de libro contable, de modo que un único protocolo pueda funcionar con diferentes tipos de bloques y/o libros contables. Por lo tanto, cada una de estas tres responsabilidades define su propia “visión” de los datos que requiere.

Comprobación del Líder

El chequeo del líder se ejecuta en cada franja, y debe determinar si el nodo debe producir un bloque. En general, la verificación del líder puede requerir alguna información extraída del estado del libro contable:

Por ejemplo, en el protocolo de consenso de Ouroboros Praos que inicialmente dará poder a Shelley, la probabilidad de que un nodo sea elegido líder (es decir, que se le permita producir un bloque) depende de su participación; la participación del nodo, por supuesto, proviene del estado de libro contable.

Selección de la cadena

La “selección de la cadena” se refiere al proceso de elegir entre dos cadenas que compiten entre sí. El criterio principal aquí es la longitud de la cadena, pero algunos protocolos pueden tener requisitos adicionales. Por ejemplo, los bloques suelen estar firmados por una clave “caliente” que existe en el servidor, que a su vez se genera por una clave “fría” que nunca existe en ningún dispositivo de la red. Cuando la clave caliente está comprometida, el operador del nodo puede generar una nueva a partir de la clave fría, y “delegar” en esa nueva clave. Así, en una elección entre dos cadenas de igual longitud, ambas con una punta firmada por la misma llave fría pero una llave caliente diferente, un protocolo de consenso preferirá la nueva llave caliente. Para permitir que los protocolos de consenso específicos establezcan requisitos como éste, introducimos por tanto un SelectView, especificando qué información espera el protocolo que esté presente en el bloque:

Validación de la cabecera

La validación de bloques es en su mayor parte una preocupación del libro contable; verificaciones tales como la verificación de que todas las entradas de una transacción están disponibles para evitar el doble gasto se definen en la capa del libro contable. La capa de consenso no es en su mayor parte consciente de lo que hay dentro de los bloques; de hecho, puede que ni siquiera sea una criptografía, sino una aplicación diferente de la tecnología blockchain.

Sin embargo, los bloques (más precisamente, las cabeceras de los bloques) también contienen algunas cosas específicamente para apoyar la capa de consenso. Para ceñirse al ejemplo de Praos, éste espera que haya varias pruebas criptográficas relacionadas con la derivación de la entropía de la blockchain. Verificarlas es responsabilidad de la capa de consenso, por lo que definimos una tercera y última visión de los campos que el consenso debe validar:

Definición del protocolo

Necesitamos una familia* de tipos más: cada protocolo puede requerir alguna información estática para ejecutarse; claves para firmar los bloques, su propia identidad para la comprobación del líder, etc. Llamamos a esto la “configuración de nodos”, y la definimos como

Ahora podemos unir todo en la definición abstracta de un protocolo de consenso. Los tres métodos de la clase corresponden a las tres responsabilidades que mencionamos; a cada método se le pasa la configuración de nodo estático, así como la vista específica requerida para ese método. Obsérvese que p aquí es simplemente una etiqueta de nivel de tipo que identifica un protocolo específico; nunca existe a nivel de valor.

Ejemplo: BFT permisivo (PBFT)

El BFT permisivo es un simple protocolo de consenso donde los nodos producen bloques de acuerdo a un programa de round robin: primero el nodo A, luego el B, luego el C, y luego de vuelta al A. Llamamos al índice de un nodo en ese programa el “NodeId”. Este es un concepto para PBFT solamente, no algo que el resto de la capa de consenso necesite conocer:

La configuración de nodos requerida por PBFT es la propia identidad del nodo, así como las claves públicas de todos los nodos que pueden aparecer en el programa:

Dado que PBFT es un simple programa de round-robin, no necesita ninguna información del libro mayor:

La selección de la cadena sólo mira la longitud de la cadena; en aras de la simplicidad, asumiremos aquí que las cadenas más largas terminarán en un número de franja mayor**, y por lo tanto la selección de la cadena sólo necesita saber el número de franja de la punta:

Finalmente, al validar los bloques, debemos comprobar que están firmados por alguno de los nodos que están autorizados a producir bloques (que pueden aparecer en el programa):

Definir el protocolo es ahora simple:

La selección de la cadena sólo compara los números de franja; la validación comprueba si la llave es una de las líderes de franja permitidas, y la selección de líder hace algo de aritmética modular para determinar si es el turno de ese nodo.

Obsérvese que el programa de round-robin sólo se utiliza para comprobar si somos o no líderes; en el caso de los bloques históricos (es decir, al hacer la validación), sólo comprobamos que el bloque fue firmado por alguno de los líderes permitidos, no en qué orden. Esta es la parte “permisiva” de la PBFT; la implementación real hace una comprobación adicional (que no hay ningún líder que firme más que su parte), que omitimos en esta entrada del blog.

Ejemplo de combinador de protocolos: programa de líderes explícitos

Como se mencionó en la introducción, para las pruebas es útil poder decidir explícitamente el orden en el que los nodos firman los bloques. Para ello, podemos definir un combinador de protocolos que toma un protocolo y sólo anula la comprobación de es-líder para comprobar un cronograma fijo en su lugar:

La configuración necesaria para ejecutar tal protocolo es el cronograma y el ID del nodo dentro de este cronograma, además de cualquier configuración que requiera el protocolo subyacente p:

La selección de la cadena no se modifica, y por lo tanto también lo hace la vista de los bloques necesarios para la selección de la cadena:

Sin embargo, necesitamos anular la validación del encabezado, de hecho, deshabilitarla por completo, porque si el protocolo subyacente verifica que los bloques están firmados por el nodo correcto, entonces obviamente tal verificación sería incompatible con esta anulación. Por lo tanto, no necesitamos nada del bloque para validarlo (ya que no hacemos ninguna validación):

Definir el protocolo es ahora trivial:

La selección de la cadena sólo utiliza la selección de la cadena del protocolo subyacente p, la validación del bloque no hace nada, y la comprobación is-leader se refiere al programa estático.

De los bloques a los protocolos

Al igual que el protocolo de consenso, una elección específica de bloque también puede venir con sus propios datos de configuración requeridos:

Muchos bloques pueden utilizar el mismo protocolo, pero un tipo específico de bloque está diseñado para un tipo específico de protocolo. Por lo tanto, introducimos una familia de tipos mapeando un tipo de bloque a su protocolo asociado:

Luego decimos que un bloque soporta su protocolo si podemos construir las vistas en ese bloque requeridas por su protocolo específico:

Ejemplo: Bloques (simplificados) de Byron

Desnudos hasta los huesos, los bloques de la blockchain de Byron se ven algo así como

Ya hemos mencionado que la familia de protocolos de consenso de Ouroboros asume que el tiempo se divide en franjas horarias. Además, también asumen que estas franjas horarias se agrupan en épocas. Los bloques Byron no contienen un número absoluto, sino un número de época y un número relativo de franjas dentro de esa época. El número de franjas por época en la cadena Byron está fijado en algo más de 10k, pero para las pruebas es útil poder variar k, el parámetro de seguridad, y así es configurable; esta es (parte de) la configuración necesaria para trabajar con los bloques Byron:

Dada la configuración de Byron, se trata entonces de convertir el par de un número de época y una franja relativa en un número de franja absoluta:

¿Qué hay del protocolo? Bueno, la cadena de Byron funciona con PBFT, y así podemos definir

Probar que el bloque Byron apoya este protocolo es fácil:

El estado de libro contable

La capa de consenso no sólo dirige el protocolo de consenso, sino que también gestiona el estado del libro contable. Sin embargo, no le importa mucho cómo se ve específicamente el estado de libro contable; en cambio, simplemente asume que algún tipo de estado de libro contable mayor está asociado con un tipo de bloque:

Necesitaremos una familia de tipos adicional. Cuando aplicamos un bloque al estado de libro contable, podríamos obtener un error si el bloque no es válido. El tipo específico de errores del libro contable está definido en la capa de libro contable, y es, por supuesto, muy específico del libro contable. Por ejemplo, el libro contable Shelley tendrá errores relacionados con la participación, mientras que el libro contable Byron no, porque no admite participación; y los libros contable que no son criptomonedas tendrán tipos de errores muy diferentes.

Ahora definimos dos clases de tipos. La primera sólo describe la interfaz de la capa del libro contable, diciendo que debemos ser capaces de aplicar un bloque al estado del libro contable y obtener un error (si el bloque no era válido) o el estado actualizado del libro contable:

En segundo lugar, definiremos una clase de tipo que conecta el libro contable asociado a un bloque con el protocolo de consenso asociado a ese bloque. Al igual que el BlockSupportsProtocol proporciona funcionalidad para derivar vistas del bloque requerido por el protocolo de consenso, el LedgerSupportsProtocol proporciona igualmente funcionalidad para derivar vistas del estado de libro contable requerido por el protocolo de consenso:

En la siguiente sección veremos por qué es útil separar la integración con el libro contable (UpdateLedger) de su conexión con el protocolo de consenso (LedgerSupportsProtocol).

Combinadores de bloques

Como ejemplo final del poder de los combinadores, mostraremos que podemos definir un combinador en bloques y sus libros contables asociados. Como ejemplo de donde esto es útil, la blockchain Byron viene con una implementación así como una especificación ejecutable. Es útil instanciar la capa de consenso con ambos libros contables, para que podamos verificar que la implementación concuerda con la especificación en todos los puntos del camino. Esto significa que los bloques en esta configuración de “libro doble” son, de hecho, un par de bloques***.

La definición del estado de doble libro y el error de doble libro son similares:

Para aplicar un bloque dual al estado de doble libro contable, simplemente aplicamos cada bloque a su estado asociado. Este combinador particular supone que los dos libros contable deben estar siempre de acuerdo en si un bloque particular es válido o no, lo que es adecuado para comparar una implementación y una especificación; también son posibles otras opciones (otros combinadores):

Dado que la intención del libro contable doble es comparar dos implementaciones del libro contable, bastará con que todas las preocupaciones de consenso sean impulsadas por el primer bloque (“principal”); no necesitamos una instancia para ProtocolLedgerView para el bloque auxiliar, y, de hecho, en general podría no ser capaz de dar una. Esto significa que el protocolo de bloque del bloque dual es el protocolo de bloque del bloque principal:

La configuración de bloque que necesitamos es la configuración de bloque de ambos bloques:

Ahora podemos mostrar fácilmente que el bloque dual también soporta su protocolo:

Conclusiones

La capa de consenso de Cardano fue diseñada inicialmente para la blockchain de Cardano, que actualmente dirige Byron y pronto dirigirá Shelley. Se podría argumentar que esto significa que los ingenieros de IOHK deberían diseñar para esa blockchain específica inicialmente, y generalizar sólo más tarde al usar la capa de consenso para otras blockchain. Sin embargo, hacerlo tendría importantes inconvenientes:

  • Obstaculizaría nuestra capacidad de hacer pruebas. No seríamos capaces de anular el programa de qué nodos producen bloques cuando, no seríamos capaces de correr con un doble libro contable, etc.

  • Enredaríamos cosas que son lógicamente independientes. Con el enfoque abstracto, el libro contable Shelley consiste en tres partes: una cadena Byron, una cadena Shelley, y un combinador de bifurcación dura mediando entre estos dos. Sin las abstracciones en su lugar, tal separación de la preocupación sería más difícil de lograr, lo que conduce a más difícil de entender y mantener el código.

  • El código abstracto es menos propenso a los errores. Como ejemplo simple, ya que el combinador de doble libro contable es polimórfico en los dos libros contables que combina, y tienen diferentes tipos, no podríamos escribir código correcto de tipo que, sin embargo, trate de aplicar, digamos, el bloque principal al libro contable auxiliar.

  • Cuando llegue el momento y queramos instanciar la capa de consenso para una nueva blockchain (como inevitablemente sucederá), escribirlo de forma abstracta desde el principio nos obliga a pensar cuidadosamente en el diseño y a evitar acoplar cosas que no deben ser acopladas, o hacer suposiciones que resultan justificadas para la blockchain real que se está considerando pero que pueden no ser ciertas en general. Arreglar tales problemas después de que el diseño esté en su lugar puede ser difícil.

Por supuesto, todo esto requiere un lenguaje de programación con excelentes habilidades de abstracción; afortunadamente, Haskell encaja perfectamente en ese proyecto.

Esta es la primera de una serie de posts de Deep Dive dirigidos a los desarrolladores

*A diferencia de las diversas vistas, NodeConfig es una familia de datos; ya que la configuración del nodo se pasa a todas las funciones, el hecho de que NodeConfig sea una familia de datos ayuda a la inferencia de tipos, ya que determina p. Dejar el resto como familias de tipos puede ser conveniente y evitar envoltorios y desenvolvimientos innecesarios.

** Los números de franja son un proxy para la longitud de la cadena sólo si cada franja contiene un bloque. Esto es cierto para PBFT sólo en ausencia de particiones de red temporales, y no es cierto en absoluto para algoritmos de consenso probabilístico como Praos. En otras palabras, una cadena de menor densidad puede tener un número de franja más alto en su punta que otra cadena más corta pero de mayor densidad.

*** El verdadero combinador DualBlock guarda una tercera pieza de información que relaciona los dos bloques (cosas como los ID de las transacciones). Lo hemos omitido de este post por simplicidad.

1 Like