Datos enlazados: estructuras arbitrarias verificables por hash
Un modelo donde el enlace es un tipo de dato y su valor es un identificador de contenido convierte cualquier estructura en un grafo recorrible y verificable, con límites muy concretos.
Las cuatro lecciones anteriores han montado las piezas: la recursión que define el grafo, la propiedad de resumen que lo hace comparable, el troceado que fabrica nodos donde solo había bytes y el identificador que se explica a sí mismo. Falta ponerles encima un modelo de datos, porque hasta ahora hemos hablado de nodos con datos y enlaces sin decir nunca de qué tipo son esos datos ni qué es exactamente un enlace. La respuesta que ha terminado imponiéndose consiste en tomar el repertorio de tipos que cualquiera reconocería de un formato de intercambio moderno —nulos, booleanos, números, cadenas, bytes, listas y mapas— y añadirle uno más, el enlace, cuyo valor es un identificador de contenido. Ese añadido, que en la lista parece el menos importante, es el que convierte una serialización cualquiera en un grafo verificable de alcance arbitrario, y esta lección cierra el nivel explicando qué se puede construir con él y qué queda deliberadamente fuera.
- Entender por qué el enlace debe ser un tipo del modelo y no una cadena que casualmente contiene un identificador.
- Manejar el recorrido por rutas que atraviesan fronteras de bloque de forma transparente.
- Reconocer qué habilita el modelo: lectura parcial verificada, esquemas sobre códecs distintos y adopción de formatos previos.
- Delimitar con precisión sus límites, de la ausencia de mutación a la imposibilidad de consultar por valor.
El enlace como tipo de primera clase
La tentación evidente al construir esto es no hacer nada: guardar el identificador como una cadena dentro de un mapa y que cada aplicación sepa qué campos son enlaces y cuáles no. Funciona, y falla en cuanto el sistema deja de ser una sola aplicación. Si el enlace no está tipado, ninguna herramienta genérica puede recorrer el grafo, porque no distingue un identificador de una cadena que se le parece; el recolector de basura no sabe qué está referenciado y qué no; el sincronizador no puede calcular el cierre transitivo de lo que hay que transferir; y el verificador no sabe dónde termina un bloque y empieza otro. Todas esas funciones exigen entender la topología sin entender la semántica, y para eso el enlace tiene que ser reconocible por el analizador, no por la aplicación.
// El repertorio del modelo, con el enlace como un tipo mas
const nodo = {
titulo: "informe", // cadena
paginas: 128, // entero
revisado: true, // booleano
firma: bytesDe("..."), // bytes opacos
secciones: [ // lista
{ enlace: "id-de-la-seccion-uno" }, // ENLACE, no una cadena cualquiera
{ enlace: "id-de-la-seccion-dos" },
],
anterior: { enlace: "id-de-la-version-previa" },
};
Hay además una razón de higiene que refuerza el argumento. Si el enlace fuera una cadena, nada impediría construirlo concatenando texto, compararlo con expresiones regulares o guardarlo con la escritura textual que viniera dada, y cada una de esas prácticas introduce una forma distinta de que dos referencias al mismo bloque dejen de parecerse. Con un tipo propio, el analizador decodifica a bytes al leer y vuelve a codificar al escribir, de modo que la identidad canónica está garantizada por el formato y no por la disciplina de quien programa.
Esa distinción tiene una consecuencia conceptual que conviene enunciar. Con el enlace tipado, la frontera entre un bloque y otro deja de ser una decisión semántica y pasa a ser una decisión de empaquetado. Que las secciones del informe estén dentro del mismo bloque o en bloques aparte no cambia lo que el documento significa; cambia cuánto hay que descargar para leer el título, con qué granularidad se deduplica y qué se invalida al editar. El modelo permite mover esa frontera sin tocar el esquema, y esa libertad es lo que hace posible afinar el troceado de la lección anterior sin renegociar el formato.
Un modelo de datos no es un formato. Los mismos valores lógicos —un mapa con estas claves, esta lista, este enlace— se pueden serializar de varias maneras: una binaria compacta pensada para almacenamiento, una textual legible pensada para depuración, o una adaptada a un formato que ya existía antes. Como el códec forma parte del identificador, esas serializaciones conviven sin ambigüedad en el mismo grafo, y una herramienta genérica puede recorrer un enlace que apunta a un bloque de otro formato sin saber nada de él más allá de qué códec lo decodifica. Ahí está la puerta que permite incorporar estructuras heredadas al grafo en lugar de reimportarlas.
Rutas: recorrer el grafo como si fuera un documento
Con el enlace tipado, el recorrido se puede expresar de una forma que oculta por completo dónde termina cada bloque. Una ruta es una secuencia de claves e índices, y el resolutor la sigue campo a campo; cuando el valor que encuentra en un paso es un enlace, resuelve el bloque correspondiente, lo verifica contra el identificador que ya tenía y continúa dentro de él como si nunca hubiera cambiado de bloque.
// El resolutor cruza fronteras de bloque sin que la ruta se entere
async function resolver(id, ruta) {
let bloque = await leerYVerificar(id);
let valor = bloque;
for (const paso of ruta) {
valor = valor[paso];
if (esEnlace(valor)) {
bloque = await leerYVerificar(valor.enlace); // salto de bloque
valor = bloque;
}
}
return valor;
}
flowchart TB R[raiz del documento] -->|secciones 2| S[bloque de la seccion tres] S -->|parrafos 0| P[bloque del parrafo] P -->|texto| T[valor final] R -.->|no se descarga| O[resto de secciones] style T fill:#a6e3a1,color:#11111b style O fill:#6c7086,color:#cdd6f4
Lo que el diagrama muestra es la lectura parcial verificada, que es la capacidad más útil del modelo y la que separa esto de un simple formato de serialización. Para leer un campo profundo de una estructura de cien gigabytes se descargan únicamente los bloques del camino, y cada uno se autentica al llegar contra el identificador que venía en el bloque anterior, que a su vez se autenticó contra el suyo, hasta la raíz de la que partías. No hace falta confiar en quien sirve los bloques ni descargar nada que no vayas a mirar, y las dos cosas se obtienen del mismo mecanismo.
Merece la pena señalar que el número de saltos de red es la longitud del camino, no su tamaño, y que esos saltos son inevitablemente secuenciales porque no sabes el identificador del siguiente bloque hasta haber leído el anterior. Es la misma latencia acumulada que sufre cualquier estructura enlazada sobre un medio con retardo, y las implementaciones serias la combaten igual: enviando la ruta entera al servidor para que devuelva todos los bloques del camino en una sola respuesta, que el cliente verifica en orden sin haber pagado una ronda por nivel.
Leer un campo profundo de una estructura de cien gigabytes
bloques del camino ....... uno por nivel, del orden de cinco a diez
bytes descargados ........ los de esos bloques, no los del subgrafo
verificaciones ........... una por bloque, encadenadas desde la raiz
confianza requerida ...... solo el identificador de raiz que ya tenias
el servidor puede ser hostil y el resultado sigue siendo correcto
Esa última línea es la que conviene retener, porque describe una propiedad poco común. En una interfaz convencional, pedir un campo concreto a un servidor implica confiar en que el servidor devuelva el campo correcto, y no hay forma de comprobarlo desde el cliente sin descargar lo suficiente para recalcularlo. Aquí no hay nada que confiar: el servidor puede negarse a responder, puede responder despacio o puede mandar basura, y las tres cosas se detectan, pero no puede mandar un valor falso que pase la verificación. La distinción entre negar el servicio y falsificar la respuesta se vuelve nítida, y solo la primera sigue siendo posible.
Lo que el modelo habilita
Con el enlace tipado y las rutas encima, aparecen cuatro capacidades que ninguna de ellas por separado da. La primera ya está dicha: leer una parte de una estructura enorme con verificación de extremo a extremo. La segunda es la compartición estructural entre versiones, que ahora funciona sobre datos con esquema y no solo sobre ficheros: una versión nueva de un documento reutiliza todos los subárboles intactos y escribe únicamente los nodos del camino modificado, exactamente como en la primera lección pero con campos, tipos y significado.
La tercera es la interoperabilidad hacia atrás. Cualquier sistema anterior que ya nombrara sus objetos por el hash de su contenido resulta ser, mirado desde aquí, una instancia de este modelo con un códec propio: un sistema de control de versiones cuyos objetos apuntan a otros objetos por hash, una cadena de bloques donde cada bloque referencia al anterior, un fichero de bloqueo de dependencias que fija cada paquete por su resumen. Reservarles un código de códec los incorpora al grafo sin migrar nada, y esa es la razón práctica de que el códec esté dentro del identificador.
La cuarta es que las herramientas genéricas se vuelven posibles. Un recolector de basura que marque desde las raíces, un sincronizador que calcule el cierre transitivo de lo alcanzable, un validador de esquema que compruebe la forma de un nodo, un explorador que permita navegar el grafo a mano: todos ellos se escriben una sola vez y funcionan sobre cualquier aplicación, porque operan sobre la topología y no sobre el significado.
Ese último punto merece insistencia porque marca la diferencia entre un formato y un ecosistema. En una arquitectura convencional, cada aplicación reimplementa su propio recorrido, su propia recolección y su propia sincronización, y ninguna de esas piezas se puede reutilizar porque todas dependen del esquema concreto. Aquí la separación entre topología y semántica es explícita y está en el tipo de dato, de modo que la infraestructura se escribe contra la primera y las aplicaciones contra la segunda. Es exactamente la misma división de trabajo que hace útil a un sistema de ficheros, donde el núcleo entiende directorios sin entender qué hay dentro de los ficheros.
Lectura parcial verificada
Se descargan solo los bloques del camino y cada uno se autentica contra el identificador que traía el anterior.
Estructuras arbitrarias
Listas, mapas y enlaces bastan para expresar árboles, índices, historiales y grafos de aplicación completos.
Formatos previos incorporados
Cualquier esquema que ya nombrara por hash entra en el grafo con su propio códec, sin reescribir lo publicado.
Herramientas genéricas
Recolección, sincronización y validación se escriben una vez porque solo dependen de la topología.
Los límites, que son tan importantes como las capacidades
Conviene enumerarlos sin suavizarlos, porque son los que decidirán si este modelo encaja en un problema concreto o si lo estás forzando. Cuatro de ellos son estructurales y no se arreglan con una implementación mejor: la ausencia de mutación, la ausencia de tiempo y autoría, la imposibilidad de consultar por valor y la separación entre nombrar y tener. Los cuatro tienen soluciones conocidas que viven fuera del esquema, y en los cuatro casos el trabajo de diseño consiste en decidir dónde poner esa capa de fuera y qué garantías vas a exigirle.
El primero y el que más consecuencias arrastra: aquí no hay mutación. Cambiar algo significa escribir nodos nuevos y obtener una raíz nueva, y nada dentro del grafo indica que esa raíz sustituya a la anterior. Todo sistema real necesita por tanto una capa de indirección mutable —un nombre estable que apunte a una raíz cambiante— y esa capa vive fuera del esquema, con sus propios problemas de autoridad, concurrencia y consenso. Es la frontera exacta donde este nivel entrega el testigo a los relojes lógicos, a los CRDT y a los motores de sincronización del resto del track.
El segundo es la ausencia de tiempo y de autoría. El grafo dice qué contiene cada nodo y nada más: no dice quién lo escribió, ni cuándo, ni en qué orden se produjeron dos raíces distintas. Todo eso hay que añadirlo como datos dentro de los nodos —firmas, marcas causales, referencias a los padres— y esos datos serán verificables como contenido, pero su semántica la aporta la capa de arriba, no el hash.
Merece la pena señalar que ese puntero mutable concentra, él solo, casi todos los problemas difíciles que el grafo elimina. Quién tiene derecho a moverlo, qué ocurre si dos escritores lo mueven a la vez, cómo se propaga su nuevo valor y cómo sabe un lector que el valor que ve es el último son exactamente las preguntas de un sistema distribuido con estado compartido. El Merkle DAG no las resuelve, pero hace algo valioso: las reduce a un único dato diminuto, de modo que todo el aparato de consenso, firmas o convergencia se aplica sobre unas decenas de bytes en lugar de sobre la estructura entera.
El tercero es que no se puede consultar por valor. Con un identificador vas del nombre al contenido en un paso; con un criterio sobre el contenido no vas a ninguna parte, porque el hash destruye toda relación entre valores parecidos. Preguntar qué nodos tienen el campo autor igual a cierto valor exige un índice construido aparte, y ese índice es a su vez una estructura que hay que mantener, versionar y sincronizar. La estructura de índice que se acaba usando —un árbol de búsqueda cuyos nodos son bloques enlazados por hash— es en sí misma un ejercicio interesante, y el motivo de que existan bibliotecas dedicadas a ello.
Es el límite que más sorprende en producción. Un enlace es siempre correcto y siempre verificable, incluso cuando nadie en el mundo conserva el bloque al que apunta. No hay enlaces rotos en el sentido clásico, hay enlaces irresolubles, y el remedio no es reparar la referencia sino conseguir que alguien vuelva a alojar los bytes. Un sistema que dé por hecho que un identificador implica disponibilidad tendrá un fallo que aparece meses después, cuando el nodo que servía cierto subárbol se apaga y el grafo, formalmente intacto, deja de poder recorrerse. Nombrar, almacenar y servir son tres problemas distintos, y este nivel solo resuelve el primero.
Conviene cerrar el nivel con la observación que lo unifica, porque explica por qué estos cinco temas tan dispares terminan siendo el mismo tema. Si nombras los datos por su contenido y algún dato contiene el nombre de otro, ya has construido un Merkle DAG, aunque nunca hayas oído la expresión y aunque tu diseño no la mencione. La recursión no es una decisión de diseño que se pueda tomar o no: es lo que ocurre inevitablemente en cuanto un contenido direccionado por hash referencia a otro contenido direccionado por hash. Por eso el patrón reaparece, con vocabularios distintos y sin coordinación alguna, en sitios que no se hablan entre sí: un sistema de control de versiones cuyos árboles apuntan a sus contenidos, un fichero de bloqueo de dependencias que fija cada paquete por su resumen y cada resumen depende del de sus propias dependencias, un registro de imágenes cuyas capas se nombran por su hash, una cadena de bloques donde cada bloque incluye el del anterior, un sistema de copias de seguridad que trocea y comparte. Ninguno de esos equipos se propuso construir un grafo de Merkle; lo construyeron porque la propiedad que buscaban —que un nombre pequeño baste para garantizar una colección grande— solo se obtiene de esa forma, y una vez la tienes obtienes también, quieras o no, la deduplicación estructural, la comparación en tiempo constante, la verificación incremental y la inmutabilidad. Reconocer ese parentesco tiene un valor práctico inmediato, y es la razón por la que este nivel merece las cinco lecciones: cuando te encuentres uno de estos sistemas por primera vez, sabrás qué preguntas hacerle —cómo trocea, cómo canonicaliza, qué lleva su identificador, dónde vive su puntero mutable— y las respuestas te dirán casi todo sobre su comportamiento antes de leer su documentación. Y hay un último corolario que conviene llevarse porque delimita las expectativas para el resto del track: el direccionamiento por contenido resuelve la identidad con una perfección casi total y no resuelve absolutamente nada del resto. No te dice qué versión es la buena, ni quién puede escribir, ni en qué orden ocurrieron las cosas, ni dónde encontrar los bytes, ni cómo fusionar dos ramas que divergieron. Esa modestia es justamente su virtud: al ocuparse de una sola cosa y hacerla de forma incondicionalmente verificable, se convierte en el cimiento sobre el que las capas de arriba pueden discutir todo lo demás sin tener que discutir también si están hablando del mismo dato.
- Elige una entidad de una aplicación tuya y exprésala en el modelo, decidiendo qué campos son valores y cuáles enlaces.
- Justifica cada frontera de bloque en términos de qué se descarga al leer y qué se invalida al escribir.
- Escribe un resolutor de rutas y comprueba que leer un campo profundo descarga solo los bloques del camino.
- Produce una segunda versión modificando una hoja y cuenta cuántos bloques nuevos aparecen frente a cuántos se reutilizan.
- Diseña dónde vivirá el puntero mutable que apunta a la raíz vigente y qué pasa si dos escritores lo mueven a la vez.
- Elige un sistema que ya uses y que nombre por hash, y dibuja su Merkle DAG identificando nodos, enlaces y raíz.