-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Add EIP: Hard limit and cost reduction for transient storage allocation #9894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add EIP: Hard limit and cost reduction for transient storage allocation #9894
Conversation
File
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allright, great EIP! My main points are about the actual memory usage (which is likely not 64 bytes per key/value pair but more due to the choosen data structure overhead) and some thoughts/questions about the transient storage size limit.
### Gas Cost Changes | ||
|
||
1. The gas cost for `TLOAD` (opcode `0x5c`) is reduced from 100 to `GAS_TLOAD`. | ||
2. The base gas cost for `TSTORE` (opcode `0x5d`) is reduced from 100 to `GAS_TSTORE`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not mention GAS_TSTORE_ALLOCATE
which from code is allocated if the key does not exist yet in the key-value map of the transient storage for that specific address.
|
||
1. The gas cost for `TLOAD` (opcode `0x5c`) is reduced from 100 to `GAS_TLOAD`. | ||
2. The base gas cost for `TSTORE` (opcode `0x5d`) is reduced from 100 to `GAS_TSTORE`. | ||
3. The gas cost for warm `SLOAD` (opcode `0x54`) is reduced from 100 to `GAS_WARM_SLOAD`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be renamed to use the terminology from https://eips.ethereum.org/EIPS/eip-2929
3. The gas cost for warm `SLOAD` (opcode `0x54`) is reduced from 100 to `GAS_WARM_SLOAD`. | |
3. The `WARM_STORAGE_READ_COST` parameter from [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929) for `SLOAD` (opcode `0x54`) is changed to `GAS_WARM_SLOAD`. |
This EIP already depends on EIP-2929, because without it the concept of "warm SLOAD
" does not exist.
type: Standards Track | ||
category: Core | ||
created: 2025-06-12 | ||
requires: 1153 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
requires: 1153 | |
requires: 1153, 2929 |
|
||
1. At the beginning of each transaction, initialize a counter `transient_slots_used` to 0. | ||
2. When `TSTORE` is executed: | ||
- If the slot has not been written to during this transaction (across any contract), increment `transient_slots_used`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to imply that if a key is "warm" on any address, it is warm on all addresses? Why is this? The natural way to implement TSTORE/TLOAD I would think is a map from address to the transient storage map, and thus the storage map the key-value map for that address. But this seems to imply there is instead a map from key to a map of addresses where the address point to the value? Is this correct? Or should it be metered within the same address to only check there if the transient storage slot is warm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, maybe the wording is unclear -- a slot is an (address, slot)
pair
2. When `TSTORE` is executed: | ||
- If the slot has not been written to during this transaction (across any contract), increment `transient_slots_used`. | ||
- If `transient_slots_used` exceeds `MAX_TRANSIENT_SLOTS`, the transaction MUST exceptionally halt. | ||
3. The counter persists across all message calls in the transaction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this rule can be removed, because we init it to 0 and reset it to 0 at the end of the transaction (maybe even that reset rule could be removed, but I like that it is explicit)
`MAX_TRANSIENT_SLOTS` of 131072 allows: | ||
|
||
- Sufficient slots for typical user applications | ||
- Memory usage bounded to approximately 8 MB per transaction (131072 slots * 64 bytes), which prevents OOM-based denial-of-service attacks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This analysis does not take into account the overhead costs of the data structure (and thus also the revert journaling which should be stored somewhere)
2. Superlinear Pricing: Adds complexity, and still punishes "common" (non-DOS) use cases. | ||
3. No Limit: May allow memory-based DOS attack if transaction-level gas limits change, or if pricing changes in the future. | ||
|
||
The benefit of a hard limit is that the resource consumption is bounded in a predictable way even in the presence of other parameter changes in the protocol. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm ok this is actually a good point, maybe we should indeed do a limit. Although if we would raise the max tx gas limit there should be benchmarks and considerations about that increment thus also revisiting this EIP and checking if the new tx gas limit still puts a reasonable bound on the lookup time and the memory usage.
|
||
# Check limit | ||
if len(self.unique_slots) > MAX_TRANSIENT_SLOTS: | ||
raise ExceptionalHalt("Transient storage limit exceeded") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what does this do? Does this exit the current message frame and thus "forgetting" about the changes made to the current transient storage? Or does this invalidate the whole transaction?
There could be a situation where call frame A fills the entire transient storage, and then there are some other calls (which could be a deep call stack) which try to TSTORE
on a new key. How should this work when the limit has been reached? Because in this situation it is clear that the topmost call frame A is the culprit here, which thus means all CALLs after the current contract cannot write to the storage.
So the question here is should it invalidate the transaction? In the sense that it will rollback all execution changes? Or should it only invalidate the current call frame?
if slot_id not in self.unique_slots: | ||
self.unique_slots.add(slot_id) | ||
|
||
self.charge_gas(GAS_TSTORE_ALLOCATE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gas is charged after the operation, it should be done before, since we want to be sure we are paying before any "heavy" computation is done like self.unique_slots.add(slot_id)
.
|
||
## Security Considerations | ||
|
||
With `MAX_TRANSIENT_SLOTS` = 131072, maximum memory allocation is bounded to 8MB per transaction (131072 * 64 bytes). Compared to limits under current pricing (100 gas), a 30M gas transaction can allocate up to 300,000 slots (1 8000 9.2 MB). This EIP reduces the maximum allocated amount by 56%. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also does not take into account the memory usage of the actual data structure, which has overhead and there is also the revert logic which should be stored.
The commit 050a80d (as a parent of 42ecca8) contains errors. |
ATTENTION: ERC-RELATED PULL REQUESTS NOW OCCUR IN ETHEREUM/ERCS
--
When opening a pull request to submit a new EIP, please use the suggested template: https://github.com/ethereum/EIPs/blob/master/eip-template.md
We have a GitHub bot that automatically merges some PRs. It will merge yours immediately if certain criteria are met: