diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 9eb1bdbf5f..6167cb28a6 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -147,7 +147,7 @@ func Transaction(ctx *cli.Context) error { } // For Prague txs, validate the floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations()))) if err != nil { r.Error = err results = append(results, r) diff --git a/core/state_transition.go b/core/state_transition.go index 07d8055322..e55e95fd78 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -151,8 +151,11 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set return gas, nil } -// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). -func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) { +// FloorDataGas computes the minimum gas required for a transaction based on its +// data tokens (EIP-7623). On Amsterdam it also includes the EIP-8131 per-auth +// tx-content floor and the EIP-8279 per-auth Block Access List floor, which +// together form the static floor seed extended at runtime by EIP-8279. +func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList, numAuths uint64) (uint64, error) { var ( tokens uint64 tokenCost uint64 @@ -203,7 +206,23 @@ func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) return 0, ErrGasUintOverflow } // Minimum gas required for a transaction based on its data tokens (EIP-7623). - return params.TxGas + tokens*tokenCost, nil + floor := params.TxGas + tokens*tokenCost + + // EIP-8131 / EIP-8279: each EIP-7702 authorization contributes a static + // per-auth floor. EIP-8131 prices the 101-byte authorization tuple + // (FloorCostPerAuth) and EIP-8279 adds the worst-case BAL bytes the auth + // writes when applied (BALBytesPerAuthorization at FloorGasPerByte). The + // per-auth BAL term is folded into the static floor because set_delegation + // runs outside the EVM's out-of-gas handler and cannot extend the floor at + // runtime. + if rules.IsAmsterdam && numAuths > 0 { + const perAuth = params.FloorCostPerAuth + params.BALBytesPerAuthorization*params.FloorGasPerByte + if (math.MaxUint64-floor)/perAuth < numAuths { + return 0, ErrGasUintOverflow + } + floor += numAuths * perAuth + } + return floor, nil } // toWordSize returns the ceiled word size required for init code payment calculation. @@ -355,6 +374,11 @@ type stateTransition struct { initReservoir uint64 // initial state-gas reservoir carved out of GasLimit (EIP-8037) state vm.StateDB evm *vm.EVM + + // floorGas is the EIP-8279 Block Access List floor accumulator, seeded with + // the static floor and extended at runtime via the EVM. It is nil before + // Amsterdam. settleGas reads its final value to apply the receipt floor. + floorGas *vm.FloorGasAccumulator } // newStateTransition initialises and returns a new state transition object. @@ -645,7 +669,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Validate the EIP-7623 calldata floor against the gas limit. The floor inflates // the total gas usage at tx end, so the gas limit must be sufficient to cover that. if rules.IsPrague { - floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) + floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList, uint64(len(msg.SetCodeAuthorizations))) if err != nil { return nil, err } @@ -661,6 +685,15 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } + // EIP-8279: seed the per-transaction Block Access List floor accumulator + // with the static floor and bound it by the transaction gas limit. The + // accumulator is extended at runtime as opcodes contribute BAL bytes; at + // settlement the receipt gas becomes max(execution_gas, floor_gas_used). + if rules.IsAmsterdam { + st.floorGas = vm.NewFloorGasAccumulator(floorDataGas, msg.GasLimit) + st.evm.SetFloorGas(st.floorGas) + } + if rules.IsEIP4762 { st.evm.AccessEvents.AddTxOrigin(msg.From) @@ -837,25 +870,34 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g gasLeft += refund gasUsed = gasUsedBeforeRefund - refund - // EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, calldata_floor). + // EIP-8279: the effective floor is the runtime accumulator (seeded with the + // static floorDataGas and extended by the BAL bytes opcodes contributed). It + // is always >= floorDataGas when the accumulator is active (Amsterdam); the + // max keeps the pre-Amsterdam / accumulator-less path on the static floor. + floorGas := floorDataGas + if st.floorGas != nil { + floorGas = max(floorGas, st.floorGas.FloorGasUsed()) + } + + // EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, floor). peakUsed = gasUsedBeforeRefund - if rules.IsPrague && gasUsed < floorDataGas { - diff := floorDataGas - gasUsed + if rules.IsPrague && gasUsed < floorGas { + diff := floorGas - gasUsed if st.evm.Config.Tracer.HasGasHook() { st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{Regular: gasLeft - diff}, tracing.GasChangeTxDataFloor) } gasLeft -= diff - gasUsed = floorDataGas - peakUsed = max(peakUsed, floorDataGas) + gasUsed = floorGas + peakUsed = max(peakUsed, floorGas) } 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) + // spec accumulates max(tx_regular_gas, floor) into block_gas_used, so the + // block must never count fewer regular units than the floor the sender + // was charged. EIP-8279 widens the floor to include BAL byte costs. + blockRegularGas := max(txRegularGas, floorGas) if err = st.gp.ChargeGasAmsterdam(blockRegularGas, txStateGas, gasUsed); err != nil { return 0, 0, err } diff --git a/core/state_transition_test.go b/core/state_transition_test.go index be2de7f511..0a3220f1f7 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -123,7 +123,7 @@ func TestFloorDataGas(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rules := params.Rules{IsAmsterdam: tt.amsterdam} - got, err := FloorDataGas(rules, tt.data, tt.accessList) + got, err := FloorDataGas(rules, tt.data, tt.accessList, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 3b30dd30ef..08c66cc392 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -134,7 +134,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction can cover floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations()))) if err != nil { return err } diff --git a/core/vm/evm.go b/core/vm/evm.go index 8fd02a09ac..9dd6e069cd 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -131,6 +131,13 @@ type EVM struct { returnData []byte // Last CALL's return data for subsequent reuse arena *stackArena + + // floorGas is the per-transaction EIP-8279 Block Access List byte-floor + // accumulator. It is set by the state transition at the start of each + // transaction and extended at runtime as opcodes contribute BAL bytes. It + // is nil before EIP-8279 (Amsterdam) or in contexts without BAL + // construction, in which case the runtime extensions are no-ops. + floorGas *FloorGasAccumulator } // NewEVM constructs an EVM instance with the supplied block context, state @@ -222,6 +229,26 @@ func (evm *EVM) SetTxContext(txCtx TxContext) { evm.TxContext = txCtx } +// SetFloorGas installs the per-transaction EIP-8279 floor accumulator. It is +// called by the state transition once the static floor seed and gas limit are +// known. Passing nil disables runtime floor extensions for the transaction. +func (evm *EVM) SetFloorGas(acc *FloorGasAccumulator) { + evm.floorGas = acc +} + +// FloorGas returns the active EIP-8279 floor accumulator, or nil if none is set. +func (evm *EVM) FloorGas() *FloorGasAccumulator { + return evm.floorGas +} + +// extendFloor extends the EIP-8279 floor accumulator by numBytes BAL bytes. It +// is a no-op when no accumulator is installed (pre-Amsterdam or BAL-less +// contexts). The returned error, when non-nil, is ErrOutOfGas and MUST abort +// the operation before the matching BAL byte is inserted. +func (evm *EVM) extendFloor(numBytes uint64) error { + return evm.floorGas.extendFloor(numBytes) +} + // Cancel cancels any running EVM operation. This may be called concurrently and // it's safe to be called multiple times. func (evm *EVM) Cancel() { @@ -524,6 +551,15 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } } + // EIP-8279: an opcode-level CREATE/CREATE2 records the deployed address in + // the BAL. The top-level creation transaction's contract address is part of + // the implicit per-tx bytes covered by TX_BASE headroom, so only nested + // creations extend the floor here. + if evm.depth > 0 { + if err = evm.extendFloor(params.BALBytesPerAddress); err != nil { + return nil, common.Address{}, gas.ExitHalt(), err + } + } // We add this to the access list _before_ taking a snapshot. Even if the // creation fails, the access-list change should not be rolled back. if evm.chainRules.IsEIP2929 { @@ -562,6 +598,21 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value // acts inside that account. evm.StateDB.CreateContract(address) + // EIP-8279: an opcode-level CREATE/CREATE2 sets the new contract's nonce + // (and, with a non-zero endowment, its balance), both recorded in the BAL. + // The floor is extended after the collision check, before the state + // mutation. The top-level creation transaction's nonce is covered by + // TX_BASE headroom, so only nested creations extend the floor here. + if evm.depth > 0 { + if err = evm.extendFloor(params.BALBytesPerNonce); err != nil { + return nil, common.Address{}, gas.ExitHalt(), err + } + if !value.IsZero() { + if err = evm.extendFloor(params.BALBytesPerBalance); err != nil { + return nil, common.Address{}, gas.ExitHalt(), err + } + } + } if evm.chainRules.IsEIP158 { evm.StateDB.SetNonce(address, 1, tracing.NonceChangeNewContract) } @@ -668,6 +719,16 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) (ret return ret, true, err } } + // EIP-8279: a successful opcode-level CREATE/CREATE2 deploy records the + // deployed code in the BAL. Extend the floor by the code length before + // set_code. A top-level creation transaction's deployed code is bounded by + // the calldata floor on its init code, so only nested creations extend the + // floor here. + if evm.depth > 0 && len(ret) > 0 { + if err := evm.extendFloor(uint64(len(ret))); err != nil { + return ret, true, err + } + } if len(ret) > 0 { evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation) } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 7fcfe4f595..d640acf216 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -504,6 +504,15 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m } // Stateful check if evm.chainRules.IsAmsterdam { + // EIP-8279: a CALL transferring non-zero value to a different account + // records a balance change for the recipient in the BAL. Extend the + // floor by the balance bytes before the transfer. A self-call moves no + // value out of the executing account and adds no balance bytes. + if transfersValue && address != contract.Address() { + if err := evm.extendFloor(params.BALBytesPerBalance); err != nil { + return 0, err + } + } // EIP-8037: the cost of creating a new account via a value-bearing // CALL is metered as state gas (NEW_ACCOUNT * CostPerStateByte), // not the legacy regular CallNewAccountGas. It drains the state @@ -604,6 +613,10 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory address = common.Address(stack.peek().Bytes20()) ) if !evm.StateDB.AddressInAccessList(address) { + // EIP-8279: a cold beneficiary access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddAddressToAccessList(address) gas.RegularGas = params.ColdAccountAccessCostEIP2929 @@ -612,6 +625,14 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory if contract.Gas.RegularGas < gas.RegularGas { return gas, ErrOutOfGas } + // EIP-8279: SELFDESTRUCT moving a non-zero balance to a different beneficiary + // records the beneficiary's balance change in the BAL. A self-targeted + // SELFDESTRUCT moves no value out and adds no balance bytes. + if address != contract.Address() && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + if err := evm.extendFloor(params.BALBytesPerBalance); err != nil { + return GasCosts{}, err + } + } // Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist // in the current state yet still be considered non-existent by EIP-161 if its // nonce, balance, and code are all zero. Such accounts can appear temporarily @@ -641,12 +662,29 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo ) // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + // EIP-8279: a cold SSTORE adds the storage key to the BAL. Extend the + // floor before the slot is recorded; an out-of-gas here aborts the + // opcode before the unpaid BAL byte exists. + if err := evm.extendFloor(params.BALBytesPerStorageKey); err != nil { + return GasCosts{}, err + } cost = GasCosts{RegularGas: params.ColdSloadCostEIP2929} // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } value := common.Hash(y.Bytes32()) + // EIP-8279: an SSTORE that changes the slot's current value contributes a + // post-value to the BAL. Charge the value bytes whenever the value differs, + // mirroring the BAL StorageWrite. This may over-charge when the same slot is + // written more than once in a transaction (the BAL records only one final + // post-value per slot), which is safe: it never under-charges. + if current != value { + if err := evm.extendFloor(params.BALBytesPerStorageValue); err != nil { + return GasCosts{}, err + } + } + if current == value { // noop (1) // EIP 2200 original clause: // return params.SloadGasEIP2200, nil diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 2206fb95fa..c9d99584e7 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -103,6 +103,12 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me slot := common.Hash(loc.Bytes32()) // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + // EIP-8279: a cold SLOAD adds the storage key to the BAL. Extend the + // floor before the slot is recorded; an out-of-gas here aborts the + // opcode before the unpaid BAL byte exists. + if err := evm.extendFloor(params.BALBytesPerStorageKey); err != nil { + return GasCosts{}, err + } // If the caller cannot afford the cost, this change will be rolled back // If he does afford it, we can skip checking the same thing later on, during execution evm.StateDB.AddSlotToAccessList(contract.Address(), slot) @@ -126,6 +132,10 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(addr) var overflow bool // We charge (cold-warm), since 'warm' is already charged as constantGas @@ -148,6 +158,10 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddAddressToAccessList(addr) // The warm storage read cost is already charged as constantGas @@ -165,6 +179,10 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g // the cost to charge for cold access, if any, is Cold - Warm coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 if !warmAccess { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(addr) // Charge the remaining difference here already, to correctly calculate available // gas for call @@ -286,6 +304,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // Perform EIP-2929 checks (stateless), checking address presence // in the accessList and charge the cold access accordingly. if !evm.StateDB.AddressInAccessList(addr) { + // EIP-8279: a cold account access adds the address to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(addr) // The WarmStorageReadCostEIP2929 (100) is already deducted in the form @@ -321,6 +343,11 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { if evm.StateDB.AddressInAccessList(target) { eip7702Cost = params.WarmStorageReadCostEIP2929 } else { + // EIP-8279: resolving a cold delegation target adds its address + // to the BAL. + if err := evm.extendFloor(params.BALBytesPerAddress); err != nil { + return GasCosts{}, err + } evm.StateDB.AddAddressToAccessList(target) eip7702Cost = params.ColdAccountAccessCostEIP2929 } diff --git a/params/protocol_params.go b/params/protocol_params.go index 69e10fa5d9..6725db91f2 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -103,6 +103,28 @@ const ( TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037 + // FloorCostPerAuth is the per-authorization tx-content floor contribution + // defined by EIP-8131: an EIP-7702 authorization tuple is 101 bytes, charged + // at the floor rate (101 * TxCostFloorPerToken7976 * TxTokenPerNonZeroByte). + FloorCostPerAuth uint64 = 101 * TxCostFloorPerToken7976 * TxTokenPerNonZeroByte // 6464 + + // EIP-8279: Block Access List Byte Floor. Each byte an opcode adds to the + // EIP-7928 Block Access List extends the transaction's floor accumulator by + // FloorGasPerByte gas, charged at runtime before the BAL grows. + FloorGasPerByte uint64 = TxCostFloorPerToken7976 * TxTokenPerNonZeroByte // 64: per-byte floor rate (EIP-7976) + BALBytesPerAddress uint64 = 20 // BAL bytes for an account address + BALBytesPerStorageKey uint64 = 32 // BAL bytes for a storage key + BALBytesPerStorageValue uint64 = 32 // BAL bytes for a storage post-value + BALBytesPerBalance uint64 = 32 // BAL bytes for a balance change + BALBytesPerNonce uint64 = 8 // BAL bytes for a nonce change + BALDelegationCodeBytes uint64 = 23 // EIP-7702 delegation marker length + // BALBytesPerAuthorization is the worst-case BAL contribution an EIP-7702 + // authorization adds when it is applied: the authority address, the + // delegation marker written to its code, and its nonce change. It is folded + // into the static floor seed since set_delegation runs outside the EVM's + // out-of-gas handler. + BALBytesPerAuthorization uint64 = BALBytesPerAddress + BALDelegationCodeBytes + BALBytesPerNonce // 51 + // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. CallGasEIP150 uint64 = 700 // Static portion of gas for CALL-derivates after EIP 150 (Tangerine) diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 91f7d6c3ec..bc6c369e37 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -92,7 +92,7 @@ func (tt *TransactionTest) Run() error { if rules.IsPrague { var floorDataGas uint64 - floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList(), uint64(len(tx.SetCodeAuthorizations()))) if err != nil { return }