DApp Development – How to Detect Fork or Chain Reorganization Using Web3.js

blockchain-forkchain-reorganizationdapp-developmenteventsweb3js

Take an example of a voting DApp. A user clicks on a vote button, then behind the scenes a transaction gets mined on the blockchain, and finally the DApp tells the user their vote has been recorded.

Now for some reason, there is a chain reorganization (maybe the user's node lost and regained network connectivity).

How can a DApp use web3.js to detect this, so that it can check if the user's transaction has been undone and if the user needs to submit their vote again? Does web3.js fire an event to notify the DApp? Are there any code snippets, such as what event to listen on and how? Or are there any libraries with examples of their use?

Best Answer

Here's code that waits specified number of blocks and verifies the transaction receipt is still valid. If a fork occurs and the replay fails, the receipt check should fail and the callback will call with Error set.

I've only tested this for success and timeout failures, I've not tested it on an actual fork of the blockchain, because I haven't figured out how to reliably cause that to happen yet in a test framework. Appreciate any hints on how to do that.

Per the question, it only uses web3.js calls, and no libraries. I have to tell you using callbacks instead of promises is very painful for me ;-P

I haven't implemented validating the transaction multiple RPC nodes, but there's a note in the code on where to do that. You will probably want to use at least Async.join to do that, which would be an external library.

 //
 // @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] - out of gas is greater than or equal, not greater...

Related Topic