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
xprvu 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
- Crea una carpeta desechable y entra en ella:
mkdir descriptor-checksum-practice
cd descriptor-checksum-practice
- 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()
- 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:
- falta el separador y el checksum
- hay separador pero no checksum
- hay nueve caracteres de checksum en vez de ocho
- hay siete caracteres en vez de ocho
- cambia un carácter de la carga pero se conserva el checksum anterior
- cambia un carácter del checksum
- 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
- BIP 380: operación general de descriptores de scripts de salida, incluido el código del checksum y los vectores, consultado el 23 de octubre de 2026.
- Versión Bitcoin Core 31.1, publicada el 8 de julio de 2026.
- Implementación de descriptores de Bitcoin Core 31.1, consultada el 23 de octubre de 2026.
- Documentación de descriptores de Bitcoin Core, consultada el 23 de octubre de 2026.
