Replay attack mitigation


With the cut through feature, the chain is somewhat vulnerable to the replay attack.
Some discussion threads can be found in grin forum:


Some experiment is done to reproduce replay attack in dev’s local environment.

experiment to create an output with certain amount in the other wallet

With some tweak of wallet code, we were able to do this.

i) send to a file tx.tx from wallet1(latest slate version 4 and lock_later was used), at the same time, modify the wallet code to save
this context with a different uuId, say, fake_uuid.

ii) receive the file tx.tx in wallet2 and response file tx.tx.response was generated.

iii) finalize tx.tx.response in wallet1

iiii) modify the uuid in tx.tx.response to fake_uuid and finalize this file in wallet1.
It will finalize. If output created in wallet2 by the first transaction has not been spent, posting will have duplicate UXTO error.

If the output in wallet2 was spent already, the posting will probably succeed.


Checking for duplicate kernels from node is straight forward but expensive, requiring
a hard fork.

Here we propose the following solutions to prevent replay-attack. It is a combined solution including update on both wallet and node, it doesn’t require
hard fork or complicated change, and should cover most of the cases.

i) Save the tx offset info to TxLogEntry and receiver wallet can check if duplicate offset exists. To keep backward compatibility,
this field will be optional. That will help if wallet is not restored form the seed in which case, offset is empty.

ii) Node can check duplication for tx kernel and outputs for last 2 week old block (node has that info). The output check will include
both UXTO and spent. We will add a cache for the spent output and keep them for 2 weeks. This will be soft fork for the node code.

iii) add height info when we build the output commitment. During the wallet scan, for the outputs with height difference more then 2 weeks,
trigger self spend workflow. Of course, user will loose some tx fee for each self-spend. There will be a self spend configuration on the QT
wallet, user can opt out if they understand and want to take the risk.


Thanks for the experiment and the post.

if I understand correct, you save a duplicated context with a different uuid, and reuse the true tx.response for this saved fake context to finalize and post. imo, this will never work, I don’t think the post will be accepted because you must prepare the valid Input (by creating a duplicated/same output as your spent one/s).

Adding timestamp info in the Output is indeed a simple and valid fix for the the replay attack, i.e. your iii) add height info when we build the output commitment. And it does not need any hardfork/softfork, just reuse the Bulletproof message which still has some reserved spaces to contain that. I had some discussion with @Konstantin before about this.

And checking duplication of tx kernel is absolutely a hard fork, and has some other side effect also. Because it is a consensus change, since the node with old versions will accept blocks with duplicated kernel, but new version nodes will reject them.

For tx offset to TxLogEntry, iirc, it’s already included and which does not help anything for us. Pls correct me if I missed sth.

1 Like

Recently grin introduce ‘compact slate’ with ‘lock later’ flag that allow to lock output on finalize. That flag repopulate the inputs and change output. The only receiver’s output will stay the same. This feature used in this experiments, it does exactly what we need.


How we discussed it is a soft fork, similar to bug fix. The checking is done when transaction adding into the pool. And checking is done before horizon. Because of that the old nodes will work as it is. The new nodes will prevent this transaction to be added into the pool. If miner pools make an upgrade that will be enough.

If attacker will mine that transaction into the block - that will be a problem and it is not covered. In order to cover that - we need a hard fork. We probably should do that with NIT hard fork.
@yang-dev, it is not enough to check transaction when it enter transaction pool. We really need to change a consensus.

tx offset will work only for compact slates. I think if tx offset will be changed, that will make output is invalid. So it can be really used to detect that scenario.

But I just realized, that attacker really can go around this scenario. In the experiment, for reply attack the receiver is not involved at all and it is make sense. As a result attacker can send the normal transaction, not finalize it, and after do reply attack. Even transaction will not be confirmed, the balance will be updated, so attacker can convince that there was a glitch and payment was sent. If victim will restore the wallet from the seed - that output will really look correctly, the time will be exactly what attacker expect. It is problem.

So like the consensus hardfork is only that will work.

Seems like on the node side only hardfork solution can be effective. The nodes need to check for duplicate commits before horizon (1 week).

In this case
i) can be eliminated, that will not stop attacker in any case.
ii) still need to be done for tx pool. But also after the fork the same need to be done for the new blocks at consensus. We can do tx pool now and activate the consensus after the hard fork.
iii) do as planned. Just keep in mind, the horizon is 1 week, not 2. But idea still will be the same.

I think it is fine, the feature will be implemented now but will start working after the HF will be done.

@suem @yang-dev what do you think?

Thanks for the comments and thoughts.
I think checking kernel offset by adding it to TxLogEntry can help in some cases.
And on the consensus level change, are we going to check blocks in the last one or two weeks, or all the blocks? Checking all the blocks could have some performance impact.

Also, do we have a date for the NIT hard fork?

Yes, Here are the plans Non-Interactive Transaction and Stealth Address

According to the TxLogEntry structure you sent in Discord, there is a kernel_excess field and you added a new kernel_offset field. I don’t understand why the existing kernel_excess field can not help in your case and you have to add a new kernel_offset field into TxLogEntry. Could you please give more detail on the reason? Thanks.

pub struct TxLogEntry {
	pub parent_key_id: Identifier,
	pub id: u32,
	pub tx_slate_id: Option<Uuid>,
	pub tx_type: TxLogEntryType,
	pub address: Option<String>,
	pub creation_ts: DateTime<Utc>,
	pub confirmation_ts: Option<DateTime<Utc>>,
	pub confirmed: bool,
	pub output_height: u64,
	pub num_inputs: usize,
	pub num_outputs: usize,
	pub amount_credited: u64,
	pub amount_debited: u64,
	pub fee: Option<u64>,
	pub ttl_cutoff_height: Option<u64>,
	pub messages: Option<ParticipantMessages>,
	pub stored_tx: Option<String>,
	pub kernel_excess: Option<pedersen::Commitment>,
	pub kernel_offset: Option<pedersen::Commitment>,
	pub kernel_lookup_min_height: Option<u64>,
	pub payment_proof: Option<StoredProofInfo>,
	pub input_commits: Vec<pedersen::Commitment>,
	pub output_commits: Vec<pedersen::Commitment>,

My thought was that kernel_excess was built using input, output, fee and kernel offset. Based on my experiment, a tx can be fabricated containing the same kernel offset. This is why I added kernel offset, with the assumption that no two valid txs should have the same kernel offset. Perhaps we can use kernel excess if we want to do this check?

I didn’t know that, indeed a repopulation of inputs and changes works then.

Taking a tx with same kernel offset as a fabricated one could be a false killing. On the MW protocol level it’s allowed to have same kernel offset, even it’s quite impossible in the current wallet implementation.

And I see the input_commits and output_commits also has been added into TxLogEntry, what’s the usage for them in wallet?

btw, I’m not against for these design :slight_smile: just for better understanding and avoid the unnecessary complexity/heavy.

Perhaps we need look more into the Replay Attack itself, to analysis the core characteristic of Replay Attack.

input_commits & output_commits are used to control transaction state and consistency. We are checking if both TxKernel and commits are exist on the blockchain or not. It is done in the scan. Those changes was triggered by response to reorg attacks.

Grin has a transaction index at the output, but it is not enough because the same output can be input and output of many different transactions (some can be unconfirmed and cancelled). In case of reorg that become more complicated, so the Tx and output relationship needs to be reversible. Also users wants to know the inputs and outputs for the transaction as well. And users expecting that old, cancelled, unconfirmed transaction still had information abotu what inputs and outputs they had.

Because of that every transaction has a copy of the outputs that owned by this wallet.