Transaction Nonce – What Happens When Nonce is Too High?

consensusNetworknoncereplay-attacktransactions

From Design Rationale:

One weakness of the account paradigm is that in order to prevent replay attacks, every transaction must have a "nonce", such that the account keeps track of the nonces used and only accepts a transaction if its nonce is 1 after the last nonce used.

There have been some questions on this site about transactions nonces that are too low. What happens when a transaction nonce is too high?

Best Answer

Summary

  • Transactions with too low a nonce get immediately rejected.
  • Transactions with too high a nonce get placed in the transaction pool queue.
  • If transactions with nonces that fill the gap between the last valid nonce and the too high nonce are sent and the nonce sequence is complete, all the transactions in the sequence will get processed and mined.
  • When the geth instances are shut down and restarted, transactions in the transaction pool queue disappear.
  • The transaction pool queue will only hold a maximum of 64 transactions with the same From: address with nonces out of sequence.
  • The geth instance can be crashed by filling up the transaction pool queue with 64 transactions with the same From: address with nonces out of sequence by, for example:
    • Creating many accounts with a minimal amount of ethers (0.05 ETH in my tests)
    • Sending 64 transactions from each account with a large data payload
    • For the 4 Gb memory limit that I placed on my geth instance, 400 x 64 transactions with a payload of about 4,500 bytes would crash the geth instance (from my limited testing anyway).
    • These transactions with too high a nonce do NOT propagate to other nodes and crash other nodes on the Ethereum network.
  • The Ethereum World Computer cannot be brought down with transactions with too high a nonce. Good work, developers!


Details below.



What Happens When The Transaction Nonce Is Too Low?

I've created two fresh accounts on my private --dev network using

geth -datadir ./data --dev account new
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase: 
Repeat Passphrase: 
Address: {c18f2996c11ba48c7e14233710e5a8580c4fb9ee}

geth -datadir ./data --dev account new
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase: 
Repeat Passphrase: 
Address: {e1fb110faa8850b4c6be5bdb3b7940d1ede87dfb}

I have started a mining geth instance in a separate window, so the first account (coinbase) is getting deposited with more funds as new blocks are being mined.

geth --datadir ./data --dev --mine --minerthreads 1 console

In my main window I have started a geth instance attaching to the mining instance.

geth --datadir ./data --dev attach

I send my first transaction from my first account to my second account

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(1, "ether")})
Unlock account c18f2996c11ba48c7e14233710e5a8580c4fb9ee
Passphrase: 
"0xd17510a4c9880155b0237cac58423e05b61ad3f2d5ee90f72d3db2b7d4ea2d47"

Here are the transaction details for the first transaction. A nonce of 0 has been automatically allocated for this transaction.

 > eth.getTransaction("0xd17510a4c9880155b0237cac58423e05b61ad3f2d5ee90f72d3db2b7d4ea2d47")
 {
   blockHash: "0x519ddf8c3d1a094933d2975bb7c9cdf3680c9d66b880ba22b26627f70d90bb54",
   blockNumber: 88,
   from: "0xc18f2996c11ba48c7e14233710e5a8580c4fb9ee",
   gas: 90000,
   gasPrice: 20000000000,
   hash: "0xd17510a4c9880155b0237cac58423e05b61ad3f2d5ee90f72d3db2b7d4ea2d47",
   input: "0x",
   nonce: 0,
   to: "0xe1fb110faa8850b4c6be5bdb3b7940d1ede87dfb",
   transactionIndex: 0,
   value: 1000000000000000000
 }

I send a second transaction from my first account to my second account, specifying a nonce of 0, and get the expected result of "Nonce too low".

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(1, "ether"), nonce:0})
Nonce too low
    at InvalidResponse (<anonymous>:-81662:-106)
    at send (<anonymous>:-156322:-106)
    at sendTransaction (<anonymous>:-133322:-106)
    at <anonymous>:1:1



What Happens When The Transaction Nonce Is Too High?

I now send a third transaction from my first account to my second account, specifying a nonce of 10000, and get a transaction hash back that the transaction has been sent to the transaction pool.

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(1, "ether"), nonce:10000})
"0x5b09270d6bcd33297527a1f6b08fa1528deec01e82a100c7e62ee93fbdcd1f7d"

In the mining window, a message shows the transaction has been received. However the transaction is never mined.

I0409 15:25:07.699859   10726 worker.go:569] commit new work on block 95 with 0 txs & 0 uncles. Took 289.587µs
I0409 15:25:08.493883   10726 xeth.go:1028] Tx(0x5b09270d6bcd33297527a1f6b08fa1528deec01e82a100c7e62ee93fbdcd1f7d) to: 0xe1fb110faa8850b4c6be5bdb3b7940d1ede87dfb
> I0409 15:26:13.472919   10726 worker.go:348] 🔨  Mined block (#95 / 7fe1ada0). Wait 5 blocks for confirmation
I0409 15:26:13.473634   10726 worker.go:569] commit new work on block 96 with 0 txs & 0 uncles. Took 630.605µs
I0409 15:26:13.473707   10726 worker.go:447] 🔨 🔗  Mined 5 blocks back: block #90
I0409 15:26:13.474252   10726 worker.go:569] commit new work on block 96 with 0 txs & 0 uncles. Took 447.451µs
I0409 15:26:18.921404   10726 worker.go:348] 🔨  Mined block (#96 / 760e117c). Wait 5 blocks for confirmation
I0409 15:26:18.922033   10726 worker.go:569] commit new work on block 97 with 0 txs & 0 uncles. Took 547.204µs
I0409 15:26:18.922096   10726 worker.go:447] 🔨 🔗  Mined 5 blocks back: block #91

I try to retrieve the transaction details for my third transactions. The blockHash and blockNumber remain null for ever.

> eth.getTransaction("0x5b09270d6bcd33297527a1f6b08fa1528deec01e82a100c7e62ee93fbdcd1f7d")
{
  blockHash: null,
  blockNumber: null,
  from: "0xc18f2996c11ba48c7e14233710e5a8580c4fb9ee",
  gas: 90000,
  gasPrice: 20000000000,
  hash: "0x5b09270d6bcd33297527a1f6b08fa1528deec01e82a100c7e62ee93fbdcd1f7d",
  input: "0x",
  nonce: 10000,
  to: "0xe1fb110faa8850b4c6be5bdb3b7940d1ede87dfb",
  transactionIndex: null,
  value: 1000000000000000000
}

I check the transaction pool status and I am assuming that the transaction with nonce 10000 is in the queue.

> txpool.status
{
  pending: 0,
  queued: 1
}

I try sending a fourth transaction with a nonce of 1. The transaction gets mined.

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(1, "ether"), nonce:1})
"0x545af0a0276e154a8669921373de8904a330b829318a7c83f5bd9f9771e71ff8"
> eth.getTransaction("0x545af0a0276e154a8669921373de8904a330b829318a7c83f5bd9f9771e71ff8")
{
  blockHash: "0xc125f5da96e36ac87728a35eae8ff8046bcc08c6242825daa4b6bb1e7b460a01",
  blockNumber: 101,
  from: "0xc18f2996c11ba48c7e14233710e5a8580c4fb9ee",
  gas: 90000,
  gasPrice: 20000000000,
  hash: "0x545af0a0276e154a8669921373de8904a330b829318a7c83f5bd9f9771e71ff8",
  input: "0x",
  nonce: 1,
  to: "0xe1fb110faa8850b4c6be5bdb3b7940d1ede87dfb",
  transactionIndex: 0,
  value: 1000000000000000000
}

So I try sending a fifth transaction with a nonce of 3 (there is now a gap as the last valid nonce is 1). The transaction goes into the transaction pool queue and does not get mined.

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(1, "ether"), nonce:3})
"0x895ec329c3a1d53acf7a429721025f2ff01d5558feee0595daa0fa9c0282d461"
> eth.getTransaction("0x895ec329c3a1d53acf7a429721025f2ff01d5558feee0595daa0fa9c0282d461")
{
  blockHash: null,
  blockNumber: null,
  from: "0xc18f2996c11ba48c7e14233710e5a8580c4fb9ee",
  gas: 90000,
  gasPrice: 20000000000,
  hash: "0x895ec329c3a1d53acf7a429721025f2ff01d5558feee0595daa0fa9c0282d461",
  input: "0x",
  nonce: 3,
  to: "0xe1fb110faa8850b4c6be5bdb3b7940d1ede87dfb",
  transactionIndex: null,
  value: 1000000000000000000
}

I send a sixth transaction with a nonce of 2 (this fills in the gap between the last valid nonce of 1 and the queued transaction with a nonce of 3).

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(1, "ether"), nonce:2})
"0xea7a6350d6f7aa61a5f515452021de905917338d3b4d354e19fc53d8bd4982f4"

Both the transactions with the nonces of 2 and 3 now get mined.

> eth.getTransaction("0xea7a6350d6f7aa61a5f515452021de905917338d3b4d354e19fc53d8bd4982f4")
{
  blockHash: "0xabcfea8140fdbe3d04bab05cb0232a8c73de4a6bc2307907ede9d45ad58d7107",
  blockNumber: 170,
  from: "0xc18f2996c11ba48c7e14233710e5a8580c4fb9ee",
  gas: 90000,
  gasPrice: 20000000000,
  hash: "0xea7a6350d6f7aa61a5f515452021de905917338d3b4d354e19fc53d8bd4982f4",
  input: "0x",
  nonce: 2,
  to: "0xe1fb110faa8850b4c6be5bdb3b7940d1ede87dfb",
  transactionIndex: 0,
  value: 1000000000000000000
}
> eth.getTransaction("0x895ec329c3a1d53acf7a429721025f2ff01d5558feee0595daa0fa9c0282d461")
{
  blockHash: "0xabcfea8140fdbe3d04bab05cb0232a8c73de4a6bc2307907ede9d45ad58d7107",
  blockNumber: 170,
  from: "0xc18f2996c11ba48c7e14233710e5a8580c4fb9ee",
  gas: 90000,
  gasPrice: 20000000000,
  hash: "0x895ec329c3a1d53acf7a429721025f2ff01d5558feee0595daa0fa9c0282d461",
  input: "0x",
  nonce: 3,
  to: "0xe1fb110faa8850b4c6be5bdb3b7940d1ede87dfb",
  transactionIndex: 1,
  value: 1000000000000000000
}

I check the transaction pool status and the transaction with nonce 10000 is still in the queue, and will remain forever.

> txpool.status
{
  pending: 0,
  queued: 1
}

I shut down my mining geth instance and my attached geth instance, and restart both of them. I now check the transaction pool status and the transaction with nonce 10000 has disappeared.

> txpool.status
{
  pending: 0,
  queued: 0
}



Transaction Pool Source Code

Looking at core/tx_pool.go (#48-50), there is a maximum queue size of 64 transactions for transactions with out-of-order nonce sequence per sending address.

const (
    maxQueued = 64 // max limit of queued txs per address
)

And core/tx_pool.go (#436-456) shows the code that removes transactions if the queue is too full:

for i, entry := range promote {
    // If we reached a gap in the nonces, enforce transaction limit and stop
    if entry.Nonce() > guessedNonce {
        if len(promote)-i > maxQueued {
            if glog.V(logger.Debug) {
                glog.Infof("Queued tx limit exceeded for %s. Tx %s removed\n", common.PP(address[:]), common.PP(entry.hash[:]))
            }
            for _, drop := range promote[i+maxQueued:] {
                delete(txs, drop.hash)
            }
        }
        break
    }
    // Otherwise promote the transaction and move the guess nonce if needed
    pool.addTx(entry.hash, address, entry.Transaction)
    delete(txs, entry.hash)

    if entry.Nonce() == guessedNonce {
        guessedNonce++
    }
}



Crash Testing Geth With Too High A Nonce

  • For my testing I created one mining geth instance with a peer-to-peer connected non-mining geth instance, on a private dev network.
  • I limited the mining geth instance with 4Gb by switching off my swap file (running sudo swapoff -a on Linux) and running other memory hogging programs.
  • I created a Perl script to iteratively (1 .. 20,000) create new accounts on my mining geth instance and transfer 0.05 ETH from my coinbase into the new accounts.
  • I created another Perl script to iteratively (1 .. 20,000) unlock each new account in the mining geth instance and send 64 transaction with the nonce set too high and with a data payload of ~ 4,500 bytes.
  • The transactions with too high a nonce ended up filling the transaction pool queue of the mining geth instance and crashing geth at about the 400th iteration. My computer also shut down.
  • The non-mining geth instance did not receive any of the transaction with too high a nonce.
  • To confirm that the transaction with too high a nonce does not propagate from node to node, I also manually created transaction with too high a nonce on the non-mining geth instance and only the transaction pool queue of the non-mining geth instance filled up.
Related Topic