Update EIP-8037: improve EIP-7702 authorization gas accounting in EIP-8037#11715
Conversation
|
✅ All reviewers have approved. |
05bf829 to
e974821
Compare
|
I disagree; we cannot overcharge for a very common use case in the protocol, as this contradicts the goal of only charging for the created state portion of this EIP. |
|
@Helkomine What do you mean? It's exactly the purpose of this PR, to not overcharge the state gas in certain scenarios. |
|
I mean, the case where the invalid authorization but the state gas is not refilled even though there is no state growth. |
misilva73
left a comment
There was a problem hiding this comment.
Left a few suggestions, mostly wording and clarity. As proposal expands the refunds previously provided by 7702, I would like to hear more opinions before approving. Do we want to expand the refunds given to 7702 authorizations? Can we think of any attack vectors that may arrise?
|
@Helkomine Okay, it makes sense to reconsider this case. |
|
Yes, I'll keep the PR as it is for now and wait the feedback from other teams whether the additional complexity is worthwhile. Personally, I think the complexity is justified, since it aligns the gas accounting for authorizations more closely with the actual net state changes. |
| ##### Gas accounting for [EIP-7702](./eip-7702.md) authorizations | ||
|
|
||
| While computing the intrinsic gas cost, [EIP-7702](./eip-7702.md) authorizations are charged the worst-case cost for each delegation. Then, during authorization processing, the following gas adjustments are made for each processed authorization: | ||
| Intrinsic gas charges the worst-case state-gas cost (`(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) × CPSB`) for each authorization. However, an authority's account leaf and its 23 byte delegation indicator can only be written once per transaction, even if the same authority appears in multiple authorizations. Therefore, per authorization adjustments are applied while processing the authorization list. |
There was a problem hiding this comment.
| Intrinsic gas charges the worst-case state-gas cost (`(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) × CPSB`) for each authorization. However, an authority's account leaf and its 23 byte delegation indicator can only be written once per transaction, even if the same authority appears in multiple authorizations. Therefore, per authorization adjustments are applied while processing the authorization list. | |
| Intrinsic gas charges the worst-case gas cost (`(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) × CPSB`) for each authorization. However, an authority's account leaf and its 23 byte delegation indicator can only be written once per transaction, even if the same authority appears in multiple authorizations. Therefore, per authorization adjustments are applied while processing the authorization list. |
it's not just the worst-case state but ACCOUNT_WRITE is also given back
| - the authority is in `billed` (a prior authorization in this transaction has already been charged the auth-base creation for this authority) | ||
| - `authorization.address` is `ZERO` (the authorization is clearing the delegation) | ||
|
|
||
| **4. First charge.** If the authorization does not meet the conditions of rule 1, 2 and 3, add the authority to `billed`; this authorization is the one charged for the per-tx auth-base creation. This charge was already included in the transaction's intrinsic cost. |
There was a problem hiding this comment.
| **4. First charge.** If the authorization does not meet the conditions of rule 1, 2 and 3, add the authority to `billed`; this authorization is the one charged for the per-tx auth-base creation. This charge was already included in the transaction's intrinsic cost. | |
| **4. First charge.** If the authorization does not meet the conditions of rule 1 and 3, add the authority to `billed`; this authorization is the one charged for the per-tx auth-base creation. This charge was already included in the transaction's intrinsic cost. |
Depending on rule 2 to be on billed breaks the steps below, I don't think it should depend on it. Case for a normal EOA (no code deployed) at the start authorization processing:
- rule 1 doesn't trigger - it's a valid delegation;
- rule 2 triggers, account already exists - STATE_BYTES_PER_NEW_ACCOUNT + ACCOUNT_WRITE refunded;
- rule 3 no trigger - this is a legit delegation as the auth bytes are being set for first time so you are billed for STATE_BYTES_PER_AUTH_BASE;
- rule 4 no trigger - since it was triggered for 2;
Then the next time you loop, in rule 3 if there's a duplicate authorization, you won't see it in billed whereas you should. Then you will charge it again.
|
After the 2 issues are addressed I think the proposal is sound. Agree that it's not fair to charge for state the won't exist in the end so complexity is justified. |
| Processing maintains two pieces of per-transaction bookkeeping: | ||
|
|
||
| `execution_state_gas_used` decreases by the corresponding amount of state-gas refills. | ||
| - `billed`: tracks which authorities have already been charged for auth-base creation in the current transaction. | ||
| - `account state`: | ||
| - **pre-transaction state**: the account state before transaction execution. | ||
| - **current state**: the account state while processing authorizations, updated after each authorization. | ||
|
|
||
| Because `execution_state_gas_used` is initialized to `0`, this refill may bring it below zero before any execution-time charges. Implementations may equivalently track the refill separately and subtract it from `tx_state_gas` when accumulating `block_state_gas_used`. | ||
| Authorization processing follows 5 gas accounting rules: | ||
|
|
||
| **1. Invalid authorizations.** Invalid authorizations are skipped without per-auth processing. Their entire intrinsic state-gas portion, `(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) × CPSB`, is refilled to `state_gas_reservoir` and refund `ACCOUNT_WRITE` to the refund counter. | ||
|
|
||
| **2. Account-creation refill.** If the authority's account leaf already exists in the **current state** (non-zero nonce, non-zero balance, or non-empty code), refill `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` to `state_gas_reservoir` and refund `ACCOUNT_WRITE` to the refund counter. | ||
|
|
||
| **3. Auth-base refill.** Refill `STATE_BYTES_PER_AUTH_BASE × CPSB` to `state_gas_reservoir` if **any** of: | ||
|
|
||
| - the authority was already delegated in the **pre-transaction state** state | ||
| - the authority is in `billed` (a prior authorization in this transaction has already been charged the auth-base creation for this authority) | ||
| - `authorization.address` is `ZERO` (the authorization is clearing the delegation) | ||
|
|
||
| **4. First charge.** If the authorization does not meet the conditions of rule 1, 2 and 3, add the authority to `billed`; this authorization is the one charged for the per-tx auth-base creation. This charge was already included in the transaction's intrinsic cost. | ||
|
|
||
| **5. Cancel a prior auth-base creation.** If `authorization.address` is `ZERO` **and** the authority is in `billed`, additionally refill `STATE_BYTES_PER_AUTH_BASE × CPSB` to `state_gas_reservoir` and remove the authority from `billed`. The chain of authorizations for this authority now writes zero net delegation bytes, so the earlier creation charge is no longer justified. This covers patterns such as `0->a->0`, where the creation by an earlier authorization is undone by a later clearing one in the same list. |
There was a problem hiding this comment.
Building on the rule‑4 issue already raised above — rather than fixing how billed gets populated, I think the whole set can be dropped. billed is reconstructing something that's already in current state: once an earlier authorization delegates A, its codeHash is non-empty, so "was this authority delegated earlier in the tx?" is just a codeHash check. The only thing current state can't distinguish is pre-tx vs in-tx delegation, which matters solely for the cancel case and is a single read of the pre-tx snapshot.
That collapses the 5 rules to 3. It's equivalent to the intended behavior — the rule‑3 condition pre_delegated OR billed OR ZERO is exactly cur_delegated OR pre_delegated OR ZERO once billed = cur_delegated AND NOT pre_delegated — but reading codeHash directly can't have the rule‑4 failure mode, and it fixes 0→a→0 for funded EOAs (which rule 5 misses today because those authorities never enter billed).
Concrete suggestion (also drops the billed set, fixes the "state state" typo, and tidies rule 1's grammar):
| Processing maintains two pieces of per-transaction bookkeeping: | |
| `execution_state_gas_used` decreases by the corresponding amount of state-gas refills. | |
| - `billed`: tracks which authorities have already been charged for auth-base creation in the current transaction. | |
| - `account state`: | |
| - **pre-transaction state**: the account state before transaction execution. | |
| - **current state**: the account state while processing authorizations, updated after each authorization. | |
| Because `execution_state_gas_used` is initialized to `0`, this refill may bring it below zero before any execution-time charges. Implementations may equivalently track the refill separately and subtract it from `tx_state_gas` when accumulating `block_state_gas_used`. | |
| Authorization processing follows 5 gas accounting rules: | |
| **1. Invalid authorizations.** Invalid authorizations are skipped without per-auth processing. Their entire intrinsic state-gas portion, `(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) × CPSB`, is refilled to `state_gas_reservoir` and refund `ACCOUNT_WRITE` to the refund counter. | |
| **2. Account-creation refill.** If the authority's account leaf already exists in the **current state** (non-zero nonce, non-zero balance, or non-empty code), refill `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` to `state_gas_reservoir` and refund `ACCOUNT_WRITE` to the refund counter. | |
| **3. Auth-base refill.** Refill `STATE_BYTES_PER_AUTH_BASE × CPSB` to `state_gas_reservoir` if **any** of: | |
| - the authority was already delegated in the **pre-transaction state** state | |
| - the authority is in `billed` (a prior authorization in this transaction has already been charged the auth-base creation for this authority) | |
| - `authorization.address` is `ZERO` (the authorization is clearing the delegation) | |
| **4. First charge.** If the authorization does not meet the conditions of rule 1, 2 and 3, add the authority to `billed`; this authorization is the one charged for the per-tx auth-base creation. This charge was already included in the transaction's intrinsic cost. | |
| **5. Cancel a prior auth-base creation.** If `authorization.address` is `ZERO` **and** the authority is in `billed`, additionally refill `STATE_BYTES_PER_AUTH_BASE × CPSB` to `state_gas_reservoir` and remove the authority from `billed`. The chain of authorizations for this authority now writes zero net delegation bytes, so the earlier creation charge is no longer justified. This covers patterns such as `0->a->0`, where the creation by an earlier authorization is undone by a later clearing one in the same list. | |
| The adjustments enforce a single invariant: the `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` account-leaf portion is charged at most once per authority and only when the account did not exist before the transaction, and the `STATE_BYTES_PER_AUTH_BASE × CPSB` delegation-indicator portion is charged at most once per authority and only when the authority ends the transaction delegated having started it undelegated. | |
| Processing reads the account state at two points: | |
| - **pre-transaction state**: the account state before transaction execution. | |
| - **current state**: the account state while processing authorizations, updated after each authorization. Because an in-transaction delegation leaves a delegation indicator in the authority's `code` field, the current state already records whether an earlier authorization in this transaction delegated the same authority. | |
| For each authorization, let `cur_delegated` be `true` when the authority has a non-empty delegation indicator in the **current state** (`codeHash != emptyHash`), and `pre_delegated` be `true` when it had one in the **pre-transaction state**. Authorization processing follows 3 gas accounting rules: | |
| **1. Invalid authorizations.** Invalid authorizations are skipped without per-auth processing. Their entire intrinsic state-gas portion, `(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) × CPSB`, is refilled to `state_gas_reservoir` and `ACCOUNT_WRITE` is refunded to the refund counter. | |
| **2. Account-leaf refill.** If the authority's account leaf already exists in the **current state** (non-zero nonce, non-zero balance, or non-empty code), refill `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` to `state_gas_reservoir` and refund `ACCOUNT_WRITE` to the refund counter. | |
| **3. Delegation-indicator refill.** | |
| - If `authorization.address` is not `ZERO` (setting a delegation), refill `STATE_BYTES_PER_AUTH_BASE × CPSB` to `state_gas_reservoir` when `cur_delegated` or `pre_delegated` is `true`. In that case the 23-byte indicator slot is already occupied, so this authorization overwrites it without writing new bytes. Otherwise this authorization performs the net-new creation and its intrinsic charge stands. | |
| - If `authorization.address` is `ZERO` (clearing the delegation), refill `STATE_BYTES_PER_AUTH_BASE × CPSB` to `state_gas_reservoir`, since the clear writes no indicator. Additionally, when `cur_delegated` is `true` and `pre_delegated` is `false`, refill another `STATE_BYTES_PER_AUTH_BASE × CPSB`: the delegation being cleared was created by an earlier authorization in this same transaction, so the chain writes zero net delegation bytes and the earlier creation charge is no longer justified. This covers patterns such as `0->a->0`, where the creation by an earlier authorization is undone by a later clearing one in the same list. |
|
The commit a2a92bc (as a parent of 2a6c84b) contains errors. |
misilva73
left a comment
There was a problem hiding this comment.
Looks good. Approving now
eth-bot
left a comment
There was a problem hiding this comment.
All Reviewers Have Approved; Performing Automatic Merge...
This PR improves gas accounting for EIP-7702 authorizations, covering several corner cases such as authorization cancellation within the same transaction.
State gas charging is now aligned with the net state changes introduced by authorizations, except for the case clearing a pre-existing authorization which is intentionally not refunded.