EIP-2929: Gas cost increases for state access opcodes

I don’t understand this argument. It would only break contracts that use transfer or send if those contracts themselves are called by other contracts and those other contracts make the call with a fixed gas limit (a very rare pattern nowadays). If you mean a contract calling send to immediately forward received funds inside the pre-allotted 2300 gas, I don’t think that’s possible anyway because value-transferring calls have a gas cost of 9000.

This is the current implementation in Solidity (setting the gas limit to the stipend) as far as I remember (I would need to read up again on the concrete limits used by solidity).

My main point was that the previous analysis done for EIP-1884 are not 1:1 valid for this EIP and I would love if something similar would be done again.

Edit: Just to avoid that I am misunderstanding something, afaik EIP-1884 only had a breaking effect when the receiving contract did additional logic (e.g. a proxy or emitting events) → this would result in an OOG is the receiver contract. This EIP would break in the calling contract (with an OOG). Would that be correct?

Edit 2: Stipend docs from Solidity → Security Considerations — Solidity 0.7.1 documentation

Here I have interesting case for a CALL with non-zero value.

  1. The cold destination D is added to accessed_accounts.
  2. It turns out D does not exist: additional penalty of 25000.
  3. This penalty causes out-of-gas exception.

Now we repeat the above steps. The difference is D is warm now although it still does not exist.

There’s nothing that says something in the accesslist must exist (e.g. 2930 can add whatever that doesn’t exist).
So it shouldn’t be an edgecase (as in, semantically it’s pretty clear-cut, IMO), but it may very well be, so good catch. We should make a test for this, and I’ll also try it out on Yolov3.

Actually, you can simplify this case by not relying on 25000 penalty. The call can simply fail because of the cold account access penalty of 2600. Still the D will be added to the accessed_accounts on the first call.

I don’t understand all consequences of this, but try exploring this mechanism as a way to bypass the cold account access cost. For a series of failing calls to non-existing account D only the first one will consider the D cold. But in practice, a node implementation must do full D lookup unless it maintain a cache of non-existing accounts. Of course, these failing calls must be wrapped with another calls to proceed.

I think this would work “safer” if the 2600 gas is strictly required up front to proceed with any call, independently of the actually initial call cost computed later. Similarly to the required minimal gas amount to proceed with a SSTORE.

Clients maintain a cache of non-existing accounts within a transaction call. Or at least keep in memory all the trie nodes needed to read to that account, so disk access doesn’t happen after the first lookup.

Yep, at least that’s what happens in geth – if it’s trie-backed, the resolved nodes will be in memory after the first time, even though the state-object doesn’t exist in “object” form in the cache.
If reads are done from the flat-db-backed snapshot, the data will similarly be cached in the diskLayer, and avoid hitting disk again.

If you use trie nodes, is there a risk that, because addresses N and N+1 share the same trie node, that if you access address N then address N+1 will appear warm?

Also, what happens if address N gets accessed, the sub-call that did the access gets reverted, and then N gets accessed again? The spec says that you should pay the higher cold-access cost twice.

No, you misunderstood me. So hot and cold are tracked by an explicit list. I mean that even if we don’t explicitly cache the account in object-form in the ‘state’, but are forced to lookup the thing from the trie the second, and third time, it’s still a cheap operation, because the trie is sufficiently preloaded that we don’t actually have to resolve any hashes. We have the trie in memory (or, if the snapshot is used, we have the empty slice of bytes representing the account in memory).

Yes. That’s what happens… I guess the question was based on some misunderstanding on how we use tries?

I’m gonna need a clarification here what the “set of precompiles” is. Is this the set of active precompiles (which thus have special code assigned to it)? I do remember an EIP (which I sadly cannot find again) which defines the addresses as 0x00…00 to 0x00…00ffff (or something similar) as the precompile addresses.

I also don’t understand why a slot is cold when a sub-call which accessed that slot reverts and made that slot warm. It doesn’t make sense to me. We can cache the value of this slot during the transaction and therefore we should not need to charge the cold cost of accessing it twice. It also means we have to do complex bookkeeping with committing/checkpointing/reverting to track the accessed slots/addresses.

Another point: why is the target of CREATE2 added to warm addresses immediately? We need to do a disk read on CREATE2, especially because we can try to deploy a contract on the same address, i.e. we have to check that the account is empty and thus do an IO read.

Yes.

Because otherwise you could do a 100 gas call to X, and X tries to CALL an account Y, but doesn’t have the funds to succeed. If it now reverts, suddenly you have Y in the hotlist although you didn’t pay the price for it. So it would be a backdoor, basically.

(And that same case can be made analoguous with slots)

I think the reasoning is that CREATE2 is sufficiently expensive, and it was in line with how create worked. I don’t know if there’s any more elaborate reason behind it cc @vbuterin

1 Like

Ah I didn’t think of this, very good point.

Thanks for the other answers as well!

Small note here: this EIP1352 and EIP2929 are not very consistent and we should be careful with terminology here.

Excerpt from EIP2929:

We should initialize the accessed accounts as:

and the set of all precompiles.

In combination with EIP 1352, one could interpret it as “all precompiles” being 0x00…00 to 0x00…00ffff, but this is not the case, since we only should add precompiles which have code.

The tests at testcases.md · GitHub use gas limit of 18446744073709551615 which exceeds the gas limit gap of 9223372036854775807 (2**63-1).

IIUIC the coinbase is not part of the starting accessed_addresses, i.e. the cold cost is paid when transferring to it from a contract. I would assume the coinbase is always warm in a block, and as a result in a transaction?

Is this just an a) oversight, b) simplification, or c) there is a definitive reason for this?

1 Like

I know it is late to ask, but were any benchmarks published on this matter?

Yeah basically this, CREATE2 is sufficiently expensive that we don’t have to care.

Somewhere between a) and b), I’d say.