In a recent thread , Kurt outlined some possible replay attacks, and suggested consensus changes to avoid them.
Since then there has been some more keybase discussion of the seriousness of these attacks, as well as alternative ways to deal with them.
In this post I’d like to provide a summary of our current understanding of replay attacks and the options for mitigation.
Let’s consider the simplest possible form of a replay: that of a transaction tx with single input A, single output B, and single kernel K. In practice, replayed transactions would probably feature a change output as well, but they don’t fundamentally change the nature of the problem, so in the interest of clarity we’ll leave them out. A replay of tx happens when
tx: A --> B (K)
appears for the 2nd time on chain, say at height h2, after an earlier occurence at height h1. This is only possible under certain circumstances:
i) The same output can appear multiple times in the output MMR.
This is actually always possible in MimbleWimble with cut-through, since the consensus model doesn’t include the set of all spent outputs. Duplicate unspent outputs are not allowed simultaneously though: an output can appear at most once in the UTXO set. So B must have been spent in between h1 and h2, let’s say by some tx2.
ii) The same kernel can appear multiple times in the kernel MMR.
Since the Mimblewimble consensus model does include the set of all kernels, it’s up to each MW implementation whether to allow reoccurence of kernels, or to enforce unique kernels. The latter comes at a nontrivial cost though; necessitating a lookup of every new kernel in the set of existing ones. For the sake of efficiency, and simplicity of the consensus model, Grin chose not to do that.
iii) Output A must be re-created. This is not possible with publicly available wallet implementations. An attacker would need to make some minor changes to the source code to achieve this.
With all 3 circumstances holding, tx can then be replayed. At first glance this appears to be just giving free money to Bob, the original owner of B. But the following scenario shows how this could be an attempt to defraud Bob.
Let’s say that in the past, Alice paid Bob with tx to buy 1 trinket, and that Bob
ends up spending output B in tx2 to pay Charlie: A -> B -> C. Bob doesn’t realize that Charlie is colluding with Alice (or simply is an alter ego for Alice).
Now Alice proposes to buy another trinket from Bob, and they construct a transaction tx3 to pay for it. But instead of publishing tx3, Alice replays tx.
Bob’s wallet will notice output B as one it has generated.
If the wallet simply shows B as part of Bob’s spendable balance, and fails to show the pending status of tx3, then Bob may easily be fooled into accepting the replay as payment for another trinket. In which case Alice plans to replay tx2 as well after receiving the trinket. and thereby recover some or all of her payment, defrauding Bob.
This is what we must prevent from happening.
As mentioned before, one way is to change the consensus model, and enforce unique kernels, at some loss in efficiency.
The other way is to make wallets aware of previously spent utxos.
If the wallet still remembers that output B was spent earlier (in tx2), then it should consider new appearances of B as invalid, and either ignore them completely, or mark them as replayed so that Bob may realize the defrauding attempt.
This appears to be suffice for mitigating replay attacks, but we cannot guarantee that wallets remember all earlier spends, since they may become corrupted or inaccessible. In that case Bob would perform a wallet restore from his backed-up seed phrase.
Recall that we said above that “Bob’s wallet will notice output B as one it has generated.”
We can now see that this should be strengthened to “Bob’s wallet will notice output B as one it has generated after its last restore” if the wallet was in fact restored.
If the wallet should find an output that was generated before the last restore, then it cannot know whether it was spent before. Such outputs should be marked as suspect, and would not contribute to the spendable balance.
Instead, the user should be given the option to either spend them back to oneself, or mark them as safe, with the user accepting any possible risk of replay.
This leaves the wallet restore process itself, that also finds lots of previously generated unspent outputs. This presents the greatest burden on the user (but there are other reasons why a restore cannot generally be expected to be a seamless experience).
They could again be given the above options for each output, or to sets of outputs that they group together. Perhaps they would respend all those above a certain value and accept the risk of replay on smaller ones.
Together, these suggested wallet behaviours should make replay attacks mostly pointless.
They do come at the cost of some UX complications in the presence of attacks or in the case of a wallet restore, hopefully both rare events.