ZYCORD documentación
Español
ZycordDocumentaciónProtocolo de red

Protocolo de red

Lo que los nodos de Zycord se dicen entre sí, y las reglas que impiden que un protocolo entre pares se convierta en un amplificador. Una guía de la especificación normativa, no un sustituto de ella.

Esta página es una guía, no la especificación

La especificación es spec/: los archivos de parámetros y el corpus de vectores. Donde esta página y el árbol discrepen, manda el árbol — una implementación es conforme cuando reproduce el id de génesis y pasa los vectores, y nada de lo que hay aquí cambia eso. Lo que sigue es un mapa de dónde están los requisitos.

Transporte#

TCP, luego TLS 1.3. La identidad del par es una clave Ed25519 que va en un certificado X.509 autofirmado, presentado por ambas partes. Las cadenas de certificados no se validan — no hay autoridad de certificación ni nada que una pudiera atestiguar — así que la verificación exige exactamente un certificado, exige que su clave pública sea Ed25519, y extrae esa clave como identidad del par.

La propiedad que se obtiene es una vinculación del canal a una clave, no una autorización: TLS garantiza que quien completó el saludo posee la clave privada de la identidad presentada, y que el flujo no puede leerse ni modificarse en tránsito. No dice nada sobre si ese par es honesto. Nada en el protocolo concede autoridad sobre la base de la identidad.

Las fechas de validez del certificado son constantes fijas y no una ventana alrededor de la hora actual, a propósito: una ventana relativa hace que la aceptación de un certificado dependa de la concordancia de los relojes, y una red que se particiona por desviación de reloj es una red que se particiona por una razón ajena al consenso.

Las implementaciones no deben reutilizar como identidad de par una clave de ninguna otra capa. El nodo genera una nueva en cada arranque y nunca la escribe en disco.

Entramado#

offset  size  field
0       4     length   uint32, little-endian - payload length, excluding this header
4       1     kind     uint8  - see below
5       n     payload
  • length debe comprobarse contra MaxMessageBytes = 8 MiB, y contra el techo de su propio tipo, antes de reservar nada. Un receptor que reserva memoria según una longitud declarada tiene un fallo remoto de agotamiento de memoria al margen de lo que venga después.
  • El kind 0 se rechaza. Un tipo por encima del más alto que conoce una compilación es uno que añadió una compilación más reciente: su carga útil se lee y se descarta bajo MaxUnknownPayloadBytes = 64 KiB, a un ritmo acotado por par, en lugar de cerrar la conexión. Por encima de ese ritmo se puntúa como una petición excesiva.

Tipos de mensaje#

ValorNombreDirecciónCarga útil
1helloambos, primeroSaludo inicial
2certificatedifusiónssz(Certificate)
3block-announcedifusiónCabecera más los hashes ordenados de los ejemplares de certificado
4get-blockpeticiónid de bloque de 32 bytes ‖ índice de fragmento u32
5blockrespuestaUn fragmento de ssz(Block)
6get-headerspeticiónLocalizador
7headersrespuestaSerie de cabeceras
8get-peerspeticiónvacío
9peersrespuestaLista de direcciones
10service-carddifusión, por capacidadUn anuncio de servicio firmado
11block-unavailablerespuestaId de bloque ‖ motivo: por debajo del suelo de cuerpos del emisor, o nunca tenido
12open-intentdifusión, por capacidadUn intent abierto para el camino forzado

No hay ninguna codificación específica de red de un objeto de consenso, que es lo que hace de "el id de lo que recibí" una afirmación comprobable.

La versión del protocolo es 3, y el saludo inicial exige una coincidencia exacta. hello la lleva junto con el id de red — el id de la génesis, que se compromete con cada parámetro de consenso —, la altura y el trabajo del emisor, su dirección de escucha, una palabra de capacidades y su suelo de cuerpos, la altura más baja cuyo cuerpo de bloque sirve. Un tipo condicionado a una capacidad nunca se envía a un par que no la haya anunciado, y un bit de capacidad que una compilación no define se ignora, nunca se rechaza. Un get-block que el receptor no va a servir se responde con block-unavailable en lugar de dejarlo al plazo del solicitante.

Un open-intent es el único tráfico del camino forzado: el intent de un usuario sin secuenciador nombrado, que cualquier productor puede ejecutar al final de su bloque. El certificado forzado resultante nunca se difunde como certificado; nace en el bloque, y cada nodo lo comprueba allí.

Todo mensaje entrante le cuesta a quien lo envía#

Esta es la parte de la especificación que más merece la pena leer entera, porque es donde un protocolo entre pares suele tener fugas. La rigen dos reglas.

La regla de orden por coste#

El trabajo se hace en orden creciente de coste, y un mensaje se cobra antes del paso caro que le sigue. Un receptor que verifica primero y puntúa después ha construido un amplificador: lo barato de enviar es lo caro de comprobar.

Todo desenlace tiene precio#

No hay ningún Free sin nombre. Cada tipo de mensaje, cruzado con cada desenlace que puede producir, aparece en una tabla con una puntuación asociada. Un desenlace que atraviesa la tabla sin ser cobrado es el defecto que esta regla existe para evitar — y se ha alcanzado en la práctica por vías más estrechas que una fila sin precio: una implementación que desviaba una trama malformada lejos de su único contador hizo que esa trama fuera gratis mientras hubiera una petición pendiente, sin añadir ninguna fila a ninguna tabla.

Servir también se mide. Los bytes de un bloque son la única respuesta con órdenes de magnitud más grande que la petición que la solicita, así que llevan su propio presupuesto de bytes además del recuento de peticiones.

Las puntuaciones se asocian a la identidad de un par: un mensaje inválido cuesta 20, una violación del protocolo 50, un cuerpo no servido 10 y una petición excesiva 5; un mensaje útil gana 1, y un par queda vetado en −100. Las comprobaciones costosas — épocas de clave y evaluaciones de proof of work — tienen un presupuesto por par y se reembolsan cuando el bloque resulta útil, de modo que un anuncio que no lleva a ninguna parte no puede hacer que un nodo muela trabajo.

Retransmisión y sincronización#

La retransmisión es primero por hash. Los certificados se difunden por sí solos y se comprueba su validez, sin estado y en paralelo, al llegar; un retransmisor no necesita estado para filtrar spam. Los bloques viajan como una cabecera más los hashes ordenados de los ejemplares, contra el mempool del receptor, y el receptor verifica el trabajo de la cabecera antes de descargar ningún cuerpo. Un cuerpo viaja en fragmentos de 4 MiB, cada uno con su propia petición y respuesta, de modo que el límite de trama acota un mensaje y nunca un bloque. Los cuerpos son datos de la cadena: un bloque solo es válido si sus cuerpos pueden recuperarse, y un nodo que poda indica dónde está su suelo en el saludo inicial.

La sincronización elige por trabajo verificado. Las cabeceras se validan como cadena antes de descargar los cuerpos, y un nodo prefiere al par cuyas cabeceras han demostrado más trabajo frente al que afirma la mayor altura. Una rama competidora solo se adopta cuando conecta dentro del horizonte de deshacer de 1.024 bloques y lleva más trabajo. Los checkpoints fijados en la versión, aceptados solo para bloques de al menos una época de antigüedad, rechazan una cadena contradictoria por pesada que sea; un operador puede añadir uno mediante un flag. A partir de la transición a proof of stake en la era 2, la elección es en cambio por longitud: gana la cadena que ocupa más slots (§11 del whitepaper), y el horizonte de deshacer y los checkpoints fijados se mantienen, como defensa contra la reescritura con claves que ya retiraron su fianza.

Gestión de conexiones, y la defensa contra el eclipse#

Esta sección acumula más fallos medidos que ninguna otra, y cada requisito de abajo existe porque una implementación sin él fue eclipsada en una medición.

  • Los objetivos salientes deben elegirse con diversidad de direcciones, de modo que un solo rango de alojamiento no pueda llenar las ranuras salientes de un nodo.
  • Los objetivos salientes también deben acotarse por fuente de difusión. La diversidad de direcciones por sí sola no basta, por una razón aritmética y no de criterio: un grupo de direcciones es una propiedad de una dirección que un atacante posee, pero una dirección que un par declara son bytes que se inventó, y cuatro bytes cualesquiera son un host IPv4 válido. Un atacante sin dirección alguna acuña un grupo de diversidad nuevo por cada cadena al precio de una trama. Lo que no puede inventar es la conexión por la que llegó la declaración.
  • El socket por el que llegó una conexión entrante no debe convertirse en objetivo saliente. Es la dirección de origen del par — un puerto efímero que eligió su sistema operativo, no algo en lo que escuche — así que una ranura gastada en llamarlo es una ranura gastada en una dirección que no puede responder.
  • Ambas cotas deben contarse contra las conexiones que un nodo mantiene, no contra una sola llamada de selección. Si no, un bucle de llamadas que excluye a los pares ya conectados reconstruye ambos presupuestos llenos en cada ronda, y la cota retrasa al atacante una ronda por cada margen en lugar de acotarlo. Medido: un informante tomó 2, luego 4, luego 6, luego 8 de 8 ranuras salientes a lo largo de cuatro rondas.
  • El almacén de pares debe persistirse, y debe estar acotado. Un nodo que arranca en blanco tras cada reinicio le da al atacante una oportunidad nueva en cada reinicio; uno que arranca con la lista del atacante se la da de forma permanente.
  • Un almacén acotado no debe rechazar una dirección bien formada por estar lleno; desaloja para hacerle sitio. Una dirección honesta nunca contactada no es mejor que una inventada nunca contactada, así que un atacante que llene el almacén primero lo cierra a cal y canto contra todo lo que se ofrezca después — incluida la lista de arranque del propio operador.
  • Lo que elige el desalojo importa más que el hecho de que ocurra. "Toma la víctima de la población más grande" se lee como una inundación se desplaza a sí misma, y lo es solo mientras la inundación sea lo más grande del almacén. En un nodo arrancado desde un único par servicial, la población más grande es la libreta de direcciones de ese par. Medido en una implementación ordenada así: 200 direcciones inventadas de una sola fuente desalojaron 200 honestas sin coste alguno para el atacante.
  • Entre entradas indistinguibles, el desempate final no debe ser nada que elija el par que difunde — la dirección incluida — y esto se aplica a la selección exactamente igual que al desalojo. Medido en una implementación cuyo selector acababa recurriendo a la cadena de la dirección: 8 direcciones honestas contra 8 inventadas devolvieron 8 de 8 inventadas, y cero conexiones salientes honestas a lo largo de diez rondas.

Lo que está ausente deliberadamente#

AusentePor qué
Atravesamiento de NATEl coste se declara y la condición para reabrir se mide en lugar de suponerse: la proporción de nodos accesibles en la testnet pública es la primera condición para reabrir la decisión.
CompresiónUn compresor sobre un flujo controlado por el atacante es una superficie de ataque, a cambio de un ahorro de ancho de banda cuya necesidad nadie ha medido.
Firmas a nivel de mensajeEl transporte autentica el canal; los objetos de consenso llevan sus propias firmas. Una tercera capa autenticaría al retransmisor, que no es algo de lo que dependa ninguna decisión.
Ids de peticiónPetición y respuesta se emparejan por conexión y orden, así que la sincronización corre por su propia conexión — lo que elimina una máquina de estados. Cuando un par no es accesible, la sincronización puede correr sobre una conexión de difusión existente y debe entonces emparejar respuestas por contenido.
Negociación de versiónEl saludo inicial exige una versión exacta del protocolo y desconecta si no coincide. Lo que evoluciona sin subir la versión es estrecho y acotado: un nuevo tipo de mensaje tras un bit de capacidad, que una compilación más antigua lee y descarta por debajo de 64 KiB a un ritmo limitado.

Lo que el id de un certificado no cubre#

Merece la pena repetirlo aquí porque decide una regla de retransmisión. El id de un certificado se compromete con lo que autoriza y nunca con lo que meramente demuestra, así que las firmas quedan fuera de la preimagen del id. Eso hace de la evidencia algo que cualquiera en tránsito puede sustituir: toma un certificado, cambia su firma por basura, y propágalo — mismo id, ejemplar ahora inválido.

Dos reglas lo cierran. Un bloque se compromete con la evidencia que lleva, mediante una raíz sobre hashes de ejemplares en lugar de sobre ids. Y la retransmisión trata con ejemplares, no con ids: un nodo que recibe un ejemplar cuya evidencia no verifica descarta ese ejemplar sin perjuicio del id — el id no se marca, no se cachea como inválido, y un ejemplar posterior que sí verifique se retransmite con normalidad. Una copia mutilada le cuesta a quien la mutiló el ancho de banda de enviarla y no le cuesta nada al certificado.

Implementar esto desde cero#

Una implementación independiente que pase los vectores de referencia es un par, no un fork — ese es justo el sentido de especificarlo así. Empieza por spec/README.md para los objetos de consenso, y luego comprueba tu trabajo con zcd vectors.