During the launch of the frontier network, users were cautioned not to reuse keys from the Olympic testnet in order to prevent "replay attacks". What is a replay attack, and why would re-using a key from the testnet make someone vulnerable to one?
Replay Attack – Understanding Replay Attacks in Blockchain
accountsattackskeymanagementreplay-attacktestnets
Related Solutions
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.
A guide specifically for protecting against replay attacks from the TheDAO hard fork, from https://blog.ethereum.org/2016/07/26/onward_from_the_hard_fork
Users who are interested in taking any actions with their ETC, including creating and participating in applications, converting to another asset, etc are advised to use the splitter contract at address
0xaa1a6e3e6ef20068f7f8d8c835d2d22fd5116444
to move their ETC to a separate newly created account so as to avoid replay attacks; we also encourage the ETC community to consider adopting a secondary hard fork to change transaction formats to make further replay attacks impossible. Until and unless that happens, once ETH and ETC are “split” they should be managed via separate wallets.To use the splitter contract from inside of the Ethereum Wallet, click on Contracts -> Watch Contract, copy the address and ABI from the above linked etherscan page, and click “OK”; then, click on the contract in the Contracts tab, select “Write to Contract”, and select the “Split” function. It will ask for two addresses; for the first, put the address where you want your ETH to go (feel free to put the same address you are sending from), for the second put the address where you want the ETC to go. Make sure to try this with a very small amount of ether first to verify that it works before increasing the amount. You may use the Ethereum Classic Explorer here to verify that ETC balances have been transferred. A more detailed community-provided guide can be found here.
Best Answer
A replay attack is a valid data transmission that is maliciously or fraudulently repeated or delayed.
Extending this to blockchains, a replay attack is taking a transaction on one blockchain, and maliciously or fraudulently repeating it on another blockchain.
For example, an attacker taking someone's testnet transaction, and repeating it on the "real" blockchain, to steal "real" funds.
As @libertylocked commented, EIP 155 Simple replay attack protection has been implemented.
More Info
In Bitcoin, addresses in testnet use a different prefix from addresses in mainnet: thus keys are different.
In Ethereum, there are currently no "prefixes". (Probably done to keep creation of new addresses simpler.) So a transaction signed by a key, that is valid on one Ethereum network/chain, is valid for all Ethereum chains.
This means that if in "testing", funds are sent from accountB to accountTest, that same transaction can be replayed (broadcasted) to the public Ethereum blockchain: a replay attack. The replay attack will "succeed" if accountB does have funds on the public blockchain. To fully succeed, an attacker would need to know the private key to accountTest to steal the funds, but given that accountTest was created for testing, its private key may not be secure (maybe it is just a "brainwallet" with password "test").
Replay attacks are eliminated by using different addresses/keys between the frontier network, and all other Ethereum chains. (A little like using a different password for valuable stuff, from less valuable or less trustworthy websites.) Also see: How to prevent a replay attack between two competing chains? and as noted by @libertylocked comment, EIP 155 Simple replay attack protection has been implemented.