wandres.dev
DIRECCIONAR POR CONTENIDO II · Merkle DAG y CID

Identificadores de contenido: el hash que dice cómo leerse

Un identificador de contenido lleva dentro versión, códec, función de hash y longitud además del resumen, y esa autodescripción es lo que permite que el formato evolucione sin romper lo ya publicado.

⏱ 19 min

Hasta aquí hemos hablado del identificador de un nodo como si fuera el hash y nada más: treinta y dos bytes que salen de la función y que sirven de nombre. Esa simplificación se sostiene mientras el sistema es uno solo, usa una única función de hash y una única serialización, y no va a cambiar nunca. En cuanto alguna de esas tres condiciones falla —y las tres fallan siempre, porque las funciones se rompen, los formatos mejoran y los sistemas se interconectan— aparece un problema que no tiene solución posterior: un hash desnudo no dice de dónde viene ni qué hay al otro lado, de modo que dos identificadores producidos por sistemas distintos son indistinguibles entre sí y no hay forma de saber cuál interpretar cómo. La respuesta a ese problema es hacer que el identificador se describa a sí mismo, y esta lección explica qué lleva dentro, por qué cada campo está donde está y qué clase de evolución habilita.

🎯 Al terminar esta lección sabrás
  • Entender por qué un hash desnudo es insuficiente como identificador en un sistema que va a durar.
  • Desmontar la anatomía de un identificador autodescriptivo campo por campo y saber para qué sirve cada uno.
  • Manejar la agilidad de hash y de códec, y por qué permiten migrar sin una fecha de corte global.
  • Delimitar qué problemas resuelve la autodescripción y cuáles reintroduce o deja intactos.

Por qué el hash desnudo no basta

Imagina que recibes treinta y dos bytes y te dicen que son el nombre de un nodo. Hay al menos cuatro preguntas que no puedes responder mirándolos. Qué función de hash los produjo, porque muchas funciones modernas dan salidas de la misma longitud y sus resultados son indistinguibles. Qué son los bytes que obtendrás al resolverlos: datos opacos, un nodo con enlaces, un objeto de otro sistema. Si están completos o truncados, porque una salida recortada de una función larga se parece a la salida entera de una función corta. Y cómo se escriben esos bytes como texto sin ambigüedad, porque hay varias codificaciones razonables y el receptor tiene que acertar.

Ninguna de esas preguntas se puede resolver más tarde con metadatos laterales, y la razón es exactamente la que hace valioso el direccionamiento por contenido: el identificador viaja solo. Se copia en un mensaje, se pega en un chat, se guarda en un enlace dentro de otro nodo, se imprime en un papel. Cualquier información necesaria para interpretarlo que no viaje con él acabará perdiéndose, y el momento en que se pierda será el momento en que ya no se pueda reconstruir.

Conviene notar que esas cuatro preguntas no son igual de graves. Confundir la codificación textual produce un error ruidoso e inmediato, y quien lo sufre lo arregla en cinco minutos. Confundir la función de hash produce algo mucho peor: una verificación que no cuadra y que es indistinguible de un bloque corrupto o de un servidor malicioso, de modo que el diagnóstico apunta al sitio equivocado. Y confundir el códec produce lo más insidioso de todo, un bloque que se verifica correctamente y se interpreta mal, es decir, datos silenciosamente erróneos que ninguna comprobación de integridad detectará.

La consecuencia práctica de no resolverlo se ve con claridad en el escenario de migración. Un sistema que usa una función de hash y decide adoptar otra —porque la primera se ha debilitado, o porque la nueva es tres veces más rápida— se encuentra con que sus identificadores nuevos y viejos ocupan el mismo espacio de nombres sin distinguirse. Verificar un identificador exige entonces probar todas las funciones que el sistema haya usado alguna vez, lo que es lento, ambiguo y una superficie de ataque en sí mismo. Sin autodescripción, cambiar de función obliga a una migración global y simultánea, que es justo lo que un sistema descentralizado no puede coordinar.

ℹ️
El identificador es el contrato más difícil de cambiar de todo el sistema

Conviene calibrar bien la asimetría entre las piezas de un diseño así. El formato de los bloques se puede cambiar introduciendo un códec nuevo, el protocolo de red se negocia por versiones, y hasta la estructura del grafo admite convivencia de esquemas. El identificador, no: está copiado en enlaces dentro de nodos inmutables que ya no se pueden reescribir, en marcadores de usuarios, en documentos publicados y en índices de terceros. Cualquier decisión que lo afecte es efectivamente permanente, y por eso el único diseño sensato es uno que anticipe que todo lo demás cambiará y reserve, desde el primer día, el espacio para expresar ese cambio.

Anatomía de un identificador autodescriptivo

La solución estándar tiene cinco componentes concatenados, y su orden no es casual: los campos que dicen cómo leer lo que viene después van primero, de modo que un analizador que lea de izquierda a derecha siempre sepa qué esperar.

Composicion de un identificador de contenido

  version ......... que generacion del formato es este identificador
  codec ........... que hay dentro de los bytes que resolveras
  funcion ......... que algoritmo de hash produjo el resumen
  longitud ........ cuantos bytes ocupa el resumen que viene ahora
  resumen ......... el hash propiamente dicho

  y por encima, solo en la forma textual
  prefijo de base .. con que alfabeto se han escrito los bytes anteriores
// Componer y descomponer, con enteros de longitud variable
function componer({ version, codec, funcion, resumen }) {
  return concat(varint(version), varint(codec), varint(funcion),
                varint(resumen.length), resumen);
}

function descomponer(bytes) {
  const [version, r1] = leerVarint(bytes);
  const [codec, r2] = leerVarint(r1);
  const [funcion, r3] = leerVarint(r2);
  const [longitud, r4] = leerVarint(r3);
  return { version, codec, funcion, resumen: r4.slice(0, longitud) };
}

Cada campo responde a una de las preguntas de la sección anterior y merece un comentario. La versión permite que el propio formato del identificador evolucione, que es la única forma de no quedar atrapado por las decisiones de hoy. El códec dice cómo interpretar los bytes una vez recuperados, y es lo que permite que datos opacos, nodos de un formato binario y objetos importados de otro sistema convivan en el mismo grafo sin confundirse. El código de función identifica el algoritmo dentro de una tabla compartida, y la longitud explícita separa un resumen completo de uno truncado. Los cuatro campos se codifican como enteros de longitud variable para que los valores frecuentes ocupen un byte y quede espacio para valores futuros arbitrariamente grandes.

El prefijo de base pertenece a otra capa y conviene no mezclarlo. Los cuatro campos anteriores y el resumen son bytes; cuando esos bytes hay que escribirlos como texto, se elige un alfabeto y se antepone un carácter que dice cuál. Eso permite tener una forma en minúsculas para sitios donde las mayúsculas se pierden, una forma compacta para ahorrar caracteres y una forma legible para depurar, todas ellas convertibles entre sí sin ambigüedad y todas ellas designando exactamente los mismos bytes.

La elección de enteros de longitud variable para los cuatro campos merece un comentario, porque resuelve una tensión que de otro modo no tendría salida. Con campos de tamaño fijo habría que decidir hoy cuántos bits reservar para códecs que aún no existen: si se reservan pocos, el espacio se agota; si se reservan muchos, cada identificador del mundo carga con bytes vacíos para siempre. El entero de longitud variable elimina la disyuntiva porque el coste lo paga quien usa valores grandes, de modo que los códecs y las funciones frecuentes ocupan un solo byte y el espacio de valores futuros sigue siendo prácticamente ilimitado. Es la misma técnica que usan los formatos binarios longevos, y por la misma razón.

Coste tipico de la autodescripcion frente al resumen

  version ......... 1 byte
  codec ........... 1 o 2 bytes segun el valor
  funcion ......... 1 o 2 bytes segun el valor
  longitud ........ 1 byte
  resumen ......... 32 bytes en el caso habitual

  el sobrecoste ronda el diez por ciento y se paga en cada enlace del grafo
flowchart LR
B[bytes del bloque] --> H[funcion de hash] --> R[resumen]
R --> ID[identificador con version codec funcion y longitud]
C[codec del contenido] --> ID
ID --> T[forma textual con prefijo de base]
style ID fill:#89b4fa,color:#11111b
style T fill:#a6e3a1,color:#11111b

Qué habilita la autodescripción

El beneficio se manifiesta como tres formas de agilidad que un identificador opaco no puede ofrecer. La primera es la agilidad de hash. Cuando aparece una función mejor o cuando la actual se debilita, un nodo puede empezar a publicar con la nueva sin coordinar nada con nadie: los identificadores nuevos se distinguen de los viejos por su campo de función, ambos siguen resolviéndose correctamente y el receptor sabe qué verificador aplicar. La migración deja de ser un evento y se convierte en un gradiente que puede durar años.

Vale la pena calibrar por qué eso es tan valioso justamente aquí. En un sistema con autoridad central, cambiar de función de hash es un proyecto: se anuncia una fecha, se reindexa el almacén y se obliga a los clientes a actualizar. En un sistema donde los identificadores están copiados en enlaces inmutables de nodos que ya nadie controla, esa coordinación es sencillamente imposible, y sin autodescripción la única salida sería quedarse con la función original hasta que resultara insegura y luego romperlo todo a la vez. La agilidad de hash no es una comodidad: es lo que separa un formato que puede durar décadas de uno con fecha de caducidad.

La segunda es la agilidad de códec. El mismo resumen sobre los mismos bytes, con dos códecs distintos, produce dos identificadores distintos, y eso resuelve un problema sutil: los mismos bytes pueden ser válidos bajo dos interpretaciones, y sin el códec en el nombre no habría forma de saber cuál pretendía el autor. Además abre la puerta a incorporar estructuras que ya existían con otro formato —objetos de un sistema de control de versiones, bloques de una cadena, formatos binarios propios— reservándoles un código de códec y traduciendo su serialización, sin reescribir ni un byte de lo ya publicado.

La tercera es la agilidad de longitud. Un entorno con restricciones severas de espacio puede truncar el resumen a la mitad, aceptando conscientemente la reducción de margen frente a colisiones, y el identificador resultante sigue siendo autodescriptivo y distinguible de uno completo. La decisión de cuánta seguridad comprar deja de ser global y pasa a tomarse por caso de uso.

Conviene ver el mecanismo de la migración con concreción, porque es donde la autodescripción demuestra su valor. Un nodo que decide adoptar una función nueva empieza a publicar identificadores con el código correspondiente; los nodos que ya la entienden los verifican sin más, y los que no la entienden se encuentran con un identificador que declara honestamente algo que no saben calcular, de modo que fallan de forma explícita y diagnosticable en lugar de aceptar bytes sin verificar. Ese fallo limpio es tan importante como el éxito: sin el campo de función, el mismo escenario produciría una verificación que no cuadra y un error indistinguible de la corrupción de datos.

🔄

Agilidad de hash

Se puede adoptar una función nueva sin fecha de corte, porque los identificadores dicen con qué algoritmo verificarse.

📦

Agilidad de códec

El mismo grafo admite datos opacos, nodos estructurados y formatos importados sin ambigüedad de interpretación.

📏

Longitud explícita

Un resumen truncado se distingue de uno completo, así que el margen de seguridad se elige por contexto.

✍️

Forma textual sin ambigüedad

El prefijo de base permite varias escrituras del mismo identificador, todas convertibles y ninguna confundible.

⚠️
Convertir la forma textual nunca debe cambiar los bytes subyacentes

El error de implementación más frecuente con estos identificadores consiste en tratar la cadena de texto como si fuera la identidad. No lo es: la identidad son los bytes, y la cadena es una de sus escrituras posibles. Comparar dos identificadores como cadenas puede dar falso negativo si están escritos en bases distintas, y normalizar por reescritura textual sin decodificar puede corromperlos. La regla es sencilla y no admite atajos: se decodifica a bytes, se compara sobre bytes, y la forma textual se elige solo en el momento de mostrar o transportar. Cualquier caché, índice o clave primaria construido sobre la forma textual acabará con dos entradas para el mismo contenido.

Lo que la autodescripción no arregla

Conviene ser preciso sobre los límites, porque la comodidad de un identificador que se explica solo invita a atribuirle propiedades que no tiene. Lo primero: la autodescripción es descriptiva, no normativa. El identificador dice con qué función se calculó el resumen, no que esa función sea segura; si alguien publica con un algoritmo roto, el identificador lo declarará honestamente y tú tendrás que decidir si lo aceptas. La política de qué funciones se admiten es tuya y hay que escribirla, porque un verificador que acepte cualquier código de función que sepa calcular está aceptando también los que ya no debería.

Lo segundo es una tensión con la lección anterior a esta. La propiedad de resumen decía que dos raíces iguales implican grafos iguales, y ahora resulta que el mismo contenido lógico puede tener identificadores distintos por diferir en el códec, en la función o en la longitud. La deduplicación, que era automática, deja de serlo entre poblaciones que no comparten esas elecciones, y eso reintroduce un problema de equivalencia que el direccionamiento por contenido parecía haber eliminado. No hay solución elegante: se elige un perfil por defecto, se documenta y se acepta que la interoperabilidad plena exige convenciones además de formato.

Merece la pena insistir en esa segunda tensión porque tiene una consecuencia operativa concreta. Si dos comunidades adoptan perfiles distintos —una publica con una función y una serialización, otra con otras—, sus grafos serán mutuamente verificables pero no compartirán ni un bloque, y el ahorro por deduplicación entre ellas será exactamente cero por muy idénticos que sean sus datos. La interoperabilidad de formato y la interoperabilidad de almacenamiento son cosas distintas, y la autodescripción garantiza la primera sin acercar en nada la segunda.

Lo tercero es que la canonicidad de la serialización sigue siendo responsabilidad de quien escribe el códec. Un códec que admita dos formas válidas de codificar el mismo valor rompe la deduplicación de manera invisible, y el identificador no avisará porque su trabajo es decir qué códec se usó, no si ese códec era determinista. Y lo cuarto, más prosaico: la autodescripción ocupa. Unos pocos bytes por identificador parecen nada hasta que un nodo interno guarda mil enlaces y el grafo tiene millones de nodos internos.

Un formato solo puede envejecer bien si su identificador reserva sitio para lo que aún no existe

La lección de fondo aquí no trata de hashes sino de longevidad de formatos, y es de las que se aprenden caro. Todo esquema de nombres nace en un momento con unas decisiones técnicas concretas —esta función, esta serialización, esta longitud— que en ese momento parecen las obvias y las definitivas. Lo que la experiencia enseña es que ninguna de esas decisiones sobrevive al horizonte temporal en el que los nombres siguen circulando, y esa asimetría es el problema entero: los identificadores se copian en sitios inmutables y llegan al futuro intactos, mientras que las decisiones que los produjeron caducan. La autodescripción es la única respuesta estructural a esa asimetría, y funciona porque convierte una elección implícita en un dato explícito. Mientras la función de hash sea una convención global sabida por todos, cambiarla exige que todos cambien a la vez, lo cual en un sistema sin autoridad central significa que no se cambia nunca; en cuanto la función es un campo dentro del nombre, cambiarla pasa a ser una decisión local de quien publica, y la migración se convierte en algo que puede ocurrir gradualmente durante una década sin romper nada. Ese es el patrón que conviene extraer y llevarse a cualquier formato que vayas a diseñar: identifica las decisiones que hoy son constantes globales implícitas —la codificación, el algoritmo, la versión del esquema, la unidad de medida— y pregúntate qué pasaría si dentro de ocho años necesitaras cambiar una de ellas mientras siguen circulando datos escritos con la vieja. Si la respuesta es que haría falta que todo el mundo actualizara simultáneamente, tienes un formato que morirá el día que esa necesidad aparezca. Y hay un corolario incómodo que conviene aceptar de entrada: la autodescripción se paga por adelantado y en todos los casos, para beneficiar a unos pocos que llegarán tarde. Cada identificador carga con unos bytes que casi siempre valen cero, exactamente igual que un número de versión al principio de un fichero. Esos bytes son la prima de un seguro contra la única certeza que tiene el diseño de sistemas duraderos, que es que las decisiones de hoy serán las restricciones de mañana.

⚔️ Desmonta identificadores reales
  1. Toma tres identificadores de contenido de sistemas reales y decodifícalos campo a campo hasta separar el resumen.
  2. Comprueba que el resumen coincide con aplicar la función declarada a los bytes que obtienes al resolverlos.
  3. Reescribe uno de ellos en dos bases textuales distintas y verifica que los bytes decodificados son idénticos.
  4. Construye dos identificadores del mismo contenido cambiando solo el códec y observa que no son iguales.
  5. Trunca un resumen a la mitad, ajusta el campo de longitud y razona qué margen de seguridad has perdido.
  6. Escribe la política de funciones aceptables de tu sistema y decide qué hará tu verificador ante una que no reconozca.