diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 22a3d71c3..b54f19fd0 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -52,7 +52,9 @@ func memoryGasCost(mem *Memory, newMemSize uint64) (*multigas.MultiGas, uint64, fee := newTotalFee - mem.lastGasCost mem.lastGasCost = newTotalFee - return multigas.ZeroGas(), fee, nil + // Memory expansion considered as computation. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + return multigas.ComputationGas(fee), fee, nil } return multigas.ZeroGas(), 0, nil } @@ -404,82 +406,106 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (*multigas.MultiGas, uint64, error) { var ( - gas uint64 + multiGas = multigas.ZeroGas() transfersValue = !stack.Back(2).IsZero() address = common.Address(stack.Back(1).Bytes20()) ) + + // Storage slot writes (zero → nonzero) considered as storage growth. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md if evm.chainRules.IsEIP158 { if transfersValue && evm.StateDB.Empty(address) { - gas += params.CallNewAccountGas + multiGas.SafeIncrement(multigas.ResourceKindStorageGrowth, params.CallNewAccountGas) } } else if !evm.StateDB.Exist(address) { - gas += params.CallNewAccountGas + multiGas.SafeIncrement(multigas.ResourceKindStorageGrowth, params.CallNewAccountGas) } + + // Value transfer to non-empty account considered as computation. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md if transfersValue && !evm.chainRules.IsEIP4762 { - gas += params.CallValueTransferGas + multiGas.SafeIncrement(multigas.ResourceKindComputation, params.CallValueTransferGas) } - multiGas, memoryGas, err := memoryGasCost(mem, memorySize) + + memoryMultiGas, _, err := memoryGasCost(mem, memorySize) if err != nil { return multigas.ZeroGas(), 0, err } - // TODO(NIT-3484): Update multi dimensional gas here - var overflow bool - if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { + multiGas, overflow := multiGas.SafeAdd(multiGas, memoryMultiGas) + if overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } + if evm.chainRules.IsEIP4762 && !contract.IsSystemCall { if transfersValue { - gas, overflow = math.SafeAdd(gas, evm.AccessEvents.ValueTransferGas(contract.Address(), address)) - if overflow { + valueTransferGas := evm.AccessEvents.ValueTransferGas(contract.Address(), address) + // Account lookups considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + if overflow := multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, valueTransferGas); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } } } - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) + + singleGas, _ := multiGas.SingleGas() + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, singleGas, stack.Back(0)) if err != nil { return multigas.ZeroGas(), 0, err } - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { + // Call gas forwarding considered as computation. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + if overflow = multiGas.SafeIncrement(multigas.ResourceKindComputation, evm.callGasTemp); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } - return multiGas, gas, nil + singleGas, _ = multiGas.SingleGas() + return multiGas, singleGas, nil } func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (*multigas.MultiGas, uint64, error) { - multiGas, memoryGas, err := memoryGasCost(mem, memorySize) + memoryMultiGas, _, err := memoryGasCost(mem, memorySize) if err != nil { return multigas.ZeroGas(), 0, err } var ( - gas uint64 + multiGas = multigas.ZeroGas() overflow bool ) if stack.Back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 { - gas += params.CallValueTransferGas + // Value transfer to non-empty account considered as computation. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + multiGas.SafeIncrement(multigas.ResourceKindComputation, params.CallValueTransferGas) } - // TODO(NIT-3484): Update multi dimensional gas here - if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { + multiGas, overflow = multiGas.SafeAdd(multiGas, memoryMultiGas) + if overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } if evm.chainRules.IsEIP4762 && !contract.IsSystemCall { address := common.Address(stack.Back(1).Bytes20()) transfersValue := !stack.Back(2).IsZero() if transfersValue { - gas, overflow = math.SafeAdd(gas, evm.AccessEvents.ValueTransferGas(contract.Address(), address)) - if overflow { + valueTransferGas := evm.AccessEvents.ValueTransferGas(contract.Address(), address) + // Account lookups considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + if overflow = multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, valueTransferGas); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } } } - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) + + singleGas, _ := multiGas.SingleGas() + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, singleGas, stack.Back(0)) if err != nil { return multigas.ZeroGas(), 0, err } - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { + // Call gas forwarding considered as computation. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + if overflow = multiGas.SafeIncrement(multigas.ResourceKindComputation, evm.callGasTemp); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } - return multiGas, gas, nil + + singleGas, _ = multiGas.SingleGas() + return multiGas, singleGas, nil } func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (*multigas.MultiGas, uint64, error) { @@ -491,12 +517,14 @@ func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me if err != nil { return multigas.ZeroGas(), 0, err } - var overflow bool - // TODO(NIT-3484): Update multi dimensional gas here - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { + // Call gas forwarding considered as computation. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + if overflow := multiGas.SafeIncrement(multigas.ResourceKindComputation, evm.callGasTemp); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } - return multiGas, gas, nil + + singleGas, _ := multiGas.SingleGas() + return multiGas, singleGas, nil } func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (*multigas.MultiGas, uint64, error) { @@ -508,12 +536,14 @@ func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo if err != nil { return multigas.ZeroGas(), 0, err } - var overflow bool - // TODO(NIT-3484): Update multi dimensional gas here - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { + // Call gas forwarding considered as computation. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + if overflow := multiGas.SafeIncrement(multigas.ResourceKindComputation, evm.callGasTemp); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } - return multiGas, gas, nil + + singleGas, _ := multiGas.SingleGas() + return multiGas, singleGas, nil } func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (*multigas.MultiGas, uint64, error) { diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 5b9dd903b..461dd6730 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -42,7 +42,10 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { ) // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + // Cold slot access considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, params.ColdSloadCostEIP2929) + // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } @@ -51,6 +54,9 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { if current == value { // noop (1) // EIP 2200 original clause: // return params.SloadGasEIP2200, nil + + // Warm slot access considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, params.WarmStorageReadCostEIP2929) singleGas, _ := multiGas.SingleGas() return multiGas, singleGas, nil // SLOAD_GAS @@ -58,6 +64,8 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) if original == current { if original == (common.Hash{}) { // create slot (2.1.1) + // Creating a new slot considered as storage growth. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md multiGas.SafeIncrement(multigas.ResourceKindStorageGrowth, params.SstoreSetGasEIP2200) singleGas, _ := multiGas.SingleGas() return multiGas, singleGas, nil @@ -67,6 +75,9 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } // EIP-2200 original clause: // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + + // Storage slot writes (nonzero → zero) considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, params.SstoreResetGasEIP2200-params.ColdSloadCostEIP2929) singleGas, _ := multiGas.SingleGas() return multiGas, singleGas, nil // write existing slot (2.1.2) @@ -94,6 +105,9 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } // EIP-2200 original clause: //return params.SloadGasEIP2200, nil // dirty update (2.2) + + // Warm slot access considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, params.WarmStorageReadCostEIP2929) singleGas, _ := multiGas.SingleGas() return multiGas, singleGas, nil // dirty update (2.2) @@ -194,12 +208,14 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g // also become correctly reported to tracers. contract.Gas += coldCost - // TODO(NIT-3484): Update multi dimensional gas here - var overflow bool - if gas, overflow = math.SafeAdd(gas, coldCost); overflow { + // Cold slot access considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + if overflow := multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, coldCost); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } - return multigas.ZeroGas(), gas, nil + + singleGas, _ := multiGas.SingleGas() + return multiGas, singleGas, nil } } @@ -265,8 +281,8 @@ var ( func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (*multigas.MultiGas, uint64, error) { var ( - total uint64 // total dynamic gas used - addr = common.Address(stack.Back(1).Bytes20()) + multiGas = multigas.ZeroGas() // total dynamic gas used + addr = common.Address(stack.Back(1).Bytes20()) ) // Check slot presence in the access list @@ -280,7 +296,9 @@ func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return multigas.ZeroGas(), 0, ErrOutOfGas } - total += coldCost + // Cold slot access considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, coldCost) } // Check if code is a delegation and if so, charge for resolution. @@ -295,7 +313,10 @@ func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { if !contract.UseGas(cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return multigas.ZeroGas(), 0, ErrOutOfGas } - total += cost + + // Target address resolution considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, cost) } // Now call the old calculator, which takes into account @@ -312,12 +333,14 @@ func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { // adding it to the return, it will be charged outside of this function, as // part of the dynamic gas. This will ensure it is correctly reported to // tracers. - contract.Gas += total + contract.Gas += multiGas.Get(multigas.ResourceKindStorageAccess) var overflow bool - if total, overflow = math.SafeAdd(old, total); overflow { + if multiGas, overflow = multiGas.SafeAdd(multiGas, multiOld); overflow { return multigas.ZeroGas(), 0, ErrGasUintOverflow } - return multigas.ZeroGas(), total, nil + + singleGas, _ := multiGas.SingleGas() + return multiGas, singleGas, nil } } diff --git a/core/vm/operaions_gas_test.go b/core/vm/operations_gas_test.go similarity index 53% rename from core/vm/operaions_gas_test.go rename to core/vm/operations_gas_test.go index 8a80dd4b1..ff9ea9b27 100644 --- a/core/vm/operaions_gas_test.go +++ b/core/vm/operations_gas_test.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -449,3 +450,506 @@ func TestGasSStore4762(t *testing.T) { t.Errorf("Expected single gas %d, got %d", expectedSingleGas, singleGas) } } + +type GasCallFuncTestCase struct { + name string // descriptive name for the test case + slotInAccessList bool // whether the slot is in the access list + transfersValue bool // whether the call transfers value + valueTransferGas uint64 // gas argument passed on stack + targetExists bool // whether the target account exists + targetEmpty bool // whether the target account is empty (no balance/code) + isEIP158 bool // whether EIP-158 rules apply (empty account handling) + isEIP4762 bool // whether EIP-4762 rules apply (Verkle trees) + addWitnessGas bool // whether to add witness gas for EIP-4762 + isEIP2929 bool // whether EIP-2929 rules apply (access lists) + isSystemCall bool // whether this is a system call (bypasses some gas costs) + memorySize uint64 // memory size for the operation (triggers memory expansion) +} + +func testGasCallFuncFuncWithCases(t *testing.T, config *params.ChainConfig, gasCallFunc gasFunc, testCases []GasCallFuncTestCase, isCallCode bool) { + t.Helper() + + contractGas := uint64(100000) + slotKey := common.HexToHash("0xdeadbeef") // any dummy key + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + stateDb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + stack := newstack() + + // Configure chain rules + if tc.isEIP4762 { + verkleTime := uint64(0) + config.VerkleTime = &verkleTime + } + + blockCtx := BlockContext{ + BlockNumber: big.NewInt(1), + } + evm := NewEVM(blockCtx, stateDb, config, Config{}) + + // Set chain rules based on test case + evm.chainRules.IsEIP158 = tc.isEIP158 + evm.chainRules.IsEIP4762 = tc.isEIP4762 + + // Setup target address + caller := common.Address{1} + targetAddr := common.Address{2} + + // Setup caller account + stateDb.CreateAccount(caller) + stateDb.SetBalance(caller, uint256.NewInt(1000000), tracing.BalanceChangeUnspecified) + + if tc.targetExists { + if tc.targetEmpty { + stateDb.CreateAccount(targetAddr) + } else { + stateDb.CreateAccount(targetAddr) + stateDb.SetBalance(targetAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + } + } + + if tc.slotInAccessList { + stateDb.AddSlotToAccessList(targetAddr, slotKey) + } + + // Setup contract + contract := NewContract(caller, caller, new(uint256.Int), contractGas, nil) + contract.IsSystemCall = tc.isSystemCall + contract.Gas = contractGas + + // Setup stack: [value, address, gas] (bottom to top) + if tc.transfersValue { + stack.push(new(uint256.Int).SetUint64(1)) // value + } else { + stack.push(new(uint256.Int).SetUint64(0)) // value + } + stack.push(new(uint256.Int).SetBytes(targetAddr.Bytes())) // address + stack.push(new(uint256.Int).SetUint64(tc.valueTransferGas)) // gas + + // Setup two instances of memory to avoid side effects in `memoryGasCost` + mem := NewMemory() + memForExpected := NewMemory() + + // Mock AccessEvents for EIP4762 + if tc.isEIP4762 { + accessEvents := state.NewAccessEvents(stateDb.PointCache()) + evm.AccessEvents = accessEvents + } + + // Initialize expected multi gas with transfer gas value + expectedMultiGas := multigas.ComputationGas(tc.valueTransferGas) + + // For EIP-2929 (access lists) + wasColdAccess := !tc.isEIP2929 || !evm.StateDB.AddressInAccessList(targetAddr) + if tc.isEIP2929 && wasColdAccess && !tc.slotInAccessList { + expectedMultiGas.SafeIncrement(multigas.ResourceKindStorageAccess, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929) + } + + // Apply cahin rules + if tc.transfersValue && !tc.isEIP4762 { + expectedMultiGas.SafeIncrement(multigas.ResourceKindComputation, params.CallValueTransferGas) + } + + // Account creation gas only applies to gasCall, not gasCallCode + if !isCallCode && wasColdAccess { + if tc.isEIP158 { + if tc.transfersValue && tc.targetEmpty { + expectedMultiGas.SafeIncrement(multigas.ResourceKindStorageGrowth, params.CallNewAccountGas) + } + } else if !tc.targetExists { + expectedMultiGas.SafeIncrement(multigas.ResourceKindStorageGrowth, params.CallNewAccountGas) + } + } + + // Call `memoryGasCost` to get the memory gas cost + memoryMultiGas, _, err := memoryGasCost(memForExpected, tc.memorySize) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expectedMultiGas, _ = expectedMultiGas.SafeAdd(expectedMultiGas, memoryMultiGas) + + // EIP4762 storage access gas for value transfers + if tc.isEIP4762 && tc.transfersValue && !tc.isSystemCall { + valueTransferGas := evm.AccessEvents.ValueTransferGas(contract.Address(), caller) + if overflow := expectedMultiGas.SafeIncrement(multigas.ResourceKindStorageAccess, valueTransferGas); overflow { + t.Fatalf("Expected multi gas overflow for test case %s", tc.name) + } + } + + // For EIP-4762 (Witnesses gas) + if tc.addWitnessGas && !contract.IsSystemCall { + // Calculated in `touchAddressAndChargeGas` WitnessBranchReadCost + WitnessChunkReadCost = 2100 + expectedMultiGas.SafeIncrement(multigas.ResourceKindStorageAccess, 2100) + } + + // Call the function + multiGas, _, err := gasCallFunc(evm, contract, stack, mem, tc.memorySize) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if *multiGas != *expectedMultiGas { + t.Errorf("Expected multi gas %d, got %d", expectedMultiGas, multiGas) + } + }) + } +} + +// CALL gas function test +func TestGasCall(t *testing.T) { + testCases := []GasCallFuncTestCase{ + { + name: "No value transfer with memory expansion", + transfersValue: false, + valueTransferGas: 80000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: false, + isSystemCall: false, + memorySize: 500, + }, + { + name: "Value transfer to existing account", + transfersValue: true, + valueTransferGas: 70000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: false, + isSystemCall: false, + memorySize: 0, + }, + { + name: "Value transfer to empty account (EIP158)", + transfersValue: true, + valueTransferGas: 60000, + targetExists: true, + targetEmpty: true, + isEIP158: true, + isEIP4762: false, + isSystemCall: false, + memorySize: 0, + }, + { + name: "Non-existent account (pre-EIP158)", + transfersValue: false, + valueTransferGas: 55000, + targetExists: false, + targetEmpty: false, + isEIP158: false, + isEIP4762: false, + isSystemCall: false, + memorySize: 0, + }, + { + name: "EIP4762 value transfer (not system call)", + transfersValue: true, + valueTransferGas: 45000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + isSystemCall: false, + memorySize: 0, + }, + { + name: "EIP4762 value transfer (system call)", + transfersValue: true, + valueTransferGas: 40000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + isSystemCall: true, + memorySize: 0, + }, + { + name: "EIP4762 no value transfer with memory", + transfersValue: false, + valueTransferGas: 35000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + isSystemCall: false, + memorySize: 256, + }, + } + testGasCallFuncFuncWithCases(t, params.TestChainConfig, gasCall, testCases, false) +} + +// CALLCODE gas function test +func TestGasCallCode(t *testing.T) { + // NOTE: targetExists/targetEmpty fields don't affect gasCallCode but kept for consistency + testCases := []GasCallFuncTestCase{ + { + name: "No value transfer with memory expansion", + transfersValue: false, + valueTransferGas: 80000, + memorySize: 500, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: false, + isSystemCall: false, + }, + { + name: "Value transfer (pre-EIP4762)", + transfersValue: true, + valueTransferGas: 70000, + memorySize: 0, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: false, + isSystemCall: false, + }, + { + name: "Value transfer with memory expansion", + transfersValue: true, + valueTransferGas: 65000, + memorySize: 256, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: false, + isSystemCall: false, + }, + { + name: "EIP4762 value transfer (not system call)", + transfersValue: true, + valueTransferGas: 45000, + memorySize: 0, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + isSystemCall: false, + }, + { + name: "EIP4762 value transfer (system call)", + transfersValue: true, + valueTransferGas: 40000, + memorySize: 0, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + isSystemCall: true, + }, + { + name: "EIP4762 no value transfer with memory", + transfersValue: false, + valueTransferGas: 50000, + memorySize: 128, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + isSystemCall: false, + }, + { + name: "Pre-EIP158 with value transfer", + transfersValue: true, + valueTransferGas: 55000, + memorySize: 0, + targetExists: true, + targetEmpty: false, + isEIP158: false, + isEIP4762: false, + isSystemCall: false, + }, + } + + testGasCallFuncFuncWithCases(t, params.TestChainConfig, gasCallCode, testCases, true) +} + +// CALL decorated with makeCallVariantGasCallEIP2929 gas function test +func TestCallVariantGasCallEIP2929(t *testing.T) { + testCases := []GasCallFuncTestCase{ + { + name: "Cold access to existing account", + slotInAccessList: false, + transfersValue: false, + valueTransferGas: 80000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: false, + isEIP2929: true, + isSystemCall: false, + memorySize: 0, + }, + { + name: "Warm access (repeated)", + slotInAccessList: true, + transfersValue: false, + valueTransferGas: 80000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: false, + isEIP2929: true, + isSystemCall: false, + memorySize: 0, + }, + } + + gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall, 1) + testGasCallFuncFuncWithCases(t, params.TestChainConfig, gasCallEIP2929, testCases, false) +} + +func TestVariantGasEIP4762(t *testing.T) { + testCases := []GasCallFuncTestCase{ + { + name: "EIP4762 non-system call with witness cost", + transfersValue: false, + valueTransferGas: 50000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + addWitnessGas: true, + isSystemCall: false, + memorySize: 64, + }, + { + name: "EIP4762 system call skips witness cost", + transfersValue: false, + valueTransferGas: 50000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP4762: true, + addWitnessGas: true, + isSystemCall: true, + memorySize: 64, + }, + } + + gasCallEIP4762 = makeCallVariantGasEIP4762(gasCallCode) + testGasCallFuncFuncWithCases(t, params.TestChainConfig, gasCallEIP4762, testCases, true) +} + +func TestCallVariantGasCallEIP7702(t *testing.T) { + testCases := []GasCallFuncTestCase{ + { + name: "7702 cold access, non-delegate, no value", + slotInAccessList: false, + transfersValue: false, + valueTransferGas: 50000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP2929: true, + isSystemCall: false, + memorySize: 128, + }, + { + name: "7702 warm access, with value transfer", + slotInAccessList: true, + transfersValue: true, + valueTransferGas: 60000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP2929: true, + isSystemCall: false, + memorySize: 64, + }, + { + name: "7702 cold access, empty account, value transfer", + slotInAccessList: false, + transfersValue: true, + valueTransferGas: 55000, + targetExists: true, + targetEmpty: true, + isEIP158: true, + isEIP2929: true, + isSystemCall: false, + memorySize: 0, + }, + { + name: "7702 no value, warm slot", + slotInAccessList: true, + transfersValue: false, + valueTransferGas: 45000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP2929: true, + isSystemCall: false, + memorySize: 32, + }, + { + name: "7702 system call skips access gas", + slotInAccessList: false, + transfersValue: false, + valueTransferGas: 40000, + targetExists: true, + targetEmpty: false, + isEIP158: true, + isEIP2929: true, + isSystemCall: true, + memorySize: 16, + }, + } + + wrapped := makeCallVariantGasCallEIP7702(gasCall) + testGasCallFuncFuncWithCases(t, params.TestChainConfig, wrapped, testCases, false) +} + +func testGasDelegateOrStaticCall(t *testing.T, gasImplFunc gasFunc) { + t.Helper() + + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + evm := NewEVM(BlockContext{}, statedb, params.TestChainConfig, Config{}) + + caller := common.Address{} + contractAddr := common.Address{1} + contractGas := uint64(100000) + contract := NewContract(caller, contractAddr, new(uint256.Int), contractGas, nil) + + stack := newstack() + mem := NewMemory() + memForExpected := NewMemory() + + stack.push(new(uint256.Int).SetUint64(50000)) + memorySize := uint64(64) + + expectedMultiGas, memorySingleGas, err := memoryGasCost(memForExpected, memorySize) + if err != nil { + t.Fatalf("Failed memoryGasCost: %v", err) + } + + callGas, err := callGas(evm.chainRules.IsEIP150, contractGas, memorySingleGas, stack.Back(0)) + if err != nil { + t.Fatalf("Failed callGas: %v", err) + } + + expectedMultiGas.SafeIncrement(multigas.ResourceKindComputation, callGas) + expectedSingleGas := memorySingleGas + callGas + + // Call gasImplFunc + multiGas, singleGas, err := gasImplFunc(evm, contract, stack, mem, memorySize) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if *multiGas != *expectedMultiGas { + t.Errorf("Expected multi gas %d, got %d", expectedMultiGas, multiGas) + } + if singleGas != expectedSingleGas { + t.Errorf("Expected single gas %d, got %d", expectedSingleGas, singleGas) + } +} + +func TestGasDelegateCall(t *testing.T) { + testGasDelegateOrStaticCall(t, gasDelegateCall) +} + +func TestGasStaticCall(t *testing.T) { + testGasDelegateOrStaticCall(t, gasStaticCall) +} diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go index 9222821ea..302e04814 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -93,16 +93,20 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc) gasFunc { return multigas.ZeroGas(), 0, err } if contract.IsSystemCall { - return multigas.ZeroGas(), gas, nil + return multiGas, gas, nil } if _, isPrecompile := evm.precompile(contract.Address()); isPrecompile { - return multigas.ZeroGas(), gas, nil + return multiGas, gas, nil } witnessGas := evm.AccessEvents.MessageCallGas(contract.Address()) if witnessGas == 0 { witnessGas = params.WarmStorageReadCostEIP2929 } - return multiGas, witnessGas + gas, nil + // Witness gas considered as storage access. + // See rationale in: https://github.com/OffchainLabs/nitro/blob/master/docs/decisions/0002-multi-dimensional-gas-metering.md + multiGas.SafeIncrement(multigas.ResourceKindStorageAccess, witnessGas) + singleGas, _ := multiGas.SingleGas() + return multiGas, singleGas, nil } }