-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,333 @@ | ||
--- | ||
eip: 0 | ||
Check failure on line 2 in EIPS/eip-000.md GitHub Actions / EIP Walidatorfile name must reflect the preamble header `eip`
|
||
title: Global Warming | ||
description: Block-level warming of addresses and slots with access lists | ||
author: Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn) | ||
discussions-to: | ||
Check failure on line 6 in EIPS/eip-000.md GitHub Actions / EIP Walidatorpreamble header `discussions-to` is not a valid URL
Check failure on line 6 in EIPS/eip-000.md GitHub Actions / EIP Walidatorpreamble header `discussions-to` should point to a thread on ethereum-magicians.org
|
||
status: Draft | ||
type: Standards Track | ||
category: Core | ||
created: 2023-10-01 | ||
--- | ||
|
||
## Abstract | ||
|
||
A mechanism for a fair distribution of the gas costs associated with access to addresses and storage slots | ||
among multiple transactions with shared items in their `accessList`. | ||
|
||
## Motivation | ||
|
||
[EIP-2929: Gas cost increases for state access opcodes](./eip-2929) introduced a new gas cost model that differentiates | ||
between "cold" and "warm" access to accounts and storage slots.\ | ||
However, the cost of every cold access is borne by each transaction separately, even though the validator only | ||
needs to fetch the state object once for the entire block.\ | ||
When multiple transactions access the same state object in the same block the fees charged for these transactions | ||
do not accurately reflect the computations that block builders and validators perform for the blockchain state access | ||
during transaction execution. | ||
|
||
[EIP-2930: Optional access lists](./eip-2930) made it possible for transactions to pre-specify and pre-pay for the | ||
accounts and storage slots that the transaction plans to access, | ||
however, the cost is still paid repeatedly by each transaction rather than once at the block level. | ||
|
||
With the [EIP-6800: Ethereum state using a unified verkle tree](./eip-6800) on the roadmap for inclusion, | ||
the cost of reading from the Ethereum state and especially the contract code is expected to increase.\ | ||
Especially affected by this upcoming change will be transactions that involve smart contracts with a high code size.\ | ||
Each such transaction in the block will be forced to pay the full "retail" price for loading smart contract | ||
bytecode during a transaction. | ||
|
||
The validators, however, only have to perform the actual reading from the Ethereum state once per block, | ||
and all subsequent reads of the values that were already referenced are significantly more efficient.\ | ||
If witnesses are introduced to Ethereum blocks, the same witness can be reused by multiple transactions.\ | ||
Forcing each transaction to pay regardless of the contents of the block is unfair and inefficient. | ||
|
||
Another change that is on the roadmap for Ethereum is [EIP-xxxx: Native Account Abstraction](./eip-xxxx).\ | ||
This change will see a large share of transactions being initiated by smart contracts directly.\ | ||
It is reasonable to expect many of these Smart Contract Accounts to rely on the same core wallet implementations.\ | ||
If each Account Abstraction transaction is charged a full gas cost of loading the Smart Contract Account code repeatedly,\ | ||
such transactions would become significantly overpriced.\ | ||
This difference is especially noticeable when compared to an EOA, which gets its validation logic loaded and executed for free.\ | ||
In this scenario, the base gas fees would be taken from the senders and burned needlessly while block proposers | ||
would be enjoying an unjustified excessive earning of priority gas fees. | ||
|
||
## Specification | ||
|
||
The [EIP-2930: Optional access lists](./eip-2930) already introduced the first part of the solution. | ||
Each transaction can specify an array of `accessed_addresses` and `accessed_storage_keys` to announce its intention to | ||
read those values during the execution of the transaction.\ | ||
The sender of the transaction is then pre-charged with the cost of accessing this data but is given a small discount | ||
compared to unannounced access. | ||
|
||
The missing component is a mechanism to aggregate the gas costs of the cold access and redistribute the resulting | ||
savings amongst the participating transactions. | ||
|
||
### Participant transactions mapping | ||
|
||
After the block builder finalizes the contents of the block, it iterates over all included transactions to read | ||
the `accessList` component of each supported transaction. | ||
|
||
The block builder then constructs an array containing each accessed address and each accessed slot, and an array of | ||
transaction senders' addresses that initiated at least one access to the given address or slot, | ||
as well as the `priorityFeePerGas` that was paid for such access. | ||
|
||
A sample JSON representation of the data structure that represents such a structure and is used in the pseudocode below: | ||
|
||
```json | ||
[ | ||
{ | ||
"address": "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", | ||
"accessors": [ | ||
{ | ||
"sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", | ||
"priorityFeePerGas": "1000" | ||
}, | ||
{ | ||
"sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", | ||
"priorityFeePerGas": "2000" | ||
}, | ||
{ | ||
"sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", | ||
"priorityFeePerGas": "1000" | ||
}, | ||
{ | ||
"sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", | ||
"priorityFeePerGas": "2000" | ||
}, | ||
{ | ||
"sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b", | ||
"priorityFeePerGas": "2000" | ||
}, | ||
{ | ||
"sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b", | ||
"priorityFeePerGas": "3000" | ||
} | ||
], | ||
"slots": [ | ||
{ | ||
"id": "0x0000000000000000000000000000000000000000000000000000000000000003", | ||
"accessors": [ | ||
{ | ||
"sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", | ||
"priorityFeePerGas": "1000" | ||
}, | ||
{ | ||
"sender": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", | ||
"priorityFeePerGas": "2000" | ||
}, | ||
{ | ||
"sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b", | ||
"priorityFeePerGas": "2000" | ||
}, | ||
{ | ||
"sender": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b", | ||
"priorityFeePerGas": "3000" | ||
} | ||
] | ||
}, | ||
{ | ||
"id": "0x0000000000000000000000000000000000000000000000000000000000000007", | ||
"accessors": [ | ||
{ | ||
"sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", | ||
"priorityFeePerGas": "1000" | ||
}, | ||
{ | ||
"sender": "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", | ||
"priorityFeePerGas": "2000" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
] | ||
|
||
``` | ||
|
||
### Calculating a refund of the burned base fee | ||
|
||
Considering that the same amount of computation is needed to access an address or a slot regardless of the number of | ||
transactions using one, it is reasonable for the protocol to only burn the gas cost of the cold access once. | ||
As all transactions in the same block pay exactly the same `baseFeePerGas`, the single cost of accessing a cold item is | ||
divided evenly among all transactions containing such access and the rest of the burned base fee is refunded. | ||
|
||
### Calculating a refund of the charged priority fee | ||
|
||
Each transaction pays an individual `priorityFeePerGas` value and redistributing this part of the cold access cost | ||
is more complex.\ | ||
We propose the following approach to a fair refund of the paid `priorityFee`: | ||
|
||
1. The validator gets paid the `priorityFee` for each cold access only once, but according to the highest `priorityFee` | ||
among the transactions containing the said cold access. | ||
2. The rest of the Ether that was charged by the validator as a `priorityFee` is redistributed back to all the senders | ||
of transactions containing the same cold access in proportion to their **marginal contribution** to the total refund. | ||
3. The `marginal contribution` of a transaction to the total refund is defined as the difference between the sum total | ||
refund value calculated when all transactions in a block are included, | ||
and when all transactions are included except for this particular transaction: | ||
|
||
> 𝑀𝐶𝑖 = 𝑣(𝑆 ∪ {𝑖}) − 𝑣(𝑆) | ||
Note that in practice this means that almost all transactions contribute exactly the `priorityFee * gasCost` to | ||
the refund.\ | ||
The most expensive transaction contributes a different value because it determines the value of the validator charge.\ | ||
A single transaction accessing an address or a slot that is not shared by other transactions does not trigger a refund, | ||
and therefore has a zero marginal contribution. | ||
|
||
### Pseudocode implementation of the refund calculation algorithm | ||
|
||
```typescript | ||
export function calculateBlockColdAccessRefund ( | ||
baseFeePerGas: string, | ||
accessDetailsMap: AddressAccessDetails[] | ||
): Map<string, Refund> { | ||
const refunds = new Map<string, Refund>() | ||
for (const accessDetail of accessDetailsMap) { | ||
calculateItemColdAccessRefund(accessDetail.accessors, baseFeePerGas, COLD_ACCOUNT_ACCESS_COST, refunds) | ||
for (const slot of accessDetail.slots) { | ||
calculateItemColdAccessRefund(slot.accessors, baseFeePerGas, COLD_SLOAD_COST, refunds) | ||
} | ||
} | ||
return refunds | ||
} | ||
|
||
function calculateItemColdAccessRefund ( | ||
unsortedAccessors: AccessDetails[], | ||
baseFeePerGas: string, | ||
accessGasCost: string, | ||
refunds: Map<string, Refund> | ||
): void { | ||
const sortedAccessDetails = unsortedAccessors.sort((a, b) => { return parseInt(b.priorityFeePerGas) - parseInt(a.priorityFeePerGas) }) | ||
const addressAccessN = sortedAccessDetails.length | ||
const refundPercent = (addressAccessN - 1) / addressAccessN | ||
const refundsFromCoinbase = calculatePriorityFeeRefunds(sortedAccessDetails, accessGasCost) | ||
for (let i = 0; i < sortedAccessDetails.length; i++) { | ||
const accessor = sortedAccessDetails[i] | ||
const refund = refunds.get(accessor.sender) ?? { refundFromBurn: 0n, refundFromCoinbase: 0n } | ||
refund.refundFromBurn += BigInt(Math.floor(parseInt(accessGasCost) * parseInt(baseFeePerGas) * refundPercent)) | ||
refund.refundFromCoinbase += BigInt(refundsFromCoinbase[i]) | ||
refunds.set(accessor.sender, refund) | ||
} | ||
} | ||
|
||
export function calculatePriorityFeeRefunds (sortedAccesses: AccessDetails[], accessGasCost: string) { | ||
// Validator charge is based on the highest paid priority fee per gas | ||
const validatorFee = parseInt(sortedAccesses[0].priorityFeePerGas) * parseInt(accessGasCost) | ||
// Notice that the two most expensive transactions have the same contribution to the refund | ||
const topTransactionContribution = parseInt(sortedAccesses[1].priorityFeePerGas) * parseInt(accessGasCost) | ||
|
||
// Accumulate the sum of all "contributions", at least the top transaction contribution | ||
let totalContributions = topTransactionContribution | ||
// Accumulate cost of gas paid to validator for accessing the same address/slot/chunk | ||
let totalSendersCharged = parseInt(sortedAccesses[0].priorityFeePerGas) * parseInt(accessGasCost) | ||
for (let i = 1; i < sortedAccesses.length; i++) { | ||
const charge = parseInt(sortedAccesses[i].priorityFeePerGas) * parseInt(accessGasCost) | ||
totalContributions += charge | ||
totalSendersCharged += charge | ||
} | ||
|
||
// Calculate the total amount of ether to be refunded for this access | ||
const totalRefund = totalSendersCharged - validatorFee | ||
if (totalRefund == 0) { | ||
// protect from NaN if all priority fees are 0 | ||
return Array(sortedAccesses.length).fill(0) | ||
} | ||
|
||
// Calculate actual charges and refunds | ||
const refunds = [Math.floor(totalRefund * topTransactionContribution / totalContributions)] | ||
for (let i = 1; i < sortedAccesses.length; i++) { | ||
const charge = parseInt(sortedAccesses[i].priorityFeePerGas) * parseInt(accessGasCost) | ||
refunds.push(Math.floor(totalRefund * charge / totalContributions)) | ||
} | ||
return refunds | ||
} | ||
``` | ||
|
||
Note that two accumulating values, `refundFromBurn` and `refundFromCoinbase`, | ||
are necessary in light of [EIP-1559: Fee market change for ETH 1.0 chain](./eip-1559) in order to differentiate | ||
between the Ether refund that is originating from a reduced block gas burn, | ||
and from the reduced block proposer priority fee per gas reward. | ||
|
||
### Future EIP-6800 gas reform support | ||
|
||
Once [EIP-6800](./eip-6800) is active, the cost of accessing a contract code for a cold address is expected to change. | ||
|
||
Instead of being a constant value of `COLD_ACCOUNT_ACCESS_COST` (currently 2600 gas), | ||
the total cost will be determined by the number of 31-byte "chunks" the code consists of. | ||
Each "chunk" of code will have a cost of `CODE_CHUNK_ACCESS_COST` (currently 200 gas). | ||
|
||
For a maximum contract size of `24576 bytes` defined by [EIP-170](./eip-170) the cost of accessing this contract | ||
surges from `2600` to `158600` gas. | ||
|
||
This change will likely require the `accessList` parameter of transactions to be adjusted for transactions | ||
to be able to specify which code chunks will be accessed. | ||
|
||
In such case the changes are reflected in the refund function as well, which is updated by adding the following code | ||
in order to redistribute the shared cost of accessing the same code chunk in multiple transactions: | ||
|
||
```typescript | ||
const refundsFromCoinbase = calculatePriorityFeeRefunds(sortedCodeChunkAccessDetails, CHUNK_ACCESS_COST) | ||
for (let i = 0; i < sortedAccessDetails.length; i++) { | ||
const refund = refunds.get(accessor.sender) | ||
refund.refundFromBurn += CHUNK_ACCESS_COST * block.baseFeePerGas * refundPercent | ||
refund.refundFromCoinbase += refundsFromCoinbase[i] | ||
} | ||
``` | ||
|
||
### Cost redistribution system operation | ||
|
||
The [EIP-4895: Beacon chain push withdrawals as operations](eip-4895) sets a precedent by introducing a concept of a `system-level withdrawal operation`. | ||
|
||
We propose the introduction of yet another system-level operation called `cost redistribution`. | ||
The `redistributions` in an execution payload are processed after any user-level transactions are applied. | ||
|
||
The block builder or a validator prepares a list of refund information. | ||
|
||
For each `redistribution` in the list, the implementation increases the balance of the address specified by the amount `refundFromBurn + refundFromCoinbase`. | ||
|
||
The balance of the `coinbase` is reduced by a sum of all `refundFromCoinbase` values. | ||
|
||
## Rationale | ||
|
||
### Current cold storage gas cost is unfair | ||
|
||
As described in the [Motivation](#Motivation) section, the amount of gas that users spend on accessing the contract code does not reflect the actual cost of this access for the block builder or a validator. | ||
Check failure on line 291 in EIPS/eip-000.md GitHub Actions / Markdown LinterLink fragments should be valid [Expected: #motivation; Actual: #Motivation] [Context: "[Motivation](#Motivation)"]
|
||
|
||
The more popular the contract code or a storage slot is, the more transactions in each block should share the cost. However, the current system multiplies the cost for the users instead of dividing it. | ||
|
||
### Issuing a regular gas refund after a transaction is not possible | ||
|
||
There exists a list of EVM instructions that trigger both a gas charge and a gas refund. A notable example of such operations is the `0x55 SSTORE` opcode as defined in [EIP-1283: Net gas metering for SSTORE without dirty maps](./eip-1283). Intuitively it seems reasonable to issue the gas refunds for the shared cold storage access in the same fashion. | ||
|
||
However, this approach significantly complicates the block-building process. In this case, the inclusion or exclusion of a transaction at the end of the block triggers observable effects in transactions included at the beginning of the block, and this makes the job of finding a valid set of transactions for a block potentially computationally unsolvable. | ||
|
||
Therefore, we propose performing the refunds at the end of the block, where it cannot change the behavior of any transaction in the block. | ||
|
||
### Weighting priority fee refund | ||
|
||
A common game-theoretical answer to the problem of calculating a fair redistribution of the payoff of the | ||
results of the participants' cooperation is the use of Shapley values.\ | ||
However, we argue that the proposed distribution of the `priorityFee` refunds is sufficiently fair while being | ||
a lot easier to compute or articulate. | ||
|
||
## Backwards Compatibility | ||
|
||
This proposal does not introduce a change to any behavior that can be observed by a smart contract during its execution. The only effect this change has is a lower effective gas cost for the transaction senders. | ||
|
||
|
||
## Test cases | ||
Check failure on line 315 in EIPS/eip-000.md GitHub Actions / EIP Walidatorbody has extra section(s)
|
||
|
||
This repository contains all the test cases and related work: | ||
|
||
https://github.com/eth-infinitism/global-warming-test-cases | ||
Check failure on line 319 in EIPS/eip-000.md GitHub Actions / EIP Walidatornon-relative link or image
|
||
|
||
## Security Considerations | ||
|
||
The upper limit of storage reads in one block is not affected by this change as the gas charge is done with the | ||
full cost of `COLD_ACCOUNT_ACCESS_COST` or `COLD_SLOAD_COST`. | ||
|
||
The maximum amount of memory and computation required to calculate the refunds according to the | ||
specified algorithm is insignificant. | ||
|
||
It appears that this change does not have any negative security implications. | ||
|
||
## Copyright | ||
|
||
Copyright and related rights waived via [CC0](../LICENSE.md). |