Guide

Verificar sin conexión el checksum de un descriptor de Bitcoin

Añade y verifica los ocho caracteres del checksum de un descriptor de salida sintético con un pequeño programa Python sin conexión. El ejercicio no usa wallet, claves ni red.

9 min de lecturaEstándares de carteras
Verificar sin conexión el checksum de un descriptor de Bitcoin

Qué harás

Una política de wallet copiada puede parecer correcta aunque tenga un carácter equivocado. Esta guía crea el checksum que usan los descriptores de salida de Bitcoin, verifica un vector publicado y rechaza siete versiones incorrectas.

Un descriptor de salida es una receta de texto para los scripts que observa una wallet. Puede incluir tipos de script, claves públicas, orígenes de claves y rutas de derivación. BIP 380 permite que esa receta termine con # y un checksum de ocho caracteres. El checksum ayuda a detectar errores al copiar. No demuestra que el descriptor te pertenezca ni que su política sea segura.

El ejercicio solo usa raw(deadbeef), un descriptor sintético de los vectores de prueba de BIP 380. No contiene claves privadas, claves públicas extendidas, direcciones de wallet ni saldos reales. No lo sustituyas por una exportación real mientras aprendes con este programa.

Antes de empezar

Necesitas Python 3 y un editor de texto en una carpeta desechable. El programa siguiente se ejecutó con Python 3.12.14 el 23 de octubre de 2026. Solo usa el lenguaje estándar y no realiza peticiones de red.

BIP 380 define el conjunto de caracteres, el alfabeto del checksum, los valores generadores y los vectores de prueba. Bitcoin Core 31.1 era la última versión estable al comprobarlo. Su implementación de descriptores usa la misma construcción, y su documentación de descriptores explica el lenguaje de expresiones más amplio.

Ten claro el límite antes de ejecutar nada:

  • un checksum detecta muchos errores de texto, no una política maliciosa o equivocada
  • un checksum válido no comprueba la propiedad de las claves, las rutas de derivación ni el tipo de dirección
  • un descriptor puede contener un xprv u otra forma de clave privada, por lo que una exportación real puede ser secreta
  • este programa solo comprueba el checksum y no interpreta la expresión del script

Crear el verificador sin conexión

  1. Crea una carpeta desechable y entra en ella:
mkdir descriptor-checksum-practice
cd descriptor-checksum-practice
  1. Guarda el programa siguiente como descriptor_checksum.py. Léelo antes de ejecutarlo. No acepta archivos ni valores por la línea de comandos. Sus datos de prueba están fijados en el código.
#!/usr/bin/env python3

INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
GENERATOR = [0xF5DEE51989, 0xA9FDCA3312, 0x1BAB10E32D, 0x3706B1677A, 0x644D626FFD]


def polymod(symbols):
    checksum = 1
    for value in symbols:
        top = checksum >> 35
        checksum = ((checksum & 0x7FFFFFFFF) << 5) ^ value
        for index, generator in enumerate(GENERATOR):
            if (top >> index) & 1:
                checksum ^= generator
    return checksum


def expand(text):
    groups = []
    symbols = []
    for character in text:
        position = INPUT_CHARSET.find(character)
        if position == -1:
            raise ValueError("invalid_character")
        symbols.append(position & 31)
        groups.append(position >> 5)
        if len(groups) == 3:
            symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2])
            groups = []
    if len(groups) == 1:
        symbols.append(groups[0])
    elif len(groups) == 2:
        symbols.append(groups[0] * 3 + groups[1])
    return symbols


def add_checksum(payload):
    symbols = expand(payload) + [0] * 8
    value = polymod(symbols) ^ 1
    suffix = "".join(
        CHECKSUM_CHARSET[(value >> (5 * (7 - index))) & 31]
        for index in range(8)
    )
    return f"{payload}#{suffix}"


def verify(descriptor):
    if len(descriptor) < 9 or descriptor[-9] != "#":
        return False
    suffix = descriptor[-8:]
    if any(character not in CHECKSUM_CHARSET for character in suffix):
        return False
    try:
        symbols = expand(descriptor[:-9])
    except ValueError:
        return False
    symbols.extend(CHECKSUM_CHARSET.find(character) for character in suffix)
    return polymod(symbols) == 1


def main():
    payload = "raw(deadbeef)"
    expected = "raw(deadbeef)#89f8spxm"
    if add_checksum(payload) != expected:
        raise RuntimeError("published_vector_creation_failed")
    if not verify(expected):
        raise RuntimeError("published_vector_verification_failed")

    invalid = [
        payload,
        "raw(deadbeef)#",
        "raw(deadbeef)#89f8spxmx",
        "raw(deadbeef)#89f8spx",
        "raw(deedbeef)#89f8spxm",
        "raw(deadbeef)#89f8spxq",
        "raw(Ü)#00000000",
    ]
    if any(verify(candidate) for candidate in invalid):
        raise RuntimeError("invalid_vector_accepted")

    print(f"created={expected}")
    print("valid_accepted=1 invalid_rejected=7")


if __name__ == "__main__":
    main()
  1. Ejecútalo:
python3 descriptor_checksum.py

El resultado esperado es:

created=raw(deadbeef)#89f8spxm
valid_accepted=1 invalid_rejected=7

La primera línea debe coincidir exactamente con el vector de BIP 380. La segunda confirma que el vector válido pasó y que los siete controles negativos fallaron.

Seguir el cálculo

El descriptor anterior a # es la carga. BIP 380 asigna a cada carácter permitido una posición en una lista de 96 caracteres. La función expand convierte esas posiciones en símbolos pequeños.

Cada carácter aporta sus cinco bits inferiores. El número del grupo superior se recoge por separado. Después de cada tres caracteres, los tres números de grupo se convierten en un símbolo adicional. Esta disposición facilita que el código detecte sustituciones frecuentes y errores de mayúsculas.

La función polymod mueve un estado de 40 bits por esos símbolos. Cinco valores generadores fijos cambian el estado cuando se activan determinados bits superiores. Para crear un checksum, add_checksum añade ocho símbolos cero, ejecuta el estado y aplica la constante final. El resultado se escribe con el mismo alfabeto de 32 caracteres que usa Bech32.

La verificación llega al mismo límite por otra ruta. Expande la carga, convierte los ocho caracteres proporcionados en símbolos y procesa la secuencia completa. Un descriptor válido termina con un valor polymod igual a 1.

Es un código para detectar errores, no cifrado ni un hash de contraseña. Quien pueda cambiar el descriptor también puede calcular un checksum nuevo. El checksum protege contra cambios accidentales. No autentica quién creó el texto.

Leer los controles negativos

Las siete entradas rechazadas ejercitan errores distintos:

  1. falta el separador y el checksum
  2. hay separador pero no checksum
  3. hay nueve caracteres de checksum en vez de ocho
  4. hay siete caracteres en vez de ocho
  5. cambia un carácter de la carga pero se conserva el checksum anterior
  6. cambia un carácter del checksum
  7. la carga contiene un carácter fuera del conjunto permitido

La mutación de la carga es importante. deadbeef pasa a ser deedbeef, pero el sufijo sigue siendo 89f8spxm. Una revisión visual podría no detectar el cambio. El cálculo sí lo hace.

El programa no afirma encontrar todos los conjuntos imaginables de errores. BIP 380 especifica garantías más fuertes para descriptores cortos e indica que un error aleatorio tiene una probabilidad de uno entre 2 elevado a 40 de no detectarse. Esa probabilidad se aplica a errores accidentales dentro del modelo del checksum. No estima la seguridad frente a alguien que sustituye deliberadamente la política y su checksum.

Compararlo con Bitcoin Core

Bitcoin Core puede añadir e inspeccionar checksums con sus herramientas para descriptores. La implementación publicada relevante está en src/script/descriptor.cpp. Su sección de checksum usa el mismo alfabeto de entrada, alfabeto de salida y los cinco valores generadores de este ejercicio.

El programa Python se comparó con ese código y con los vectores de BIP 380. En esta ejecución no se usó ningún binario ni RPC de Bitcoin Core. El ejercicio tampoco demuestra que raw(deadbeef) sea útil, gastable o apropiado para una wallet. Solo demuestra que se reproduce el vector publicado y que se rechazan las cadenas incorrectas elegidas.

Para un descriptor real, usa software mantenido de wallet o nodo que interprete la expresión completa. Un verificador de checksum no puede decir si wpkh, tr, multi o una ruta de derivación coincide con la wallet que quieres restaurar. Compara la política, las huellas de las claves, las rutas y la red con una copia de confianza antes de importar nada.

Si algo sale mal

Python informa de un error de sintaxis. Copia el programa completo como texto plano. Conserva sin cambios la barra invertida y las comillas dentro de INPUT_CHARSET. Las comillas tipográficas cambian el código y evitan que se ejecute.

El checksum creado es distinto. Confirma que la carga sea exactamente raw(deadbeef). Las mayúsculas, los espacios y la puntuación forman parte de la carga. Un espacio final antes de # produce otro checksum.

Se acepta un vector incorrecto. Deja de usar esa copia. Compara las constantes y los alfabetos con BIP 380. No cambies el resultado esperado para conseguir que pase la prueba.

Una wallet rechaza un descriptor con checksum válido. Es posible. La expresión puede usar una función no admitida, una clave inválida, una derivación endurecida sin material privado o una política que la wallet no pueda importar. La validez del checksum es solo una capa.

El descriptor contiene xprv, WIF u otra clave privada. No lo pegues en este ejercicio, una web, un chat o un informe público. Trabaja con una exportación de datos públicos o un vector sintético en un entorno sin conexión. El checksum no oculta el secreto.

Qué se verificó y qué no

El programa se ejecutó con Python 3.12.14. Recreó el vector raw(deadbeef)#89f8spxm de BIP 380, aceptó ese vector y rechazó siete controles negativos. Su código se comparó con BIP 380 y con la implementación publicada del checksum de descriptores de Bitcoin Core 31.1.

No se usaron wallets, nodos, RPC, importaciones de descriptores, derivaciones de direcciones, claves, peticiones de red ni fondos. El programa no interpreta la semántica del descriptor y no debe usarse como única comprobación antes de restaurar o importar una política real.

Fuentes

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