Mi transacción sin procesar destruyó 0.0284377 BTC. ¿Qué hice mal?

Hace años diseñé un módulo .NET que facilita la transmisión de BTC a mis clientes. Crea una representación binaria de la transacción deseada basada en el material presentado aquí y aquí :

La representación binaria luego se convierte a hexadecimal y se envía a través de varios servicios gratuitos como BlockExplorer, BlockCypher, etc.

El sistema ha estado funcionando sin problemas durante años. Hasta ayer. Un cliente solicitó que se enviaran 0.0284377 BTC a 38MRMGjMBMp4k7vZhKLHhcM9Pm8AMLy18v . Nunca lo recibió. Efectivamente, tenía razón. Nunca fue enviado.

Revisé todos mis registros y vi que mi software, de hecho, envió una transacción con las siguientes entradas y salidas:

  • entrada: 13P38hMYJXFdxDJJn8TtPUJZFXmcpf2J99
  • salida #1: 38MRMGjMBMp4k7vZhKLHhcM9Pm8AMLy18v (mi cliente)
  • salida #2: 1Ny3CV3rAsNMWpLfpxhXW3Fh71YmMEXXU7 (mi cambio de dirección)

Todo se veía bien de mi lado, pero efectivamente, cuando miré el explorador de bloques, lo que vi me sorprendió. Considere la transacción 9a138b14dcc8ae740073c06933ae04e3b08fe6be6ada0dc175e6484250dfe269 y observe las entradas y salidas:

Como puede ver, mi entrada es correcta. Mi dirección de cambio XU7 también es correcta. Pero ahora mira la dirección de mi cliente. Dice 17fQRjEudTVgexE8aDfhGyzDFEqSnnJJCA (al que me referiré como JCA) en lugar del esperado 38MRMGjMBMp4k7vZhKLHhcM9Pm8AMLy18v (al que me referiré como 18v). ¿Que demonios? Ni yo ni mi cliente conocemos la dirección de JCA. Es una dirección completamente desconocida.

Claramente, el problema está en mi código en alguna parte, así que profundicé más para descubrir qué hizo que esta transacción en particular fuera única. Identifiqué que su dirección deseada (18v) comienza con un '3', mientras que todas las demás transacciones salientes que he completado a lo largo del historial de mi aplicación tienen direcciones específicas que comienzan con '1'.

Mi medida provisional fue obligar a los usuarios a especificar solo las direcciones que comienzan con 1. Pero esa es una solución temporal. Necesito averiguar qué estoy haciendo mal.

Me volví a Google. La investigación indicó que las direcciones que comienzan con '3' son típicamente direcciones SegWit, mientras que las que comienzan con '1' son direcciones tradicionales de la vieja escuela. Recuerdo toda la conversación sobre SegWit hace unos meses, pero pensé que no me afectaba y que mis transacciones heredadas aún se liquidarían correctamente. Obviamente, fue una decisión equivocada de mi parte, así que ahora tengo que averiguar qué hice incorrectamente y de dónde provino la dirección JCA falsa.

Ahí es donde estoy atascado. Creo que mi problema podría tener que ver con las claves públicas sin comprimir frente a las comprimidas según las discusiones aquí y aquí :

Esto es lo que sé: cuando llega el momento de crear los resultados de mi transacción, mi código hace lo siguiente (a continuación se explica):

Func<String,byte[]> makeOutScript=(btcAddrHex)=>{
    byte[] addrBytes=BTCUtils.Base58Decode(btcAddrHex);
    byte[] pubKeyBytes=addrBytes.Take(addrBytes.Length-4).Skip(1).ToArray();

    using(MemoryStream ms=new MemoryStream()){
        using(BinaryWriter bw=new BinaryWriter(ms)){
            bw.Write((byte)0x76);   //op_dup
            bw.Write((byte)0xa9);   //op_hash160
            bw.Write((byte)20);     //size of public key
            bw.Write(pubKeyBytes);  //public key
            bw.Write((byte)0x88);   //op_equalverify
            bw.Write((byte)0xac);   //op_checksig

            bw.Flush();
            return ms.ToArray();
        }
    }
};

Lo que estoy haciendo aquí es: convertir la dirección de salida en bytes decodificándola en Base58. Elimino los últimos cuatro bytes, que son la suma de comprobación, y el primer byte, que siempre es 0x00 (aparentemente, un indicador de red de algún tipo). Eso me deja con la clave pública sin procesar. En realidad, mirando mis notas internas, en realidad no es la clave pública sino 0x04 antepuesto a la clave pública y luego pasó a través de SHA256 y luego RIPEMD160. Pero me referiré a este blob como la clave pública. A partir de ahí, la secuencia de bytes es 0x76, 0xa9, 20 (el tamaño del blob de clave pública), el propio blob de clave pública, 0x88 y 0xac.

No pretendo entender lo que significan todas esas entradas de un solo byte, pero las publicaciones de blog anteriores decían que las incluyera, así que eso es lo que hice, y hasta ahora funcionó bien.

La pregunta es: ¿Cómo diablos se transformó la dirección de 18v en la dirección JCA cuando se envió a la red? ¿Es mi valor de "tamaño de clave pública" (20) incorrecto, tal vez? A primera vista, parece extraño que una clave pública comprimida (18v) y sin comprimir (JCA) tengan un tamaño constante de 20. Pero tal vez estoy en el camino equivocado y la compresión no tiene nada que ver con eso.

No tengo que decirte lo terrible que sería si esta transacción fuera de 100 BTC en lugar de 0,0284377. Tuve la suerte de que mi error se activó en una transacción de bajo valor. Pero realmente quiero resolver esto. ¿Puedes señalarme el camino correcto?

Respuestas (1)

Las direcciones que comienzan con 3...son direcciones P2SH y existen desde hace casi 6 años. Históricamente vieron un uso limitado, utilizado por billeteras multisig como Copay y GreenAddress. Sin embargo, el modo de compatibilidad de SegWit también usa P2SH, lo que hace que su uso sea más común recientemente.

El problema principal es que su código para convertir una dirección en un scriptPubKey no mira el byte de la versión. Después de que Base58 decodifique la dirección, debe obtener una estructura de <versión de 1 byte> <hash de 20 bytes> <suma de comprobación de 4 bytes>. Si esa versión es 0, que es para 1...direcciones, ese hash es un hash de clave pública y construye correctamente el scriptPubKey correspondiente.

Sin embargo, si ese número de versión es 5, es una dirección P2SH, que se especificó en BIP13 , que usa un scriptPubKey correspondiente especificado en BIP16 .

Si bien SegWit inicialmente solo usaba el antiguo formato P2SH, también se está introduciendo un nuevo formato de dirección para SegWit (que brinda un rendimiento, flexibilidad y capacidades de detección de errores ligeramente mejores), descrito en BIP173 . (descargo de responsabilidad: soy autor de BIP173). Estas direcciones comenzarán con bc1....

Le aconsejo que detenga su servicio y, como mínimo, implemente la verificación de que se espera el número de versión, junto con un conjunto de prueba que puede encontrar en muchas implementaciones. Si lo desea, también puede implementar direcciones BIP13 y BIP173, las cuales probablemente serán más frecuentes pronto.

Vaya, esto es una locura interesante. No sé cómo he pasado todos estos años sin tratar con una dirección de Bitcoin que comience con algo que no sea '1'. Incluso fuera de mi aplicación, todas mis direcciones de Electrum también comienzan con '1'. Ok, entonces tienes razón, cuando descomprimo la dirección 18v, de hecho tiene un byte inicial establecido en 5. Entonces estás diciendo: (1) ¡verifica ese byte de versión! y (2) si es un 5, ¿debo cambiar la estructura de ese script? ¿Qué es exactamente "BIP"? ¿Es ese el documento de referencia? ¿Por qué tantas personas afirman que el '3' inicial es una dirección "segwit"? Tanta información mala por ahí :(
No, lo primero que debe hacer es verificar la suma de verificación y el número de versión. Pensar que la versión es siempre cero es la causa de este problema. Incluso si cree que siempre será cero, su software debería fallar si no lo es.
BIP, o propuestas de mejora de Bitcoin, es un proceso para publicar ideas sobre cambios en las reglas de Bitcoin o su uso: github.com/bitcoin/bips
SegWit necesitaba una forma de ser compatible con el software antiguo (para que las billeteras antiguas pudieran enviar a las billeteras SegWit), por lo que se puede usar en modo de compatibilidad (que usa direcciones P2SH 3...) o en modo nativo (que usa BIP173 bc1. .. direcciones). Antes de SegWit, el único software que entregaba direcciones P2SH eran billeteras multisig como GreenAddress o copago.
Entendido. Cuando el usuario envía una dirección a la aplicación, es cuando ejecuto la validación de la suma de comprobación. Tengo esa parte cubierta. Mi error fue pensar que lo que ahora sé es que el "byte de versión" era solo un cero constante. Ahora puedo comprobarlo y tomar medidas. ¡Gracias! Todavía estoy un poco confundido sobre cómo cambiará el formato de mi scriptPubKey si el número de versión es 5. Presumiblemente, podré buscarlo en alguna parte.
Lo encontrará en BIP16.
hola chicos @PieterWuille gracias por este excelente control de calidad, increíble. me ha llevado a hacer una pregunta.. bitcoin.stackexchange.com/questions/64166
Wow, una gran lectura chicos.