Guías

Guías · Parte 6 de 6

Certificados de inclusión

Has publicado una raíz de Merkle bajo la label 309, junto con la lista de hojas fuera de la cadena que la respalda. Esa raíz es un compromiso perfectamente válido, pero no es algo que puedas entregarle a un colega, adjuntar a un contrato o presentar en un expediente judicial. Un certificado de inclusión es ese elemento entregable: un pequeño archivo autocontenido que fija una o más hojas a la raíz publicada, incorpora cada hermano necesario para volver a derivar esa raíz y nombra la transacción de Cardano cuya hora de bloque da fe de todo el conjunto. Cualquiera puede verificarlo sin conexión, contra cualquier explorador, sin cuenta y sin confiar en quien lo haya producido.

Si conoces OpenTimestamps, es la misma idea —un recibo portátil de prueba de existencia— con dos diferencias deliberadas. La autoridad de marca temporal es la hora de bloque de la cadena de Cardano, no un servidor de calendario. Y la prueba se genera y se verifica enteramente del lado del cliente: sin pasarela, sin servidor emisor, sin confianza en nosotros en ningún paso. Es el equivalente, en clave de prueba de existencia, de un archivo .ots, anclado en Cardano y verificable de forma autónoma.

Qué demuestra un certificado, y qué no

Un certificado hace exactamente dos afirmaciones criptográficas, cada una comprobable de forma independiente por cualquiera:

  1. Inclusión. Una leaf dada ocupa la posición index de un árbol de Merkle SHA-256 según RFC 9162 de tamaño tree_size cuya raíz es root. Esto se demuestra recalculando la raíz a partir de la hoja, su índice, el tamaño del árbol y la ruta de hermanos incorporada. Es autocontenido: basta con el propio archivo del certificado.
  2. Anclaje. Esa root aparece textualmente en el campo merkle[].root del registro de la label 309 que transporta la transacción tx_hash. Esto se demuestra leyendo esa transacción en cualquier explorador público de Cardano y comparando los bytes. Necesita el archivo del certificado más un explorador, nada más.

Juntas demuestran que el contenido de la hoja existía en o antes de la hora de bloque de tx_hash.

Qué NO demuestra un certificado

La hora la afirma la cadena de bloques pública, no queda ligada criptográficamente a la prueba —exactamente igual que con OpenTimestamps y Chainpoint, confías en la hora de bloque de la cadena, nunca en quien produjo el certificado—. Un certificado no es una marca temporal electrónica «cualificada» eIDAS (RFC 3161 / una TSA cualificada); es una marca temporal anclada en una cadena de bloques: una prueba corroborante sólida de una afirmación temporal, en la misma categoría que otras marcas temporales basadas en cadenas de bloques, y la redacción del archivo lo dice tal cual. No dice nada sobre quién es el autor del contenido (eso es una firma de registro opcional; consulta Firmas), ni demuestra que el contenido no se conociera antes. Demuestra existencia para una fecha límite, no autoría ni novedad.

Por qué la prueba no va firmada

Un Receipt COSE estricto del IETF es un COSE_Sign1 cuyo payload es la raíz de Merkle, firmado por alguna autoridad. Aquí la autoridad es la cadena de bloques, no una clave en nuestro poder, así que firmar la raíz con nuestra clave reintroduciría confianza en el servidor y rompería la propiedad de verificabilidad autónoma sobre la que descansa todo el estándar. En lugar de eso, el certificado emite la estructura CBOR de prueba de inclusión del IETF exactamente como está especificada, y lleva el anclaje en la cadena de bloques en lugar de la firma. La matemática de la prueba es idéntica byte a byte a la codificación del IETF; no va firmada de forma deliberada y está anclada en la cadena de bloques.

El certificado JSON

El artefacto principal es label-309-inclusion-certificate-v1: un archivo JSON legible tanto para humanos como para máquinas. Un solo archivo cubre una o muchas hojas, y cada elemento incorpora su ruta completa de hermanos, de modo que el archivo se vuelve a verificar para siempre: sin recuperación desde Arweave, sin pasarela, sin necesidad del publicador original.

contract.cert.json
{
  "format": "label-309-inclusion-certificate-v1",
  "generated_at": "2026-06-16T12:00:00.000Z",   // informational only, never trusted
  "anchor": {
    "chain": "cardano",
    "network": "mainnet",
    "tx_hash": "…64hex…",
    "metadata_label": 309,
    "block_time": 1781611200,                    // POSIX seconds — explorer-asserted
    "block_time_iso": "2026-06-16T12:00:00.000Z",
    "block_height": 12345678,                    // optional; explorer-asserted
    "explorer_urls": [
      "https://cardanoscan.io/transaction/…",
      "https://adastat.net/transactions/…"
    ]
  },
  "merkle": {
    "tree_alg": "rfc9162-sha256",
    "root": "…64hex…",
    "tree_size": 1024,                           // === the on-chain leaf_count
    "leaves_list_uri": "ar://<txid>"             // optional source reference
  },
  "items": [
    {
      "leaf": "…64hex…",                         // the content hash committed as a leaf
      "leaf_alg": "sha2-256",                    // how to hash a file to reproduce `leaf`
      "index": 42,
      "proof": ["…64hex…", "…64hex…"],           // siblings, leaf→root; [] for a single-leaf tree
      "verified": true,                          // proof recomputes to merkle.root at build time
      "label": "contract.pdf"                    // optional note/filename
    }
  ],
  "claim": "Each listed hash was included in a Merkle tree whose root was published on the Cardano blockchain in the referenced transaction under metadata label 309; therefore each hash provably existed on or before the stated block time.",
  "verification": {
    "method": "RFC 9162 (Certificate Transparency) SHA-256 inclusion proof. For each item, recompute the Merkle root from leaf+index+tree_size+proof and compare to merkle.root; then confirm merkle.root equals the merkle[].root in the label 309 record of anchor.tx_hash on any public Cardano explorer.",
    "requires_trust_in_cardanowall": false,
    "time_asserted_by": "Cardano blockchain (block time), via public explorers"
  }
}

Algunos detalles que conviene conocer:

  • root, leaf y cada entrada de proof[] son valores en bruto de 32 bytes representados en hex. Los productores los emiten en minúscula; un verificador acepta cualquiera de los dos casos y rechaza cualquier carácter no hexadecimal o cadena de longitud impar.
  • El "verified": true almacenado es el resultado del productor en el momento de la construcción. Un verificador nunca confía en él: recalcula la prueba por sí mismo y reporta su propio veredicto. Una hoja que no se encontró en el árbol se registra con "verified": false y un campo "error", nunca se descarta en silencio, de modo que el archivo es honesto sobre los fallos.
  • block_time es la marca temporal POSIX —afirmada por el explorador— del bloque que la incluye. block_time_iso es su representación en UTC, solo por comodidad.

La prueba de inclusión en CBOR (alineada con COSE / RFC 9162)

Junto al JSON, cada elemento puede exportarse como un artefacto .cbor compacto cuya estructura de prueba es idéntica byte a byte a draft-ietf-cose-merkle-tree-proofs. Esto significa que cualquier verificador de estructuras de datos verificables RFC 9162 / COSE puede leer la matemática de la prueba directamente: el núcleo de interoperabilidad es estándar, no a medida. No transporta ninguna hora de bloque absoluta ni prosa jurídica (eso vive en el JSON); es únicamente el núcleo portátil de la prueba, y está anclado en la cadena de bloques y sin firmar por la razón expuesta más arriba.

La prueba de inclusión escueta del IETF —bstr .cbor [tree_size, leaf_index, inclusion_path], con el valor vds (verifiable-data-structure) 1 para RFC 9162 SHA-256— se puede extraer por sí sola para un verificador COSE puro; el anclaje en Cardano se transporta junto a ella como un pequeño mapa en lugar de la firma Sign1.

Construir y verificar con las herramientas

El formato de certificado forma parte de las herramientas públicas e independientes de la pasarela: el SDK @cardanowall/sdk-ts (con gemelos idénticos byte a byte en Python y Rust), la CLI cardanowall y las superficies de verificación de las aplicaciones. La matemática de construcción y verificación es pura y sin conexión: solo la resolución de datos frescos de la cadena toca la red.

Con la CLI

certificate build toma la lista de hojas, los objetivos (hex de hoja en bruto, o archivos a los que calcular el hash) y la transacción cuyos datos de anclaje resuelve. certificate verify vuelve a ejecutar la prueba de inclusión por cada elemento e imprime el anclaje que todavía te toca confirmar en cadena:

terminal
# Build a certificate for two files against a published root.
cardanowall certificate build \
  --leaves-list leaves.cbor \
  --tx <tx-hash> \
  --file contract.pdf --file exhibit-a.png \
  --out contract.cert.json

# Re-verify the proofs offline — no network, no trust in the producer.
cardanowall certificate verify contract.cert.json

El código de salida 0 significa que la prueba de cada elemento se recalcula hasta la raíz; un código distinto de cero señala un fallo de inclusión, una entrada incorrecta o un error de E/S, así que encaja directamente en CI. Cada elemento individual también puede extraerse a la forma canónica { tree_alg, tree_size, index, leaf, proof[] } y comprobarse con cardanowall merkle verify.

Con el SDK de TypeScript

La API certificate es pura: recuperas los bytes de la lista de hojas con el fetch propio de la plataforma y se los pasas; la ruta criptográfica nunca llega a la red:

build-and-verify.ts
import { certificate, merkle } from '@cardanowall/sdk-ts';

// `leaves` comes from decodeLeavesList(...) over the fetched leaves-list bytes.
const cert = certificate.buildInclusionCertificate({
  anchor,                       // chain facts resolved from the tx
  merkle: { treeAlg: 'rfc9162-sha256', root, treeSize: leaves.length },
  leaves,
  targets: [{ leaf, leafAlg: 'sha2-256', label: 'contract.pdf' }],
});

// Pure re-verification from the certificate alone — no Arweave, no chain.
const result = certificate.verifyInclusionCertificate(cert);
console.log(result.ok);          // true when every item's proof recomputes to root
console.log(result.anchorClaim); // the anchor you confirm on a public explorer

// Each item also re-verifies through the plain Merkle predicate.
const item = cert.items[0];
const ok = merkle.merkleSha2256VerifyInclusion(
  leafBytes, item.index, cert.merkle.tree_size, proofBytes, rootBytes,
);

verifyInclusionCertificate reporta el veredicto de la prueba y devuelve como eco el anclaje afirmado; confirmar ese anclaje en cadena es tu paso aparte y explícito, y el objeto de resultado lo dice así. El mismo módulo se replica byte a byte en los SDK de Python (certificate) y Rust (certificate).

En el navegador, en una página de transacción

Un registro de la label 309 que lleve un compromiso merkle[] muestra un panel de inclusión en su página de transacción. Pega uno o más hashes en hex, o suelta los archivos originales para calcular su hash del lado del cliente; la página recupera la lista de hojas directamente desde el almacenamiento direccionado por contenido en tu navegador, recalcula cada prueba, muestra un veredicto verde/rojo por elemento y ofrece el JSON, el CBOR y un PDF imprimible para descargar. El PDF incorpora el JSON completo como un archivo adjunto, así que él mismo es el artefacto verificable por máquina, no solo una imagen de uno. Nada de esto toca un servidor privado.

El algoritmo de verificación, de principio a fin

Para verificar un certificado de forma independiente —en tu propio código, sin ninguna herramienta nuestra— haz exactamente esto:

  1. Rechaza los campos malformados. Cada entrada de root/leaf/proof[] debe ser hex de longitud par que decodifique a 32 bytes; tree_size y cada index deben ser enteros seguros con index < tree_size y 1 ≤ tree_size ≤ 2³² − 1.
  2. Recalcula la raíz de cada elemento. Usando RFC 9162 §2.1.3.2, pliega la hoja (leaf = SHA-256(0x00 ‖ leaf_digest)) con su ruta de hermanos (node = SHA-256(0x01 ‖ L ‖ R)), dividiendo en la mayor potencia de dos estrictamente por debajo del tamaño del subárbol en curso, y compara el resultado con merkle.root byte a byte. Un árbol de una sola hoja tiene una prueba vacía.
  3. Confirma el anclaje en cadena. Recupera anchor.tx_hash en cualquier explorador público de Cardano, lee sus metadatos de la label 309 y confirma que merkle.root es igual al merkle[].root del registro. La hora de bloque que leas ahí es la marca temporal que afirma el certificado.

Los pasos 1 y 2 son la parte autocontenida: nunca salen de tu máquina. El paso 3 es la única lectura de red, y va a un explorador que tú eliges, nunca a quien produjo el certificado.

Un archivo, verificable para siempre

Como cada hermano de la ruta está incorporado, un certificado sigue verificándose mucho después de que la lista de hojas, la pasarela original o el productor hayan desaparecido. Las dos comprobaciones —recalcular la raíz a partir del archivo, confirmar la raíz en cadena— son todo lo que nadie necesita jamás. Consulta Lotes con Merkle para ver cómo se construyen la raíz y la lista de hojas en primer lugar, y Verificación para el modelo de verificador completo en el que encaja la comprobación del lado de la cadena.