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 SHA-256 de 32 bytes:

data_hash = SHA256( device_id_utf8 || timestamp_uint64_be || jcs_payload_utf8 )

Las tres partes se concatenan en este orden exacto sin separadores y sin prefijos de longitud:

#ComponenteCodificaciónDetalles
1device_id_utf8bytes UTF-8La cadena device_id, sin terminador nulo.
2timestamp_uint64_be8 bytes, uint64 big-endianÉpoca Unix en segundos. Los nanosegundos se truncan.
3jcs_payload_utf8bytes UTF-8El payload serializado con RFC 8785 JCS (más abajo).

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:

  • device_id = "D1"
  • timestamp = 1711000000 (segundos Unix)
  • payload = {"temperature": 22.5, "humidity": 65}

Paso 1 — canonicaliza el payload (JCS). Las claves se ordenan, los espacios en blanco se eliminan:

{"humidity":65,"temperature":22.5}

Paso 2 — codifica cada componente.

device_id_utf8 "D1" -> 0x4431
timestamp_uint64_be 1711000000 -> 0x0000000065FBC9C0
jcs_payload_utf8 {"humidity":65,"temperature":22.5} -> UTF-8 bytes of that string

Paso 3 — concatena en orden y SHA-256.

0x4431
|| 0x0000000065FBC9C0
|| <UTF-8 of {"humidity":65,"temperature":22.5}>

El SHA-256 de esa concatenación es el data_hash de 32 bytes. Este es 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 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 data_hash del batch son las hojas (leaves).
  • Los nodos adyacentes se emparejan y se les hace hash: parent = SHA256(left || right).
  • Si un nivel tiene un número impar de nodos, el último nodo se duplica para hacer el recuento par, y entonces continúa el emparejamiento.
  • Esto se repite nivel a nivel hasta que queda un único root.
flowchart TB
  R["Root = SHA256(H01 || H23)"]
  H01["H01 = SHA256(H0 || H1)"]
  H23["H23 = 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). Un verificador lo reproduce así:

current = data_hash
for each step in merkle_path:
if step.is_right: # sibling is on the right
current = SHA256(current || step.hash)
else: # sibling is on the left
current = 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.