Ir al contenido

Hashing y Merkle trees

Esta página especifica, byte a byte, qué compromete CORE-M a la blockchain. Tres implementaciones independientes — el servicio de telemetría, el servicio de verificación, y cualquier verificador externo — deben calcular el hash idéntico a partir de las mismas entradas, por lo que el algoritmo es totalmente determinista y se describe aquí con exactitud. Si tu implementación diverge aunque sea en un solo byte, la verificación fallará por diseño.

Cada punto de telemetría se reduce a un único hash de 32 bytes usando doble SHA-256 — SHA-256 aplicado dos veces. Es la misma construcción que se usa en cada nodo interno del Merkle tree (ver más abajo), de modo que todo el árbol es uniforme:

leaf_hash = SHA256( SHA256( preimage ) )

El preimage es la concatenación de bytes, en este orden exacto:

#ComponenteCodificaciónDetalles
1version1 byteConstante 0x01 — la versión del esquema de hash.
2tenant_id_len2 bytes, uint16 big-endianLongitud en bytes del tenant_id (UTF-8).
3tenant_idbytes UTF-8La cadena tenant_id.
4device_id_len2 bytes, uint16 big-endianLongitud en bytes del device_id (UTF-8).
5device_idbytes UTF-8La cadena device_id, sin terminador nulo.
6timestamp8 bytes, uint64 big-endianÉpoca Unix en segundos. Los nanosegundos se truncan.
7payloadbytes UTF-8El objeto de valores serializado con RFC 8785 JCS (más abajo). Sin prefijo de longitud.

El tenant_id forma parte del hash: un verificador que lo omita calcula un hash distinto, y por eso la API verify/raw exige tenant_id. Los dos prefijos de longitud uint16 hacen que el límite entre tenant_id y device_id sea inequívoco, de modo que dos pares tenant/device distintos nunca pueden producir el mismo preimage.

El payload se canonicaliza con el JSON Canonicalization Scheme de RFC 8785 antes de hacerle el hash, de modo que payloads lógicamente idénticos siempre se serialicen a los mismos bytes:

  • Las claves de los objetos se ordenan lexicográficamente por code point Unicode.
  • Sin espacios en blanco entre tokens.
  • Los números se serializan según las reglas JCS — sin ceros finales, sin punto decimal superfluo, en la forma más corta que permita el round-trip.
  • La cadena JSON resultante se codifica como UTF-8.

Como las claves se ordenan de forma determinista, el orden en que un dispositivo emite sus campos no importa: {"temperature":22.5,"humidity":65} y {"humidity":65,"temperature":22.5} se canonicalizan a exactamente los mismos bytes y por tanto producen el mismo hash.

Tomemos un punto concreto:

CampoValor
tenant_idT1
device_idD1
timestamp1711000000 (segundos Unix)
valores{"temperature":22.5,"humidity":65}
→ payload JCS{"humidity":65,"temperature":22.5}
→ preimage (hex)0100025431000244310000000065fbc9c07b2268756d6964697479223a36352c2274656d7065726174757265223a32322e357d
leaf_hash (hex)9774d8af1f5e5a0457275ec82b482e0e1b46f6fc421720a279d0e661f99755d2

Leyendo el preimage de izquierda a derecha: 01 (versión) · 0002 (longitud de tenant_id) · 5431 ("T1") · 0002 (longitud de device_id) · 4431 ("D1") · 0000000065fbc9c0 (timestamp) · después los bytes UTF-8 del payload JCS. El leaf_hash de 32 bytes es SHA256(SHA256(preimage)) — el valor que alimenta el Merkle tree y, en última instancia, el commitment on-chain.

Anclar una transacción por punto sería lento y costoso, así que un batch de hashes de puntos se compromete en conjunto mediante un Merkle tree binario de doble SHA-256. Solo el root de 32 bytes va on-chain; cada punto conserva un Merkle path corto que prueba su pertenencia.

Reglas de construcción:

  • Los valores leaf_hash del batch son las hojas (leaves).
  • Los nodos adyacentes se emparejan y se les hace doble hash: parent = SHA256(SHA256(left || right)), donde left y right son los hashes hijos de 32 bytes concatenados en orden de árbol (orden de bytes interno — sin inversión de bytes).
  • Si un nivel tiene un número impar de nodos, el último nodo se duplica (emparejado consigo mismo) para hacer el recuento par, y entonces continúa el emparejamiento.
  • Un batch no puede contener dos hashes de hoja idénticos — las hojas duplicadas se rechazan (una protección contra CVE-2012-2459).
  • Esto se repite nivel a nivel hasta que queda un único root.

Todo el árbol, hojas incluidas, es doble SHA-256 uniforme — la misma construcción que el leaf hash de arriba.

flowchart TB
  R["Root = SHA256(SHA256(H01 || H23))"]
  H01["H01 = SHA256(SHA256(H0 || H1))"]
  H23["H23 = SHA256(SHA256(H2 || H3))"]
  H0["H0 (leaf)"]
  H1["H1 (leaf)"]
  H2["H2 (leaf)"]
  H3["H3 (leaf)"]
  R --> H01
  R --> H23
  H01 --> H0
  H01 --> H1
  H23 --> H2
  H23 --> H3

El Merkle path de cada punto es la lista ordenada de hashes hermanos (siblings) necesarios para ascender desde esa hoja hasta el root, cada uno etiquetado con una dirección (si el hermano queda a la izquierda o a la derecha). La hoja es el leaf_hash del punto — el mismo valor de 32 bytes que la API de verificación y el almacén de proofs llaman data_hash. Un verificador reproduce el path así:

current = leaf_hash
for each step in merkle_path:
if step.is_right: # sibling is on the right
current = SHA256(SHA256(current || step.hash))
else: # sibling is on the left
current = SHA256(SHA256(step.hash || current))
# current must now equal the Merkle root

Para la hoja H0 en el árbol de arriba, el path es [ {hash: H1, right}, {hash: H23, right} ]: combina con H1 a la derecha para obtener H01, luego con H23 a la derecha para obtener el root. Recomputar el root a partir de un solo punto — sin ninguno de los demás puntos del batch — es precisamente lo que hace la proof portable.

Los paths se calculan en el momento del batch y se almacenan junto a cada hash (y más tarde en la tabla anchor_proofs de PostgreSQL como JSONB). El recorrido completo se muestra de principio a fin en Verificación.

La transacción de anchoring tiene una salida: un OP_FALSE OP_RETURN que lleva un payload de layout fijo. Tras los opcodes OP_FALSE OP_RETURN, los campos de datos aparecen en este orden exacto:

OffsetCampoTamañoTipo / codificación
0Prefijo de protocolo6 bytesASCII "CORE-M"
6merkle_root32 bytesMerkle root SHA-256 del batch
38batch_id16 bytesUUID, binario crudo
54timestamp8 bytesuint64 big-endian, segundos Unix
62data_point_count4 bytesuint32 big-endian

El payload total tras los opcodes es de 66 bytes.

OP_FALSE OP_RETURN
"CORE-M" 6 bytes, ASCII protocol prefix
<merkle_root> 32 bytes, SHA-256
<batch_id> 16 bytes, UUID binary
<timestamp> 8 bytes, uint64 big-endian, Unix seconds
<data_point_count> 4 bytes, uint32 big-endian

El algoritmo de hashing idéntico se comparte entre tres fronteras — telemetría (calculando hashes para el anchoring), verificación (recomputando a partir de datos crudos), y cualquier verificador externo. Ese determinismo compartido es toda la base de la garantía.

Siguiente

Continúa con Verificación para recorrer una proof completa desde los datos crudos hasta el commitment on-chain, o con Modos, SLA y finality para saber cómo se programan los batches y se hacen finales.