Guide

Verifica la suma de comprobación de una dirección Taproot con Python

Decodifica una dirección Taproot de mainnet, verifica su suma Bech32m y su programa testigo, y rechaza cuatro vectores oficiales no válidos sin conexión.

10 min de lecturaTransacciones
Verifica la suma de comprobación de una dirección Taproot con Python

Comprueba una dirección antes de confiar en lo que indica

Una dirección de Bitcoin es una instrucción para construir una salida, no una cuenta ni una prueba de que alguien controle una clave. Una dirección Taproot nativa contiene tres datos importantes: el prefijo legible de la red, la versión de testigo 1 y un programa testigo de 32 bytes. También contiene una suma de comprobación Bech32m que detecta muchos errores de transcripción.

Esta guía decodifica un vector de prueba oficial de BIP 350 con la biblioteca estándar de Python. Verifica la suma, la red, la versión de testigo, la longitud del programa y el relleno. Después rechaza cuatro vectores oficiales no válidos. El programa funciona totalmente sin conexión. No crea una wallet, deriva claves, consulta un nodo, transmite una transacción ni mueve bitcoin.

La implementación sigue BIP 350, que especifica Bech32m para las versiones de testigo 1 a 16, y BIP 173, que define la estructura Bech32 original. BIP 341 define las salidas Taproot como versión 1 con un programa de 32 bytes. Estas fuentes y la implementación Bech32 de Bitcoin Core 31.1 se comprobaron el 2 de octubre de 2026. Bitcoin Core 31.1 era la versión estable actual en esa fecha.

Requisitos y resultado esperado

Necesitas Python 3 y un editor de texto. No hace falta instalar paquetes externos ni conectarse a internet. Crea una carpeta descartable, guarda el programa siguiente como taproot_address_check.py, revísalo y ejecuta:

python3 taproot_address_check.py

El entorno probado usó Python 3.12.14. Una ejecución correcta imprime el programa de 32 bytes decodificado:

79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798

También informa de que rechazó cuatro controles negativos: una dirección de versión 1 con la suma Bech32 antigua, una suma con un carácter no válido, un programa de un byte y una cadena que mezcla mayúsculas y minúsculas.

El programa completo

#!/usr/bin/env python3

CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
CHARSET_MAP = {char: index for index, char in enumerate(CHARSET)}
BECH32_CONST = 1
BECH32M_CONST = 0x2BC830A3


def bech32_polymod(values):
    generators = (
        0x3B6A57B2,
        0x26508E6D,
        0x1EA119FA,
        0x3D4233DD,
        0x2A1462B3,
    )
    checksum = 1
    for value in values:
        top = checksum >> 25
        checksum = ((checksum & 0x1FFFFFF) << 5) ^ value
        for index, generator in enumerate(generators):
            if (top >> index) & 1:
                checksum ^= generator
    return checksum


def hrp_expand(hrp):
    return [ord(char) >> 5 for char in hrp] + [0] + [ord(char) & 31 for char in hrp]


def checksum_encoding(hrp, values):
    result = bech32_polymod(hrp_expand(hrp) + values)
    if result == BECH32_CONST:
        return "bech32"
    if result == BECH32M_CONST:
        return "bech32m"
    return None


def convertbits(values, from_bits, to_bits, pad=False):
    accumulator = 0
    bits = 0
    result = []
    maximum = (1 << to_bits) - 1
    for value in values:
        if value < 0 or value >> from_bits:
            raise ValueError("input value exceeds its bit group")
        accumulator = (accumulator << from_bits) | value
        bits += from_bits
        while bits >= to_bits:
            bits -= to_bits
            result.append((accumulator >> bits) & maximum)
    if pad:
        if bits:
            result.append((accumulator << (to_bits - bits)) & maximum)
    elif bits >= from_bits or ((accumulator << (to_bits - bits)) & maximum):
        raise ValueError("invalid padding")
    return result


def decode_taproot_address(address, expected_hrp="bc"):
    if not 8 <= len(address) <= 90:
        raise ValueError("address length is outside 8..90 characters")
    if any(ord(char) < 33 or ord(char) > 126 for char in address):
        raise ValueError("address contains a character outside printable ASCII")
    if address.lower() != address and address.upper() != address:
        raise ValueError("mixed case is not allowed")

    normalized = address.lower()
    separator = normalized.rfind("1")
    if separator < 1 or separator + 7 > len(normalized):
        raise ValueError("separator or checksum position is invalid")

    hrp = normalized[:separator]
    if hrp != expected_hrp:
        raise ValueError(f"expected HRP {expected_hrp!r}, got {hrp!r}")

    try:
        values = [CHARSET_MAP[char] for char in normalized[separator + 1 :]]
    except KeyError as error:
        raise ValueError(f"invalid Bech32 character {error.args[0]!r}") from None

    encoding = checksum_encoding(hrp, values)
    if encoding is None:
        raise ValueError("checksum does not verify")

    payload = values[:-6]
    if not payload:
        raise ValueError("witness payload is empty")
    witness_version = payload[0]
    if witness_version > 16:
        raise ValueError("witness version is outside 0..16")

    program = bytes(convertbits(payload[1:], 5, 8, pad=False))
    if not 2 <= len(program) <= 40:
        raise ValueError("witness program length is outside 2..40 bytes")
    if witness_version != 1:
        raise ValueError(f"expected Taproot witness version 1, got {witness_version}")
    if len(program) != 32:
        raise ValueError(f"expected a 32-byte Taproot program, got {len(program)}")
    if encoding != "bech32m":
        raise ValueError("Taproot requires a Bech32m checksum")

    return program.hex()


VALID = "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"
EXPECTED_PROGRAM = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
INVALID = {
    "wrong checksum family": "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd",
    "invalid checksum character": "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4",
    "program too short": "bc1pw5dgrnzv",
    "mixed case": "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq",
}


if __name__ == "__main__":
    decoded = decode_taproot_address(VALID)
    assert decoded == EXPECTED_PROGRAM
    print(f"PASS valid Taproot vector: {decoded}")
    for label, candidate in INVALID.items():
        try:
            decode_taproot_address(candidate)
        except ValueError as error:
            print(f"PASS rejected {label}: {error}")
        else:
            raise AssertionError(f"FAIL accepted {label}")

Sigue el decodificador límite por límite

La dirección empieza con la parte legible, o HRP. Las direcciones SegWit de mainnet usan bc; testnet y signet suelen usar tb; regtest usa bcrt. El último 1 antes de los datos separa el HRP de los valores codificados. El decodificador busca el último separador porque el propio HRP puede contener 1.

El alfabeto de datos contiene 32 caracteres, así que cada símbolo representa cinco bits. Los últimos seis símbolos son la suma de comprobación. bech32_polymod procesa el HRP expandido y cada valor con las constantes generadoras de BIP 173. Un resto de 1 identifica Bech32. Un resto de 0x2bc830a3 identifica Bech32m.

La diferencia es importante. Las direcciones de versión 0 usan Bech32, mientras que las versiones 1 a 16 usan Bech32m. Un decodificador que acepte cualquiera de las dos constantes puede aprobar una dirección con la familia de suma equivocada. El primer control negativo es válido bajo las reglas Bech32 antiguas, pero no como dirección de versión 1.

Después de retirar los seis valores de la suma, el primer valor de cinco bits es la versión de testigo. Los grupos restantes vuelven a convertirse en bytes. La conversión estricta rechaza relleno excesivo o distinto de cero. Los programas SegWit generales pueden tener entre 2 y 40 bytes, pero BIP 341 limita una salida Taproot a la versión 1 y exactamente 32 bytes.

Ese valor de 32 bytes es una clave pública de coordenada x en el caso de gasto por clave, o una clave de salida ajustada que también compromete un árbol de scripts. Este programa no determina cómo se creó la clave, si existe un árbol de scripts ni si alguien controla la clave privada correspondiente.

Comprueba la salida probada

La salida completa fue:

PASS valid Taproot vector: 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
PASS rejected wrong checksum family: Taproot requires a Bech32m checksum
PASS rejected invalid checksum character: invalid Bech32 character 'o'
PASS rejected program too short: witness program length is outside 2..40 bytes
PASS rejected mixed case: mixed case is not allowed

El SHA-256 del archivo fuente fue b0982a21567485792ccbd08bb58084790a6d51ba2b40dfa61b94816656277efd. El SHA-256 de la salida capturada fue b71fb65a4798d7428d850ffd67297df88c0f71c8a7c5d9eb648d32974ba57897. Estos hashes identifican el ejemplo ejecutado. No autentican código copiado de una fuente no confiable.

Solución de problemas

Una dirección conocida indica un HRP incorrecto: elige la red de forma explícita. expected_hrp="bc" debe rechazar una dirección de testnet. No conviertas silenciosamente un prefijo de red en otro.

La suma falla después de copiar: compara cada carácter. Bech32 excluye del alfabeto de datos caracteres visualmente ambiguos como 1, b, i y o, pero la transcripción todavía puede cambiar un símbolo válido.

Una dirección en mayúsculas funciona, pero una mezcla falla: una cadena totalmente en mayúsculas o minúsculas puede ser válida. Mezclar ambos casos en una cadena no lo es. Las wallets suelen mostrar minúsculas.

Se rechaza una dirección de versión 0: esta función es específica de Taproot. La versión 0 usa Bech32 y requiere un programa de 20 o 32 bytes. No debilites las comprobaciones Taproot para aceptarla. Usa un decodificador SegWit general con reglas dependientes de la versión.

Los bytes no coinciden con la clave interna de una wallet: el programa testigo es la clave de salida Taproot después de cualquier operación TapTweak. No tiene por qué ser igual a la clave interna sin ajustar.

Lo que la suma no demuestra

Una suma válida establece que los caracteres son internamente coherentes con Bech32m. No demuestra que la dirección proceda del destinatario previsto, pertenezca a la red que esperaba tu aplicación, haya aparecido en la cadena o se pueda gastar.

Para un pago real, compara la dirección completa mediante un canal autenticado y deja que una wallet mantenida aplique las reglas de red y salida. Un malware puede sustituir una dirección válida completa por otra, y la suma no detecta esa sustitución. Un código QR reduce la entrada manual, pero no autentica el origen del código.

El ejemplo sin conexión demuestra el límite de codificación con un vector oficial válido y cuatro no válidos. Sirve para aprender y para pruebas de regresión. No sustituye a Bitcoin Core, al analizador de direcciones de una wallet ni a la revisión de los datos de pago antes de firmar.

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