diff --git a/core/state/state_object.go b/core/state/state_object.go index ce456e7668..2165fad57a 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -572,6 +572,30 @@ func (s *stateObject) Code() []byte { return code } +// GetCommittedCode returns the contract code committed at the start of the +// current execution, ignoring any intra-execution SetCode modifications. +// Used by EIP-7702 authorization application to make refund decisions +// relative to the originally-committed delegation, matching the spirit of +// GetCommittedState for storage slots. +func (s *stateObject) GetCommittedCode() []byte { + // The account did not exist at the start of the current execution. + if s.origin == nil { + return nil + } + hash := common.BytesToHash(s.origin.CodeHash) + if hash == types.EmptyCodeHash { + return nil + } + // If the code has not been touched in this execution, the live cache + // already holds the committed code. + if !s.dirtyCode { + return s.Code() + } + // Code was modified within the current execution. Reach for the on-disk + // blob keyed by the origin hash. + return s.db.reader.Code(s.address, hash) +} + // CodeSize returns the size of the contract code associated with this object, // or zero if none. This method is an almost mirror of Code, but uses a cache // inside the database to avoid loading codes seen recently. diff --git a/core/state/statedb.go b/core/state/statedb.go index 1c49d46020..15c175fd30 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -397,6 +397,18 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { return common.Hash{} } +// GetCommittedCode returns the contract code committed at the start of the +// current execution, ignoring any in-progress SetCode mutations. Returns +// nil when the account had no code (or did not exist) prior to this +// execution. +func (s *StateDB) GetCommittedCode(addr common.Address) []byte { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetCommittedCode() + } + return nil +} + // GetState retrieves the value associated with the specific key. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { stateObject := s.getStateObject(addr) diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 98d01343a4..184629ea41 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -75,6 +75,10 @@ func (s *hookedStateDB) GetCode(addr common.Address) []byte { return s.inner.GetCode(addr) } +func (s *hookedStateDB) GetCommittedCode(addr common.Address) []byte { + return s.inner.GetCommittedCode(addr) +} + func (s *hookedStateDB) GetCodeSize(addr common.Address) int { return s.inner.GetCodeSize(addr) } diff --git a/core/state_transition.go b/core/state_transition.go index 1314fdcf4b..b8ca6714fa 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -744,12 +744,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) // Apply EIP-7702 authorizations. - if msg.SetCodeAuthorizations != nil { - for _, auth := range msg.SetCodeAuthorizations { - // Note errors are ignored, we simply skip invalid authorizations here. - st.applyAuthorization(rules, &auth) - } - } + st.applyAuthorizations(rules, msg.SetCodeAuthorizations) // Perform convenience warming of sender's delegation target. Although the // sender is already warmed in Prepare(..), it's possible a delegation to @@ -902,8 +897,48 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio return authority, nil } +// applyAuthorizations applies every EIP-7702 code delegation in the tx and, +// under EIP-8037, reconciles the per-authority creation budget so that the +// total AuthorizationCreationSize state gas charged matches the net change +// in on-disk delegation bytes. +// +// Invalid authorizations are silently skipped (their auth-base intrinsic +// state gas remains charged, matching the pre-existing behavior). +func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { + if len(auths) == 0 { + return + } + // Under EIP-8037 each authority can be billed at most one + // AuthorizationCreationSize. applyAuthorization records authorities it + // has billed; we reconcile after the loop by refunding any creation that + // was billed but whose final delegation state in this tx ended up empty + // (e.g., 0→a→0). + var billed map[common.Address]struct{} + if rules.IsAmsterdam { + billed = make(map[common.Address]struct{}) + } + for _, auth := range auths { + // Errors are ignored — invalid authorizations are simply skipped. + st.applyAuthorization(rules, &auth, billed) + } + // End-of-loop reconciliation: a billed creation whose authority is no + // longer delegated wrote zero net bytes to disk, so the auth-base + // intrinsic state gas should not be retained. + for authority := range billed { + if _, isDelegated := types.ParseDelegation(st.state.GetCode(authority)); !isDelegated { + st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte) + } + } +} + // applyAuthorization applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) error { +// +// authBilledCreations, when non-nil, tracks the set of authorities for which +// this tx has been billed one AuthorizationCreationSize charge (the per- +// authority "first creation" budget). The caller is expected to do an +// end-of-loop pass over this set and refund any entry whose final delegation +// state ended up empty. +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, authBilledCreations map[common.Address]struct{}) error { authority, err := st.validateAuthorization(auth) if err != nil { return err @@ -920,14 +955,34 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } prevDelegation, isDelegated := types.ParseDelegation(st.state.GetCode(authority)) if rules.IsAmsterdam { - // EIP-8037: also refund the auth-base state gas when no new delegation - // indicator bytes are written. Two cases: - // - the authority already has a delegation (overwrite in place); or - // - the auth is no-op (auth.Address == 0). - // In both cases the 23 delegation bytes are reused, so the auth-base - // portion of the intrinsic state gas is refilled. - if isDelegated || auth.Address == (common.Address{}) { + // EIP-8037: refund the auth-base state gas unless this auth is the + // first one in this tx to write delegation bytes to an authority + // whose committed code was empty. Refund when ANY of: + // + // - the authority was already delegated at the start of the tx + // (the 23 bytes are already accounted for in committed state, + // and any auth against it just re-writes them); + // + // - the auth is no-op / clearing (auth.Address == 0) — no bytes + // are written in this step at all; + // + // - we have already billed a creation for this authority in + // this tx (per-authority creation budget is 1). + // + // Modeling it this way mirrors the SSTORE "reset to original" + // pattern (EIP-2200 / EIP-3529) and avoids both the undercount in + // a→0→b (committed had delegation, second auth missed the refund) + // and the overcount in 0→a→0→c (each later auth was previously + // billed as a fresh creation). The remaining 0→a→0 case — a + // creation is billed and then undone within the same auth list — + // is handled by the caller's end-of-loop adjustment over + // authBilledCreations. + _, committedDelegated := types.ParseDelegation(st.state.GetCommittedCode(authority)) + _, alreadyBilled := authBilledCreations[authority] + if committedDelegated || alreadyBilled || auth.Address == (common.Address{}) { st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte) + } else { + authBilledCreations[authority] = struct{}{} } } diff --git a/core/vm/interface.go b/core/vm/interface.go index a9938c2a28..dc897f0c69 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -42,6 +42,11 @@ type StateDB interface { GetCodeHash(common.Address) common.Hash GetCode(common.Address) []byte + // GetCommittedCode returns the contract code at the start of the current + // execution, ignoring any in-progress SetCode mutations. Returns nil when + // the account had no code prior to this execution. + GetCommittedCode(common.Address) []byte + // SetCode sets the new code for the address, and returns the previous code, if any. SetCode(common.Address, []byte, tracing.CodeChangeReason) []byte GetCodeSize(common.Address) int