38

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?

0

6 Answers 6

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

11
  • 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
    Commented Apr 11, 2016 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
    Commented Apr 11, 2016 at 1:08
  • Thought more and I'm satisfied too :) Hope it can be integrated as you're planning.
    – eth
    Commented Apr 11, 2016 at 7:44
  • @PaulS what if transaction is mined successfully with gas used = gas provided? Commented Aug 9, 2017 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
    Commented Oct 16, 2017 at 2:30
6

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.

4
  • 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
    Commented Feb 9, 2016 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
    Commented Apr 8, 2016 at 17:11
  • (and do the same thing with several different nodes of different implementations)
    – Paul S
    Commented Apr 8, 2016 at 19:04
  • What exactly does this mean? Say a there are 12 honest blocks but a malicious node mines 24 empty blocks secretly then publishes them at once and causes a 12 block fork - How will the transactions in the now 12 missing blocks be "replayed"? Do you mean to say that all of those transactions will be included within the next 12 blocks? Commented Oct 8, 2020 at 18:35
4

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

0
3

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
  • 1
    Thanks, I see it was added to the wiki in 2017. An example would help improve this answer.
    – eth
    Commented Apr 29, 2017 at 10:40
2

Yes , there is a way to do it:

When a fork occurs, the hash of the Block (or State) will change, so all you have to do is to get the hash of the last Block (or State) before you submit a transaction. Then keep monitoring incoming block hashes to check that the chain is still valid. After 10-20 confirmations you could stop this monitoring process and consider transaction as permanently stored.

Simplified sequence of steps would be:

  1. Before doing eth_sendRawTransaction do: eth_blockNumber, then eth_getBlockByNumber and store the Hash of the block (or State)
  2. Submit the transaction with eth_sendRawTransaction
  3. In a loop, query new blocks, and connect them to the hash of the block you retrieved just before eth_sendRawTransaction call. If a block has arrived, that has consecuitve number and doesn't match the hash of the parent block, then a fork has occurred, and you can show your User a message.

You can use Block's hash or State's hash, it doesn't matter, both values change upon fork event. You also should consider that there could be many chains while your transaction is being processed, so you would need to check that your transaction is stored in the longest chain. This is the basic idea, but of course, the implementation may be more complex than I described.

0

Web3 provides a way to do this very elegantly. I'm not sure why the other answers haven't suggested this solution - there is a way to listen to chain reorgs invalidating events:

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");
    });

From the web3 docs:

"changed" returns Object: Fires on each event which was removed from the blockchain. The event will have the additional property "removed: true".

NB: As of right now it seems that it's not possible to do a similar thing in EthersJS; only in Web3.js.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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