Capítulo 4

Los relays y el protocolo

Mira cómo REQ, EVENT, EOSE y compañía viajan entre clientes y relays.

¡Llegan paquetes! Ponte cómodo, vamos a espiar a un relay. Totalmente legal, ya lo comprobé.

Ya tienes un par de claves y sabes firmar un evento. Pero una nota firmada que se queda en tu computadora portátil es un diario, no una red social. Para llegar a otras personas tiene que viajar a algún lugar. En Nostr, ese lugar es un relay.

Los relays son tontos (y es un cumplido)

Imagina un relay como el tablón de anuncios de una cafetería. Cualquiera puede clavar una tarjeta, cualquiera puede acercarse a leer las tarjetas, y el dueño del café pone las reglas de la casa: nada de publicidad, solo socios, las tarjetas se retiran al mes. Eso es todo. El tablón no conoce a tus amigos, no ordena tu feed y no habla con el tablón de la cafetería de enfrente.

Tu cliente (la app que usas) es lo contrario: es tu asistente personal. Sabe qué tablones usan tus amigos, va a varios de ellos, recoge las tarjetas, descarta las repetidas, comprueba cada firma y te lo presenta todo bien ordenado.

Esta división es intencional:

  • Los relays siguen siendo simples, así que son baratos de mantener. Miles de personas operan uno, desde grandes relays públicos hasta uno diminuto en el armario de alguien.
  • La inteligencia está en los clientes, así que tú eliges el algoritmo, no una empresa.
  • Nada depende de un solo relay. Si un tablón desaparece, tu asistente simplemente lee otro. De ahí viene la resistencia a la censura de Nostr.
Los relays no pueden mentir sobre lo que dijiste

Un relay puede rechazar tu nota, perderla u ocultarla. Lo que no puede hacer es editarla ni publicar en tu nombre: cada evento lleva tu firma, y los clientes la verifican en su propio dispositivo (capítulo 3). Lo peor que puede hacer un relay es quedarse callado, y por eso usamos más de uno.

Escuchando la línea

Clientes y relays hablan a través de un WebSocket: una línea telefónica que se queda abierta, para que ambos lados puedan hablar cuando quieran. Cada mensaje en esa línea es un pequeño array JSON cuyo primer elemento es una palabra en mayúsculas, como si gritara: el verbo.

Elige una conversación abajo y luego dale a reproducir, avanza paso a paso con las flechas o arrastra la barra de progreso. Haz clic en cualquier tipo de paquete de la leyenda para saltar directamente a él. No pierdas de vista la libreta del cliente: es toda la contabilidad que los relays no hacen.

Tu cliente quiere las dos notas más recientes de Alice. Pregunta a tres relays a la vez y combina las respuestas.

Tipos de paquete
La línea, paquete a paquete

Un diagrama de secuencia: tu cliente a la izquierda, los relays a la derecha. Cada flecha es un mensaje JSON en un WebSocket. Avanza paso a paso, dale a reproducir o arrastra la barra de progreso.

Tu clienteRelay AlphaRelay BetaRelay Gamma
Paso 1 de 12

Listo. Presiona reproducir o avanza un paso para enviar el primer mensaje.

Versión en texto
#DeParaMensaje
1Tu clienteRelay AlphaTu cliente abre la suscripción "feed" en el Relay Alpha: las notas de Alice, las 2 más recientes.
2Tu clienteRelay BetaEl mismo REQ al Relay Beta. Preguntar a varios relays hace que uno lento o caído no deje tu feed en blanco.
3Tu clienteRelay GammaY al Relay Gamma. Los relays nunca hablan entre sí, así que es el cliente quien pregunta.
4Relay AlphaTu clienteAlpha responde con la nota más reciente de Alice, envuelta en un EVENT con el id de la suscripción.
5Relay BetaTu clienteBeta envía exactamente la misma nota. Mismo id, así que tu cliente la muestra una sola vez. Deduplicar es trabajo del cliente.
6Relay AlphaTu clienteAlpha envía la segunda nota. Tu cliente verifica cada firma por su cuenta: nunca se confía en los relays.
7Relay AlphaTu clienteEOSE de Alpha: eso es todo lo que tenía guardado. Hora de quitar el indicador de carga de Alpha.
8Relay BetaTu clienteEOSE de Beta. La suscripción sigue abierta, así que las notas nuevas seguirían llegando.
9Relay GammaTu clienteGamma se niega con CLOSED y el motivo "rate-limited:". Sin drama: ya respondieron dos relays.
10Tu clienteRelay AlphaLectura terminada. Tu cliente envía CLOSE para que Alpha deje de enviar novedades.
11Tu clienteRelay BetaY CLOSE a Beta. El WebSocket sigue abierto para el próximo REQ.

Todo el vocabulario cabe en un pósit

NIP-01 define el lenguaje básico. Tu cliente dice tres cosas:

El cliente dice Significado
REQ “Envíame los eventos que coincidan con estos filtros, y mantenme al tanto.” Abre una suscripción con un nombre que tú eliges.
EVENT “Por favor, guarda este evento firmado.” (Publicar.)
CLOSE “Ya terminé con esa suscripción.”

El relay responde con cinco:

El relay dice Significado
EVENT “Aquí tienes un evento para tu suscripción llamada feed.”
EOSE “Eso es todo lo que tenía guardado.” End Of Stored Events (fin de los eventos guardados): ya puedes quitar el indicador de carga. La suscripción sigue abierta para eventos nuevos.
OK “Sobre ese evento que enviaste: guardado (true) o rechazado (false), y este es el motivo.”
CLOSED “Voy a cerrar tu suscripción”, con un motivo.
NOTICE Un mensaje legible para humanos, como una nota pegada en la puerta.

Y hay un par extra para iniciar sesión, de NIP-42: AUTH. El relay envía un desafío aleatorio; tu cliente lo firma con tu clave y lo devuelve. Ninguna contraseña viaja por la línea, solo una firma. (También existe COUNT, de NIP-45, para preguntas del tipo “¿cuántos hay?”, pero casi nunca lo verás.)

Bajo el capó: los mensajes exactos Mostrar detalles

Cada mensaje es un array JSON enviado como un único mensaje de texto del WebSocket.

["REQ", "feed", {"authors": ["e550c6e9…"], "kinds": [1], "limit": 2}]
["EVENT", "feed", {"id": "…", "pubkey": "…", "created_at": 1735686600, "kind": 1, "tags": [], "content": "…", "sig": "…"}]
["EOSE", "feed"]
["CLOSE", "feed"]

["EVENT", {"id": "…", "kind": 1, "…": "…"}]
["OK", "<event id>", true, ""]
["OK", "<event id>", false, "pow: difficulty 16 or more is required"]
["CLOSED", "feed", "rate-limited: slow down, too many requests"]
["NOTICE", "heads up: this relay restarts at 03:00 UTC"]

Reglas que vale la pena conocer, directamente de NIP-01:

  • El id de suscripción es cualquier cadena no vacía de hasta 64 caracteres, elegida por el cliente y válida solo dentro de una conexión. Enviar un nuevo REQ con un id que ya está abierto reemplaza esa suscripción.
  • Un REQ puede llevar varios filtros; se combinan con OR. limit solo se aplica al primer lote, el de eventos guardados. Con limit: 0 el relay se salta los eventos guardados y solo transmite los nuevos.
  • Después de EOSE, el relay sigue enviando en tiempo real los eventos que coincidan hasta que envíes CLOSE, él envíe CLOSED o se caiga la conexión.
  • Un relay debe responder con OK a cada EVENT publicado, y debe enviar CLOSED cuando rechaza un REQ.
  • Los mensajes OK y CLOSED empiezan con un prefijo legible por máquinas: duplicate:, pow:, blocked:, rate-limited:, invalid:, restricted:, mute: o error:. NIP-42 añade auth-required:. Ojo: ["OK", id, true, "duplicate: …"] es un éxito: el relay ya lo tenía.
Bajo el capó: iniciar sesión con AUTH (NIP-42) Mostrar detalles

El relay puede enviar un desafío en cualquier momento, normalmente justo después de conectarte o justo antes de rechazar algo:

["AUTH", "ostrich-7f3a9c2e"]
["CLOSED", "dms", "auth-required: gift wraps are only served to their recipient"]

El cliente responde con un evento firmado de kind 22242 que nunca se publica, con tags para la URL del relay y el desafío. Los relays comprueban la firma, el desafío, la URL y que created_at sea reciente (aproximadamente de los últimos 10 minutos):

["AUTH", {"kind": 22242, "tags": [["relay", "wss://relay.delta.example"], ["challenge", "ostrich-7f3a9c2e"]], "content": "", "…": "id, pubkey, created_at, sig"}]
["OK", "<auth event id>", true, ""]

Si ya iniciaste sesión pero tu clave sigue sin tener permiso, el relay usa el prefijo restricted:. Los relays de pago y los que sirven gift wraps privados (capítulo 8) dependen de esto. Especificación: NIP-42.

Bajo el capó: hablar con relays usando nostr-tools Mostrar detalles

SimplePool gestiona un WebSocket por relay y reparte tus peticiones entre todos ellos, deduplicando los eventos por id por ti:

import { SimplePool } from "nostr-tools/pool";
import { finalizeEvent, generateSecretKey } from "nostr-tools/pure";

const relays = ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.primal.net"];
const pool = new SimplePool();

// REQ a cada relay; onevent se dispara una vez por evento único, oneose cuando todos enviaron EOSE.
const sub = pool.subscribeMany(relays, { kinds: [1], limit: 3 }, {
  onevent: (event) => console.log(event.pubkey.slice(0, 8), event.content),
  oneose: () => sub.close(), // envía CLOSE a cada relay
});

// Publicar = un EVENT por relay; cada promesa se resuelve con el OK de ese relay.
const event = finalizeEvent(
  { kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [], content: "hola relays" },
  generateSecretKey(),
);
const results = await Promise.allSettled(pool.publish(relays, event));
console.log(results.map((r) => r.status)); // p. ej. ["fulfilled", "fulfilled", "rejected"]

Para una sola conexión está Relay.connect(url) de nostr-tools/relay, con relay.subscribe, relay.publish y relay.auth(sign) para NIP-42. Por debajo son exactamente los mensajes que viste arriba.

Extra: un relay puede describirse a sí mismo. Pide su URL por HTTPS con la cabecera Accept: application/nostr+json y obtendrás un documento JSON con su nombre, contacto, NIPs soportados y límites (NIP-11, especificación).

No pongas todas tus notas en un solo relay

Como los relays no copian eventos entre sí, tu cliente suele enviar cada nota a varios relays a la vez. Si uno se desconecta, se satura o decide que no le caes bien, tus seguidores aún pueden encontrar tu nota en los demás. Algunos relays son gratuitos, otros son de pago (una pequeña cuota es un gran filtro contra el spam), y cada uno tiene sus propias reglas. Combínalos a tu gusto.

Pruébalo: elige dónde publicar y luego empieza a tumbar relays.

No pongas todas tus notas en un solo relay

Elige en qué relays se publica tu nota y luego tumba relays. ¿Pueden tus seguidores encontrarla todavía?

  • Relay Alpha en línea · tiene una copia
  • Relay Beta en línea · tiene una copia
  • Relay Gamma en línea · sin copia
  • Relay Delta (de pago) en línea · sin copia

Tu nota todavía se puede encontrar. Copias todavía accesibles: 2 de 2

¿Cómo sabe el cliente de Bob qué relays revisar para encontrar tus notas? Publicas una pequeña lista de relays “donde escribo”, y su cliente va allí. Eso es el modelo outbox (NIP-65), y lo veremos en el capítulo 7.

Los relays tienen reglas, y está bien

OK false no es un error del protocolo: es un relay ejerciendo su derecho a elegir. Un relay puede exigir prueba de trabajo, otro solo acepta socios, y un tercero borra todo después de una semana. Como ningún relay es especial, simplemente eliges los que tengan reglas que te gusten.

Escucha la versión real

Todo lo anterior estaba guionizado. Este panel envía un REQ real y muestra cada mensaje exactamente como llega. Por defecto habla con nuestros relays de práctica; activa el Modo en vivo en la cabecera y hablará con relays públicos reales. Tranquilo: solo lee. Nunca publicamos nada.

Mensajes reales de relays reales

Relays de práctica

Envía un REQ (las 3 notas de texto más recientes) y mira cómo vuelve el JSON en crudo. Solo lectura: nunca publicamos.

Estás usando relays de práctica. Activa el modo en vivo en la cabecera para hablar con relays reales.

En espera.

  1. Aún no hay mensajes. Pulsa "Enviar REQ".

Fíjate en los mensajes CLOSE del final: en cuanto todos los relays han dicho EOSE, el panel cuelga amablemente. Un cliente real a menudo mantendría la suscripción abierta para recibir notas nuevas a medida que se publican.

Comprobación rápida

Tu cliente recibe ["EOSE", "feed"]. ¿Qué significa?

Elige una respuesta

El relay Beta se vuelve malicioso. ¿Cuál de estas cosas puede hacerles a las notas de Alice?

Elige una respuesta

Alice publica en tres relays y recibe OK true, OK true y OK false "pow: …". ¿Qué debería concluir su cliente?

Elige una respuesta

Lo esencial

  • Un relay es un servidor sencillo que guarda eventos firmados y los entrega cuando se los piden. Los relays no hablan entre sí.
  • El trabajo inteligente lo hacen los clientes: eligen relays, combinan y deduplican las respuestas, y verifican cada firma por su cuenta.
  • Toda la conversación es un puñado de arrays JSON sobre un WebSocket: REQ, EVENT, EOSE, CLOSE, OK, NOTICE, CLOSED y AUTH.
  • Publicar el mismo evento en varios relays hace que se pueda seguir encontrando cuando un relay se cae o te echa.
  • Un relay puede rechazar u olvidar tus eventos, pero nunca puede modificarlos ni inventar eventos nuevos en tu nombre.