Sending Web3 Transactions In Node.js — Nonce Hell. | by Nonse Odion | May, 2022

The nonce is an important part of EVM accounts. Know how it is managed

Hellish Numbers. Credit: Unsplah

Have you ever been greeted with the following error when sending a web3 transaction in Node?

replacement fee too low [ See: https://links.ethers.org/v5-errors-REPLACEMENT_UNDERPRICED ]

or

the tx doesn’t have the correct nonce. account has nonce of: 95 tx has nonce of: 94

This implies that your transaction contains a nonce that has already been used in one of your previous transactions.

A nonce is a number attached to every transaction you send on Ethereum or any other blockchain that uses the Ethereum Virtual Machine (EVM) like the Binance Smartchain and Avalanche.

It starts at zero for your first transaction and increments by one (+1) for subsequent transactions. This allows each transaction from an address to have a unique nonce. Since it is unique for every transaction, it helps prevent double-spend attacks.

A double-spend attack is when an address tries to spend its current balance twice. Something as simple as collecting your change from the salesgirl in your local grocery store and calling the store owner to collect the same money is a double-spend attack. Two transactions from an address can not have the same nonce, one of them will be rejected by the blockchain network. So that error comes from the transaction the blockchain rejected.

Note: There’s also the block nonce used to mine blocks in Proof of Work (POW) chains like Ethereum and Bitcoin. It is completely different and separate from the nonce mentioned here.

When you send a transaction in Node.js using a web3 library, it automatically detects the nonce for you if you do not specify it. It does this by querying your nonce from the blockchain and attaching it to your transaction. It gets wild from here, stay with me.

Let’s say you want to send two transactions at the same time and you don’t specify their nonce, the library automatically detects the nonce and attaches it to the first transaction. When it gets to the second transaction, it automatically detects it again and attaches it. But there’s a catch, can you try guessing where the issue is before you read on?

The first transaction sent gets the latest nonce but when the second transaction is being sent, it gets the same nonce as the first which makes it invalid. It gets the same nonce as the first because when the library tries to get the nonce again, it receives the same nonce from the blockchain. This happens because the first transaction hasn’t been mined yet. If it was mined, it would update the nonce and the library would receive the right value.

Frontend code makes use of a wallet to send transactions. These wallets such as Metamask and Trustwallet are able to track the nonce so you never get to see these errors. The nonce can be changed on these wallets before sending transactions. This is useful for canceling transactions.

It’s quite simple really, you just have to know the right nonce before sending the transaction. You can decide to implement a nonce tracker that keeps track of the transactions you send or just simply wait until a transaction is mined before sending the next one. Let’s look at how a nonce tracker would work with a little node app.

The following code is written in node with Typescript and Ethers.js and runs on Rinkeby Testnet. It’s hosted on Github. Here’s the first piece of code.

config.ts

We configure the Ethers.js library with the Rinkeby provider and our private key from the environment variable. Let’s look at the sendTx function which handles sending transactions and tracks the nonce.

Send.ts

This piece of code receives the configured web3 and uses it to send the transaction. Each transaction is expected to send the transaction parameters. We don’t make use of Ethers.js famous transaction.wait because we are not giving feedback to a user, unlike frontend code. This allows us to send multiple transactions that can all get mined in the same block. The two most important parts of this code are the nonce and the lock. Let’s have a look at them.

Before we send the transaction, we attach the nonce we think is the most recent. The nonce is first gotten from the blockchain and incremented after every transaction. We also make sure to check for the latest nonce before sending the transaction just in case there are other transactions sent with this address but not from us ie the wallet might be used in a different place. This foreign transaction would also affect the nonce, so we have to be aware of it. We define the nonce outside the function so its value persists even after every sendTx call. The sendTx function accesses it using Javascripts closure property.

From the code you will notice a lock is used, the lock helps to pause each sendTx call until the call preceding it is completed. This helps make sure all sendTx calls no matter where they originate from our little node app are executed sequentially. The lock works by putting all the calls in an array and executing them one after the other. lock.acquire puts the call in the array and pauses it while lock.release lets the next call in the array run. Without the lock, calls to sendTx would run in parallel and make use of the same nonce value.

Here are our simple transactions:

txs.ts

The two transactions just send ETH.

We run them like this.

index.ts

tx1 and tx2 are intentionally called without an await to show how our code would handle sendTx calls from independent places in our app. The code sends 10 transactions at almost the same time by calling sendTx. The transactions are all queued and executed sequentially because of our lock. They can even all be mined in the same block.

When we run the code it queues all the transactions, gets the nonce and executes them sequentially. This way we are able to ensure each transaction gets the right nonce. The queuing and nonce tracking occur because of our lock and the way we track the nonce without them we would get the error above.

Just so you know, if you run the code above, you are effectively deploying 10 contracts. This is because contract deployment transactions don’t specify a receiver and we didn’t in our code. We apply the contracts with zero code and sent the 0.01/0.001 ether to them. Here’s one of my transactions on Etherscan. You can access the code on Github and run it with a simple command on your terminal. There’s a short readme there that tells you how to setup.

So that’s how we handle nonce errors on the backend. The nonce is a very important part of EVM accounts and we’ve seen how it is managed. Next time you are racing against that deadline and experience this error, you are free to copy this piece of code and implement it. What are devs for? I’ll see you in my next article. I will be rewriting the contract used in the Fei protocol/Rari capital hack. Wish me luck.

Leave a Comment