Guide

Construye y verifica una prueba de Merkle de Bitcoin con Python

Construye una raíz de Merkle y una rama de inclusión al estilo de Bitcoin con identificadores sintéticos, y prueba la evidencia y tres casos de fallo sin conexión.

9 min de lecturaTransacciones
Construye y verifica una prueba de Merkle de Bitcoin con Python

Demuestra que una transacción está comprometida por una raíz de Merkle

La cabecera de un bloque de Bitcoin contiene una raíz de Merkle de 32 bytes, no todos los identificadores de transacción. Una prueba de inclusión aporta el camino que falta: el identificador de la transacción, su posición, el número de hojas y un hash hermano por cada nivel del árbol. Al repetir el hash por parejas de Bitcoin se debe recuperar la raíz de la cabecera.

Esta guía construye ese proceso con la biblioteca estándar de Python e identificadores de transacción sintéticos y deterministas. No conecta con un nodo, descarga un bloque, crea una wallet ni mueve bitcoin. Puedes borrar el ejemplo al terminar.

La implementación sigue el código de Merkle de Bitcoin Core 31.1 y sus pruebas de Merkle, consultados el 25 de septiembre de 2026. Bitcoin Core 31.1 era la versión estable actual en esa fecha. El ejercicio también usa las reglas de orden de bytes y hojas impares de la referencia para desarrolladores de Bitcoin.

El resultado es deliberadamente limitado. Una rama válida demuestra que un valor está incluido en una raíz de Merkle declarada. No demuestra que esa raíz pertenezca a la mejor cadena, que el bloque cumpla todas las reglas de consenso ni que una transacción omitida no exista.

Requisitos y resultado esperado

Necesitas Python 3 y un editor de texto. No hace falta instalar paquetes ni conectarse a internet. Guarda el programa como merkle_proof_check.py en una carpeta descartable y ejecuta:

python3 merkle_proof_check.py

El entorno probado usó Python 3.12.14. Una ejecución correcta imprime cinco transacciones, una rama de tres hashes y esta raíz:

3f36159f47fbb33f41a3409375003e64da72078e547525ccae47307ae329bdf1

Después muestra True para la prueba prevista y False tras cambiar el identificador, la posición o el primer hermano. Las aserciones exigen los cuatro resultados. Si alguna falla, investiga la implementación o tus cambios locales en lugar de borrar la aserción.

Lee el programa antes de ejecutarlo. Solo acepta cadenas definidas localmente, no usa archivos ni red y escribe únicamente en la salida estándar. Si lo adaptas para analizar datos externos, añade controles estrictos de longitud hexadecimal y límites de recursos antes de considerar segura esa entrada.

El programa completo

#!/usr/bin/env python3
"""Build and verify a Bitcoin-style Merkle branch using synthetic txids."""

from __future__ import annotations

from hashlib import sha256


def hash256(data: bytes) -> bytes:
    return sha256(sha256(data).digest()).digest()


def make_txid(label: str) -> str:
    """Return a display-order txid for a deterministic synthetic transaction label."""
    return hash256(label.encode())[::-1].hex()


def internal_hash(display_hex: str) -> bytes:
    """Convert display-order hex to Bitcoin's internal byte order."""
    return bytes.fromhex(display_hex)[::-1]


def display_hash(internal: bytes) -> str:
    return internal[::-1].hex()


def merkle_root(txids: list[str]) -> str:
    if not txids:
        raise ValueError("need at least one txid")
    level = [internal_hash(txid) for txid in txids]
    while len(level) > 1:
        if len(level) % 2:
            level.append(level[-1])
        level = [hash256(level[i] + level[i + 1]) for i in range(0, len(level), 2)]
    return display_hash(level[0])


def merkle_branch(txids: list[str], position: int) -> list[str]:
    if not 0 <= position < len(txids):
        raise IndexError("position outside txid list")
    level = [internal_hash(txid) for txid in txids]
    branch: list[str] = []
    index = position
    while len(level) > 1:
        if len(level) % 2:
            level.append(level[-1])
        branch.append(display_hash(level[index ^ 1]))
        level = [hash256(level[i] + level[i + 1]) for i in range(0, len(level), 2)]
        index //= 2
    return branch


def branch_depth(leaf_count: int) -> int:
    depth = 0
    while leaf_count > 1:
        leaf_count = (leaf_count + 1) // 2
        depth += 1
    return depth


def verify_branch(
    txid: str,
    branch: list[str],
    position: int,
    leaf_count: int,
    expected_root: str,
) -> bool:
    if leaf_count < 1 or not 0 <= position < leaf_count:
        return False
    if len(branch) != branch_depth(leaf_count):
        return False
    current = internal_hash(txid)
    index = position
    for sibling_hex in branch:
        sibling = internal_hash(sibling_hex)
        current = hash256(sibling + current) if index & 1 else hash256(current + sibling)
        index //= 2
    return display_hash(current) == expected_root.lower()


def main() -> None:
    txids = [make_txid(f"synthetic-tx-{i}") for i in range(5)]
    position = 2
    root = merkle_root(txids)
    branch = merkle_branch(txids, position)

    print(f"transactions: {len(txids)}")
    print(f"position: {position}")
    print(f"txid: {txids[position]}")
    print(f"branch_hashes: {len(branch)}")
    print(f"merkle_root: {root}")
    proof_valid = verify_branch(txids[position], branch, position, len(txids), root)
    print(f"proof_valid: {proof_valid}")

    wrong_txid = make_txid("different-synthetic-transaction")
    wrong_txid_valid = verify_branch(wrong_txid, branch, position, len(txids), root)
    wrong_position_valid = verify_branch(
        txids[position], branch, position + 1, len(txids), root
    )
    print(f"wrong_txid_valid: {wrong_txid_valid}")
    print(f"wrong_position_valid: {wrong_position_valid}")

    changed_branch = branch.copy()
    changed_branch[0] = make_txid("different-sibling")
    changed_branch_valid = verify_branch(
        txids[position], changed_branch, position, len(txids), root
    )
    print(f"changed_branch_valid: {changed_branch_valid}")

    assert proof_valid
    assert not wrong_txid_valid
    assert not wrong_position_valid
    assert not changed_branch_valid


if __name__ == "__main__":
    main()

Lee el árbol desde las hojas hacia arriba

make_txid crea valores reproducibles de 32 bytes mediante el hash de etiquetas. Se parecen a identificadores de transacción, pero no representan transacciones serializadas. Esa separación mantiene el ejercicio centrado en el árbol.

El software de Bitcoin suele mostrar los identificadores con los bytes en el orden inverso al hash interno de 32 bytes. internal_hash convierte el hexadecimal mostrado en los bytes usados para el hash. display_hash devuelve el resultado final al orden visible. Invertir los caracteres hexadecimales, o invertir después de cada ronda de SHA-256, produce un árbol diferente.

hash256 aplica SHA-256 dos veces. Cada pareja se concatena de izquierda a derecha antes del hash. Cuando un nivel tiene un número impar de nodos, el último hash se empareja consigo mismo. Cinco hojas se convierten en seis entradas en el primer nivel y después en tres nodos padre. El último padre se duplica en el siguiente nivel. El árbol llega a una raíz en tres rondas.

index ^ 1 selecciona al hermano. Cambia el último bit del índice actual: un nodo izquierdo con índice par selecciona al siguiente nodo derecho, mientras que un nodo derecho con índice impar selecciona al anterior. Dividir el índice entre dos lleva a la posición del padre para la ronda siguiente.

El verificador necesita la posición original porque el orden de concatenación importa. Si el nodo actual estaba a la derecha, el hermano se procesa primero. Si estaba a la izquierda, el nodo actual va primero. Es la misma regla del bit de posición que ejercitan las pruebas de ComputeMerkleRootFromBranch en Bitcoin Core.

Comprueba la salida

La salida completa probada fue:

transactions: 5
position: 2
txid: f542a9f5c9bee4f3adbbe12c4f5f36106f56a73fbfdd3dc136741d06afbd13e0
branch_hashes: 3
merkle_root: 3f36159f47fbb33f41a3409375003e64da72078e547525ccae47307ae329bdf1
proof_valid: True
wrong_txid_valid: False
wrong_position_valid: False
changed_branch_valid: False

El SHA-256 del archivo fuente fue f8819d70829c7630fab0ca93cf34372e79df34085cc92119205a87e0ddc617fa. El SHA-256 de la salida capturada fue 1436f36f5a55de88e7814f83e986c64e2903c1499b40fb90867132e5f2a4f182. Estos hashes permiten distinguir este ejemplo probado de ediciones posteriores. No autentican código copiado de una página no confiable.

Adapta el ejemplo con cuidado

Para probar otro árbol sintético, cambia las etiquetas o el número de hojas. Para verificar datos de un bloque real, obtén de fuentes confiables los identificadores ordenados, la posición objetivo, el total de transacciones y la raíz de Merkle. Una rama sin posición es ambigua. Una posición sin el orden original tampoco es suficiente.

No trates la respuesta de un explorador de bloques como prueba independiente de sí misma. Para una comprobación más fuerte, obtén la cabecera y el contexto de cadena de tu propio nodo, y compara la raíz reconstruida con la cabecera. La validación completa también verifica la sintaxis de las transacciones, scripts, cantidades, reglas de coinbase y relación con bloques anteriores. Este programa no comprueba nada de eso.

El documento original de Bitcoin describe las ramas de Merkle para la verificación simplificada de pagos. BIP 37 especificó después árboles parciales para bloques filtrados, pero documenta un límite importante: un par deshonesto puede omitir transacciones coincidentes. La evidencia de inclusión no demuestra integridad de la lista.

La implementación de Bitcoin Core también registra una condición de mutación causada por duplicar hashes idénticos al final de un nivel. El problema histórico está asociado con CVE-2012-2459. El ejemplo reproduce el cálculo, pero no expone el indicador de mutación de Core. No lo uses como reemplazo del código de consenso ni como analizador de bloques no confiables.

Solución de problemas

La raíz es completamente distinta: revisa primero el orden de bytes. Convierte cada identificador mostrado a bytes internos antes del hash y solo invierte la raíz final para mostrarla.

Funciona con un número par de hojas pero falla con cinco: duplica el último nodo en cada nivel impar, no solo en la lista original.

Falla la transacción correcta: confirma su posición desde cero en el orden original. Ordenar alfabéticamente los identificadores cambia el árbol.

Una posición cambiada todavía pasa: exige el número total de hojas y la profundidad esperada de la rama. No se deben ignorar bits de posición que queden fuera del árbol.

Los datos de un bloque real no coinciden: comprueba que la primera transacción sea la coinbase, que usaste identificadores de transacción y no identificadores de transacción con testigo, y que el explorador no haya cambiado el orden.

Qué demuestra este ejercicio

El programa se ejecutó sin conexión y con entradas sintéticas deterministas. La prueba prevista pasó y tres controles negativos fallaron. No se usó ningún bloque, transacción, nodo o wallet real.

Eso basta para demostrar el mecanismo: hojas ordenadas, orden interno de bytes, doble SHA-256, duplicación de nodos impares y colocación izquierda o derecha de la rama. No basta para afirmar que un pago real fue confirmado. Esa conclusión requiere una cabecera autenticada, contexto de cadena y las garantías de validación que esperas de un nodo.

Etiquetado

Boletín

Bitcoin, sin ruido

Qué ha pasado en Bitcoin, qué cambia de verdad, y las fuentes para que puedas comprobarnos. Un número cada vez, directo a tu correo.

  • Un correo por número, nunca una secuencia automática
  • Sin píxeles de seguimiento y sin compartir direcciones
  • Baja desde cualquier número con un clic

Recibe el próximo número

Un correo por número, sin píxeles de seguimiento, y te puedes dar de baja desde cualquiera de ellos. No compartimos tu dirección. Política de privacidad