diff --git a/EIPS/eip-000.md b/EIPS/eip-000.md new file mode 100644 index 00000000000000..c387ec6793a1dd --- /dev/null +++ b/EIPS/eip-000.md @@ -0,0 +1,333 @@ +--- +eip: 0 +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: +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 { + const refunds = new Map() + 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 +): 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. + +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 + +This repository contains all the test cases and related work: + +https://github.com/eth-infinitism/global-warming-test-cases + +## 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).