EIP-1344: Add chain id opcode

This is an interesting proposal, thank you.

So, I’m not sure about this as a requirement, but let’s explore.

Assume a truly contentious fork occurs, and chainId is eventually resolved such that the fork of lesser community size is socially coerced to adopt a different value (if they refuse, this proposal does not add value). Also assume that a subset of applications that rely on the value of chainId either prefer the minority chain, or wish to continue supporting both chains in the fork.

Therefore, messages signed on the majority chain only require the domain separator for the old chainId, which has the hash 0x1234. The minority chain must accept the old hash 0x1234, or the domain separator includes the new value for chainId, which has the hash 0xABCD.

On the minority chain, both should be accepted, although to be strictly accurate we should ensure that only those messages signed after the fork date use the domain separator hash 0xABCD or else we can replay a user’s signed messages from after the change on the majority fork. So, we need to delegate to an oracle to ensure that we have proper ordering of time for these signed messages, or we risk the above happening (another opcode would be too complex to specify).


I think all in all, this is too improbable a scenario to try and design some explicit mechanism to handle. I think the added friction of this scenario occuring would ensure that the minority chain has little support from the builders of these applications, because users will have to jump through some hoops to get their old signed messages processed in the case of a contentious split like that described above.

The only alternative I can think of where this is not true is the one where the Ethereum Foundation, who has registered the trademark, is the supporter of the minority chain, meaning they forced a split that community does not find to be legitimate. However, the trademark has nothing to do with the value of chainId and I would think the majority fork community would not prefer to modify their value if a hostile fork occurs, and instead try to socially coerce the minority chain (in this scenario, run by the EF and very few other parties) to change theirs because the community does not recognize the minority chain as legitimate.


This is way down in the weeds, I hope you’ll agree. 99% of the use case for this opcode is for replay protection of testnet transactions.

This is not so complex situation that you seem to make it (unless I misunderstood something :slight_smile: )

The idea was that on every fork a new chainId is generated (hence me pointing to @fulldecent proposal) so if part of the chain community disagree with the changes being made as part of a fork, they simply need to create their own fork to keep the old behaviour except for a new chainId.

After that, all off-chain message signed before the fork will still work in both forked chain. A desirable property in my opinion since it allow users to choose which fork they use without having their current use affected.

But note that updated wallet will make sure new message are not signed with old chainId since these message could be replayed.

Sorry, I edited my response to say “improbable” versus “complex”. I believe this to be a fairly unlikely event, but certainly possible.

It is possible for replays to occur after the forking event on the updated chain since both chainIds are valid there. You need a time oracle to prevent this.

Sorry, I edited my response to say “improbable” versus “complex”. I believe this to be a fairly unlikely event, but certainly possible.

I am actually not sure what you were trying to say with your scenario. From what I understood it is solved if both fork get a new chainId.

It is possible for replays to occur after the forking event on the updated chain since both chainId s are valid there. You need a time oracle to prevent this.

I don’t think we need time oracle. We simply assume that every message that was signed with an old chainId are considered valid for both chain. In other words such message should be replayable on all chain that have that chainId in its chainId history. That is how we define it. This is similar how old transactions are included in both chain except that for offchain messages, it is the responsibilities of the wallet to use the latest chainId

And wallet supporting the “chain that forked in the first place” will be aware in advance of the fork and could deal with the update itself.

Similarly wallet supporting the “chain who forked simply to get a new chainId”, can do the same.

Regarding gas. The gas savings of implementing CHAINID opcode is zero. Currently, chain ID be gotten for ~800 gas (G_sload) from a naive oracle implementation, or it can be hardcoded in a contract for 3 gas (G_verylow). This new approach would reduce that to 2 gas (G_base). The gas savings calculation for adding an opcode is:

savings = Δgas × number_of_uses

Here, the calculation is:

0 = 798 × 0

Compare to EIP-145. Their equation is approximately:

a very lot = some × a lot

In summary, from the perspective of gas, CHAINID opcode is a premature optimization. The argument in favor of CHAINID opcode on the basis of gas savings would be much stronger if people were added to this discussion who are currently using the ~800 gas oracle approach or the 3 gas hardcoded approach and see other merits of this addition.

Regarding safety. It is not our job to babysit developers. By explaining the potential problems with CHAINID opcode I illustrated that the use cases are not yet well known enough to say if it provides any value whatsoever.

Regarding trusted third party. It is simple to create an oracle that works on all existing networks that returns the correct chain ID for ~800 gas. After that is done, no further trust is required. If additional networks are created they are welcome to also implement the oracle. This is implementable TODAY. We can open a separate thread if anybody is interested in this. If nobody is interested in this then clearly CHAINID opcode is not needed.

Regarding “why not?” There are many things which can be implemented. For example, an opcode which concatenates strings or rotates the top three stack items would be very useful. The threshold for adding new features should not be “this proposal doesn’t make it worse than what it already is”. The threshold should be “this feature is badly needed, workarounds are already in widespread use”.

Regarding what if the oracle is set up incorrectly. Then it would work incorrectly.

Regarding an alternative to find chain ID histories.

Thank you for the proposal. I believe the best solution is to have two new opcodes: GENESIS returns the hash of the genesis block and CONSENSUSCLIENT returns the hash of the consensus client used to validate the last block.

This approach with a simple trustless tool allows finding the lineage of any block and maintains usefulness on both sides of a contentious or non-contentious fork.

Regarding building for the future.

^ Yes, we should build standards based on the future and a good understanding of the use cases.

Regarding Ethereum Foundation.

I disagree, in the event of a contentious fork, both networks would call themselves “Ethereum”. If the Ethereum Foundation supported version is the less used fork then they may assert trademark rights against people using the other network. It is undocumented whether they would do this. A request to document this is here Branding guidelines -- clarify legal rights around trademark · Issue #841 · ethereum/ethereum-org · GitHub This is a fundamental issue preventing enterprises from using Ethereum mainnet.

Does the trademark include chainId? No. Can you trademark a number? I’m pretty sure the answer is no, but lawyers are creative.

I don’t think this line of argument holds any value to this discussion.

There are currently 3 production Plasma implementations, with more coming. There are 2 production state/payment channel implementations, with more coming. All of these implementations are exploring upgrades to incorporate EIP-712 when client libraries add support, so while your calculation is currently correct, it will not be for very long. The only safe way to implement this in contract code is through a state variable, which reduces the cost estimate to 198 from your oracle solution (which I do not believe to be safe). Since L2 will see increasing prominence as more solutions come online, I believe this equation is therefore:

198 x a very lot = a very, very lot

So, I contend you are drastically underestimating the potential impact, but that’s fine we can use a state variable to store this value in the mean time while we consider the potential defects in this approach.

I do not believe an oracle to be a sound solution here.

I am not sure, but it feels that the original purpose of the EIP is left out in a lot of these discussion.

We have EIP-155 to protect regular transactions agains replay attacks. And the (simplified) purpose of this EIP is to enable something similar for signatures.

It would probably also be possible to change “ecrecover” to somehow handle this, but that would be way more complex.

Concerning the validity of a signature after a hardfork: my expectation would be that my signature is only valid on 1 of the chains.

Concerning the change of a chainID after a hardfork: while it is true that there is no process, the case of ETC shows that a community will choose a new chain id to separate themselves from the other chains. Why did ETC choose a different chain id? I am pretty sure, not just because the ETH community said so.

For me the implementation via an opcode is the simplest way, but having it as a precompile would also be a big improvement (over having nothing). Any oracle deployed by a third party needs somebody who updates it, therefore I don’t think this is a viable solution.

I think this is your principle argument. There are lots of examples of how this will be used in practice, and shortly, because EIP-712 supports these use cases being safe. If you have an issue with using chainId as a domain separator, I think you should have brought it up during the standardization process of EIP-712.

This proposal is very simple. It does not have to be used. It enables clear support for an existing use case that is seeing more support recently, it reduces human error, and it saves a small but not insignificant amount of gas. It aligns directly with the protocol, instead of leveraging trusted third party solutions like oracles.

Yes, in some ways I am trying to build in support for use cases that are not widely demonstrated yet, but many see the utility in having this available, and I think it would be better to have this available than to design for potential scenarios that have only occurred 1 time in the past 5 years of the project, and also have a few potential mitigations. How we set chainId could be changed in a future discussion, leveraging some hash-based mechanism for modifying it, but that is out of scope of this proposal, which aligns L2 use cases with the protocol domain separator that clients will directly use to sign L2 messages.

1 Like

I think actually there may be a good strategy to this that we can incorporate as a mitigation if those building with this opcode choose to protect against. They would cache the value of chainId in contract storage and compare every transaction against it (200 gas), and update the value if necessary, also logging the time.

This would protect against an upgrade of the value this opcode provides, without relying on a oracle or pre-compile contract. Newer contracts wouldn’t cache the older values if they’re deployed post-fork, protecting later deployments from replay attacks against earlier fork values that your proposal may expose them to.

I do think your concern is legitimate in that we should ensure a trustless and seamless upgrade for this value in the case of contentious hard fork, but I don’t believe an oracle provides this, and your proposal leaves a bit of hole after the value upgrade occurs in that presently signed transactions can be replaced after the fork (without a more complicated, time-based mechanism to ensure transaction ordering prior to the fork)

I just want to chime in and say that:

  • we use EIP-712 to sign meta transactions from our mobile SDK
  • we use side chains (potentially many) and want to prevent replay attacks

It seems to me that the original proposal would be very beneficial. At the same time I don’t see any downside.

I also don’t see a viable alternative proposed so far.

Maybe @benjaminbollen or @pro want to add something as they are more involved with the meta transactions than I am.

3 Likes

your proposal leaves a bit of hole after the value upgrade occurs in that presently signed transactions can be replaced after the fork

What do you mean by “replaced” ?
If you mean replayed, that is for me completely fine. I consider a message signed in the past belonging to the pre-fork era. It should thus be valid in all future fork. Like a transaction included in a chain is present in all future fork. This is actually a very important feature. Imagine you play a state channel based game and you hold your opponent losing move’s message. If this message get invalid because of a chainId update, you would lose your opportunity to grab the price. This is not what we want. We actually want such message to be redeemable in all forks. Think of these set of messages as part of the state of pre-fork era.

I might not understand a crucial component of your concerns though. Any way to give a concrete example?

As for newly created message, as mentioned this will be the responsibility of wallet to ensure such messages use latest chainId (to avoid replayability of the message in other forks)

1 Like

Ah yes, I think I see the disconnect.

So, imagine a split has already occurred, and the minority chain accepts both the old and new chainId. If time is not taken into account (and specifically the time of the fork/chainId upgrade) then it would be possible to replay messages signed for the majority chain after the fork date on the minority chain. You would need some mechanism for ensuring the time the message was signed was prior to the fork date, whether through timestamps or an ordering mechanism (such as Plasma transactions referencing the prior block)

This assumes the other chain (non-minority chain) did not fork with a different chainID.

The solution as mentioned above is for that chain to fork with a new chainId. So both have different latest chainId preventing replays.

It is not like contentious fork happen without any notice.

In any case the non-forked chain would benefit in forking since without it all its newly signed message would be replayable on the forked chain.

As such this situation should not be a concern as chain can always fork to a new chainId to avoid the problem.

It would be better in that scenario, but I would claim this is unlikely to occur unless some mechanism is enforced such as what @fulldecent is describing with hash-based ID. But what the number is set to is strictly outside this proposal.

To me that’s the most likely scenario. Fork that can be contentious do not happen by surprise.

And yes the idea as I mentioned earlier was to use @fulldecent proposal but it work without it being hash based. The only thing necessary is to update the chainId on every upgrades so that it does not conflict with a previous or existing chainId.

Actually we can make it a process so that a new chainId is attributed to each side of the fork at every proposed changes.

At every upgrade, client software implement 2 paths (activated on command line for example), one with the changes proposed and one without any changes (except for the chainId) and each with a different updated chainId. Then if the fork is contentious part of the community will run the chain with no change (except for the new chainId), the other part will run the chain with the changes (with yet another chainId)

The opcode I propose guarantee that in such situation, contracts can continue using it without affecting their user’s previous messages

I definitely consider any such discussion of how to manage chainId out of scope for this proposal. I think that the ability to ensure safety when a change occurs is an important point, and we have a few proposals above. It really doesn’t matter what the number is that is chosen, it either is or is not what the previous value was.


Options:

  1. On-chain oracle:
  • Pros: no modifications required
  • Cons: requires building a strong oracle, increases complexity and risk of central failure
  1. History check opcode:
  • Pros: simpler, cheaper, low risk of failure
  • Cons: requires a time oracle or user-side handling to ensure safety
  1. Chain ID opcode:
  • Pros: simplest, cheapest, low risk of failure
  • Cons: requires user-side handling to ensure old chainId is accepted

This is what I see as the pros and cons of the proposals so far. 2 and 3 are very similar, except 2 is fail-safe (all old chainId transactions are valid without user-side validation, no matter when they were signed) and 3 is fail secure (old transactions are not accepted unless user-side caching is implemented).

I really don’t think 1 is viable as it creates a central point of failure that way overcomplicates this proposal, although it can be experimented with today. The best oracle is the protocol, and the protocol provides this value, so it’s something clients can come to consensus on without referencing external data, which in my opinion is the only time an oracle should be used.

On-chain oracle are a complex thing. I’m yet to see any truly decentralized, trustless solution. Not even sure that is possible.

In any case, I don’t want to see projects being built right now depend on something not completely trustless to upgrade their signature policy. That is a huge deal and could cause massive denial of service. Having the chainid as an opcode is both simple and secure, much more than any existing oracle solution.

Also there is an egg/chicken issue underlying here. To build a completely decentralized oracle we most probably need signed contribution to be verified onchain. These signatures want to be protected like any other … and they might need to be layer 2 for scalability purpose … so they most likely will rely on the onchain knowledge of the opcode.

I am currently building an app that uses ERC721. The domain separator is computed by the constructor (chainId in the arguments) and never updated. I don’t want to give anyone the power to change it, as it would have manor impact on the security of the platform. With this kind of opcode, I could put a public function that anyone could call without parameter and that would update the domain separator the use the current chainID.

1 Like

I definitely consider any such discussion of how to manage chainId out of scope for this proposal. I think that the ability to ensure safety when a change occurs is an important point, and we have a few proposals above. It really doesn’t matter what the number is that is chosen, it either is or is not what the previous value was.

I did not meant to have this as part of the proposal per se (as we will never be able guarantee that a chain will not simply reuse existing chainIds for whatever reason). I was simply pointing out that by using this process we make sure that 2 forks will always have different latest chainId which remove re-playability between them. No need for oracles. Plus even if such process is not used, the part of the community that disagree with the changes can always fork with a new chainId themselves.

We agree that 1) is not an option

as for 2)

  • Cons: requires a time oracle or user-side handling to ensure safety

I disagree with that statement. As mentioned we do not need an oracle as chain can always fork on a contentious fork. And as mentioned above, a process could make that automatic.

As for “user-side” handling, there is no extra work to be done by the smart contract when using option 2. It simply use that opcode passing the chainId used for signing the message as input and check the return value (true or false). This does not need any extra handling, contrary to option 3 where you rightly mention that it:

  • Cons: requires user-side handling to ensure old chainId is accepted

This is basically emulating the opcode of option 2)

why not then simply use option 2) ?

Basically, option number 3 I listed above, where you implement a caching function in your code that trustlessly “upgrades” the value if and when it changes? (Also keeping tracking of the time it occuring for application purposes)