diff --git a/core/state_transition.go b/core/state_transition.go index 85b519cfe1..22ccd53542 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -745,6 +745,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Execute the transaction's creation. ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) st.gasRemaining.Absorb(result) + // EIP-8037: a successful creation to a pre-existing (alive) leaf creates + // no new account, so refund the intrinsic NEW_ACCOUNT state gas to the + // reservoir (fork.py: refunded when created_target_alive). The failure + // case is handled by the vmerr branch below. + if rules.IsAmsterdam && vmerr == nil && st.evm.CreateTargetWasAlive() { + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + } } else { // Increment the nonce for the next transaction. st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) @@ -930,13 +937,13 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g } if rules.IsAmsterdam { - // EIP-7623/7976: the calldata floor applies to the block-level regular - // gas dimension as well, mirroring its effect on the receipt gas. The - // spec accumulates max(tx_regular_gas, calldata_floor) into - // block_gas_used, so the block must never count fewer regular units - // than the floor the sender was charged. - blockRegularGas := max(txRegularGas, floorDataGas) - if err = st.gp.ChargeGasAmsterdam(blockRegularGas, txStateGas, gasUsed); err != nil { + // EIP-7778: the block-level gas accounting uses the gross regular gas + // (tx_gas_used_before_refund - tx_state_gas) and ignores both the + // EIP-3529 refund and the EIP-7623/7976 calldata floor. The floor and + // refund only affect the receipt scalar (tx_gas_used) and the sender's + // ETH refund, not what the block charges (fork.py: block_gas_used += + // tx_regular_gas). + if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil { return 0, 0, err } } else { @@ -998,7 +1005,7 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio // - the delegation-indicator portion (AuthorizationCreationSize × CPSB) is // refunded when this auth writes no new indicator bytes (the authority is // already delegated, or the auth clears the delegation). -func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) error { +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, preDelegated map[common.Address]bool) error { authority, err := st.validateAuthorization(auth) if err != nil { if rules.IsAmsterdam { @@ -1018,24 +1025,39 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) } } else { - // EIP-8037 (spec apply_authorization): refund the per-auth intrinsic - // state charge for state that does not actually get newly created. + // EIP-8037/8038 (spec set_delegation): refund the per-auth intrinsic + // state charge for state that is not actually newly created. // - // - NEW_ACCOUNT is refunded when the authority account already exists - // (account_exists), since no new account is created. + // delegated_now reflects prior authorizations applied this tx; the + // pre-transaction delegation is captured on first touch of the + // authority. + delegatedNow := curDelegated + delegatedBeforeTx, seen := preDelegated[authority] + if !seen { + delegatedBeforeTx = curDelegated + preDelegated[authority] = delegatedBeforeTx + } + // - NEW_ACCOUNT (+ worst-case ACCOUNT_WRITE regular) is refunded when + // the authority account leaf already exists, since no new account + // is created. if st.state.Exist(authority) { st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) - // EIP-8038: the worst-case ACCOUNT_WRITE charged per authorization in - // the intrinsic cost is refunded to the regular refund counter when - // the authority account already exists (no new account is created). st.state.AddRefund(params.AccountWriteAmsterdam) } - // - AUTH_BASE is refunded when no new delegation-indicator bytes are - // written: either the authority already carries code/delegation - // (code_hash != EMPTY, i.e. curDelegated) or this auth clears the - // delegation (auth.address == 0). Exactly one refund per auth. - if curDelegated || auth.Address == (common.Address{}) { - st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte) + // - AUTH_BASE refill, mirroring set_delegation: + authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte + if auth.Address == (common.Address{}) { + // Clearing: refund AUTH_BASE; refund a second time when removing a + // delegation that was created earlier in this same transaction + // (delegated now but not before the tx). + st.gasRemaining.RefundState(authBase) + if delegatedNow && !delegatedBeforeTx { + st.gasRemaining.RefundState(authBase) + } + } else if delegatedNow || delegatedBeforeTx { + // Setting: refund AUTH_BASE when no new indicator bytes are written + // (the authority already carries a delegation now or pre-tx). + st.gasRemaining.RefundState(authBase) } } @@ -1058,8 +1080,14 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se // applyAuthorizations applies an EIP-7702 code delegation to the state. func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { + // preDelegated records each authority's delegation state in the + // pre-transaction state, captured on the first authorization that touches + // it. EIP-8037 distinguishes a delegation that existed before the + // transaction (delegated_before_tx) from one created earlier in the same + // transaction (delegated_now) when refilling AUTH_BASE. + preDelegated := make(map[common.Address]bool) for _, auth := range auths { - st.applyAuthorization(rules, &auth) + st.applyAuthorization(rules, &auth, preDelegated) } } diff --git a/core/vm/evm.go b/core/vm/evm.go index af0ad9b5b7..c86704c54c 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -127,6 +127,13 @@ type EVM struct { // order when the sub-call fails (spec generic_call credit_state_gas_refund). callNewAccountChargedTemp bool + // createTargetAliveTemp records, for the most recent successful create() + // frame, whether the target account leaf was already alive (non-empty) + // before the create. opCreate/opCreate2 read it to refund the EIP-8037 + // NEW_ACCOUNT state gas on a successful create to a pre-existing leaf + // (spec create_message: refund when target_alive). + createTargetAliveTemp bool + // precompiles holds the precompiled contracts for the current epoch precompiles map[common.Address]PrecompiledContract @@ -491,6 +498,13 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b return ret, exitGas, err } +// CreateTargetWasAlive reports whether the most recent successful create() +// frame targeted an already-alive (non-empty) account leaf. Used by the +// transaction-level creation path to apply the EIP-8037 NEW_ACCOUNT refund. +func (evm *EVM) CreateTargetWasAlive() bool { + return evm.createTargetAliveTemp +} + // create creates a new contract using code as deployment code. func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, err error) { // Depth check execution. Fail if we're trying to execute above the @@ -552,6 +566,15 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value // address collision while regular gas is burnt. return nil, common.Address{}, halt, ErrContractAddressCollision } + // EIP-8037: record whether the target leaf was already alive (non-empty) + // before the create. The collision check above already accessed the + // account, so this read adds nothing new to the block access list. A + // successful create to an already-alive leaf creates no new account, so + // opCreate/opCreate2 refunds the NEW_ACCOUNT charge. Captured here (rather + // than eagerly in the opcode) so early-failure paths that never reach the + // target — depth, insufficient balance — do not record a spurious access. + targetAlive := !evm.StateDB.Empty(address) + // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent // account with non-zero balance. @@ -613,7 +636,10 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value return ret, address, exit, err } // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved). - // Both packaged as a success-form GasBudget. + // Both packaged as a success-form GasBudget. Record target aliveness for + // the EIP-8037 NEW_ACCOUNT refund (set here, after any nested creates ran, + // so it reflects this frame's target rather than a deeper one). + evm.createTargetAliveTemp = targetAlive return ret, address, contract.Gas.ExitSuccess(), err } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index e18da5775a..4a2973e7a2 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -293,4 +293,9 @@ func (g *GasBudget) Absorb(child GasBudget) { g.UsedRegularGas += child.UsedRegularGas g.StateGas = child.StateGas g.UsedStateGas += child.UsedStateGas + // A successful child propagates its spilled state gas to the parent so a + // later parent halt burns it (spec incorporate_child_on_success). On revert + // or halt the child's leftover SpilledStateGas is already zero (the exit + // form refilled it), so this is a no-op there. + g.SpilledStateGas += child.SpilledStateGas } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 6b86dfb6c6..4ff30ae220 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -663,6 +663,9 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation) res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value) + // EIP-8037: a successful create to an already-alive target leaf creates no + // new account; create() records that in createTargetAliveTemp. + targetWasAlive := evm.chainRules.IsAmsterdam && suberr == nil && evm.createTargetAliveTemp // Push item on the stack based on the returned error. If the ruleset is // homestead we must check for CodeStoreOutOfGasError (homestead only // rule) and treat as an error, if the ruleset is frontier we must @@ -679,10 +682,10 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Refund the leftover gas back to current frame scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) - // EIP-8037: no account was created on any failure path, so refund the - // account-creation state gas charged before the opcode ran (gasCreateEip8037) - // in LIFO order (regular gas up to the spilled amount, then the reservoir). - if evm.chainRules.IsAmsterdam && suberr != nil { + // EIP-8037: refund the account-creation state gas charged before the opcode + // ran (gasCreateEip8037) in LIFO order when no new account leaf is created: + // any failure path, or a success where the target leaf already existed. + if evm.chainRules.IsAmsterdam && (suberr != nil || targetWasAlive) { scope.Contract.Gas.CreditStateRefund(params.AccountCreationSize * evm.Context.CostPerStateByte) } @@ -707,8 +710,12 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // reuse size int for stackvalue stackvalue := size + child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt) + // EIP-8037: a successful create to an already-alive target leaf creates no + // new account; create() records that in createTargetAliveTemp. + targetWasAlive := evm.chainRules.IsAmsterdam && suberr == nil && evm.createTargetAliveTemp // Push item on the stack based on the returned error. if suberr != nil { stackvalue.Clear() @@ -720,10 +727,10 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Refund the leftover gas back to current frame scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) - // EIP-8037: no account was created on any failure path, so refund the - // account-creation state gas charged before the opcode ran (gasCreate2Eip8037) - // in LIFO order (regular gas up to the spilled amount, then the reservoir). - if evm.chainRules.IsAmsterdam && suberr != nil { + // EIP-8037: refund the account-creation state gas charged before the opcode + // ran (gasCreate2Eip8037) in LIFO order when no new account leaf is created: + // any failure path, or a success where the target leaf already existed. + if evm.chainRules.IsAmsterdam && (suberr != nil || targetWasAlive) { scope.Contract.Gas.CreditStateRefund(params.AccountCreationSize * evm.Context.CostPerStateByte) }