wandres.dev
DIRECCIONAR POR CONTENIDO I · el hash como identidad

El cambio de idea: pedir por contenido, no por ubicación

Pasar de dame el fichero que vive en esta ruta a dame el fichero cuyo contenido tiene este resumen separa la identidad del dato de su ubicación, y esa separación reorganiza el sistema entero.

⏱ 18 min

Casi todo el software que existe pide datos de la misma manera: nombra un lugar y confía en que allí haya algo. Una ruta de fichero, una dirección web, una clave primaria, un desplazamiento dentro de un bloque de memoria. En todos esos casos el nombre describe dónde hay que mirar y no dice absolutamente nada sobre qué se va a encontrar, de modo que la relación entre el nombre y los bytes es puramente convencional y alguien tiene que sostenerla con disciplina. El direccionamiento por contenido invierte esa relación: el nombre de un dato se calcula a partir del dato mismo mediante una función resumen criptográfica, y por tanto deja de ser una convención para convertirse en un hecho comprobable por cualquiera que tenga los bytes delante. Este nivel entero desarrolla las consecuencias de ese giro. Esta primera lección se ocupa de entenderlo con precisión, porque quien lo aprende deprisa se queda con la versión pobre, que consiste en creer que se trata de una forma exótica de poner nombres.

🎯 Al terminar esta lección sabrás
  • Distinguir un nombre por ubicación de un nombre por contenido y saber qué garantiza cada uno.
  • Entender qué propiedad de las funciones resumen criptográficas sostiene todo el edificio.
  • Reconocer qué familia de problemas desaparece al separar la identidad de un dato de su ubicación.
  • Manejar el vocabulario mínimo sin caer en las confusiones habituales del término.

Lo que una ruta promete y lo que no

Conviene empezar examinando qué información transporta realmente un nombre por ubicación, porque su familiaridad hace que nadie se lo pregunte nunca. La cadena /var/datos/informe.pdf no dice nada sobre el informe: dice dónde hay que ir a mirar. La correspondencia entre ese nombre y unos bytes concretos la sostiene una tabla mutable —el directorio del sistema de ficheros, el enrutador del servidor, la caché de un intermediario— que cualquiera con permiso puede modificar sin que el nombre cambie un solo carácter. El nombre es estable; lo que hay detrás, no. Esa asimetría es la fuente de una familia entera de problemas que solemos tratar como si fueran independientes.

Vale la pena enumerarlos seguidos, porque verlos juntos es lo que revela que son el mismo. ¿Es este fichero el mismo que descargué ayer? No se puede saber por el nombre: hay que leer los bytes y compararlos. ¿Puedo reutilizar la respuesta que ya tengo guardada? No se puede saber: hay que inventar un protocolo de expiración con validadores y fechas. ¿Me han dado lo que pedí? No se puede saber: hay que confiar en quien responde y en todo el camino intermedio. ¿Tienen estos dos servidores la misma copia? No se puede saber sin transferir al menos una de las dos y compararla entera.

flowchart LR
N[nombre por ubicacion] --> T[tabla mutable]
T --> B1[unos bytes hoy]
T -.-> B2[otros bytes otro dia]
H[nombre por contenido] --> F[funcion resumen]
F --> U[unos bytes y solo esos]
style T fill:#f38ba8,color:#11111b
style F fill:#a6e3a1,color:#11111b

Un ejemplo cotidiano fija la idea mejor que cualquier definición. Dos personas descargan el mismo instalador desde la misma dirección con una semana de diferencia y obtienen ficheros distintos, porque quien lo publica corrigió algo sin cambiar la dirección. Ninguna de las dos tiene forma de detectarlo mirando lo que pidió, y el fallo que una reproduce y la otra no tardará días en atribuirse a su causa real. El nombre coincidía; los bytes no.

# Lo unico que comparten estas dos descargas es la cadena que se escribio
curl -O https://ejemplo.org/instalador.tar.gz   # un lunes
curl -O https://ejemplo.org/instalador.tar.gz   # el lunes siguiente
# nombre identico, bytes quiza distintos, y nada en el nombre lo revela

La respuesta habitual de la industria a esa lista ha sido construir maquinaria encima: cabeceras de validación, marcas de versión, firmas digitales sobre la respuesta, comprobantes de integridad publicados aparte. Toda esa maquinaria funciona, y toda ella comparte una característica que conviene nombrar sin rodeos: es un remiendo sobre una decisión de nombrado que se tomó antes y que nadie revisó. Se está reconstruyendo, con protocolo y con confianza, una propiedad que podría haber estado en el nombre desde el principio.

Conviene además notar que el nombrado por ubicación no es una torpeza histórica, sino una elección con ventajas reales que hay que reconocer antes de abandonarla. Un nombre por ubicación es legible por personas, es corto, se puede memorizar y se puede recordar de una versión a la siguiente, que es exactamente lo que se necesita para que alguien escriba una dirección en una libreta. Todo el diseño que sigue consiste en conservar esa comodidad donde hace falta y quitarla de todos los sitios donde su precio no compensa.

El nombre que se calcula a partir del dato

Una función resumen criptográfica toma una secuencia de bytes de cualquier longitud y devuelve una cadena de longitud fija. Lo que la hace utilizable como sistema de nombres no es que comprima, porque no comprime nada, sino dos propiedades: es determinista, de modo que los mismos bytes producen siempre la misma salida en cualquier máquina y en cualquier época; y es resistente a colisiones, lo que significa que nadie sabe cómo construir dos entradas distintas con la misma salida, ni siquiera dedicando a ello un presupuesto grande.

Esa segunda propiedad es la que convierte el resumen en una identidad y no en un mero código de detección de errores. Un código de detección de errores basta para descubrir corrupción accidental; una función resistente a colisiones resiste además a un adversario que intenta fabricar una coincidencia a propósito. La diferencia no es de grado sino de género, y es la razón de que en este contexto se usen funciones como sha256 o blake3 y no un crc32.

# El nombre no se elige: se calcula a partir de los bytes
printf 'hola mundo' | sha256sum
# 0b894166... este resumen es el nombre del dato, y lo es en todas partes

# Cambiar un solo byte produce un nombre completamente distinto
printf 'hola Mundo' | sha256sum
# 5b1e5b8b... ninguna relacion visible con el anterior

En cuanto el nombre se calcula, la operación de guardar cambia de forma. Ya no se recibe un nombre y unos bytes que hay que asociar, sino solo los bytes, y el nombre sale de ellos. El almacén deja de tener una tabla que mantener y pasa a tener una función que aplicar.

// Guardar por contenido: la clave sale del valor, no de quien escribe
async function guardar(almacen, bytes) {
  const resumen = await hash(bytes);  // sha256, blake3 u otra resistente
  almacen.set(resumen, bytes);        // escribir dos veces no cambia nada
  return resumen;                     // el nombre devuelto es el recibo
}

// Leer incluye siempre la comprobacion, porque es barata y cierra el ciclo
async function leer(almacen, resumen) {
  const bytes = almacen.get(resumen);
  if (await hash(bytes) !== resumen) throw new Error('bloque corrupto');
  return bytes;
}

Merece la pena anticipar la objeción del coste, porque es la primera que aparece en cualquier discusión y suele estar mal calibrada. Calcular un resumen exige recorrer todos los bytes, de modo que es una operación lineal, pero es una operación lineal muy barata: las funciones modernas se acercan al ancho de banda de la memoria y quedan por debajo del coste de la lectura de disco o de la transmisión por red que casi siempre las acompaña. En la práctica se paga en el mismo recorrido en que ya se iba a leer el dato, y quien mide un sistema de este tipo descubre que el resumen rara vez aparece en el perfil.

ℹ️
El resumen no oculta ni comprime: solo nombra

Es la confusión más frecuente y conviene desmontarla pronto. Un resumen criptográfico no es cifrado: no hay clave, no hay operación inversa y no protege la confidencialidad de nada. Tampoco es compresión: los bytes originales siguen ocupando lo que ocupaban y el resumen no permite reconstruirlos. Lo único que hace es asignar a cada secuencia de bytes un identificador corto y estable que cualquiera puede recalcular. Toda la potencia viene de que ese cálculo lo pueda repetir quien recibe el dato, no de ninguna propiedad de secreto.

Qué se gana al separar la identidad de la ubicación

Con el nombre calculado, la ubicación no desaparece: se degrada a pista. Sigue habiendo que averiguar dónde están los bytes, pero esa averiguación pasa a ser una cuestión de logística y deja de ser una cuestión de corrección. Si la pista falla, se prueba otra; si la pista miente, se detecta al comprobar. La identidad del dato ya no depende de ningún acuerdo sobre dónde vive.

🔗

La identidad viaja con el dato

Un bloque conserva su nombre al copiarse a otro disco, a otra máquina o a otra década, porque el nombre se recalcula sobre los mismos bytes.

🗺️

Muchas ubicaciones, una identidad

Un mismo nombre puede resolverse contra un disco local, un compañero de red o un servidor lejano, y las tres respuestas son intercambiables.

⚖️

Comparar deja de exigir transferir

Saber si dos réplicas coinciden se reduce a comparar dos cadenas cortas, sin mover un solo byte de la carga útil.

🧷

Las referencias dejan de romperse

Una referencia por contenido no apunta a un sitio que puede cambiar de manos, sino a unos bytes que o están disponibles o no lo están.

La tercera tarjeta es la que más lejos llega y merece un comentario aparte, porque es la base de todos los protocolos de sincronización eficientes que aparecen más adelante en el track. Cuando la identidad es un resumen corto, la pregunta qué te falta de lo que yo tengo se contesta intercambiando listas de nombres en lugar de contenidos, y la diferencia de coste entre ambas cosas es de varios órdenes de magnitud. Los árboles de resúmenes, donde el nombre de un nodo se calcula a partir de los nombres de sus hijos, extienden esa idea a colecciones enteras y permiten localizar la divergencia entre dos estructuras grandes en tiempo logarítmico.

// Un nodo interno se nombra por los nombres de sus hijos: la raiz cubre todo
async function raizDe(bloques) {
  let nivel = await Promise.all(bloques.map(hash));
  while (nivel.length > 1) {
    const siguiente = [];
    for (let i = 0; i < nivel.length; i += 2) {
      siguiente.push(await hash(nivel[i] + (nivel[i + 1] ?? nivel[i])));
    }
    nivel = siguiente;
  }
  return nivel[0];  // un solo nombre que resume la coleccion entera
}

La consecuencia de ese esquema es que un único nombre corto puede certificar una estructura arbitrariamente grande, y que comparar dos estructuras enormes empieza por comparar dos cadenas. Si coinciden, se ha terminado sin transferir nada. Si no coinciden, se desciende por los hijos que difieren y se ignoran las ramas idénticas. Ese descenso es la operación que sostiene la sincronización incremental, la comparación de copias de seguridad y la verificación parcial, y todas ellas serían impracticables si la identidad siguiera dependiendo de dónde vive el dato.

El vocabulario mínimo y sus trampas

El término técnico que se usa en la literatura es almacenamiento direccionable por contenido, y en los sistemas modernos conviene añadir dos matices al hablar de él. El primero es que el nombre casi nunca es el resumen desnudo: suele llevar delante unos bytes que indican qué función se usó y qué longitud tiene, precisamente para que el sistema pueda migrar a otra función en el futuro sin romper los nombres antiguos. El segundo es que a menudo el nombre identifica un bloque de un tamaño acotado, y un fichero grande se representa como un árbol de bloques cuya raíz es el nombre que se cita.

Ese primer matiz merece insistencia porque es donde se equivocan casi todos los diseños nuevos. Ninguna función resumen dura para siempre: las que se consideraban sólidas hace veinte años están hoy rotas, y las de hoy se romperán en algún momento. Un sistema que grabe resúmenes desnudos en todas partes tendrá que decidir, el día que su función caiga, entre reescribir todos sus nombres o vivir con una garantía que ya no vale. Un sistema que incluya en cada nombre la identificación de la función podrá empezar a emitir nombres nuevos con otra función mientras sigue resolviendo los antiguos, y hará la transición sin cortes. La agilidad criptográfica cuesta dos bytes por nombre y se decide una sola vez, al principio.

Hay además dos confusiones de vocabulario que conviene desactivar antes de continuar. La primera es tomar un nombre por contenido por una dirección de red: no lo es, no contiene ninguna indicación de dónde buscar y necesita siempre un mecanismo aparte que lo resuelva contra una o varias fuentes. La segunda es suponer que el nombre depende de cómo se representó el dato: depende de los bytes exactos, de modo que dos codificaciones distintas del mismo objeto lógico producen nombres distintos, y por eso todos estos sistemas exigen una serialización canónica antes de nombrar. Ignorar ese requisito produce el fallo más desconcertante de la familia, que es un almacén donde el mismo dato aparece dos veces con nombres diferentes.

⚠️
El nombre por contenido no dice si el dato es correcto, solo si es el que pediste

La verificación que ofrece un resumen es de integridad, no de veracidad. Si alguien te convence de pedir el nombre de un documento falso, recibirás ese documento falso con integridad perfecta y comprobada. Toda la confianza se desplaza a cómo obtuviste el nombre: de una firma, de una etiqueta publicada, de una recomendación. Este desplazamiento es una ventaja enorme porque concentra el problema en un punto pequeño y auditable, pero solo si se reconoce que el punto sigue ahí.

Un nombre es una promesa, y la pregunta de diseño es quién la sostiene

Si hay que quedarse con una sola idea de esta lección, no es la función resumen, que es un detalle de implementación reemplazable, sino la reformulación que la hace útil: todo sistema de nombres es un contrato sobre qué se puede afirmar del referente sin ir a buscarlo, y ese contrato tiene siempre un fiador. En el nombrado por ubicación el fiador es una autoridad: el sistema de ficheros, el dueño del dominio, el administrador de la base de datos. Alguien mantiene una tabla y su palabra es lo que hace verdadera la correspondencia. Ese esquema es sencillo y funciona perfectamente mientras la autoridad esté presente, sea única y siga siendo la misma con el tiempo, tres condiciones que se cumplen en una máquina y se rompen todas a la vez en cuanto hay varias réplicas, particiones de red o décadas de por medio. En el nombrado por contenido el fiador es la aritmética: la correspondencia es verdadera porque se puede recalcular, y por tanto no necesita que nadie esté vivo, ni conectado, ni sea honesto. Se sustituye una relación social por una relación computable, y eso es exactamente lo que permite que el dato sobreviva a la infraestructura que lo produjo. La consecuencia práctica es que el nombrado deja de ser una decisión menor que se toma al final, cuando ya está el modelo de datos, y pasa a ser el punto donde se decide cuánta autoridad necesita el sistema para funcionar. Cada vez que un componente exige que alguien conserve una correspondencia entre un nombre y un valor, estás comprando coordinación permanente y la estás pagando en disponibilidad. Cada vez que un nombre es autoverificable, estás comprando libertad de topología: cualquiera puede servir el dato, cualquiera puede almacenarlo, cualquiera puede comprobarlo. Casi ningún sistema puede ser enteramente autoverificable —la lección cuarta de este nivel es precisamente el inventario de lo que no se puede evitar—, pero la fracción de superficie que se deja bajo nombrado por ubicación es una decisión deliberada, y en la mayoría de las arquitecturas nadie la ha tomado nunca de forma consciente.

⚔️ Reescribe una parte de tu sistema en términos de contenido
  1. Elige un almacén que uses hoy y anota, para cada nombre que maneja, quién sostiene la correspondencia entre ese nombre y sus bytes.
  2. Calcula el resumen sha256 de tres artefactos que tu sistema produzca y comprueba si dos de ellos coinciden sin saberlo.
  3. Reescribe una función de guardado para que devuelva el resumen en lugar de aceptar una clave, y observa qué se rompe.
  4. Busca en tu código todos los sitios donde se pregunta si dos ficheros son iguales y estima cuánto se transfiere para responder.
  5. Identifica una referencia rota que hayas sufrido y decide si un nombre por contenido la habría evitado o solo la habría movido.
  6. Escribe la lista de nombres de tu arquitectura que necesitan una autoridad viva y ordénala por lo caro que sale que se caiga.