To protect off-chain messages from being reused across different chain, a mechanism need to be given to smart contract to only accept messages for that chain. Since a chain can change its chainID, the mechanism should consider old chainID valid.
Abstract
This EIP adds an opcode that returns whether the specific number passed in has been a valid chainID (EIP-155 unique identifier) in the history of the chain (including the current chainID).
Motivation
EIP-155 proposes to use the chain ID to prevent replay attacks between different chains. It would be a great benefit to have the same possibility inside smart contracts when handling signatures, especially for Layer 2 signature schemes using EIP-712.
EIP-1344 is attempting to solve this by giving smart contract access to the tip of the chainID history. This is insufficient as such value is changing. Hence why EIP-1344 describes a contract based solution to work around the problem. It would be better to solve it in a simpler, cheaper and safer manner, removing the potential risk of misuse present in EIP-1344.
Specification
Adds a new opcode VALID_CHAINID at 0x46, which uses 1 stack argument : a 32 bytes value that represent the chainID to test. It will push 0x1 onto the stack if the uint256 value is part of the history (since genesis) of chainIDs of that chain, 0x0 otherwise.
The operation costs G_blockhash to execute.
The cost of the operation might need to be adjusted later as the number of chainID in the history of the chain grows.
Note though that the alternative to keep track of old chainID is to implement a smart contract based caching solution as EIP-1344 proposes comes with an overall higher gas cost. As such the gas cost is simply a necessary cost for the feature.
Rationale
The only approach available today is to specify the chain ID at compile time. Using this approach will result in problems after a contentious hardfork as the contract can’t accept message signed with a new chainID.
The approach proposed by EIP-1344 is to give access to the latest chainID. This is in itself not sufficient and pose the opposite of the problem mentioned above since as soon as a hardfork that change the chainID happens, every L2 messages signed as per EIP-712 (with the previous chainID) will fails to be accepted by the contracts after the fork.
That’s why in the rationale of EIP-1344 it is mentioned that users need to implement/use a mechanism to verify the validity of past chainID via a trustless cache implemented via smart contract.
While this works (except for a temporary gap where the immediately previous chainID is not considered valid), this is actually a required procedure for all contracts that want to accept L2 messages since without it, messages signed before an hardfork that updated the chainID would be rejected. In other words, EIP-1344 expose such risk and it is easy for contract to not consider it by simply checking chainID == CHAIN_ID() without considering past chainIDs.
Indeed letting contracts access the latest chainID for L2 message verification is dangerous. The latest chainID is only the tip of the chainID history. As a changing value, the latest chainID is thus not appropriate to ensure the validity of L2 messages.
Users signing off-chain messages expect their messages to be valid from the time of signing and do not expect these message to be affected by a future hardfork. If the contract use the latest chainID as is for verification, the messages would be invalid as soon as a hardfork that update the chainID happens. For some applications, this will require users to resubmit a new message (think meta transaction), causing them potential loss (or some inconvenience during the hardfork transition), but for some other applications (think state channel) the whole off-chain state become inaccessible, resulting in potentially disastrous situations.
In other words, we should consider all off-chain messages (with valid chainID) as part of the chain’s offchain state. The opcode proposed here, offer smart contracts a simple and safe method to ensure that the offchain state stay valid across fork.
As for replay protection, the idea of considering all of the off-chain messages signed with valid chainID as part of the chain’s offchain-state means that all of these off-chain messages can be reused on the different forks which share a common chainID history (up to where they differ). This is actually an important feature since as mentioned, users expect their signed messages to be valid from the time of signing. From that time onwards these messages should be considered as part of the chain’s offchain state. A hardfork should not thus render them invalid. This is similar to how the previous on-chain state is shared between 2 hardforks.
The wallets will make sure that at any time, a signing message request use the latest chainID of the chain being used. This prevent replay attack onto chain that have different chainID histories (they would not have the same latest chainID).
Now it is argued in the EIP1344 discussion that when a contentious hardfork happen and one side of the fork decide to not update its chainID, that side of the chain would be vulnerable to replays since users will keep signing with a chainID that is also valid in the chain that forked. An issue also present in EIP-1344.
This is simply a natural consequence of using chainID as the only anti-replay information for L2 messages. But this can indeed be an issue if the hardfork is created by a small minority. In that case if the majority ignore the fork and do not update its chainID, then all new message from the majority chain (until they update their chainID) can be replayed on the minority-led hardfork since the majority’s current chainID is also part of the minority-led fork’s chainID history.
To fix this, every message could specify the block number representing the time it was signed. The contract could then verify that chainID specified as part of that message was valid at that particular block.
While EIP-1344 can’t do that accurately as the caching system might leave a gap, this proposal can solve it if it is modified to return the blockNumber at which a chainID become invalid. Unfortunately, this would be easy for contracts to not perform that check. And since it suffice of only one important applications to not follow this procedure to put the minority-led fork at a disadvantage, this would fail to achieve the desired goal of protecting the minority-led fork from replay.
Since a minority-led fork ignored by the majority means that the majority will not keep track of the messages to be submitted (state channel, …), if such fork get traction later, this would be at the expense of majority users who were not aware of it. As such this proposal assume that minority-led fork will not get traction later and thus do not require to be protected.
Test Cases
TBD
Implementation
TBD
Backwards Compatibility
This EIP is fully backwards compatible with all chains which implement EIP-155 chain ID domain separator for transaction signing. Existing contract are not affected.
Similarly to EIP-1344, it might be beneficial to update EIP-712 (still in Draft) to deal with chainID separately from the domain separator. Indeed since chainID is expected to change, if the domain separator include chainID, it would have to be dynamically computed. A caching mechanism could be used by smart contract instead though.
Ronan Sandford (@wighawag), "EIP-1959: New Opcode to check if a chainID is part of the history of chainIDs [DRAFT]," Ethereum Improvement Proposals, no. 1959, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1959.