Arquitectura técnica

Miniscript

Miniscript es una forma estructurada de escribir un subconjunto de Script que permite analizarlo, componerlo y firmarlo sin código a medida. Aquí ves qué resuelve, cómo se traduce a opcodes y qué límites tiene.
En este artículo

Por qué escribir Script a mano es mala idea

Imagina que quieres una billetera (wallet) donde tú gastas cuando quieras y, pasado un año sin que te muevas, un heredero también puede. En Bitcoin Script eso son unas pocas líneas de opcodes. Pero antes de poner dinero ahí necesitas saber tres cosas: que no hay un camino por el que cualquiera pueda gastar, que el heredero no puede gastar antes de tiempo, y que el script cabe dentro de los límites de la red. Con Script en bruto, cada una de esas comprobaciones es trabajo manual y propenso a error.

Miniscript nace para eso. Según su web de referencia (bitcoin.sipa.be/miniscript), es «un lenguaje para escribir (un subconjunto de) Scripts de Bitcoin de forma estructurada, que permite análisis, composición, firma genérica y más». Lo especifica BIP-379, escrito por Pieter Wuille, Andrew Poelstra, Sanket Kanjalkar, Antoine Poinsot y Ava Chow. A octubre de 2026 su estado es Draft, aunque Bitcoin Core ya lo implementa entero desde la versión 26.0 (doc/bips.md de Core).

Qué es y qué no es

Miniscript no añade opcodes ni necesitó un soft fork. Es un subconjunto de Script que ya existe. Cada fragmento se traduce a un trozo fijo de opcodes, según la tabla de BIP-379. Y al revés: un script que encaje en Miniscript se puede leer de vuelta para recuperar sus condiciones de gasto. Pero es un subconjunto: no todo Script se puede expresar en Miniscript.

BIP-379 lo especifica para P2WSH (BIP-141) y Tapscript (BIP-342), con diferencias menores entre los dos. Deja fuera P2SH a secas y los scripts desnudos. Bitcoin Core solo lo acepta dentro de wsh() y tr() (doc/descriptors.md).

BIP-379 dice que, desde el punto de vista del usuario, Miniscript «no es un lenguaje aparte, sino una ampliación significativa del lenguaje de descriptores». O sea, de los descriptores de salida: una expresión nueva, válida solo en wsh() (BIP-382) y tr() (BIP-386). En estos esquemas la seed phrase ya no basta para recuperar los fondos: el script lleva la clave del heredero, el plazo y su estructura, y nada de eso sale de tus palabras. Hace falta guardar el descriptor completo. Esa entrada cuenta la sintaxis, el checksum y el respaldo; aquí no se repite.

Política y Miniscript: dos cosas distintas

Hay dos niveles y se confunden con frecuencia. La política de gasto (spending policy) es lo que tú quieres, escrito en un lenguaje de alto nivel: pk(A), after(n), older(n), hashes, and, or con pesos de probabilidad opcionales (9@) y thresh. Un compilador, fuera del protocolo, la convierte en Miniscript. Ese lenguaje de políticas no está en BIP-379; aparece como entrada del compilador de la web de sipa.

El resultado es Miniscript, un árbol tipado de fragmentos. Eso sí es lo que va al descriptor y, traducido, a la cadena. Y la salida depende de los pesos: la misma política con otras probabilidades puede dar otro script, porque el compilador optimiza el coste esperado de gasto. El ejemplo de más abajo lo enseña.

Fragmentos y traducción a Script

Algunos fragmentos de la tabla de traducción de BIP-379:

  • pk(key) se traduce a <key> CHECKSIG.
  • pkh(key) a DUP HASH160 <HASH160(key)> EQUALVERIFY CHECKSIG.
  • older(n) a <n> CHECKSEQUENCEVERIFY y after(n) a <n> CHECKLOCKTIMEVERIFY. El rango sintáctico es 1 ≤ n < 2³¹. Qué hacen esos opcodes lo explica script-timelocks.
  • and_v(X,Y) a [X] [Y].
  • or_d(X,Z) a [X] IFDUP NOTIF [Z] ENDIF.
  • or_i(X,Z) a IF [X] ELSE [Z] ENDIF.
  • andor(X,Y,Z) a [X] NOTIF [Z] ELSE [Y] ENDIF.
  • multi usa CHECKMULTISIG y solo vale en P2WSH. multi_a usa CHECKSIG … CHECKSIGADD … NUMEQUAL y solo en Tapscript.
  • thresh(k,…) exige que se cumplan k de las condiciones que lleva dentro. Bitcoin Core lo usa en doc/descriptors.md para una multifirma que decae: thresh(4,…) con cuatro claves y tres after(). Empieza pidiendo 4 firmas de 4 y, al llegar cada una de tres alturas de bloque fijadas, pide una menos, hasta quedarse en 1 de 4.

El sistema de tipos es lo que da valor al conjunto. Cada fragmento tiene un tipo (B, V, K o W) y unas propiedades. Un fragmento B deja un resultado en la pila, verdadero o falso; uno V no deja nada y, si falla, aborta el script. El envoltorio v: convierte lo primero en lo segundo: v:X equivale a [X] VERIFY, o a la versión VERIFY del último opcode, como CHECKSIGVERIFY en vez de CHECKSIG. Un fragmento solo encaja con otro si los tipos lo permiten, así que muchas expresiones absurdas se rechazan al leerlas. El resto de tipos, propiedades y envoltorios está en BIP-379.

Lo que se puede decir sin ejecutar nada

Como la estructura es conocida, se pueden calcular propiedades de forma estática. La web de referencia lista seis tareas que ataca: encontrar el script más barato para unas condiciones, componer condiciones, analizar un script para saber cómo se gasta, construir el testigo (witness) con las claves disponibles, predecir el coste de gasto y detectar si algún camino choca con los límites de recursos.

Maleabilidad. Las propiedades s (signed), f (forced) y e (expressive) indican si un script siempre se puede satisfacer sin que un tercero lo manipule. Desde SegWit la maleabilidad ya no invalida las transacciones hijas sin confirmar, pero BIP-379 recuerda el problema que queda: alguien que engorda el testigo baja la tasa de comisión y puede afectar a la propagación y la confirmación. Además avisa de que, con scripts no garantizados, añadir preimágenes o subir nLockTime o nSequence puede hacer fallar una satisfacción que antes funcionaba. Su algoritmo de satisfacción descarta las soluciones que un tercero podría cambiar y, entre las que quedan, elige la de testigo más pequeño.

Límites de recursos. En P2WSH, un script de más de 3600 bytes es no estándar por política, igual que un testigo de más de 100 elementos. En P2WSH, además, un gasto que ejecute más de 201 opcodes que no sean push (contando las claves de cada CHECKMULTISIG ejecutado) es inválido por consenso. En los dos contextos, la pila no puede pasar de 1000 elementos. En Tapscript no hay límite propio de tamaño de script: lo pone el del bloque. Miniscript permite comprobar si ninguno, alguno o todos los caminos chocan con esos límites.

Mezcla de timelocks. La propiedad k señala que no hay mezcla. Dentro de un AND, o de un thresh con k ≥ 2, no se pueden combinar locks absolutos por altura con locks absolutos por tiempo, ni relativos por altura con relativos por tiempo.

Bitcoin Core agrupa todo esto en una función, IsSane(), en src/script/miniscript.h. Exige que el tipo sea B, que se cumpla IsSaneSubexpression() y que el script necesite firma (propiedad s). A su vez, IsSaneSubexpression() reúne satisfacciones válidas dentro de los límites, no maleabilidad (Core la marca con la letra m), sin mezcla de timelocks (k) y sin claves duplicadas. La función GetWitnessSize() devuelve el mayor testigo posible de una satisfacción no maleable, y de ahí sale el coste de gasto.

Un ejemplo: herencia con camino de recuperación

Política: or(9@pk(CLAVE_A),and(pk(CLAVE_B),older(N))). El 9@ dice que el camino de A se usará nueve veces por cada una que se use el de B. A gasta siempre; B, solo cuando el UTXO lleve N bloques confirmado. Con N = 52560 y esos pesos, el compilador de la web de referencia devuelve or_d(pk(CLAVE_A),and_v(v:pkh(CLAVE_B),older(N))), la misma forma que usa la billetera Liana en su guía de recuperación (doc/RECOVER.md). Sin pesos devuelve otra, andor(pk(CLAVE_B),older(N),pk(CLAVE_A)), con un script de 78 bytes en vez de 68 y un elemento más en el testigo de A. Como A es quien gasta casi siempre, sale más cara.

El script que sale de la tabla de traducción:

<CLAVE_A> CHECKSIG IFDUP NOTIF
  DUP HASH160 <HASH160(CLAVE_B)> EQUALVERIFY CHECKSIGVERIFY
  <N> CHECKSEQUENCEVERIFY
ENDIF

Aquí v:pkh convierte el CHECKSIG final en CHECKSIGVERIFY. Por la rama A, los datos que satisfacen el script son solo <firma_A>; el testigo completo añade detrás el propio script, como en todo P2WSH. Por la rama B son <firma_B> <clave_B> <vacío>: el elemento vacío hace fallar pk(A) sin firma, que es la insatisfacción canónica. Además, la transacción debe tener versión 2 o superior y el nSequence de esa entrada debe pedir al menos N bloques (BIP-112). La billetera lo pone sola.

Con marcadores, el descriptor sería wsh(or_d(pk(CLAVE_A),and_v(v:pkh(CLAVE_B),older(52560)))). Cincuenta y dos mil quinientos sesenta bloques son 365 × 144, unos 365 días a diez minutos por bloque. En Taproot, el mismo esquema pone A en el camino de clave y B en una hoja: tr(CLAVE_A,and_v(v:pk(CLAVE_B),older(52560))). En la hoja basta pk. En P2WSH el compilador usa pkh en la rama poco probable para no publicar los 33 bytes de la clave del heredero en cada gasto normal, pero en Taproot la hoja solo se publica si el heredero la usa, y mientras tanto nadie ve ni su clave ni que existe. Sobre Taproot hay su propia entrada.

Las dos trampas del ejemplo

El reloj es por UTXO, no por billetera. older() cuenta desde que se confirma cada UTXO concreto (BIP-68 y BIP-112). Si pasan N bloques sin que muevas ese UTXO, el heredero puede gastarlo aunque tú sigas vivo. Para evitarlo hay que mover los fondos a una dirección nueva de la misma billetera, y que esa transacción esté confirmada antes de que venza el plazo del UTXO más antiguo. Deja margen: semanas, no días. Un refresco que sigue en la mempool cuando vence el plazo ya no te protege: el heredero puede gastar ese UTXO con otra transacción que compite con la tuya. Cada UTXO nuevo empieza con el reloj a cero. Ese refresco es una tarea periódica y es tuya: refrescar es gastar con tu clave, y quien lo haga por ti puede gastarlo todo sin esperar. Con after() (absoluto) no hace falta mover nada, pero la fecha queda fija y no se renueva. La entrada sobre herencia en Bitcoin trata el resto de opciones y sus riesgos.

Un plazo largo en older() no da error: se convierte en uno corto. Es la trampa más peligrosa. BIP-68 solo interpreta los 16 bits bajos de nSequence (máscara 0x0000ffff), y el bit 22 elige entre bloques y unidades de 512 segundos. Así que el tope relativo es 65.535 bloques, unos 455 días, o 65.535 × 512 segundos, unos 388 días. Es mucho más estrecho que el rango sintáctico de BIP-379, que admite n hasta 2³¹. Si escribes older(262800) pensando en cinco años (262.800 = 5 × 52.560), CHECKSEQUENCEVERIFY descarta los bits altos antes de comparar (CheckSequence en interpreter.cpp de Bitcoin Core). En hexadecimal, 262.800 es 0x40290; los 16 bits bajos son 0x0290, que son 656. El plazo real queda en 656 bloques, unos 4,6 días. Ni Bitcoin Core ni el analizador de la web de referencia lo rechazan: el analizador da older(262800) como una expresión sana. En una herencia, quien quiso cinco años le abre la puerta al heredero en menos de una semana. Para plazos de más de unos 455 días, la salida es after() o refrescar los fondos antes. Y antes de depositar, comprueba qué plazo da tu older() en el Conversor de timelocks (modo «Leer un nSequence o un nLockTime»), que sí avisa de este caso.

Firma genérica y soporte real

La ventaja práctica de la estructura es que un firmante, software o dispositivo, puede deducir qué firmas, preimágenes y condiciones de tiempo hacen falta en cada camino y montar el testigo más barato que no sea maleable. No necesita código hecho a medida para cada plantilla. Esto se apoya en el flujo de PSBT.

El soporte llegó por fases en Bitcoin Core, según sus notas de versión:

  • 24.0.1 (12 de diciembre de 2022): wsh() admite Miniscript, pero solo en billeteras de solo lectura. Core avisa de que todavía no se puede gastar con la billetera de Core (#24148).
  • 25.0 (26 de mayo de 2023): las billeteras de descriptores gastan de descriptores Miniscript P2WSH y finalizepsbt los finaliza (#24149). También decodescript infiere un descriptor Miniscript en P2WSH (#27037).
  • 26.0 (6 de diciembre de 2023): Miniscript dentro de descriptores Taproot, en hojas de billeteras de descriptores (#27255).

Fuera de Core, a octubre de 2026, Liana (Wizardsardine) es una billetera hecha sobre descriptores Miniscript, con un camino de recuperación por timelock relativo. Admite Taproot, que llegó como experimental en su versión 5.0. Nunchuk añadió Miniscript a sus billeteras el 27 de agosto de 2025, en SegWit nativo y Taproot. En los dispositivos el soporte depende del modelo, del firmware y del tipo de script. Coldcard, por ejemplo, solo tiene Miniscript en su firmware Edge, un canal de adelanto que el fabricante no prueba con el mismo rigor que una versión normal; entró en la 6.1.0X, del 20 de junio de 2023. Y en Liana, un BitBox02 solo puede formar parte de un descriptor Taproot desde la versión 8.0 de Liana (noviembre de 2024) y con firmware 9.21.0 o superior. Estos datos cambian rápido: si piensas usar un dispositivo concreto, comprueba en la documentación del fabricante que firma Miniscript en el tipo de script que vas a usar (SegWit nativo o Taproot).

El riesgo de implementación es real. La app Bitcoin de Ledger admitió políticas Miniscript desde la 2.1.0 (febrero de 2023), pero las versiones 2.1.0 y 2.1.1 trataban mal el fragmento a: y daban direcciones erróneas. Se corrigió en la 2.1.2 (boletín de seguridad 019, 11 de mayo de 2023). Por eso conviene comprobar que la dirección que enseña la pantalla del dispositivo es la misma que calcula tu software antes de recibir fondos importantes. Más contexto en hardware wallets.

Trade-offs y debates abiertos

Miniscript cambia expresividad por seguridad analizable. Lo que dejas fuera (scripts arbitrarios) no se puede razonar con estas herramientas. Lo que ganas es componer políticas con garantías: si la expresión pasa el análisis de sanidad, sabes que cada camino es satisfacible, que no hay mezcla de timelocks y que cabe en los límites.

El estado Draft de BIP-379 no impide que Core lo use, pero significa que la especificación puede cambiar. Los límites de P2WSH y Tapscript no son iguales, y un mismo diseño puede pesar distinto en cada contexto. Y BIP-379 nombra los compiladores de políticas, pero no los especifica: lo que bloquea tus fondos es el Miniscript que sale, así que es eso lo que se revisa con el analizador. El riesgo que queda es que un firmante implemente mal algún fragmento, como pasó con Ledger. Sobre el diseño de esquemas de varias claves está la entrada de billetera multifirma, y sobre políticas con timelocks, las de billeteras con timelock y diseño de multifirma.

Qué significa esto para ti

Si usas una billetera con política de recuperación, Miniscript es lo que hay debajo, y tu respaldo es el descriptor, no solo la seed phrase. Si vas a diseñar la tuya, empieza por la política, deja que un compilador te dé el Miniscript y compruébalo con las herramientas de análisis y, si lleva older(), también en el Conversor de timelocks antes de depositar nada. Para una herencia con older(), apunta en el calendario el refresco, con margen y contando desde el UTXO más antiguo: el reloj de cada UTXO empieza de nuevo cada vez que lo mueves, y el refresco cuenta cuando está confirmado, no cuando lo envías.

Errores habituales

  • Creer que Miniscript añade opcodes o necesitó un soft fork. Es una forma estructurada de escribir Script que ya existe, y cada fragmento se traduce a opcodes actuales.
  • Confundir la política con Miniscript. La política (or(pk(A),and(pk(B),older(N)))) es la entrada de un compilador y BIP-379 no la especifica. Lo que va al descriptor y a la cadena es Miniscript.
  • Pensar que una política con or da siempre el mismo script. El compilador optimiza según los pesos de probabilidad: or(pk(A),and(pk(B),older(N))) sale como andor(pk(B),older(N),pk(A)), y con 9@pk(A) sale como or_d(pk(A),and_v(v:pkh(B),older(N))).
  • Dar BIP-379 por Final. A octubre de 2026 está en estado Draft, aunque Bitcoin Core lo implemente.
  • Creer que desde la 24 Core ya firmaba con Miniscript. En la 24.0.1 solo servía para seguir fondos en billeteras de solo lectura; gastar en P2WSH llega con la 25.0 y en Taproot con la 26.0.
  • Creer que cualquier script se puede escribir en Miniscript. Es un subconjunto de Script.
  • Poner en older() un número mayor que 65.535 (o que 65.535 unidades de 512 segundos) para un plazo largo. No da error: el nodo descarta los bits altos y el plazo real puede quedar en días. older(262800), pensado como cinco años, da 656 bloques. Por encima de ese tope, la salida es after() o refrescar los fondos.
  • Olvidar que el reloj de older() empieza en la confirmación de cada UTXO. Si el refresco no está confirmado antes de que venza el plazo del UTXO más antiguo, el heredero puede gastar en vida del titular.
  • Suponer que con la seed phrase del heredero basta para recuperar. Hace falta el descriptor completo.

Conceptos relacionados

Fuentes primarias

  1. Wuille, Poelstra, Kanjalkar, Poinsot y Chow, «BIP-379: Miniscript» (2023)
  2. Wuille, Poelstra y Kanjalkar, «Miniscript» (web de referencia, con compilador y analizador)
  3. BIP-68, «Relative lock-time using consensus-enforced sequence numbers» (2015)
  4. BIP-112, «CHECKSEQUENCEVERIFY» (2015)
  5. BIP-382, descriptores wsh()
  6. BIP-386, descriptores tr()
  7. Bitcoin Core, notas de la versión 24.0.1 (2022)
  8. Bitcoin Core, notas de la versión 25.0 (2023)
  9. Bitcoin Core, notas de la versión 26.0 (2023)
  10. Bitcoin Core, doc/bips.md (BIP-379)
  11. Bitcoin Core, doc/descriptors.md
  12. Bitcoin Core, src/script/miniscript.h
  13. Bitcoin Core, src/script/interpreter.cpp (CheckSequence)
  14. Wizardsardine, Liana: doc/RECOVER.md
  15. Wizardsardine, Liana: CHANGELOG.md (versiones 5.0 y 8.0)
  16. Coinkite, Coldcard: registro de versiones Edge (6.1.0X, 2023)
  17. Ledger Donjon, «Ledger Security Bulletin 019» (2023)
  18. Nunchuk, «Miniscript: Programmable Bitcoin» (2025)