¿La introducción de VerifyScript provocó un cambio incompatible hacia atrás en el consenso?

Mi pregunta es sobre los cambios en bitcoin realizados por el rango de confirmación [a75560d8, 6ff5f718]y su efecto en el consenso. Fuera de este rango de cuatro confirmaciones (todas atribuidas a satoshi) durante las fechas del 30 al 31 de julio de 2010, solo la primera y la última confirmaciones son relevantes para la pregunta (las dos en el medio parecen un cambio en el sistema de compilación, no en el código).

Los dos compromisos (el primero y el segundo a partir de ahora) a75560d8se 6ff5f718atribuyen a la corrección de bastantes vulnerabilidades en la semántica y ejecución de scripts de bitcoin. Por ejemplo, en el primer compromiso:

  1. Se aplicaron restricciones a los tamaños de empujar y apilar
  2. Se eliminó el llamado mecanismo de control de versiones de secuencias de comandos y NOP#se agregaron varios códigos de operación en su lugar.
  3. Se aplicaron restricciones a operaciones bignum
  4. OP_RETURNse cambió de simplemente terminar el script a regresar falsepara la ejecución completa

En el segundo compromiso:

  1. Se aplicaron restricciones en el número máximo de códigos de operación en el script
  2. La ejecución de códigos de operación desconocidos se configuró para regresar falsepara la ejecución
  3. El tamaño máximo del script se redujo a la mitad a 10kb
  4. Se realizó un cambio en *SHIFTlos códigos de operación (aunque realmente no lo entiendo)
  5. VerifyScriptSe introdujo una función llamada en bitcoin que cambió el comportamiento de la ejecución del script.

Esta pregunta es sobre esta lista de cambios y si alguno de ellos podría no ser compatible con versiones anteriores. Específicamente, me centraré en el cambio (5) de la segunda confirmación: 6ff5f718:script.cpp

Antes de este cambio, en el punto de redención de txout, la entrada de una transacción de redención scriptSigy la prevout scriptPubKeyse unían para formar un solo script colocando un OP_CODESEPARATORentre ellos y pasando el resultado al EvalScriptque ejecutaría el script.

En el interior EvalScript, se creó una pila vacía y los resultados de las diversas operaciones realizadas en el script se insertan en ella. Cuando la ejecución del script se completa sin errores, se devuelve un valor booleano; si la pila no está vacía, el elemento superior se pasa a una CastToBoolfunción y se devuelve ( trueo false), y si la pila está vacía, falsese devuelve.

Después del cambio, la VerifyScriptfunción envuelve dos llamadas separadas a EvalScript, ejecutándose scriptSigy scriptPubKeypor separado, una tras otra. La pila vacía que se creó previamente dentro EvalScriptse crea VerifyScriptantes de que comience la ejecución de cualquier secuencia de comandos, y la verificación final del contenido de la pila (o la falta del mismo) y CastToBooltambién se movió a VerifyScript, al punto después de que ambos scriptSigy scriptPubKeycompletaron su ejecución.

En VerifyScript, primero scriptSigse pasa el de la transacción de redención EvalScriptjunto con la pila vacía. La máquina ejecuta el script y los resultados de las operaciones se envían a la pila que se pasó con él. Si no se produjeron errores durante la ejecución, truese devuelve a VerifyScript. En segundo lugar, si efectivamente truese devolvió, el scriptPubKeyde la transacción de financiación se pasa EvalScriptjunto con la pila, que en ese momento contiene el contenido sobrante de scriptSigla ejecución de . se scriptPubKeyejecuta, lo que manipula aún más el contenido de la pila y, si no se produjeron errores, vuelve truea VerifyScript. Por último, si truese devolvió, entonces la verificación de la pila vacía yCastToBoolse realizan, lo que determina el resultado final de la verificación del script.

El motivo de este cambio se citó como una solución a una posible vulnerabilidad en Script: SE answer , subproceso BCTalk . Aunque es ortogonal a esta pregunta, es por eso que incluí la lista de cambios de commit a75560d8.

No es demasiado difícil ver que considerando scripts simples (que carecen de un checksig), este cambio es de hecho compatible con versiones anteriores. En el peor de los casos, algunas secuencias de comandos pueden volverse no gastables, pero no parece haber un caso en el que una secuencia de comandos previamente no gastable pueda volverse gastable como resultado de este cambio.

Incluso para secuencias de comandos con checksig como y multisig, el código de operación siempre tiene lugar en p2pk, que estaba separado de los elementos de por un antes de este cambio. Aparentemente , la semántica siguió siendo la misma, hasta unos dos años después.p2pkhCHECKSIGscriptPubKeyscriptSigOP_CODESEPARATOR

Mirando la página wiki para OP_CHECKSIG wiki-checksig , los pasos (2) a (4) explican cómo ir de scriptCodea subScript, y específicamente qué sucede cuando OP_CODESEPARATORexiste an en scriptCode:

  1. Se crea un nuevo subguión a partir de scriptCode(el scriptCodees el guion realmente ejecutado, ya sea scriptPubKeypara guiones no segwit, no P2SH, o en redeemscriptguiones no segwit P2SH). El script desde inmediatamente después del analizado más recientemente OP_CODESEPARATORhasta el final del script es el subScript. Si no hay, OP_CODESEPARATORtodo el script se convierte en elsubScript
  2. Cualquier aparición de sigse elimina de subScript, si está presente (no es estándar tener una firma en un script de entrada de una transacción)
  3. Cualquier resto OP_CODESEPARATORSse elimina desubScript

Ahora, antes de confirmar 6ff5f718, cuando scriptSigy scriptPubKeyse unieron para formar un solo script, se scriptCodevería así:

<scriptSig> CODESEPARATOR <scriptPubKey>

Con las operaciones de checksig que tienen lugar en scriptPubKey, haciendo que subScripttodo esté a la derecha de CODESEPARATOR- básicamente <scriptPubKey>en sí mismo (a menos que haya más separadores de código o si también existen firmas consumidas por un checksig en scriptPubKey).

Después de la confirmación, en realidad habría dos ejecuciones separadas con scriptSigla primera y scriptPubKeyla segunda, donde cada ejecución tendría la suya propia scriptCodey, posteriormente, la suya propia subScript. Ahora, debido a que las operaciones de checksig todavía solo se realizan en scriptPubKey, parece que subScriptseguiría igual, pero ¿qué sucedería si CHECK(MULTI)SIGse ejecuta un código de operación en scriptSig?

El 24 de enero de 2012, se extrajo el bloque 163685 que contenía la transacción eb3b82c0884e3efa6d8b0be55b4915eb20be124c9766245bcc7f34fdac32bccb. Esta transacción, las dos inmediatamente posteriores y su financiación en b8fd633e7713a43d5ac87266adc78444669b987a56b3a65fb92d58c2c4b0e84dla que también se extrajeron en el mismo bloque se mencionan en BIP-17 , que es una implementación alternativa de p2shla semántica:

  • OP_CHECKHASHVERIFYredefinirá el OP_NOP2código de operación existente y funcionará de la siguiente manera cuando se ejecute:

  • Primero, haga un hash del final del script anterior (en el caso general, scriptSigsi no hay un script anterior, se codifica una cadena nula) comenzando desde el último evaluado OP_CODESEPARATORen adelante (o desde el comienzo del script, si no OP_CODESEPARATORestaba presente)

  • Luego, compare esto con el elemento en la parte superior de la pila (si no hay ninguno, el script falla inmediatamente)
  • Si los valores hash coinciden, no haga nada, proceda como si fuera un OP_NOP; si no coinciden, el script falla inmediatamente. Tenga en cuenta que en el caso de un hash coincidente, el elemento superior de la pila (el hash con el que se compara) no se saca de la pila. Esto es por compatibilidad con versiones anteriores.

Observe cómo en bip17 se redeemScriptproporciona como un script ejecutable real en scriptSig, en lugar de una sola inserción de datos de un blob como es el caso de bip16. Aunque los nodos más antiguos no exigirán que hash160of scriptSigsea igual al valor de 20 bytes de prevout scriptPubKey, aún tendrían que ejecutarlo y validar cualquier CHECK(MULTI)SIGoperación dentro.

Estoy asumiendo aquí que estas transacciones de ejemplo bip17 fueron extraídas y validadas por nodos que ejecutan software que fue reciente en enero de 2012, pero supongamos que todavía hay nodos en la red que ejecutan software más antiguo, y específicamente los que ejecutan versiones e inferiores v0.3.6. ¿Podrían estos nodos más antiguos llegar a un consenso con los nodos más nuevos sobre la validez del bloque 163685?

Repasando la transacción de financiación b8fd633e7713a43d5ac87266adc78444669b987a56b3a65fb92d58c2c4b0e84dy observando la salida en el índice 1, vemos que scriptPubKey( NOP2en realidad no está activo OP_CHECKHASHVERIFY)

0x14 0x2a9bc5447d664c1d0141392a842d23dba45c4f13 NOP2 DROP

Y en la transacción de gasto eb3b82c0884e3efa6d8b0be55b4915eb20be124c9766245bcc7f34fdac32bccb, de la entrada en el índice 1, tenemos elscriptSig

0 0x47 0x30440220276d6dad3defa37b5f81add3992d510d2f44a317fd85e04f93a1e2daea64660202200f862a0da684249322ceb8ed842fb8c859c0cb94c81e1c5308b4868157a428ee01 CODESEPARATOR 1 0x21 0x0232abdc893e7f0631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a 1 CHECKMULTISIG

Considere primero cómo el software de v0.3.7y superior construiría el subScriptpara el CHECKMULTISIGen scriptSig. Comenzamos con a scriptCodeque es exactamente scriptSig, y como CODESEPARATORse ejecuta a, subScriptse convierte en :

1 0x21 0x0232abdc893e7f0631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a 1 CHECKMULTISIG

Ahora considere cómo el software desde v0.3.6y hacia abajo construiría el subScriptpara el mismo CHECKMULTISIG. Lo nuestro scriptCodeya no está hecho de un solo scriptSig, sino de una unión de él y scriptPubKeypor un CODESEPARATOR. El scriptCodese vería como:

0 0x47 0x30440220276d6dad3defa37b5f81add3992d510d2f44a317fd85e04f93a1e2daea64660202200f862a0da684249322ceb8ed842fb8c859c0cb94c81e1c5308b4868157a428ee01 CODESEPARATOR 1 0x21 0x0232abdc893e7f0631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a 1 CHECKMULTISIG CODESEPARATOR 0x14 0x2a9bc5447d664c1d0141392a842d23dba45c4f13 NOP2 DROP

Recuerda las reglas de la wiki. En el punto en que CHECKMULTISIGse ejecuta, subScriptestá todo desde el punto posterior a la última ejecución CODESEPARATORy hasta el final del script, con todas CODESEPARATORlas s en el lado derecho del operador checksig eliminadas:

1 0x21 0x0232abdc893e7f0631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a 1 CHECKMULTISIG 0x14 0x2a9bc5447d664c1d0141392a842d23dba45c4f13 NOP2 DROP

Si esto es correcto, entonces parece que los nodos antiguos con versiones v0.3.6e inferiores no pueden ponerse de acuerdo sobre cuál es la correcta sighashpara esta transacción de gasto, pero dado que no puedo ejecutar de manera realista ningún software de versión anterior, no puedo estar absolutamente seguro. Parcheé una versión reciente del núcleo de bitcoin con algunos cambios que me permitieron simular la validación de tx eb3b82c0884e3efa6d8b0be55b4915eb20be124c9766245bcc7f34fdac32bccby, como era de esperar, no pasó la validación. Agregaré los dos sighashes a continuación junto con la clave pública y la firma para comparar.

Así que mi pregunta es , ¿hay algo que esté malinterpretando o que tal vez me haya pasado por alto que permitiría que los nodos anteriores v0.3.7y posteriores v0.3.7llegaran a un consenso sobre la validez del bloque 163685?

(también relevante: esta pregunta SE )


Pubkey : 0232abdc893e7f0631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a
Signature (DER) : 30440220276d6dad3defa37b5f81add3992d510d2f44a317fd85e04f93a1e2daea64660202200f862a0da684249322ceb8ed842fb8c859c0cb94c81e1c5308b4868157a428ee
Signature (r,s) : (276D6DAD3DEFA37B5F81ADD3992D510D2F44A317FD85E04F93A1E2DAEA646602, F862A0DA684249322CEB8ED842FB8C859C0CB94C81E1C5308B4868157A428EE)

Pre- v0.3.7sighash (no valida) :

01000000
02
4de8b0c4c2582db95fa6b3567a989b664484c7ad6672c85a3da413773e63fdb8 00000000
00
FFFFFFFF
4de8b0c4c2582db95fa6b3567a989b664484c7ad6672c85a3da413773e63fdb8 01000000
3C 51210232abdc893e7f0631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a51ae142a9bc5447d664c1d0141392a842d23dba45c4f13b175
FFFFFFFF
02
E0FD1C0000000000 19 76a914380cb3c594de4e7e9b8e18db182987bebb5a4f7088ac
C0C62D0000000000 17 142a9bc5447d664c1d0141392a842d23dba45c4f13b175
00000000
01000000

sha256  : 1EB276326D72CB358F6C275D6542F76EED4E36364727CB82D40A116244EBDDB5
sha256d : 11491E74778E1FA8C40CC8E07E1F835677CF1AC81F54255EC1C7125C1894939A

Post- v0.3.7sighash (sí valida):

01000000
02
4de8b0c4c2582db95fa6b3567a989b664484c7ad6672c85a3da413773e63fdb8 00000000
00
FFFFFFFF
4de8b0c4c2582db95fa6b3567a989b664484c7ad6672c85a3da413773e63fdb8 01000000
25 51210232abdc893e7f0631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a51ae
FFFFFFFF
02
E0FD1C0000000000 19 76a914380cb3c594de4e7e9b8e18db182987bebb5a4f7088ac
C0C62D0000000000 17 142a9bc5447d664c1d0141392a842d23dba45c4f13b175
00000000
01000000

sha256  : 3858A592C15A47F3058010689883DECCD4AF41F5367B9776429613DFB3339883
sha256d : 8D7AD159644D312664472F90E7B823071B1361725CAC78531569FD836EA90350
Creo que estás en lo cierto. Este es un buen ejemplo de por qué los cambios en cualquier cosa relacionada con el consenso deben examinarse con mucho cuidado para no introducir cambios incompatibles con el consenso.
Gracias por leerlo. Es muy apreciado.
¡Muy buena exploración! ¡Buen trabajo!

Respuestas (1)

Casi todos los compromisos con el cliente antes de los compromisos que ha mencionado aquí fueron bifurcaciones duras debido a la inclusión de OP_VERy OP_VERIF, incluso si el cambio en el cliente no fue un cambio incompatible.

        case OP_VER:
        {
            CBigNum bn(VERSION);
            stack.push_back(bn.getvch());
        }
        break;

En pocas palabras, el código de operación se inserta VERSIONen la pila, que es el número de versión del código que se utiliza para validar. Este número se actualizó con cada lanzamiento, por lo que no hay dos versiones anteriores a su eliminación que sean compatibles entre sí.

Hay varios otros que no ha mencionado, como que nLockTimese cambió a una variable sin firmar con una bifurcación dura basada en la altura, algunos cambios menores en las implementaciones de los códigos de operación y la eliminación de los códigos de operación de doble byte. Es muy difícil enumerar cuáles de estas versiones son realmente compatibles entre sí debido a la gran cantidad de cambios y las herramientas deficientes que tenemos que son compatibles con estas versiones antiguas.