Capítulo 3
Anatomía de un evento
Cada publicación, cada like y cada perfil es un evento JSON firmado. Vamos a desarmar uno.
¡Hora de operar! Abramos un evento para ver cómo funciona por dentro. Tranquilo, no siente nada.
En el capítulo anterior obtuviste un
Un sobre sellado con huella
Piensa en un evento como una postal que guardas en una funda de plástico transparente:
- La postal es lo que quieres decir: el texto, cuándo lo escribiste, qué tipo de mensaje es y unas cuantas etiquetas adhesivas.
- La huella estampada en la funda se calcula a partir de la postal. Cambia una sola letra de la postal y la huella deja de coincidir.
- Tu sello de lacre va encima de la huella. Solo tú puedes estamparlo, pero cualquiera puede comprobar que es tuyo.
Eso es todo. Eso es un evento. La huella es el
Aquí tienes uno de verdad, escrito por Alice. Haz clic en cualquier campo para saber qué hace.
El laboratorio de eventos
Un evento firmado por Alice. Edítalo y mira cómo se recalculan el id y la firma, o cambia de bando e intenta colar un cambio sin su clave.
La nota de Alice está firmada y es válida.
Edita algo y mira cómo se revuelve el id.
Lo que hace todo cliente y todo relay antes de confiar en un evento: serializar, hashear, comparar el id y comprobar la firma.
-
Ordena [0, pubkey, created_at, kind, tags, content] como JSON compacto.
-
Hashea los bytes UTF-8. El resultado es el id que el evento debería tener.
-
¿El id calculado es igual al id que dice tener el evento?
-
¿La firma Schnorr corresponde a este id y esta pubkey?
Todavía no empezó.
id
Toma pubkey, created_at, kind, tags y content, ordénalos en un arreglo JSON fijo, hashea los bytes con SHA-256 y escribe el resultado de 32 bytes como 64 caracteres hexadecimales. Cambia cualquier cosa, aunque sea una letra, y el id cambia por completo. Por eso sirve a la vez como sello contra alteraciones y como nombre único.
8e009ba1ca2749c02711f96539e77d2e045b35aec917d8a5619187d814568714 Pruébalo ya: cambia a Un falsificador (sin clave) y modifica el texto. El contenido se actualiza, pero el id y la firma viejos siguen pegados, y la verificación te descubre en la primera comprobación. Vuelve a Alice y la misma edición está perfecta, porque ella puede volver a firmar.
Los siete campos
Todo evento tiene exactamente estos siete campos. No se exige nada más, y no se permite nada menos.
| Campo | En palabras simples |
|---|---|
id |
La huella: un hash de todo lo demás |
pubkey |
Quién lo escribió (la |
created_at |
Cuándo dice el autor que lo escribió, en segundos desde 1970 |
kind |
Qué clase de evento es: un número |
tags |
Etiquetas adhesivas: enlaces a otros eventos, personas, hashtags |
content |
El mensaje en sí (o los datos) |
sig |
El sello de lacre: una firma que solo el autor puede crear |
kind: un número, muchos trabajos
El 1 es una nota
de texto corta, el kind 0 es tu perfil, el kind 3 tu lista de seguidos y el kind 7 una reacción (un “like”).
El número también les dice a los relays cómo guardarlo: la mayoría de los eventos se guardan para siempre,
pero algunos son reemplazables (solo importa tu perfil más reciente) y otros son efímeros (pásalo, no lo guardes).
Recorreremos todo el zoológico en el capítulo 6.
tags: etiquetas adhesivas
Los
["e", "<event id>"]apunta a otro evento. Así funcionan las respuestas y las citas.["p", "<pubkey>"]menciona a una persona, para que su app le avise.["t", "nostr"]es un hashtag.
Las etiquetas de una sola letra son especiales: los relays las indexan, así que un cliente puede pedir
“dame todas las notas con el tag #nostr” o “todas las respuestas a este evento”.
created_at: una afirmación, no un reloj
created_at es una marca de tiempo Unix que elige el autor. Nada impide que Alice escriba
“enero de 2009”. Los clientes ordenan los feeds según este campo, y muchos relays rechazan eventos de un
futuro demasiado lejano, pero no lo tomes como prueba de cuándo ocurrió algo.
Como created_at entra en el hash del id, nadie puede cambiarle la fecha a la nota de Alice en silencio
después de que ella la firme. Prueba created_at +1 s en el laboratorio mientras juegas de falsificador: el id se rompe.
Cómo se hace la huella
Para calcular el id, todos los clientes siguen la misma receta de
- Serializar. Pon los campos en un orden fijo dentro de un arreglo JSON, sin espacios extra:
[0, pubkey, created_at, kind, tags, content]. El0inicial es un marcador de versión. - Hashear. Codifica ese texto como bytes UTF-8 y pásalos por
. Salen 32 bytes, escritos como 64 caracteres hexadecimales. Ese es elSHA-256 SHA-256: La función hash que usa Nostr: convierte cualquier entrada en 32 bytes (64 caracteres hex).id. - Firmar. Alice firma el id con su clave secreta usando una firma
. Esa es laSchnorr Firma Schnorr: El esquema de firma que usa Nostr (BIP-340 sobre secp256k1): 64 bytes que demuestran que quien tiene una clave privada aprobó un id de evento.sig.
Un
Bajo el capó: los bytes exactos Mostrar detalles
Esta es la cadena que realmente se hashea para una nota pequeñita. Fíjate: sin espacios, sin saltos de línea, y con los campos exactamente en este orden.
[0,"a73883912a48c3551c1548fd454876d577126a61c76b5486c807bfec5869e183",1735686600,1,[],"testing testing is this thing on"]Dentro de content (y de los valores de los tags) se aplica el escapado de JSON: los saltos de línea se vuelven \n,
las comillas dobles \", las barras invertidas \\, además de \r, \t, \b y \f. Todo lo demás, emojis incluidos,
se escribe como UTF-8 crudo. Dos clientes que no coincidan en un solo byte aquí calcularán ids distintos, por eso
NIP-01 detalla las reglas con tanto cuidado.
Para texto normal, JSON.stringify sobre el arreglo produce exactamente esta forma en JavaScript. Solo difiere en
caracteres de control poco comunes (como U+0001, que escribe como \u0001) y en surrogates sueltos, que NIP-01
pide emitir tal cual.
import { sha256 } from "@noble/hashes/sha2.js";
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
const serialized = JSON.stringify([0, pubkey, created_at, kind, tags, content]);
const id = bytesToHex(sha256(utf8ToBytes(serialized)));Comprobar el sello
Cuando tu app recibe un evento de un
- Vuelve a calcular el id a partir de los campos. ¿Coincide con el
idque dice tener el evento? Si no, algo se cambió después de firmar. - Comprueba la
sigcontra el id y lapubkey. Si falla, el evento no fue firmado con esa clave.
Ambas comprobaciones son matemática pura y se ejecutan en tu propio dispositivo. Esa es la gran idea: no necesitas confiar en el relay. Un relay puede negarse a entregar las notas de Alice, pero no puede cambiar ni una palabra de ellas, ni inventar notas nuevas en su nombre, sin que lo descubran.
En el laboratorio, pulsa Cambia un dígito de la sig. El id sigue coincidiendo (el contenido no cambió), así que la verificación llega hasta la última etapa antes de fallar. Ahora prueba Cambia una letra en content: falla antes, en “Comparar id”.
Bajo el capó: firmar y verificar con nostr-tools Mostrar detalles
Las firmas son firmas Schnorr BIP-340
sobre la curva pubkey es la clave pública “x-only” de 32 bytes, siempre en
hexadecimal en el protocolo (la forma npub… del capítulo 2 es solo para humanos).
import { finalizeEvent, generateSecretKey, verifyEvent } from "nostr-tools/pure";
const sk = generateSecretKey(); // Uint8Array(32)
const event = finalizeEvent(
{
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [["t", "nostr"]],
content: "¡Hola Nostr!",
},
sk,
); // agrega pubkey, id y sig
verifyEvent(event); // true
// Una copia alterada, tal como llegaría desde la red (JSON plano):
const tampered = JSON.parse(JSON.stringify({ ...event, content: "¿Hola Nostr?" }));
verifyEvent(tampered); // false: el id ya no coincidefinalizeEvent y verifyEvent recuerdan su resultado en el objeto del evento bajo un símbolo oculto.
¡verifyEvent({ ...event, content: "changed" }) copia ese true en caché y sigue diciendo
true! Los eventos parseados desde JSON (lo que recibes de los relays) no traen caché, así que la verificación real funciona,
pero ten cuidado cuando modifiques objetos en tu propio código.
finalizeEvent calcula el id con getEventHash (la receta de serializar + SHA-256 de arriba) y lo firma.
verifyEvent hace lo contrario: vuelve a calcular el hash, lo compara con id y luego comprueba la sig.
Los relays hacen las mismas comprobaciones antes de aceptar un evento (NIP-01 les indica que rechacen los inválidos).
Bajo el capó: los formatos de los tags Mostrar detalles
Según NIP-01:
["e", "<32-byte hex event id>", "<recommended relay URL, optional>", "<author pubkey, optional>"]
["p", "<32-byte hex pubkey>", "<recommended relay URL, optional>"]
["a", "<kind>:<pubkey hex>:<d tag value>", "<recommended relay URL, optional>"]Se espera que los relays indexen todos los tags de una sola letra (a–z, A–Z), así que un
{"#t": ["nostr"]} o {"#e": ["<id>"]} funciona.
Las convenciones para hilos (los marcadores root / reply que verás en la 4.ª posición de los tags e en
las respuestas) están en NIP-10. Los tags con
nombres más largos (imeta, client, …) también están permitidos, solo que no se indexan.
Inspecciona cualquier evento
¿Tienes un evento que sacaste de otro lado? Pégalo en la herramienta inspector de eventos: comprueba el id y la firma directamente en tu navegador y explica cada campo.
Comprobación rápida
Lo esencial
- Todo en Nostr (notas, likes, perfiles, listas de seguidos) tiene una sola forma: un evento JSON con siete campos.
- El id es el hash SHA-256 del evento serializado, así que cambiar un solo byte cambia el id.
- La sig es una firma Schnorr sobre ese id: solo quien tiene la clave secreta puede crearla, y cualquiera puede comprobarla.
- Los tags son listas con etiqueta que conectan eventos con otros eventos, personas y temas; los tags de una sola letra se pueden buscar en los relays.
- created_at es lo que afirma el autor, no una prueba del momento.