Hosted onseedteamtalks.hyper.mediavia theHypermedia Protocol

SegWit: Una ingeniosa soluciónUn repaso por el funcionamiento de SegWit y algunas reflexiones

    SegWit es una actualización soft-fork de Bitcoin, que tiene por objetivos principales:

      Solventar la maleabilidad de transacciones -> permite segundas capas

      Solventar el ASIC Boost

      Solventar el bug "Quadratic SigHash Scaling Bug"

    A tal fin, cambia la manera en la que se construyen internamente las transacciones y proto-transacciones, la información que debe incluirse en la coinbase transaction, la manera en la que se mide el tamaño del bloque y la manera en la que se transmite la información entre nodos.

    SegWit se activa el 24 de Agosto de 2017. La condición para su activación era que más del 95% de los bloques minados señalizaran el apoyo a esta actualización, en cualquiera de los periodos de 2016 bloques que ocurrieran entre el 15 de Nov de 2016 y el 15 de Nov de 2017 [1]. Con este umbral del 95% se aseguraría que el hashrate a favor de SegWit fuera capaz de construir la cadena de bloques más larga —la que es aceptada como válida—. Dos periodos antes del 24 de Agosto se alcanzó una señalización del 100% por parte de los mineros, haciendo que SegWit se activara.

    Serialización

      Todos los campos de una transacción están expresados en hexadecimal.

      Transacción

        Una transacción non-SegWit tiene la siguiente estructura:

        [nVersion (LE)][nInputs][txins][nOutputs][txouts][nLockTime (LE)]
        

        Donde:

        txins = [
        for i in nInputs:
        	+ iOutpoint (LE),
        	+ iOutput (LE),
        	+ ScriptSigSize,
        	+ ScriptSig,
        	+ Sequence (LE),
        ]
        
        txouts = [
        for i in nOutputs:
        	+ Amount (LE),
        	+ ScriptPubKeySize,
        	+ ScriptPubKey,
        ]
        

        Mientras que una SegWit tendrá esta estructura:

        [nVersion (LE)][marker][flag][nInputs][txins][nOutputs][txouts][witness][nLockTime (LE)]
        

        Donde:

        witness = [
        for i in nInputs:
        	+ nStackItems[i]
        		+ StackItemSize
        		+ StackItem
        ]
        

        Aparecen los campos marker y flag, los cuales deben tener los valores 0x00 y 0x01 respectivamente, para indicar que es una transacción SegWit. Por ahora estos campos siempre se ponen con estos valores, a la espera de que una futura actualización los emplee.

      Proto-transacción

        La proto-transacción es el conjunto de información sobre el que se firma digitalmente. La transacción final será —en líneas generales— el resultado de añadir a la proto-transacción, las firmas digitales.

        Una proto-transacción non-SegWit tiene la siguiente estructura (para un input de este tipo): [2]

        [nVersion (LE)][nInputs][txins][nOutputs][txouts][nLockTime (LE)][sigHashType]
        

        Donde:

        txins = [
        for i in nInputs:
        	+ iOutpoint,
        	+ iOutput (LE),
        	+ iPrevScriptPubKeySize,
        	+ iPrevScriptPubKey,
        	+ iSequence (LE),
        ]
        
        txouts = [
        for i in nOutputs:
        	+ Amount (LE),
        	+ ScriptPubKeySize,
        	+ ScriptPubKey,
        ]
        

        Una proto-transacción SegWit tiene la siguiente estructura (para un input de este tipo): [3]

        [nVersion (LE)][hashPrevouts][hashSequences][txin][hashOutputs][nLockTime (LE)][sigHashType]
        

        Donde:

        hashPrevouts = dSHA256[
        for i in nInputs:
        	iOutpoint ‖ iOutput (LE) ‖
        ]
        
        hashSequences = dSHA256[
        for i in nInputs:
        	iSequence (LE) ‖
        ]
        
        txin = [
        + iOutpoint,
        + iOutput (LE),
        + iPrevScriptPubKeySize,
        + iPrevScriptPubKey,
        + iPrevOutputValue (LE), 
        + iSequence (LE),
        ]
        
        hashOutputs = dSHA256[
        for i in nOutputs:
        	iAmount (LE) ‖ iScriptPubKeySize ‖ iScriptPubKey ‖
        ]
        

        El símbolo «‖», implica concatenar previo ser aplicado el dSHA256. El símbolo "," implica crear una lista de elementos que más tarde se concatenarán al serializar la transacción o proto-transacción.

        Una gran diferencia a la hora de serializar la proto-transacción es que con un input SegWit, a diferencia de un input non-SegWit, solo se incluye en el apartado de [txins] el input que se está firmando, y ningún caso se hará algún tipo de referencia al resto. Los demás inputs se vinculan con [hashPrevouts], a no ser que el sigHash Type sea ANYONECANPAY. Aunque esta forma de serializar también tiene sus detalles, para las transacciones más comunes (las que tienen sigHash Type ALL) se reduce la cantidad de operaciones a realizar, dado que:

          [hashPrevouts], [hashSequences] y [hashOutputs] serán lo mismo para todos los inputs SegWit.

          Cuando se incluye la información del input ([txin]), no hace falta estar permutando que scriptPubKey están vacíos y cuales no, puesto que solo se incluye ese input.

        Se elimina la necesidad de volver a calcular los valores [hashPrevouts], [hashSequences] y [hashOutputs] y además, se elimina un bucle for que iba iterando por los inputs, reduciendo la complejidad. Tal y como se establece en el BIP143 [4], la complejidad se reduce de O(n^2) a O(n), resolviendo de esta forma el "Quadratic SigHash Scaling Bug".

        Para inputs que no son SegWit la forma de serializar se mantiene.

    SegWit Script

      Actualmente existen dos versiones de scripts:

        SegWit_v0, que corresponde a P2WPKH y a P2WSH, las versiones SegWit de los previos P2PKH y P2SH.

        SegWit_v1, que corresponde a P2TR.

      Se pueden diferenciar por el tipo de Locking Script (ScriptPubKey) que se crea:

      P2WPKH:

      scriptPubKey: OP_0 <20-byte-key-hash> (0x0014{20-byte-key-hash})
      

      P2WSH:

      scriptPubKey: OP_0 <32-byte-key-hash> (0x0020{32-byte-hash})
      

      P2TR

      scriptPubKey: OP_1 <32-byte-key-hash> (0x5120{32-byte-hash})
      

      A la hora de desbloquear, vamos a desgranar como funcionaría P2WPKH:

      El ScriptSig estaría vacío: 0x00, es decir, dentro del campo [txins] de la transacción final, donde antes colocábamos las firmas y la clave pública, ahora se deja vacío. Dado que es SegWit, en el campo Witness se incluirá lo siguiente:

      [nStackItems = 02][sigSize][signature][pkSize][pk]

      Esto será el Unlocking Script (ScriptSig).

      A la hora de realizar la verificación del script, el nodo SegWit entenderá que para un dato de 20bytes (el hash de la clave pública), deberá ejecutar el siguiente 1976a9{20-byte-key-hash}88ac como Locking Script y la información que hay en el campo Witness como elementos del Unlocking Script.

      Conjuntamente, el resultado es el mismo que para un P2PKH.

    Handshake: la nueva forma de comunicar internodal y qué interpretan los nodos non-SegWit

      Un nodo SegWit señaliza al resto que efectivamente está preparado para recibir información adicional: los campos SegWit. Los nodos non-SegWit simplemente no lo señalizarán.

      De esta forma, entre nodos SegWit se pasan toda la información: la transacción al completo, con todos sus campos. Por el contrario, entre un nodo SegWit y un nodo non-SegWit, este primero envía al segundo las transacciones de manera recortada: no incluye los campos SegWit.

      Así es como se mantiene la compatibilidad con versiones anteriores: sin mentir, pero sin contar toda la verdad.

      A los ojos de un nodo non-SegWit, un script de bloqueo como el usado por P2WPKH es un script en el que no se ponen cláusulas y en el que se permite a cualquier persona gastarlo. Por tanto, dará como válido cualquier movimiento de fondos que se realice con los bitcoin bloqueados baja esa condición de gasto. Es gracias a los nodos SegWit, que sí ven toda la información, que esa condición de gasto tiene sentido y debe desbloquearse con una firma digital válida.

      Un script en Bitcoin es válido siempre y cuando sea distinto de cero. Por tanto, aunque las cláusulas empleadas para bloquear los fondos sean OP_0 OP_PUSH14 <20-byte-key-hash>, como el resultado de las mismas no es cero, el nodo non-SegWit lo dará como válido. Mientras que la clave pública no sea cero, todo bien. Dado que el scriptSig está vacío (0x00), lo que habrá en el stack será el OP_0 abajo y encima el hash de la clave pública. El nodo non-SegWit no entenderá nada, pero lo dará por válido.

      Curiosidad: Si todos los nodos y mineros volvieran a una versión de Bitcoin previa a SegWit, los fondos bloqueados en condiciones de gasto SegWit estarían desprotegidos.

      Otro factor a tener en cuenta es lo diferentes que son las proto-transacciones. Esto no afecta la compatibilidad puesto que una proto-transacción solo es reconstruida por el nodo cuando hay que verificar una firma digital. Para un nodo non-SegWit, el script de bloqueo de fondos SegWit no incluye ninguna instrucción de verificar firmas digitales, por lo que esta diferencia nunca supone un problema.

      La gracia de SegWit es lo ingenioso de construir un nuevo conjunto de normas que trasgreden las normas de validación anteriores, y diseñarlo de tal forma que los nodos que tienen esas normas de validación antiguas ni se den cuenta de ello: por un lado, no les envías toda la información; por el otro, hago que la información restante sea válida (e incluso inútil) a los ojos de los nodos no actualizados.

    Bloques

      Peso: Weight Units

        La actualización de SegWit trajo una nueva forma de medir la información (los bytes) y la cantidad máxima de bytes que puede haber en un bloque.

        Donde antes había un límite de un 1MB por bloque, ahora ese límite se cambia a 4 millones de unidades de peso (WU, Weight Units).

        Para medir lo que ocupa una transacción se hará lo siguiente:

          Cada byte que ocupan los campos SegWit: [marker][flag][witness], contará como una unidad de peso (1 WU)

          Cada byte que ocupan el resto de campos, contará como cuatro unidades de peso (4WU)

        El total de [non-SegWit fields * 4 + SegWit fields * 1] será el peso de la transacción.

        Es decir, se le está dando un descuento del 75% al espacio ocupado por los campos SegWit. Dado que los campos SegWit, principalmente el [witness] es gran parte de la transacción -las firmas ocupan mucho espacio- , estaremos consiguiendo aumentar el tamaño de bloque: lo que antes ocupaba 1 byte ahora ocupa 4WU, mientras que las nuevas partes ocupan 1WU por cada byte.

        Si el bloque estuviera compuesto solo por transacciones non-SegWit, cabrían el mismo número de transacciones, ocupando como máximo 1MB (o 4MWU). Para un nodo non-SegWit, el tamaño de bloque sigue siendo el mismo.

        Y ahora, manteniendo ese mismo límite, se incluye más información dado que para un nodo non-SegWit, los campos SegWit no existen. Esto es, a la vista de un nodo non-SegWit, hay más transacciones pero con menos información cada una de ellas.

        El aumento efectivo de bloque es a 1.8MB de media. [5]

        Tal y como lo entiendo, es una forma elegante de aumentar el tamaño de bloque sin realizar un hardfork. En realidad no estás aumentando el tamaño, solo estás dejando de incluir en un bloque non-SegWit cierta información que los nodos SegWit sí que tienen que almacenar. Para un nodo non-SegWit la máxima cantidad de información que puede entrar en un bloque sigue siendo de 1MB, dado que los bytes de información que le llegan son los que se multiplican por 4 al convertirlos en WU, manteniendo la paridad.

        Esto es, eliminar todo lo que puedas de la transacción sin que esta deje de tener sentido, que se mantenga válida para los nodos antiguos y que contenga la suficiente información como para que no sea posible engañar.

      Witness Commitment en el Bloque

        En los bloques, para mantener la compatibilidad, la merkle root sigue siendo el resultado de los hashes en árbol de los txid de cada transacción.

        Como los nodos non-SegWit no ven los campos SegWit, el txid de una transacción será dSHA256 de todos los campos que sí le llegan a un nodo non-SegWit.

        Como consecuencia, se crea un nuevo concepto: el wtxid (witness txid). Este representa el dSHA256 de toda la transacción SegWit. Lo calcularán los nodos SegWit. Si una transacción es non-SegWit, el txid y el wtxid coincidirán.

        Se plantea un problema: en los txid que finalmente componen la merkle root no se están teniendo en cuenta, y por tanto vinculando, los campos SegWit. Esto es, podrían ser modificados a posteriori sin invalidar el bloque.

        Es por ello que se obliga a los mineros SegWit a calcular un nuevo elemento: el witness merkle root —siendo este el resumen en formato merkle root de los wtxid—, e incluir su hash como una salida OP_RETURN en la transacción coinbase. De esta forma, todos los campos de la transacción sean SegWit o no, están ligados al bloque en su conjunto y no pueden ser modificados.

        Witness Merkle Root

        En esta salida OP_Return es el resultado de dSHA256(witnessMerkleRoot ‖ witnessReservedValue), siendo el witnessReservedValue un campo de información todavía no empleado. [6]

        Dado que el OP_Return y su contenido son elementos compatibles con nodos non-SegWit, esta forma de vincular la witness merkle root es también parte de que SegWit sea un soft-fork.

        Con la activación de SegWit, al incluir en el merkle root el witness merkle root, se revertía por completo cualquier ventaja que tuviera un minero de usar la técnica "Covert" del ASIC Boost, dado que si cambia el orden de las transacciones a tal fin, tiene que volver a calcular el witness merkle root.

    Ataque de Maleabilidad

      El ataque de maleabilidad se puede perpetrar dada la posibilidad de modificar la firma digital presente en una transacción sin que esta se invalide. Antes de que fuera resuelto, este ataque dificultaba la trazabilidad previo a la confirmación de la transacción e impedía que se pudieran construir segundas capas de manera segura.

      En las transacciones non-SegWit, el txid (el identificador de la transacción) es el doble SHA256 de todos los campos de la transacción. Por tanto, la firma digital también se incluye como parte de la información sobre la que se realiza el hash.

      Un cambio en la firma digital no perturba el envío de fondos pero si resulta en un txid completamente distinto, dadas las características de la función de hash.

      Este ataque se resuelve con SegWit: como veíamos antes, el txid de una transacción SegWit es el doble SHA256 de todos los campos menos del campo Witness, Marker y Flag. Puesto que en el campo Witness es donde hallan las firmas digitales, por mucho que estas varíen, el txid se mantendrá intacto.

      Métodos para alterar la Firma Digital

        Firmar digitalmente con valor "k" distinto:

        A la hora de firmar digitalmente, se emplea un valor aleatorio denominado "k". Debe ser mantenido en secreto, ya que constituye el compromiso de ocultación que evita la extracción de la clave privada. Este valor debe ser aleatorio y se puede modificar a voluntad del firmante. Un cambio en el mismo supone la creación de una firma plenamente distinta, aunque igualmente válida.

        Invertir el valor "s" de la firma digital:

        Una firma digital está compuesta por dos valores, "r" y "s". El valor "r" es la coordenada x del valor "R" en la curva elíptica y depende únicamente del valor "k". El valor "s" depende del valor "r", la clave privada, el valor "k" y el mensaje a firmar.

        La verificación de una firma digital consiste en extraer un valor "v" de un cálculo con r, s, la clave pública y el mensaje, tal que "r" sea igual a ese valor. Esto es, se realizarán unos cálculos sobre la curva elíptica de tal forma que la coordenada x de R, el valor "r", sea igual a la coordenada x de V, el valor "v". A tal fin estaremos sumando puntos en la curva elíptica que dependen del valor s.

        Dado que solo importa la coordenada x del valor V (el valor "v"), un valor de s invertido resultará en la misma coordenada x pero con una distinta coordenada y.

        La inversa de s se puede obtener fácilmente calculando: n (Orden de la Curva Elíptica) menos s.

        Con este método, un individuo ajeno a una transacción non-SegWit puede perpetrar el ataque de maleabilidad. Toma s, lo invierte y vuelve a mandar la transacción: sigue siendo válida, pero el txid ya ha cambiado.

        Sin embargo este método fue paliado mucho antes de que SegWit lo resolviera: de los dos posibles valores de s, la reglas de estandarización obligaban a que en las firmas digitales siempre se incluyera aquel que fuera más bajo —conocido como Low S—. [7]

    Notas

    Otras Notas

      [LE] = Little-Endian: manera en la que se representan los bytes de información.

      [nVersion] = Version 1 (Basic Tx), Version 2 (Relative Locktime), Version 3 (TRUC)

      [nInputs] = Número de Inputs

      [nSequence] :

        Menor o igual a 0xFFFFFFFE, nLockTime Activado.

        Menor o igual a 0xFFFFFFFD, RBF Activado (Es común que se ponga este, puesto que activa tanto el campo nLockTime como el RBF).

        Menor a 0x80000000, Relative LockTime Activado.

      [sigHashType] = El tipo de SigHash del input en cuestión

      En SegWit, las claves pública deben ser siempre comprimidas (02 o 03)

    Recursos

      Estructura más detallada de las transacciones: https://learnmeabitcoin.com/technical/transaction/#structure

      Proto-transacción: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#specification

      Creando transacciones desde cero: https://medium.com/coinmonks/creating-and-signing-a-segwit-transaction-from-scratch-ec98577b526a

      Ejemplos de Scripts SegWit: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#examples