30

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?

18
+50

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...

  • I've upvoted. Seems the best that can be done at this stage; @euri10 has to award the bounty or it will be auto-awarded according to specific rules meta.stackexchange.com/questions/16065/… – eth Apr 11 '16 at 0:13
  • " Seems the best that can be done at this stage". If it's lacking something I'd like to know. I posted this code with the hope I could get some feedback on what's wrong. It could be integrated into web3.js or ether-pudding if we want a place where everyone could use it. If so I have further functionality I would add (like combine tx_receipt and tx_info into one result structure) – Paul S Apr 11 '16 at 1:08
  • Thought more and I'm satisfied too :) Hope it can be integrated as you're planning. – eth Apr 11 '16 at 7:44
  • @PaulS what if transaction is mined successfully with gas used = gas provided? – Arthur Stankevich Aug 9 '17 at 9:15
  • 1
    I think there are other questions on how to choose a block-count. I think it depends on the underlying value. if it's $10,000 USD equivalent I'd wait a larger number of blocks than $1 USD equivalent. You could go into the ethereum history and find the worst temporary fork and see how long it lasted. There's an exponentially decreasing probability that a fork destrooyed your transaction the more blocks there are mined afterwards. – Paul S Oct 16 '17 at 2:30
4

I can't comment whether there is or is not a function for this in web3. What I do know is that Geth and Mist have transaction replay. This means that in case of a reorganisation it will process transactions that were 'lost' during the reorganisation so in theory the state should still be the same.

  • Helpful answer that I've upvoted. I've heard of the replay & it would be helpful to see solutions that are available pre-Mist, as well as the Mist recommendation and usage of its APIs/events for dealing with it, since it appears Mist should at least provide events to the DApp that there was: 1) a reorg 2) the tx replay was successful (maybe replay may also fail for some reason). A DApp should still be aware of these events even if it silently does not show the user any changes (while it hopes for the replay to succeed). – eth Feb 9 '16 at 8:38
  • This assumes you trust your node local node. I think you should assume that nodes will get hacked. So in the end I think we need a more bitcoin-wallet style solution that you e.g. wait 16 blocks to be mined and verify the transaction hash is still good. – Paul S Apr 8 '16 at 17:11
  • (and do the same thing with several different nodes of different implementations) – Paul S Apr 8 '16 at 19:04
3

Currently I don't think there is a way to do that. Currently the docs say to just wait 12 blocks to make sure that a hard fork didn't happen and use getCode(). https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethcontract

2

On web3 API, section contract events, it is said that the object given to the callback has a removed field. If you listen for your event and a reorganization occurs, you should be notified by an event in wich removed is set to true.

I never tried this but if understood correctly the doc, it should work.

  • 1
    Thanks, I see it was added to the wiki in 2017. An example would help improve this answer. – eth Apr 29 '17 at 10:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.