Ir al contenido

Anclaje

Todo el valor del registro es una fecha. Y mientras no puedas comprobar esa fecha sin preguntarnos, es nuestra palabra.

Por eso cada día el registro publica un recibo: un fichero de texto corto con un resumen criptográfico de cada versión de cada ficha que guarda, y de cada versión de las instrucciones de cada agente. Ese recibo se sella en la cadena de bloques de Bitcoin mediante OpenTimestamps, y por separado en dos autoridades de sellado RFC 3161. Ninguna de las tres la llevamos nosotros.

A partir de ahí la fecha deja de estar en nuestras manos. Si mañana cambiáramos una fecha de registro, el recibo de ayer dejaría de cuadrar, y el recibo de ayer ya está fuera de nuestro alcance.

DemuestraNo demuestra
Que estos datos existían antes de un bloque concreto de BitcoinQue quien registró sea el dueño legítimo
Que su contenido era exactamente ese en esa fechaQue la fecha de registro declarada sea cierta, solo que existía antes del ancla
Que las ediciones posteriores no borraron el historialNada sobre las fichas creadas después del último ancla

Las anclas son diarias, así que entre registrar y anclar hay una ventana de hasta 24 horas en la que sigue habiendo que fiarse de nosotros. Preferimos decirlo a que lo descubras.

Un fichero de texto plano, y esos bytes son el artefacto: no una representación de él, sino la cosa misma. Un espacio de más cambiaría su huella y rompería todas las pruebas.

TheHumanBehind registry anchor
anchor_version: 2
recipe_version: 3
cutoff_utc: 2026-08-11T03:45:00.000000Z
records: 101
versions: 128
agent_versions: 14
merkle_root: 3a02599887336f2ab822d678b50edb05bad62eff392ba826093960faaaa543cb
prev_receipt_sha256: genesis
recipe: https://thehumanbehind.com/docs/anchoring

prev_receipt_sha256 es el SHA-256 del recibo del día anterior, y encadena la serie: un recibo no se puede sustituir sin romper todos los que vinieron después. El primero de todos dice genesis.

records cuenta números de registro distintos, versions cuenta versiones del contenido de las fichas y agent_versions cuenta versiones de instrucciones de agente. El total de hojas del árbol es versions + agent_versions, que es lo que hace falta para saber cuántas hojas tenía que haber.

Las anclas del 5 al 10 de agosto de 2026 llevan el formato 1, que no tiene línea recipe_version ni agent_versions y dice anchor_version: 1. Se conserva byte a byte porque esos recibos ya están sellados.

Toda línea, en los dos formatos, es clave: valor, una por línea, y está pensada para leerse por prefijo. Un lector escrito para el formato 1 encuentra dentro de uno del 2 todo lo que ya conocía: simplemente ignora dos líneas que no había visto nunca. Si analizas los recibos por número de línea, este es el cambio que te va a romper.

recipe_version pasó a estar DENTRO de los bytes sellados a propósito. Hasta el formato 2 vivía solo en una columna de nuestra base de datos. Dentro del recibo, la sella Bitcoin.

Cada recibo se sirve para siempre en {SITE.platformUrl}/api/v0/anchors/{"{fecha}"}.txt, y la lista de anclas, con su bloque de Bitcoin, en {SITE.platformUrl}/api/v0/anchors.json.

Esto es un contrato. Una prueba sellada hoy tiene que verificar dentro de diez años, así que nada de lo que sigue puede cambiar nunca. Si algún día hiciera falta una versión nueva, tendría su propio prefijo y la anterior seguiría funcionando siempre, para que ninguna prueba quede huérfana.

Han existido tres recetas y las tres siguen vivas. Cada ancla publica su propia recipe_version en /api/v0/anchors.json y se verifica con la receta con la que se selló.

RecetaAnclasPreimagen de la hoja
v1solo la del 5-ago-2026THB-ANCHOR-v1|<registry_code>|<version>|<content_hash>
v2del 6 al 10-ago-2026THB-ANCHOR-v2|<registry_code>|<version>|<content_hash>|<leaf_salt>
v3desde el 11-ago-2026THB-ANCHOR-v3|<clase>|<registry_code>|<version>|<digest>|<leaf_salt>

Desde la v3 una hoja puede acreditar dos cosas distintas, y dice cuál en el campo 2, antes que cualquier otro dato:

THB-ANCHOR-v3|record|<registry_code>|<version>|<content_hash>|<leaf_salt>
THB-ANCHOR-v3|agent|<registry_code>|<version>|<agent_sha256>|<leaf_salt>

La hoja es el SHA-256 de esos bytes, en UTF-8.

Una hoja record acredita que el contenido público de una ficha era exactamente ese en esa versión. Una hoja agent acredita que unas instrucciones de agente existían y eran exactamente esas, en esa versión.

La clase va dentro porque, sin ella, una prueba de instrucciones sería indistinguible de una prueba de contenido de ficha, y cualquiera podría presentar la primera como si fuera la segunda. Va en el campo 2, delante de todo valor que venga de un usuario, así que dos hojas de clases distintas siempre difieren, sea cual sea el resto.

El texto de las instrucciones no entra nunca en la hoja. Entra su huella. Por eso la prueba sobrevive incluso si algún día hay que retirar ese texto por contener datos personales: la huella, la fecha, la versión y la sal siguen ahí, y el camino de inclusión sigue verificando.

Toda hoja lleva el número de registro, y no un identificador interno, por dos motivos: los identificadores internos no tienen por qué salir de la base de datos, y tú tienes que poder recalcular tu propia hoja con lo que ya tienes. Todos los valores están en la constancia descargable de tu ficha.

El número de versión entra porque, sin él, dos versiones de la misma ficha con el mismo contenido colapsarían en una sola hoja.

leaf_salt son 16 bytes aleatorios, en hexadecimal, y es lo que hace que esto sea privado. Cada versión tiene la suya, no sale nunca de nuestro servidor, y recibes la tuya, y solo la tuya, dentro de tu constancia.

Está ahí porque un hash solo esconde su entrada cuando esa entrada es difícil de adivinar. Los números de registro son secuenciales y una ficha tiene pocos campos, así que una hoja sin cegar sería un compromiso sobre datos de poca entropía, y esos se atacan probando candidatos hasta que uno cuadra. Los 16 bytes aleatorios eliminan esa posibilidad: ya no hay nada que probar. Con las instrucciones pesa todavía más que con el contenido: las plantillas de prompt se repiten entre clientes, así que sin cegar serían muy adivinables.

Ascendente por registry_code, luego por el rango de la clase (record = 0, agent = 1), y luego por version como número.

El rango es un número y no la palabra, a propósito: agent va antes que record en orden alfabético, así que ordenar por el literal pondría las instrucciones de una ficha por delante de su contenido, y el orden publicado dependería de cómo se llamen las clases.

Así que una ficha con dos versiones de contenido y dos de instrucciones aporta, en este orden:

THB-2026-00144 · record · 1
THB-2026-00144 · record · 2
THB-2026-00144 · agent  · 1
THB-2026-00144 · agent  · 2

Las versiones se comparan como números: como texto, la 10 caería entre la 1 y la 2.

Los números de registro se comparan como texto, por sus bytes. Hasta el 31 de julio de 2026 el número era de ancho fijo y eso equivalía a compararlos como números; ya no, porque el ancho pasó a ser mínimo y no fijo, así que pasadas 99.999 fichas en un año THB-2026-100000 va antes que THB-2026-99999. No pasa nada: la regla es el orden por los bytes del código, que está bien definida y la puede reproducir cualquiera. Esta página afirmaba que los dos órdenes eran equivalentes, y esa frase estaba mal.

Un árbol de Merkle binario. Cada nodo interior es el SHA-256 de sus dos hijos concatenados como bytes crudos, no como texto hexadecimal.

Cuando un nivel tiene un número impar de nodos, el último sube tal cual al nivel siguiente. No se duplica. Duplicarlo es lo que hace Bitcoin, y es lo que hizo posible CVE-2012-2459: duplicando, dos conjuntos de hojas distintos pueden dar la misma raíz, lo que en un registro significaría dos historias distintas detrás de una sola prueba.

La raíz del nivel más alto es el merkle_root del recibo.

La constancia de tu ficha te da los hermanos que hacen falta para subir desde tu hoja hasta la raíz, cada uno con el lado que le toca. Empieza por tu hoja y en cada paso hashea el par en el orden indicado. Si acabas en el merkle_root de ese recibo, tu ficha estaba en ese ancla.

El mismo código sirve para las dos clases de hoja, porque lo único que cambia es lo que dice la preimagen. Si tu ficha es de tipo agente, anchor.agent_instructions de tu constancia trae una entrada por cada versión anclada de tus instrucciones, cada una con su preimagen, su sal y su camino, todos subiendo a la misma raíz.

import hashlib

# Directo de tu proof.json: `leaf_preimage` ya viene unido, no hay nada que
# montar. Esta es una hoja de instrucciones.
preimagen = "THB-ANCHOR-v3|agent|THB-2026-00144|2|0acb5e6a…50|5051…5f"
camino = [  # tambien de tu proof.json, en orden
    ("b1ef4d226bd74200ffdcc71b40b0b361280c96c5005fab3c3e8cb511e4b0044e", "right"),
    # ...
]

actual = hashlib.sha256(preimagen.encode()).digest()
for hermano_hex, lado in camino:
    hermano = bytes.fromhex(hermano_hex)
    par = hermano + actual if lado == "left" else actual + hermano
    actual = hashlib.sha256(par).digest()

print(actual.hex())   # debe ser el merkle_root del recibo de ese dia

Todo lo anterior demuestra dónde está una ficha hoy. Lo que suele decidir una reclamación es qué decía antes, y cuándo cambió. Las dos cosas se conservan y las dos se anclan.

Una ficha tiene dos historiales de solo añadir, y cada uno produce sus propias hojas:

SerieUna versión cada vez que…Clase de hoja
Contenidocambia alguno de los ocho campos selladosrecord
Instruccionesse reemplazan las instrucciones de un agenteagent

La hoja de una versión es la misma receta que acabas de leer, con el número de esa versión en el campo 4. Con las versiones antiguas no pasa nada especial: la versión 1 de una ficha es una hoja del árbol igual que la 9, y sigue estándolo en todas las anclas desde el día en que se escribió.

Recomputar la huella de una versión antigua, con un navegador y nada más

Sección titulada «Recomputar la huella de una versión antigua, con un navegador y nada más»

El historial público de una ficha verificada está abierto:

curl {SITE.platformUrl}/api/v0/records/laia-torres/history.json

Cada entrada de la serie de contenido lleva el snapshot exacto que se hasheó. Hashéalo como explica comprobar un sello y tiene que salir el content_hash de esa versión. Sin cuenta, sin permiso y sin nada que creerte.

La serie de instrucciones publica cuántas versiones ha habido y cuándo, y nada más. Ni el texto, que es de quien lo escribió, ni su huella: las instrucciones de un agente suelen ser plantillas que se repiten entre clientes, así que publicar el digest permitiría a cualquiera con una plantilla candidata confirmar que esa ficha la usa. Eso es un oráculo de confirmación, y es justo por lo que hubo que sustituir la receta v1. La huella viaja al dueño de la ficha, dentro de su propia constancia, y a ningún sitio más: la base de datos tampoco entrega esa columna a quien lee de forma anónima, porque una promesa que la base de datos no cumple no es una promesa.

Para eso hace falta la hoja, la hoja necesita la sal, y las sales no son públicas nunca: si lo fueran, cegar no serviría de nada y volvería el ataque que mató a la v1.

Por eso el camino completo vive en el proof.json del dueño, que ahora lleva una entrada por versión, en dos sitios que reflejan las dos series:

  • Las versiones de contenido están en history.versions, cada una con el snapshot que se hasheó y un objeto leaf con la preimage ya montada, el hash de la hoja y el inclusion_path.
  • Las versiones de instrucciones están en anchor.agent_instructions.versions, cada una con su leaf_preimage, su leaf y su inclusion_path. Sus entradas en history.versions llevan leaf: null y un proof_in que apunta aquí, para no escribir la misma prueba dos veces.

Ese fichero es del dueño y puede publicarlo donde quiera, y quien lo reciba no necesita nada nuestro para comprobarlo: recomputa cada hoja desde su preimagen, recorre su camino, aterriza en la raíz de Merkle del ancla, y comprueba esa raíz contra el recibo y el .ots, que son públicos. Lo único que pones de otra persona es el fichero.

Las versiones escritas después del último ancla salen como pending_next_anchor y sin camino, porque de verdad todavía no están en ese árbol. Su sal y su preimagen se entregan igualmente, para que se puedan guardar hoy y comprobar contra el ancla de mañana.

Dos maneras. La primera si quieres acabar en un minuto, la segunda si quieres que siga funcionando dentro de veinte años.

Descarga el recibo y su prueba:

curl -O {SITE.platformUrl}/api/v0/anchors/2026-08-06.txt
curl -O {SITE.platformUrl}/api/v0/anchors/2026-08-06.ots

Suelta los dos ficheros en opentimestamps.org. Te dirá qué bloque de Bitcoin contiene ese recibo. El hash se calcula en tu navegador; el fichero no sale de tu máquina.

Esa web funciona sobre una librería de JavaScript que no recibe un cambio funcional desde 2022. Por eso la lista de anclas publica además, para cada una, la altura del bloque de Bitcoin y el merkle root de ese bloque. Busca el bloque en cualquier explorador, compara el merkle root y ya está. Sin herramientas, sin librerías, sin depender de que una web siga existiendo.

Con el cliente de OpenTimestamps instalado, esos dos valores salen de:

ots --no-bitcoin verify 2026-08-06.txt.ots

El mismo recibo, sellado por dos autoridades independientes. Se comprueban al instante, sin cadena de bloques y sin esperas:

curl -O "{SITE.platformUrl}/api/v0/anchors/2026-08-06.tsr?tsa=freetsa"
openssl ts -verify -data 2026-08-06.txt -in 2026-08-06.tsr -CAfile cacert.pem

Existen junto al ancla de Bitcoin, no en su lugar, porque las dos fallan de maneras opuestas. Si los calendarios de OpenTimestamps desaparecieran antes de completar una prueba pendiente, los tokens seguirían en pie. Si una autoridad cerrara o le comprometieran la clave, seguiría en pie Bitcoin.

OpenTimestamps no pone una transacción en la cadena por cada fichero. Los calendarios juntan miles de digests, montan un árbol y publican una sola raíz. Así que una prueba recién hecha dice pendiente durante unas horas, hasta que esa transacción se confirma, y el registro la completa solo cuando ocurre. confirmed: false en la lista de anclas es sencillamente una prueba en camino: el estado normal durante parte del día.

Los calendarios nunca ven tus datos. Antes de enviar nada, el cliente añade 16 bytes aleatorios al digest y lo vuelve a hashear, así que lo que viaja es un valor opaco del que no se puede recuperar ni el recibo ni su huella.

El recibo tampoco contiene datos personales: solo conteos y una raíz.

Y los hermanos de tu camino de inclusión no revelan nada de las fichas a las que pertenecen, porque cada hoja va cegada con su propia sal. Ese es todo el trabajo de leaf_salt: sin 128 bits de azar que no están en ningún sitio público, no hay candidato que probar contra un hermano, así que un camino demuestra que tu ficha está en el árbol sin decir nada de las demás.

Lo que los conteos sí revelan, y preferimos decirlo

Sección titulada «Lo que los conteos sí revelan, y preferimos decirlo»

El recibo publica cuántas fichas, cuántas versiones de contenido y cuántas versiones de instrucciones guardaba el registro ese día. Los números de registro son secuenciales y públicos, así que los totales nunca fueron un secreto, y con las hojas cegadas un conteo no confirma nada de ninguna ficha en concreto.

Pero mientras el registro sea pequeño, que agent_versions cambie de un día para otro dice que alguien editó las instrucciones de su agente ese día. Ni quién ni qué: el texto sigue siendo privado y la huella sigue cegada. Lo público es el hecho y la fecha de una edición. Si eso te importa, conviene saberlo antes de registrar, no después.