EIP-1344: Add chain id opcode

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)

I think we largely agree, but it’s important to establish a baseline here. Both chains caught up in a fork don’t have to upgrade their value of chainId (which is one of @fulldecent’s original points). If and when an upgrade occurs, one side could choose to upgrade while the other does not, or both could choose to upgrade. The later I would consider extremely unlikely, unless we propose (in a different EIP) a process for hard fork network upgrades of chainId (which I think is a good idea, but the details will be difficult to get right). We all agree that this process is out of scope here. I think we all agree that in this proposal, we simply need to design an adequate mitigation for these scenarios to improve handling of these situations.

So, to put it simply, we need to design for the following situations:

  1. chainId changes on one side of the fork, but not the other
  2. chainId changes on both sides of the fork

The scenario where neither changes is pretty much unresolvable (no replay protection), but I would contend that social friction will enforce either 1. or 2. happening eventually (after a fork occurs).

I hope we agree on this.

So, our two proposals are largely aligned, but make a different trade-off which I’ll demonstrate. I of course prefer my side of the trade off, as it is easier and more secure to ensure the scenarios I described previous are adequately enough.


So, in your scenario, there is an opcode where given a chainId it returns True if that value is in the history, else it returns False. These values have no context of timing. In the scenario where both forks update their value of chainId post-fork, you are correct that there is no necessary mitigation, however in the scenario where one side of the fork does not update this presents a problem: a message signed on the fork where chainId did not upgrade after the fork occurred can be replaced on the chain where chainId did upgrade. In order to resolve this, you need to involve time into this scenario to delineate the two.

You are correct that a time oracle is probably not useful for resolving this, but user-side handling has to be involved to ensure replay protection in this scenario, so you are incorrect on that point. Additionally, you have to have user-side handling anyways because how else will you get the value of chainId in the first place! (It would have to come from the signed message itself).

Lastly, this increases complexity on the client implementation because each contentious fork would have to maintain yet another value in the lookup table (so it knows to return True or False). If we upgrade the value on every fork we do, then that’s even more on the client side!


Conversely, in my proposal we do not need user-side handling to obtain the value of chainId because the opcode provides the current value. We would need user-side handling for determining when an update occurred (which could be done trustlessly, and cached) and it’s up to the user (as it is in both our suggested mitigations) to get this right.

What would be best is to retain this opcode (that returns current chainId) and propose a new suggestion that takes a value of chainId and returns the timestamp of when the fork occurred where that value was introduces. This resolves all discussions of user-side handling of time, and makes it much easier for developers to handle these upgrades, especially in the proposal where we move to every fork changing this value.

The scenario where neither changes is pretty much unresolvable (no replay protection), but I would contend that social friction will enforce either 1. or 2. happening eventually (after a fork occurs).

It is exactly for the same reason I believe we should only cater for case 2) since case 1) can be solved similarly to what you describe. This is why the opcode I propose is sufficient without requiring any “user-handling”

Lastly, this increases complexity on the client implementation because each contentious fork would have to maintain yet another value in the lookup table (so it knows to return True or False). If we upgrade the value on every fork we do, then that’s even more on the client side!

This is true. I never argued for the contrary but the idea has its purpose and should not be a problem.

In any case as you mentioned you would need a similar strategy for your version of the opcode since in order to accept message signed with old chainIds, you would need a global caching smart contract for case like counterfactual state channel situation where the contract do not exist yet for example.

Conversely, in my proposal we do not need user-side handling to obtain the value of chainId because the opcode provides the current value.

I do not consider that user-handling. This is just passing an extra parameter for message signature verification. But if that is what you are concern about, I ll let you call that user-handling.

But again with your opcode, this chainId will also need to be supplied anyway if you want to support old message, which was the whole point of my proposal.

What would be best is to retain this opcode (that returns current chainId) and propose a new suggestion that takes a value of chainId and returns the timestamp of when the fork occurred where that value was introduces. This resolves all discussions of user-side handling of time, and makes it much easier for developers to handle these upgrades, especially in the proposal where we move to every fork changing this value.

You seem to overcomplicate things here. Assuming all fork have an updated chainId (which yourself describe as likely for the case when none of the fork change Ids) none of this is necessary

To resume how I see the situation is that since forks can always update chainInds, we should consider the case where both fork update their latest chainId to different values (an outcome that is beneficial for both side of the fork)

In that scenario, the opcode I propose is safe and sufficient.

Now if you think we need to tackle situation where a community refuse to update its chainId at the expense of its users, then I think such community can decide to come up with a more complicated scheme to solve that. I am personally think this is unnecessary.

Anybody else interested to comment ?

So the issue here, and this is why it needs to handle both scenarios, is there is currently no plan for it to work like this, so we can’t rely on “the community” adopting your suggestion without another separate proposal.

The difference is that my proposal would be more flexible (you don’t have to do it unless you want to) whereas yours you would have to design in some mechanism. Counterfactual instantiation is an interesting counterpoint, but I could imagine some scheme that would only care about the current value provided by some final message signed at closing time, in which case my proposal would work (without modifications!)

Another point is that you would have to pass in the chainId in your proposal separately to generate the domain separator. I think this may be too far down in the realm of implementation details to discuss further, it’s simply an engineering tradeoff. I think we’re largely at an impasse with two equally viable methods to implement this functionality.


Here is what I suggest:

  1. We should put this EIP into Last Call as currently written as it’s well-specified and can be used as a technical standard as is.
  2. You should create a separate EIP from your proposal as I believe it has value, and I also believe both could play well together!
  3. We probably need a third proposal to specify the process of setting chainId that is completely separate from this discussion. It is likely to be political in nature (as the length of this conversation surely attests to!), so siloing the political discussion away from the conversation for this opcode (and your proposal) would help bring this to a close. There is a significant opportunity to improve resilience of any signing scheme that uses chainId, but that is out of scope for this proposal (and yours as well)

I hope everyone finds this to be reasonable, as I believe further discussions along this point may prevent this conversation from ever being brought to a close, which means nothing gets done and that’s worse for everyone. The conversation about whether this proposal (or another) is included in Istanbul is also a separate conversation I believe needs to take place on an alternative channel.

1 Like

The EIP has been moved to last call but the test cases are still TBD. We really should fix that before accepting the EIP. :slight_smile:

While a process would ensure the issue never happen (assuming the process is followed), in practise there is no need for any established process. The fact that a chain can always update its chainId is sufficient. As I said if the community of that side of the fork do not want to simply update their chainId, it is up to them to come up with a better method.

you don’t have to do it unless you want to

This is not true. Every L2 message that want to ensure its message can’t be replayed on other chain that exist at the point of signing the message would need to use chainId as part of the message. By doing so, the verifying contract would need a way to check that this chainId is or was valid in the past. Else such message would not work on a fork that change its chainId.
Imagine if many important contracts naively use the opcode as proposed and a contentious fork happen. How do we resolve the situation ? We either have all L2 messages replayable on both fork which defeat the purpose of what we tried to achieve here. Or we have one fork that accept defeat and break the validity of previously signed message for these application at the expense of its users. This could be disastrous.

So if the opcode as described is chosen, we will need to put in place a contract that can be trustlessly updated to cache the past chainId. And every contract that want to verify the chainId would need to look it up. At which point, it is logical to conclude that the opcode should deal with that itself.

By using the opcode I propose we ensure at the lowest level possible that L2 messages validity will be preserved across forks, while allowing the chainId to be updated to protect from replayability.

Another point is that you would have to pass in the chainId in your proposal separately to generate the domain separator.

EIP712 is not final and should probably be updated to allow for chainId to be added separately from the domainSeparator. Let’s not constrain the protocol at this stage.

  1. We should put this EIP into Last Call as currently written as it’s well-specified and can be used as a technical standard as is.

I don’t think it is wise to do so before we get more feedback on our discussion from other parties. The idea is not to add as many opcodes as possible to cover all possible wishes just because they are well specified. Let’s reach consensus on the best method for L2 messages non-replayability and validity preservation.

Also as mentioned above, I believe the EIP purpose is not fulfilled in the best way with the current opcode. Until we get consensus on how to resolve this by the community, the EIP should go back to draft status.

My reasoning in a nutshell :

  1. chains can always upgrade to update their chainId
  2. This ensure non-replayability
  3. chains will have their chainId change over time
  4. L2 messages signed in the past need to be valid in the future (in respect to chainId)
  5. simply providing the latest chainId (as proposed) can’t offer this guarantee without an extra caching contract
  6. adding such caching contract emulate the opcode I propose
    => let’s use that opcode instead.

Note: I really don’t think it is necessary but for extra flexibility we could return the blocknumber at which the chainId was introduced instead of a boolean. This would allow to get the latest chainId via a trustlessly updatable contract.

So test cases and/or implementation is not a strict requirement for an EIP, but it helps aid the discussion and reduces work on client implementors later on down the line.

Things get out of date so fast in feature branches, so it’s not advisable to make it a strict requirement. I was planning on implementing the test cases when I got a chance.

So, as we discussed above, this isn’t really a guarantee we can rely on, but we are largely talking past each other at this point. Can we agree that handling both scenarios (one side updates and both sides update) is important?

This contract could be the user application itself, the caching is very easy to implement. It allows the developer the flexibility to implement their own scheme for reacting to upgraded chainId, which may be application-specific. It may not even matter at all! These type of signed messages are still largely being explored, we only know that clients will need chainId. A good example is meta-transactions. It would probably not be great to allow replays of old chainId signed messages since that’s not how it’s intended to be used. Flexibility is key.

Neither one of our proposals fulfills all requirements here. I think both will work best together. For maximum flexibility, having both allows application developers to build what they see as necessary. If they get it wrong, that’s not our fault, we just need to give them the tools to do what they need to do. If your proposal doesn’t have a way to determine what the current value is, there are corner cases that could be unsafe. If this proposal doesn’t have a way to query past values of chainId, it won’t be able to accommodate older signed messages, which is also bad. Both play well together!

An EIP being Accepted doesn’t mean it will be implemented. It only means it’s well-specified and technically sound. If we spend more time arguing past each other, nothing gets done and we’ve largely wasted our breathe.

You have a proposal that has merit, you should create a separate proposal that we can move this discussion to. EIPs aren’t sacred cows, what matters is when they get implemented. Maybe the implementers like your proposal better. Maybe they like this one better. Maybe they like both in parallel, as it provides the most succinct flexibility. Let’s not spend more time modifying this proposal, second-guessing the client implementers and the rest of the community.

I think this would be required to ensure the most flexibility and security in the design, as it creates a strict ordering that can be relied on, instead of an external oracle or extra timestamp in the message. This would protect against some corner cases where a message was incorrectly signed after an update occurred, or a replay from a chain that did not choose to update their value.

2 Likes

An EIP being Accepted doesn’t mean it will be implemented. It only means it’s well-specified and technically sound. If we spend more time arguing past each other, nothing gets done and we’ve largely wasted our breathe.
You have a proposal that has merit, you should create a separate proposal that we can move this discussion to. EIPs aren’t sacred cows, what matters is when they get implemented. Maybe the implementers like your proposal better. Maybe they like this one better. Maybe they like both in parallel, as it provides the most succinct flexibility. Let’s not spend more time modifying this proposal, second-guessing the client implementers and the rest of the community.

You are right, sorry for having in some way polluted the discussion here. I was just trying to propose something that looked obvious to me and felt obliged to reply to what seemed to me incorrect statements. This has resulted in quite a back and forth. It was not totally fruitless though so thank you for engaging with me. I ll now start thinking about writing an EIP for the opcode I propose.

But before I can’t stop myself to make few clarifications :slight_smile: :

Can we agree that handling both scenarios (one side updates and both sides update) is important?

I agree it is important in itself but my point is that we do not need to worry about it since the responsibility lies elsewhere and it is in the interest of each side to make sure 2 chain will always have different latest chainId. We can thus assume it is always the case.

A good example is meta-transactions. It would probably not be great to allow replays of old chainId signed messages since that’s not how it’s intended to be used.

I would actually argue of the contrary, meta transaction user would obviously expect their transaction to be valid at the point of signing them. They would be disappointed to learn that because a fork happen in between, their tx got rejected .
I actually have yet to see a case where it is useful for a contract to get access to the latest chainId (knowing that it is a changing value) except to verify that it is indeed a valid chainId.

The test cases listed in the EIP shouldn’t be for a specific implementation. For an EIP like this I’d expect they’d just identify the key situations to test (e.g. chain ID 1, chain ID at max value for a uint256, what happens if the chain ID is bigger than a uint256).

Lol, me too.

I don’t think this is obviously true, but we are also speculating on what would happen since we haven’t had a contentious split since chainId was introduced. Agree to disagree?

So, meta transactions are interesting because they’re meant to exist short term (< 1 week). As per your point above, a switch to chainId would have to be known at least a few weeks ahead of time, so largely this wouldn’t conflict and could be handled very easily without the additional context of supporting older signed messages. It’s also largely speculation what would happen. Agree to disagree?

Yes, in the Ethereum/tests repository. Again, a test case in this repository is not a requirement for accepting an EIP, only for implementing it in the client (as should go without saying).

So I have 5 years to convince everyone that chain IDs should change at each consensus upgrade. This timeline is fair enough in the scheme of enterprise deployments. Challenge accepted.

@wignawag … [a function that returns true if chain ID is in the history of this chain]

Yes, this can be implemented as a trustless contract if the CHAINID opcode is available.


Overall, I had been looking at this proposal as a distraction or side step from the serious change management problems that exist. Looking anew, I see that this proposal plus some trustless protocols (like above and more) allows forwards compatibility with a future where every upgrade changes the CHAINID.

Now I support this proposal. For last call and also for deployment.

We should encourage application developers to consider the case that the CHAINID of their network changes. If they simply hardcode a check for CHAINID in their smart contract they might be surprised when their application suddenly wakes up with a new CHAINID and all outstanding counterfactuals and commitments are unenforceable.

Please include @wignawag’s point (could use a function to find if a chain ID is in this chain’s history) in the EIP rationale section. I hope it can be clear from the reading the EIP that most developers will want to access it using the helper tool if they are validating off-chain signatures.

And if gas is an issue (for the SLOAD) then I expect this is how people will actually use the tool:

contract ClaimValidator {
  ...

  function loadClaim(account, bytes32 signature, data claim) {
    ...
  }

  function loadClaimHistorical(bytes32 chainId, account, bytes32 signature, data claim) {
    require(ChainIdHistorian.chainDidExist(chainId));
    ...
  }
}

2 Likes

The specification section sucks.

Adds a new opcode at 0x46, which takes 0 stack arguments. It will return the chain id of the chain where the block was mined. It should cost 2 gas ( G_base ) to execute this opcode.

My request for changes:

  1. Add mnemonic. E.g. “a new opcode CHAINID at 0x46”
  2. Don’t use future tense, nor “should”.
  3. It will return → It pushes to the stack.
  4. “the block was mined”??? Nothing was mined yet. How about “chain id of the current block”? Check wording for opcodes as NUMBER, TIMESTAMP, etc.
  5. If you have decided what is the chain id size, please include this information.

Personally, I believe anything that requires data from outside of EVM should cost at least 10x more than arithmetic. But it’s probably not worth to fight for it here because it matches the cost of other similar opcodes.

This opcode does not requier data from outside the EVM. The chainID is written in the signed transaction, so it’s pretty much like msg.sender

1 Like

Will do. I agree that this is important to capture. It is possible to design something alongside it to capture, but an opcode would be nicer.

This all sounds good. Could you submit a PR?

So, I actually implemented this in Trinity leveraging the transaction context (of which chainId is a member). I think this might be the simplest way to implement, so clearly it is not some sort of oracle to the outside world since the transaction context is a core part of EVM execution (GASPRICE, CALLER, etc. are a part of this context)


Edit: should’ve read this first lol

Wording updates proposed here https://github.com/ethereum/EIPs/pull/1952

Test cases should be added ASAP. This should also specifically address situations that happen during a network ID change. Off-by-one errors will be minimized if the test case is good.

1 Like