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
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
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
de Nostr.resistencia a la censura Resistencia a la censura: La propiedad de que ninguna empresa o servidor por sí solo pueda silenciarte o borrar tu identidad.
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
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.
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.
Listo. Presiona reproducir o avanza un paso para enviar el primer mensaje.
Versión en texto
| # | De | Para | Mensaje |
|---|---|---|---|
| 1 | Tu cliente | Relay Alpha | Tu cliente abre la suscripción "feed" en el Relay Alpha: las notas de Alice, las 2 más recientes. |
| 2 | Tu cliente | Relay Beta | El mismo REQ al Relay Beta. Preguntar a varios relays hace que uno lento o caído no deje tu feed en blanco. |
| 3 | Tu cliente | Relay Gamma | Y al Relay Gamma. Los relays nunca hablan entre sí, así que es el cliente quien pregunta. |
| 4 | Relay Alpha | Tu cliente | Alpha responde con la nota más reciente de Alice, envuelta en un EVENT con el id de la suscripción. |
| 5 | Relay Beta | Tu cliente | Beta envía exactamente la misma nota. Mismo id, así que tu cliente la muestra una sola vez. Deduplicar es trabajo del cliente. |
| 6 | Relay Alpha | Tu cliente | Alpha envía la segunda nota. Tu cliente verifica cada firma por su cuenta: nunca se confía en los relays. |
| 7 | Relay Alpha | Tu cliente | EOSE de Alpha: eso es todo lo que tenía guardado. Hora de quitar el indicador de carga de Alpha. |
| 8 | Relay Beta | Tu cliente | EOSE de Beta. La suscripción sigue abierta, así que las notas nuevas seguirían llegando. |
| 9 | Relay Gamma | Tu cliente | Gamma se niega con CLOSED y el motivo "rate-limited:". Sin drama: ya respondieron dos relays. |
| 10 | Tu cliente | Relay Alpha | Lectura terminada. Tu cliente envía CLOSE para que Alpha deje de enviar novedades. |
| 11 | Tu cliente | Relay Beta | Y 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 |
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.” |
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 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
REQcon un id que ya está abierto reemplaza esa suscripción. - Un
REQpuede llevar varios filtros; se combinan con OR.limitsolo se aplica al primer lote, el de eventos guardados. Conlimit: 0el 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íesCLOSE, él envíeCLOSEDo se caiga la conexión. - Un relay debe responder con
OKa cadaEVENTpublicado, y debe enviarCLOSEDcuando rechaza unREQ. - Los mensajes
OKyCLOSEDempiezan con un prefijo legible por máquinas:duplicate:,pow:,blocked:,rate-limited:,invalid:,restricted:,mute:oerror:. NIP-42 añadeauth-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
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 (
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
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
OK false no es un error del protocolo: es un relay ejerciendo su derecho a elegir. Un relay puede
exigir
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ácticaEnví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.
- 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
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.