Minor replay attack question

Hi

Quick one - If I understand correctly, it’s up to each participant to remember in perpetuity the list of state channel nonces they have opened channels with (or just the last one if they guarantee that each new nonce is strictly greater than the last (or some equivalent scheme)).

If they forget, there’s nothing to stop them opening another channel with the same nonce (and identical details so that the channelId is the same) - and so a malicious counterparty (who may have duped the first participant into using this) can then launch challenges using the data from the old channel.

I notice that you have added in a field chainId into the calculation of channelId - which I think is supposed to prevent attacks from testnets to mainnet etc. But I can’t see anywhere where the meaning of this is enforced - it seems to be basically a second nonce. If I’ve missed something somewhere and it is doing this, then sorry.

However, would you consider additionally adding in the deployed address of the contract that’s doing the signature verification itself (in this case, the address of contract ForceMove).

This would additionally prevent replay attacks against different deployments of the force move contract. I don’t know if you intend for it to be deployed in a singleton manner, but you’d still want to retain the ability for it to be upgradable?

I think this can be achieved by simply replacing this:

    function _getChannelId(FixedPart memory fixedPart) internal pure returns (bytes32 channelId) {
        channelId = keccak256(
            abi.encode(fixedPart.chainId, fixedPart.participants, fixedPart.channelNonce)
        );
    }

with this:

    function _getChannelId(FixedPart memory fixedPart) internal pure returns (bytes32 channelId) {
        channelId = keccak256(
            abi.encode(fixedPart.chainId, fixedPart.participants, fixedPart.channelNonce, address(this))
        );
    }

(and obviously hooking it up in the off-chain code)

any thoughts?

Quick note: for this proposal I think it would need to be added to the FixedPart.

yeah I had presumed that would be the case that at first, but because a) of the way channelId is constructed, and that b) it’s really just a fixed magic number added to the data that’s hashed to generate it, I’m not sure that it actually is. Very happy to be wrong though.

Another way to ensure that a nonce is only used once, without keeping track of them, is to use a shared randomness as nonce. This is probably the way we’re gonna implement it in our state channel client. So during the phase where channel participants agree on the parameters of the channel, they perform a commit-reveal scheme to generate a shared random nonce: every participant shares with everyone else a commitment to their nonce-share, then everyone opens their commitment and finally all participants can compute the nonce as the XOR of all nonce-shares.

I think that adding the contract’s address to the channel id digest unnecessarily complicates the protocol and also adds a dependency on an implementation detail (=changing contract address) to the construction of the channel id that shouldn’t be there.

Yeah, I guess that would probably work too! I’ve used the address everywhere in my code, mainly for safety - it’s (probably) sufficient, but (probably) not necessary. I also store used nonces on chain for the same reason. It is possibly an interesting discussion whether or not you need to consider any published code on-chain when you decide to sign something that is supposed to interact with it. Regardless, I think Magmo’s approach has been to push these concerns into the funding layer, which is probably fine!

As an aside, have you done/seen any analysis comparing the randomness of:

uint256 a, b;
uint256 rand1 = xor(a, b);
uint256 rand2 = keccak256(abi.encodePacked(a, b));

?

I suspect they’re probably pretty much the same, given suitably random a and b, but I’ve not really tested this hypothesis…

@sebsta what is the value in using a random channelNonce? It doesn’t guarantee uniqueness, and as far as I can tell adding randomness doesn’t add any extra features or give you any benefits?

@zakalwe Another technique you could use if you really don’t want to have to store any information about your channels is have each participant use a timestamp on a per-second level as their channelNonce. This way you don’t need to remember which past channelNonces you have used, you can simply ensure you don’t open more than one channel every second.

Even better than that is you can simply use a new private key for every channel which is what we did in Counterfactual and I would argue is the best option here of all.

Again, though, losing your channelNonce is equivalent to losing your state channel state, which is equivalent to losing your private key essentially. So, I think it is reasonable to expect the user (or their software) to keep track of this information.

you can simply use a new private key for every channel

Isn’t that basically the same as using a random nonce? How do you come up with the key?

Here are two differences between using a random nonce and a new private key:

  1. Using a new private key ensures a unique channel id, assuming there is no hash collision on the computation of the channel id.
  2. Using a new private key means if one of your channel’s private key is compromised, funds in other channels are still safe.

In practice, if you are using a random uint256 nonce, then a hash collision is as likely as a nonce collision, if we make some assumptions on the randomness of keccak256.

Is it safe to change the contract address while the channel is ongoing?

It wouldn’t be the same as a random nonce, it would be the same as an incrementing nonce.

In Counterfactual we derived each new private key as the k-th derived private key of an extended private key based on the BIP-32 standard.

It guarantees uniqueness up to negligible probability. As long as one channel participant chooses their nonce share randomly, the XOR of all shares is random. So it is practically impossible to generate the same nonce twice. And it has the added benefit that if you need a random seed in your channel, you can use the nonce.

But the channel participants need to keep track of their state, anyway, so why not guarantee with 100% certainty a unique nonce by just picking one that is +1 greater than the largest one you ever used?

That guarantee doesn’t seem to be any stronger than the guarantee provided by choosing a random nonce. In both cases, there is an equal chance of a hash collision when computing the channel id.

I think this conversation is going round in circles!

  • hash collisions are unlikely
  • you do need to keep track of your state when you’re in a channel

I’ve generally approached things from an extremely pessimistic/paranoid point of view.

  • sure, collisions are unlikely but what if my client RNG is broken (eg Parity wallets compromised by using the default seed)?
  • what if I lose my client state while I’m not in a channel? Does that mean I can never open another one?
  • what if I want to open a new channel on a device that doesn’t have access to my old state?

and, the one that really bugs me:

  • what if some combination of these things can mean that signatures on one state channel can be replayed on another?

My losses are potentially limitless. It may be that the code that funds/unfunds channels is strong enough that these aren’t problems, but any assumptions in core code about the things that use it should be explicit and clear, front and centre.

I’m paranoid, so:
a) I always include the Ethereum address of the thing that’s verifying signatures inside the data that it’s verifying (and, at the moment, I can’t see me changing my mind on that), and
b) In each deployed contract, I store the fact that the channel ID (or nonce, or address or whatever), has been used in Storage, and explicitly prevent it being reused.

the first one is an engineering problem. The second costs gas for safety. I’m fine with both of these. YMMV!

Finally,

eek!!

For (a) we have that property, since participants is in the ChannelState, so we agree on that.

Otherwise, I still think we’re just operating on different assumptions about the data availability of the user’s state channel state. We (@AndrewStewart and I), assume the state channel state is stored in exactly the same way as any private keys associated with it. The idea being that if you lose your state channel state, then you have basically lost your private keys.

b) In each deployed contract, I store the fact that the channel ID (or nonce, or address or whatever), has been used in Storage, and explicitly prevent it being reused.

This won’t work for virtual channels, which in the happy case are never witnessed on-chain. So, while this limits your exposure to channel-replay attacks, it doesn’t eliminate it, if we are using the ForceMove and Nitro protocols.

When all is said and done, if you decide to generate a uniformly random nonce, then replay attacks can happen with probability 2^(-256), as long as keccack256 is uniformly distributed itself*. This is optimal in the ForceMove/Nitro protocol setting, and I can’t see a way around it in any virtual channel setting.

* Is this known to be true?