¿Cómo puede una DApp detectar una reorganización de bifurcación o cadena usando web3.js o bibliotecas adicionales?

Tome un ejemplo de una DApp de votación. Un usuario hace clic en un botón de voto, luego, entre bastidores, se extrae una transacción en la cadena de bloques y, finalmente, la DApp le dice al usuario que su voto ha sido registrado.

Ahora, por alguna razón, hay una reorganización de la cadena (tal vez el nodo del usuario perdió y recuperó la conectividad de la red).

¿Cómo puede una DApp usar web3.js para detectar esto, de modo que pueda verificar si la transacción del usuario se ha deshecho y si el usuario necesita enviar su voto nuevamente? ¿Web3.js activa un evento para notificar a la DApp? ¿Hay algún fragmento de código, como qué evento escuchar y cómo? ¿O hay alguna biblioteca con ejemplos de su uso?

Respuestas (6)

Aquí hay un código que espera un número específico de bloques y verifica que el recibo de la transacción aún sea válido. Si se produce una bifurcación y la reproducción falla, la verificación de recibo debería fallar y la devolución de llamada llamará con Error establecido.

Solo probé esto para el éxito y las fallas de tiempo de espera, no lo probé en una bifurcación real de la cadena de bloques, porque aún no he descubierto cómo hacer que eso suceda de manera confiable en un marco de prueba. Agradezco cualquier sugerencia sobre cómo hacerlo.

Según la pregunta, solo usa llamadas web3.js y no bibliotecas. Tengo que decirte que usar callbacks en lugar de promesas es muy doloroso para mí ;-P

No he implementado la validación de la transacción en varios nodos RPC, pero hay una nota en el código sobre dónde hacerlo. Probablemente querrá usar al menos Async.join para hacer eso, que sería una biblioteca externa.

 //
 // @method awaitBlockConsensus
 // @param web3s[0] is the node you submitted the transaction to,  the other web3s 
 //    are for cross verification, because you shouldn't trust one node.
 // @param txhash is the transaction hash from when you submitted the transaction
 // @param blockCount is the number of blocks to wait for.
 // @param timout in seconds 
 // @param callback - callback(error, transaction_receipt) 
 //
 exports.awaitBlockConsensus = function(web3s, txhash, blockCount, timeout, callback) {
   var txWeb3 = web3s[0];
   var startBlock = Number.MAX_SAFE_INTEGER;
   var interval;
   var stateEnum = { start: 1, mined: 2, awaited: 3, confirmed: 4, unconfirmed: 5 };
   var savedTxInfo;
   var attempts = 0;

   var pollState = stateEnum.start;

   var poll = function() {
     if (pollState === stateEnum.start) {
       txWeb3.eth.getTransaction(txhash, function(e, txInfo) {
         if (e || txInfo == null) {
           return; // XXX silently drop errors
         }
         if (txInfo.blockHash != null) {
           startBlock = txInfo.blockNumber;
           savedTxInfo = txInfo;
           console.log("mined");
           pollState = stateEnum.mined;
         }
       });
     }
     else if (pollState == stateEnum.mined) {
         txWeb3.eth.getBlockNumber(function (e, blockNum) {
           if (e) {
             return; // XXX silently drop errors
           }
           console.log("blockNum: ", blockNum);
           if (blockNum >= (blockCount + startBlock)) {
             pollState = stateEnum.awaited;
           }
         });
     }
    else if (pollState == stateEnum.awaited) {
         txWeb3.eth.getTransactionReceipt(txhash, function(e, receipt) {
           if (e || receipt == null) {
             return; // XXX silently drop errors.  TBD callback error?
           }
           // confirm we didn't run out of gas
           // XXX this is where we should be checking a plurality of nodes.  TBD
           clearInterval(interval);
           if (receipt.gasUsed >= savedTxInfo.gas) {
             pollState = stateEnum.unconfirmed;
             callback(new Error("we ran out of gas, not confirmed!"), null);
           } else {
             pollState = stateEnum.confirmed;
             callback(null, receipt);
           }
       });
     } else {
       throw(new Error("We should never get here, illegal state: " + pollState));
     }

     // note assuming poll interval is 1 second
     attempts++;
     if (attempts > timeout) {
       clearInterval(interval);
       pollState = stateEnum.unconfirmed;
       callback(new Error("Timed out, not confirmed"), null);
     }
   };

   interval = setInterval(poll, 1000);
   poll();
 };

[EDIT 1] - la falta de gas es mayor o igual, no mayor...

He votado a favor. Parece lo mejor que se puede hacer en esta etapa; @ euri10 tiene que otorgar la recompensa o se otorgará automáticamente de acuerdo con reglas específicas meta.stackexchange.com/questions/16065/…
"Parece lo mejor que se puede hacer en esta etapa". Si le falta algo me gustaría saber. Publiqué este código con la esperanza de poder obtener algunos comentarios sobre lo que está mal. Podría integrarse en web3.js o ether-pudding si queremos un lugar donde todos puedan usarlo. Si es así, tengo más funciones que agregaría (como combinar tx_receipt y tx_info en una estructura de resultados)
Pensé más y también estoy satisfecho :) Espero que pueda integrarse como lo está planeando.
@PaulS, ¿qué pasa si la transacción se extrae con éxito con gas usado = gas proporcionado?
Si puede encontrar una manera de saber que una transacción se extrajo con gas usado = siempre, entonces hay una pregunta que necesita respuesta. La comunidad no ha proporcionado una respuesta y no sé cuál sería... básicamente es un 'error' de ethereum
en la parte inferior de la respuesta, muestra una transacción extraída con éxito donde se usó == enviada. No he profundizado lo suficiente como para descubrir qué podría hacer programáticamente para diferenciar. ethereum.stackexchange.com/questions/6002/transaction-status/…
¿Cómo puedes elegir un buen blockCount? ¿Es solo trabajo de conjetura?
Creo que hay otras preguntas sobre cómo elegir un conteo de bloques. Creo que depende del valor subyacente. si es equivalente a $ 10,000 USD, esperaría una mayor cantidad de bloques que el equivalente a $ 1 USD. Podrías ir al historial de ethereum y encontrar la peor bifurcación temporal y ver cuánto duró. Existe una probabilidad exponencialmente decreciente de que una bifurcación destruya su transacción cuantos más bloques se extraen después.
@Paul, ¿cuál es el valor detrás de 'web3s'? ¿Nodo aquí significa cuenta asignada a este nodo? (Ya veo, parece que es solo una instancia de clase 'web3')
Por ahora, este código solo implementa una verificación de un solo nodo, pero si está tratando con cantidades de gran valor, querrá verificar su transacción en varios nodos. web3s es una lista de entidades de clase web3.js, cada una presumiblemente conectada a diferentes nodos. Si fuera yo para transacciones de alto valor, tendría nodos de tres implementaciones diferentes en tres zonas de nube diferentes que no comparten ningún aparato de seguridad.

No puedo comentar si hay o no una función para esto en web3. Lo que sí sé es que Geth y Mist tienen reproducción de transacciones. Esto significa que, en caso de una reorganización, procesará las transacciones que se 'perdieron' durante la reorganización, por lo que, en teoría, el estado debería seguir siendo el mismo.

Respuesta útil que he votado. Escuché sobre la reproducción y sería útil ver las soluciones que están disponibles antes de Mist, así como la recomendación de Mist y el uso de sus API/eventos para manejarlo, ya que parece que Mist al menos debería proporcionar eventos para la DApp que hubo: 1) una reorganización 2) la reproducción de tx fue exitosa (tal vez la reproducción también puede fallar por alguna razón). Una DApp aún debe estar al tanto de estos eventos, incluso si silenciosamente no muestra ningún cambio al usuario (mientras espera que la reproducción tenga éxito).
Esto supone que confía en el nodo local de su nodo. Creo que deberías asumir que los nodos serán pirateados. Entonces, al final, creo que necesitamos una solución más estilo billetera bitcoin en la que, por ejemplo, espere 16 bloques para ser extraídos y verifique que el hash de la transacción aún sea bueno.
(y haga lo mismo con varios nodos diferentes de diferentes implementaciones)
¿Qué significa esto exactamente? Digamos que hay 12 bloques honestos pero un nodo malicioso extrae 24 bloques vacíos en secreto y luego los publica a la vez y provoca una bifurcación de 12 bloques. ¿Cómo se "reproducirán" las transacciones en los 12 bloques que ahora faltan? ¿Quiere decir que todas esas transacciones se incluirán en los próximos 12 bloques?

Actualmente no creo que haya una manera de hacer eso. Actualmente, los documentos dicen que solo espere 12 bloques para asegurarse de que no haya ocurrido una bifurcación dura y use getCode (). https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethcontract

En la API web3, sección contract events , se dice que el objeto dado a la devolución de llamada tiene un removedcampo. Si escucha su evento y se produce una reorganización, debe recibir una notificación mediante un evento en el que removedse establece en true.

Nunca probé esto, pero si entendí correctamente el documento, debería funcionar.

Gracias, veo que se agregó a la wiki en 2017. Un ejemplo ayudaría a mejorar esta respuesta.

Sí, hay una manera de hacerlo:

Cuando ocurre una bifurcación, el hash del bloque (o estado) cambiará, por lo que todo lo que tiene que hacer es obtener el hash del último bloque (o estado) antes de enviar una transacción. Luego, siga monitoreando los hashes de bloque entrantes para verificar que la cadena aún sea válida. Después de 10 a 20 confirmaciones, puede detener este proceso de monitoreo y considerar la transacción como almacenada permanentemente.

La secuencia simplificada de pasos sería:

  1. Antes de hacer eth_sendRawTransactionhaz: eth_blockNumber, luego eth_getBlockByNumbery almacena el Hash del bloque (o Estado)
  2. Enviar la transacción coneth_sendRawTransaction
  3. En un bucle, consulte nuevos bloques y conéctelos al hash del bloque que recuperó justo antes de la eth_sendRawTransactionllamada. Si ha llegado un bloque, que tiene un número consecutivo y no coincide con el hash del bloque principal, entonces se ha producido una bifurcación y puede mostrarle un mensaje a su usuario.

Puede usar el hash de Block o el hash de State, no importa, ambos valores cambian con el evento de bifurcación. También debe considerar que podría haber muchas cadenas mientras se procesa su transacción, por lo que deberá verificar que su transacción esté almacenada en la cadena más larga. Esta es la idea básica, pero por supuesto, la implementación puede ser más compleja de lo que describí.

Web3 proporciona una manera de hacer esto muy elegante. No estoy seguro de por qué las otras respuestas no han sugerido esta solución: hay una manera de escuchar las reorganizaciones en cadena que invalidan los eventos:

myContract.events.MyVoteEvent()
    .on("data", async (error, event) => {
        console.log("vote received");
    })
    .on("changed", async (error, event) => { // Called when event is no longer valid
        console.log("vote was invalidated due to reorg");
    });

De los documentos web3 :

"cambiado" devuelve Objeto: se activa en cada evento que se eliminó de la cadena de bloques. El evento tendrá la propiedad adicional "removida: verdadero".

NB: a partir de ahora parece que no es posible hacer algo similar en EthersJS; solo en Web3.js.