From 9059157eba227b6a4d4cc2e147e65f9919294360 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Fri, 12 Jun 2026 14:29:03 +0200 Subject: [PATCH] core: implement EIP-8037, state creation gas cost increase (#33601) Implements https://eips.ethereum.org/EIPS/eip-8037 mainly done in order to judge the complexity of the EIP and to act as a jumping off point, since the eip will likely change. --------- Co-authored-by: Gary Rong --- cmd/evm/internal/t8ntool/execution.go | 17 +- cmd/evm/internal/t8ntool/transaction.go | 2 +- core/bal_test.go | 78 +-- core/bench_test.go | 2 +- core/bintrie_witness_test.go | 6 +- core/blockchain.go | 8 + core/evm.go | 25 +- core/gaspool.go | 74 ++- core/state_processor.go | 30 +- core/state_transition.go | 479 ++++++++++++------ core/state_transition_test.go | 105 +++- .../tracing/gen_gas_change_reason_stringer.go | 7 +- core/tracing/hooks.go | 4 + core/txpool/validation.go | 9 +- core/vm/common.go | 1 - core/vm/contract.go | 49 +- core/vm/contracts.go | 3 +- core/vm/contracts_fuzz_test.go | 2 +- core/vm/contracts_test.go | 8 +- core/vm/eips.go | 15 +- core/vm/evm.go | 255 ++++++---- core/vm/gas_table.go | 161 +++++- core/vm/gas_table_test.go | 12 +- core/vm/gascosts.go | 269 ++++++++-- core/vm/instructions.go | 71 +-- core/vm/interpreter.go | 16 +- core/vm/interpreter_test.go | 4 +- core/vm/jump_table.go | 6 +- core/vm/operations_acl.go | 18 +- core/vm/runtime/env.go | 24 +- core/vm/runtime/runtime.go | 22 +- eth/tracers/js/tracer_test.go | 2 +- eth/tracers/logger/logger_test.go | 2 +- params/protocol_params.go | 8 + tests/state_test.go | 6 +- tests/transaction_test_util.go | 4 +- 36 files changed, 1257 insertions(+), 547 deletions(-) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 85a581eba6..f35b28510c 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -183,14 +183,15 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, blockAccessList = bal.NewConstructionBlockAccessList() ) vmContext := vm.BlockContext{ - CanTransfer: core.CanTransfer, - Transfer: core.Transfer, - Coinbase: pre.Env.Coinbase, - BlockNumber: new(big.Int).SetUint64(pre.Env.Number), - Time: pre.Env.Timestamp, - Difficulty: pre.Env.Difficulty, - GasLimit: pre.Env.GasLimit, - GetHash: getHash, + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + Coinbase: pre.Env.Coinbase, + BlockNumber: new(big.Int).SetUint64(pre.Env.Number), + Time: pre.Env.Timestamp, + Difficulty: pre.Env.Difficulty, + GasLimit: pre.Env.GasLimit, + GetHash: getHash, + CostPerStateByte: params.CostPerStateByte, } if pre.Env.SlotNumber != nil { vmContext.SlotNum = *pre.Env.SlotNumber diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index ca19ae3386..9eb1bdbf5f 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -133,7 +133,7 @@ func Transaction(ctx *cli.Context) error { } // Check intrinsic gas rules := chainConfig.Rules(common.Big0, true, 0) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) if err != nil { r.Error = err results = append(results, r) diff --git a/core/bal_test.go b/core/bal_test.go index d20108b96c..b27901c083 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -153,6 +153,14 @@ func assertEmpty(t *testing.T, aa *bal.AccountAccess) { } } +// txGasNewAccount covers the base tx cost plus the EIP-8037 account-creation +// state-gas charge (STATE_BYTES_PER_NEW_ACCOUNT × CPSB ≈ 183,600) that is +// incurred when value is transferred to a non-existent account under Amsterdam. +// params.TxGas (21,000) alone is insufficient: the transfer would run out of +// gas, the credit would revert, and the recipient would never get a balance +// change recorded in the BAL. +const txGasNewAccount = 250_000 + // --- tx builders --- func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction { @@ -177,7 +185,7 @@ func TestBALTxSenderAndRecipient(t *testing.T) { env := newBALTestEnv(nil) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil)) + g.AddTx(env.tx(0, &to, big.NewInt(1000), txGasNewAccount, 0, nil)) }) sender := assertPresent(t, b, env.from) @@ -260,7 +268,7 @@ func TestBALSystemAddressIncludedWhenTouched(t *testing.T) { env := newBALTestEnv(nil) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil)) + g.AddTx(env.tx(0, &sys, big.NewInt(1000), txGasNewAccount, 0, nil)) }) aa := assertPresent(t, b, sys) @@ -283,7 +291,7 @@ func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, identity) @@ -315,7 +323,7 @@ func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) { env := newBALTestEnv(nil) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil)) + g.AddTx(env.tx(0, &identity, big.NewInt(5), txGasNewAccount, 0, nil)) }) aa := assertPresent(t, b, identity) @@ -334,7 +342,7 @@ func TestBALBalanceProbeOnNonExistent(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) assertEmpty(t, assertPresent(t, b, probe)) @@ -350,7 +358,7 @@ func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) assertEmpty(t, assertPresent(t, b, probe)) @@ -366,7 +374,7 @@ func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) assertEmpty(t, assertPresent(t, b, probe)) @@ -385,7 +393,7 @@ func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) assertEmpty(t, assertPresent(t, b, probe)) @@ -447,7 +455,7 @@ func TestBALCallTargetWithEmptyChangeSet(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &target, big.NewInt(0), 1_000_000, 0, nil)) }) assertEmpty(t, assertPresent(t, b, target)) @@ -465,7 +473,7 @@ func TestBALCallCodeTargetIncluded(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) assertPresent(t, b, caller) @@ -483,7 +491,7 @@ func TestBALDelegateCallTargetIncluded(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) assertPresent(t, b, caller) @@ -501,7 +509,7 @@ func TestBALStaticCallTargetIncluded(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) }) assertPresent(t, b, caller) @@ -519,7 +527,7 @@ func TestBALRevertedTxStillIncluded(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 1_000_000, 0, nil)) }) assertEmpty(t, assertPresent(t, b, reverter)) @@ -533,7 +541,7 @@ func TestBALSenderRecordedOnRevert(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 1_000_000, 0, nil)) }) sender := assertPresent(t, b, env.from) @@ -557,7 +565,7 @@ func TestBALStorageWriteRecorded(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, contract) @@ -578,7 +586,7 @@ func TestBALStorageSloadOnly(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, contract) @@ -604,7 +612,7 @@ func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, contract) @@ -632,7 +640,7 @@ func TestBALNoOpSSTOREDemotesToRead(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, contract) @@ -660,7 +668,7 @@ func TestBALStorageWriteZeroIsAWrite(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, contract) @@ -687,7 +695,7 @@ func TestBALCreateDeploysCode(t *testing.T) { init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3} b, receipts := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init)) + g.AddTx(env.tx(0, nil, big.NewInt(7), 1_000_000, 0, init)) }) created := receipts[0].ContractAddress @@ -711,7 +719,7 @@ func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) { init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} b, receipts := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init)) }) created := receipts[0].ContractAddress @@ -732,7 +740,7 @@ func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) { init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} b, receipts := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init)) }) created := receipts[0].ContractAddress @@ -743,11 +751,13 @@ func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) { // the deployed address in the BAL with an empty change set. func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) { env := newBALTestEnv(nil) - // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG. + // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG. The + // gas budget must cover EIP-8037 intrinsic state gas (account creation) + // so the tx is accepted; OOG must happen inside the init code. init := []byte{0x5b, 0x60, 0x00, 0x56} b, receipts := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init)) + g.AddTx(env.tx(0, nil, big.NewInt(0), 220_000, 0, init)) }) created := receipts[0].ContractAddress @@ -771,7 +781,7 @@ func TestBALCreateAddressCollisionStillIncluded(t *testing.T) { // Init code doesn't matter — execution never starts. init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init)) }) aa := assertPresent(t, b, collide) @@ -799,7 +809,7 @@ func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil)) + g.AddTx(env.tx(0, &factory, big.NewInt(0), 1_000_000, 0, nil)) }) // The address that WOULD have been deployed had the create succeeded. @@ -842,7 +852,7 @@ func TestBALInEVMCreateDeploysContract(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil)) + g.AddTx(env.tx(0, &factory, big.NewInt(0), 1_000_000, 0, nil)) }) // Deployed address depends on the factory's nonce at the moment of CREATE, @@ -870,7 +880,7 @@ func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) { init = append(init, 0xff) b, receipts := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init)) }) created := receipts[0].ContractAddress @@ -896,7 +906,7 @@ func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) { init = append(init, 0xff) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init)) + g.AddTx(env.tx(0, nil, big.NewInt(100), 1_000_000, 0, init)) }) ben := assertPresent(t, b, beneficiary) @@ -921,7 +931,7 @@ func TestBALSelfDestructPreExistingContract(t *testing.T) { }) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil)) + g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, suicidal) @@ -966,7 +976,7 @@ func TestBALMidTxBalanceRoundTrip(t *testing.T) { env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}}) b, _ := env.run(t, func(g *BlockGen) { - g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil)) + g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 1_000_000, 0, nil)) }) aa := assertPresent(t, b, bouncer) @@ -1072,7 +1082,7 @@ func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) { Nonce: 0, To: env.from, Value: new(uint256.Int), - Gas: 200_000, + Gas: 1_000_000, GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), GasTipCap: new(uint256.Int), AuthList: []types.SetCodeAuthorization{auth}, @@ -1112,7 +1122,7 @@ func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) { Nonce: 0, To: env.from, // tx.to is an EOA with no code: delegate is never called Value: new(uint256.Int), - Gas: 200_000, + Gas: 1_000_000, GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), GasTipCap: new(uint256.Int), AuthList: []types.SetCodeAuthorization{auth}, @@ -1131,7 +1141,7 @@ func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address, Nonce: nonce, To: to, Value: new(uint256.Int), - Gas: 400_000, + Gas: 1_000_000, GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), GasTipCap: new(uint256.Int), AuthList: auths, diff --git a/core/bench_test.go b/core/bench_test.go index 65179c54d4..fe66aeae0d 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false) + cost, _ := IntrinsicGas(data, nil, nil, false, params.Rules{}, params.CostPerStateByte) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index b49ac83bb5..e151a72801 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -64,12 +64,12 @@ var ( func TestProcessUBT(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness // will not contain that copied data. // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) - intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true, false) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain @@ -201,7 +201,7 @@ func TestProcessParentBlockHash(t *testing.T) { if isUBT { chainConfig = testUBTChainConfig } - vmContext := NewEVMBlockContext(header, nil, new(common.Address)) + vmContext := NewEVMBlockContext(header, &BlockChain{chainConfig: chainConfig}, new(common.Address)) evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{}) ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList()) } diff --git a/core/blockchain.go b/core/blockchain.go index c914a6dd81..acf2da1921 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2316,6 +2316,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation stats.CrossValidation = xvtime // The time spent on stateless cross validation + // Attach the computed block access list so it gets persisted alongside the + // block. The validator has already verified the hash matches the header. + // BAL is only meaningful from Amsterdam onward; skip pre-Amsterdam blocks + // to avoid persisting and serving empty BALs over the network. + if res.Bal != nil && block.AccessList() == nil && bc.chainConfig.IsAmsterdam(block.Number(), block.Time()) { + block = block.WithAccessListUnsafe(res.Bal.ToEncodingObj()) + } + // Write the block to the chain and get the status. var status WriteStatus if config.WriteState { diff --git a/core/evm.go b/core/evm.go index 73e4c01a99..fdea63e469 100644 --- a/core/evm.go +++ b/core/evm.go @@ -68,18 +68,19 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common } return vm.BlockContext{ - CanTransfer: CanTransfer, - Transfer: Transfer, - GetHash: GetHashFn(header, chain), - Coinbase: beneficiary, - BlockNumber: new(big.Int).Set(header.Number), - Time: header.Time, - Difficulty: new(big.Int).Set(header.Difficulty), - BaseFee: baseFee, - BlobBaseFee: blobBaseFee, - GasLimit: header.GasLimit, - Random: random, - SlotNum: slotNum, + CanTransfer: CanTransfer, + Transfer: Transfer, + GetHash: GetHashFn(header, chain), + Coinbase: beneficiary, + BlockNumber: new(big.Int).Set(header.Number), + Time: header.Time, + Difficulty: new(big.Int).Set(header.Difficulty), + BaseFee: baseFee, + BlobBaseFee: blobBaseFee, + GasLimit: header.GasLimit, + Random: random, + SlotNum: slotNum, + CostPerStateByte: params.CostPerStateByte, } } diff --git a/core/gaspool.go b/core/gaspool.go index 14f5abd93c..83420c7640 100644 --- a/core/gaspool.go +++ b/core/gaspool.go @@ -27,6 +27,10 @@ type GasPool struct { remaining uint64 initial uint64 cumulativeUsed uint64 + + // After 8037 Block gas used is max(cumulativeRegular, cumulativeState). + cumulativeRegular uint64 + cumulativeState uint64 } // NewGasPool initializes the gasPool with the given amount. @@ -37,9 +41,9 @@ func NewGasPool(amount uint64) *GasPool { } } -// SubGas deducts the given amount from the pool if enough gas is +// CheckGasLegacy deducts the given amount from the pool if enough gas is // available and returns an error otherwise. -func (gp *GasPool) SubGas(amount uint64) error { +func (gp *GasPool) CheckGasLegacy(amount uint64) error { if gp.remaining < amount { return ErrGasLimitReached } @@ -47,41 +51,73 @@ func (gp *GasPool) SubGas(amount uint64) error { return nil } -// ReturnGas adds the refunded gas back to the pool and updates +// CheckGasAmsterdam performs the EIP-8037 per-tx 2D block-inclusion check: +// the worst-case regular contribution must fit in the regular dimension and +// the worst-case state contribution must fit in the state dimension +func (gp *GasPool) CheckGasAmsterdam(regularReservation, stateReservation uint64) error { + if gp.initial-gp.cumulativeRegular < regularReservation { + return ErrGasLimitReached + } + if gp.initial-gp.cumulativeState < stateReservation { + return ErrGasLimitReached + } + return nil +} + +// ChargeGasLegacy adds the refunded gas back to the pool and updates // the cumulative gas usage accordingly. -func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error { +func (gp *GasPool) ChargeGasLegacy(returned uint64, gasUsed uint64) error { if gp.remaining > math.MaxUint64-returned { return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, returned) } - // The returned gas calculation differs across forks. - // - // - Pre-Amsterdam: - // returned = purchased - remaining (refund included) - // - // - Post-Amsterdam: - // returned = purchased - gasUsed (refund excluded) + // returned = purchased - remaining (refund included) gp.remaining += returned // gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost) - // regardless of Amsterdam is activated or not. gp.cumulativeUsed += gasUsed return nil } +// ChargeGasAmsterdam calculates the new remaining gas in the pool after the +// execution of a message. Previously we subtracted and re-added gas to the +// gaspool. After Amsterdam we only check if we can include the transaction +// and charge the gaspool at the end. +func (gp *GasPool) ChargeGasAmsterdam(txRegular, txState, receiptGasUsed uint64) error { + cumulativeRegular := gp.cumulativeRegular + txRegular + cumulativeState := gp.cumulativeState + txState + blockUsed := max(cumulativeRegular, cumulativeState) + if gp.initial < blockUsed { + return fmt.Errorf("%w: block gas overflow: initial %d, used %d (regular: %d, state: %d)", + ErrGasLimitReached, gp.initial, blockUsed, cumulativeRegular, cumulativeState) + } + gp.cumulativeRegular = cumulativeRegular + gp.cumulativeState = cumulativeState + gp.cumulativeUsed += receiptGasUsed + // TODO(rjl, marius), the semantics of this counter is slightly different + // in the context of Amsterdam, the API Gas() should be reworked. + gp.remaining = gp.initial - gp.cumulativeRegular + return nil +} + // Gas returns the amount of gas remaining in the pool. func (gp *GasPool) Gas() uint64 { return gp.remaining } -// CumulativeUsed returns the amount of cumulative consumed gas (refunded included). +// CumulativeUsed returns the cumulative gas consumed for receipt tracking. func (gp *GasPool) CumulativeUsed() uint64 { return gp.cumulativeUsed } // Used returns the amount of consumed gas. func (gp *GasPool) Used() uint64 { + // After 8037, return max(sum_regular, sum_state) + if gp.cumulativeRegular > 0 || gp.cumulativeState > 0 { + return max(gp.cumulativeRegular, gp.cumulativeState) + } + // Before 8037, return initial-remaining if gp.initial < gp.remaining { - panic("gas used underflow") + panic(fmt.Sprintf("gas used underflow: %v %v", gp.initial, gp.remaining)) } return gp.initial - gp.remaining } @@ -89,9 +125,11 @@ func (gp *GasPool) Used() uint64 { // Snapshot returns the deep-copied object as the snapshot. func (gp *GasPool) Snapshot() *GasPool { return &GasPool{ - initial: gp.initial, - remaining: gp.remaining, - cumulativeUsed: gp.cumulativeUsed, + initial: gp.initial, + remaining: gp.remaining, + cumulativeUsed: gp.cumulativeUsed, + cumulativeRegular: gp.cumulativeRegular, + cumulativeState: gp.cumulativeState, } } @@ -100,6 +138,8 @@ func (gp *GasPool) Set(other *GasPool) { gp.initial = other.initial gp.remaining = other.remaining gp.cumulativeUsed = other.cumulativeUsed + gp.cumulativeRegular = other.cumulativeRegular + gp.cumulativeState = other.cumulativeState } func (gp *GasPool) String() string { diff --git a/core/state_processor.go b/core/state_processor.go index 5f43206eb4..f40aee0301 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -271,6 +271,21 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header * return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) } +// systemCallGasBudget returns the gas budget for system calls. +func systemCallGasBudget(evm *vm.EVM) (gasLimit uint64, gasBudget vm.GasBudget) { + if !evm.GetRules().IsAmsterdam { + gasLimit = 30_000_000 + gasBudget = vm.NewGasBudget(gasLimit, 0) + } else { + // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of + // new storage slots a single system call is expected to write. + stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize + gasLimit = 30_000_000 + gasBudget = vm.NewGasBudget(gasLimit, stateBudget) + } + return gasLimit, gasBudget +} + // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root // contract. This method is exported to be used in tests. func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { @@ -280,9 +295,10 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList defer tracer.OnSystemCallEnd() } } + gasLimit, gasBudget := systemCallGasBudget(evm) msg := &Message{ From: params.SystemAddress, - GasLimit: 30_000_000, + GasLimit: gasLimit, GasPrice: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0), GasTipCap: uint256.NewInt(0), @@ -293,7 +309,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) - _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) + _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, gasBudget, common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } @@ -309,9 +325,10 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList * defer tracer.OnSystemCallEnd() } } + gasLimit, gasBudget := systemCallGasBudget(evm) msg := &Message{ From: params.SystemAddress, - GasLimit: 30_000_000, + GasLimit: gasLimit, GasPrice: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0), GasTipCap: uint256.NewInt(0), @@ -322,7 +339,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList * evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) - _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) + _, _, err := evm.Call(msg.From, *msg.To, msg.Data, gasBudget, common.U2560) if err != nil { panic(err) } @@ -351,9 +368,10 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E defer tracer.OnSystemCallEnd() } } + gasLimit, gasBudget := systemCallGasBudget(evm) msg := &Message{ From: params.SystemAddress, - GasLimit: 30_000_000, + GasLimit: gasLimit, GasPrice: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0), GasTipCap: uint256.NewInt(0), @@ -363,7 +381,7 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.AddAddressToAccessList(addr) - ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) + ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, gasBudget, common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } diff --git a/core/state_transition.go b/core/state_transition.go index 724b9963f8..dac8123530 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,13 +68,27 @@ func (result *ExecutionResult) Revert() []byte { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860, isAmsterdam bool) (vm.GasCosts, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation bool, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { // Set the starting gas for the raw transaction - var gas uint64 - if isContractCreation && isHomestead { - gas = params.TxGasContractCreation + var gas vm.GasCosts + if isContractCreation && rules.IsHomestead { + if rules.IsAmsterdam { + gas.RegularGas = params.TxGas + params.CreateGasAmsterdam + gas.StateGas = params.AccountCreationSize * costPerStateByte + } else { + gas.RegularGas = params.TxGasContractCreation + } } else { - gas = params.TxGas + gas.RegularGas = params.TxGas + } + // Add gas for authorizations + if authList != nil { + if rules.IsAmsterdam { + gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas + gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte + } else { + gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas + } } dataLen := uint64(len(data)) // Bump the required gas by the amount of transactional data @@ -85,59 +99,56 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set // Make sure we don't exceed uint64 for all data combinations nonZeroGas := params.TxDataNonZeroGasFrontier - if isEIP2028 { + if rules.IsIstanbul { nonZeroGas = params.TxDataNonZeroGasEIP2028 } - if (math.MaxUint64-gas)/nonZeroGas < nz { + if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz { return vm.GasCosts{}, ErrGasUintOverflow } - gas += nz * nonZeroGas + gas.RegularGas += nz * nonZeroGas - if (math.MaxUint64-gas)/params.TxDataZeroGas < z { + if (math.MaxUint64-gas.RegularGas)/params.TxDataZeroGas < z { return vm.GasCosts{}, ErrGasUintOverflow } - gas += z * params.TxDataZeroGas + gas.RegularGas += z * params.TxDataZeroGas - if isContractCreation && isEIP3860 { + if isContractCreation && rules.IsShanghai { lenWords := toWordSize(dataLen) - if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords { + if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords { return vm.GasCosts{}, ErrGasUintOverflow } - gas += lenWords * params.InitCodeWordGas + gas.RegularGas += lenWords * params.InitCodeWordGas } } if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) - if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses { + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas += addresses * params.TxAccessListAddressGas - if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys { + gas.RegularGas += addresses * params.TxAccessListAddressGas + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas += storageKeys * params.TxAccessListStorageKeyGas + gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas // EIP-7981: access list data is charged in addition to the base charge. - if isAmsterdam { + if rules.IsAmsterdam { const ( addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte ) - if (math.MaxUint64-gas)/addressCost < addresses { + if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas += addresses * addressCost - if (math.MaxUint64-gas)/storageKeyCost < storageKeys { + gas.RegularGas += addresses * addressCost + if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas += storageKeys * storageKeyCost + gas.RegularGas += storageKeys * storageKeyCost } } - if authList != nil { - gas += uint64(len(authList)) * params.CallNewAccountGas - } - return vm.GasCosts{RegularGas: gas}, nil + return gas, nil } // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). @@ -338,12 +349,11 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err // 5. Run Script section // 6. Derive new state root type stateTransition struct { - gp *GasPool - msg *Message - initialBudget vm.GasBudget - gasRemaining vm.GasBudget - state vm.StateDB - evm *vm.EVM + gp *GasPool + msg *Message + gasRemaining vm.GasBudget + state vm.StateDB + evm *vm.EVM } // newStateTransition initialises and returns a new state transition object. @@ -364,6 +374,24 @@ func (st *stateTransition) to() common.Address { return *st.msg.To } +// buyGas pre-pays gas from the sender's balance and initializes the +// transaction's gas budget. It is invoked at the tail of preCheck. +// +// The balance requirement is the worst-case ETH the tx may need to lock +// up: `msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) + msg.Value`, +// plus `blobGas × msg.BlobGasFeeCap` under Cancun. Insufficient balance +// returns ErrInsufficientFunds. After the check, the sender is actually +// debited `msg.GasLimit × msg.GasPrice` (plus `blobGas × blobBaseFee` +// under Cancun), the cap-vs-tip differential is settled at tx end. +// +// The gas budget is seeded into both `initialBudget` (frozen snapshot +// for tx-end accounting) and `gasRemaining` (live running balance): +// +// - Pre-Amsterdam: one-dimensional regular budget equal to +// `msg.GasLimit`; the state-gas reservoir is zero. +// - Amsterdam+ (EIP-8037): two-dimensional budget. Regular gas is +// capped at `MaxTxGas` (EIP-7825, 16_777_216); any excess from +// `msg.GasLimit` above that cap becomes the state-gas reservoir. func (st *stateTransition) buyGas() error { mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) _, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) @@ -416,22 +444,53 @@ func (st *stateTransition) buyGas() error { if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } - if err := st.gp.SubGas(st.msg.GasLimit); err != nil { + isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) + + // Reserve the gas budget in the block gas pool + var err error + if isAmsterdam { + err = st.gp.CheckGasAmsterdam(min(st.msg.GasLimit, params.MaxTxGas), st.msg.GasLimit) + } else { + err = st.gp.CheckGasLegacy(st.msg.GasLimit) + } + if err != nil { return err } - if st.evm.Config.Tracer.HasGasHook() { - empty := vm.GasBudget{} - initial := vm.NewGasBudget(st.msg.GasLimit) - st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance) + // After Amsterdam we limit the regular gas to 16M, the data gas to the transaction limit + limit := st.msg.GasLimit + if isAmsterdam { + limit = min(st.msg.GasLimit, params.MaxTxGas) } - st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit) - st.initialBudget = st.gasRemaining.Copy() + st.gasRemaining = vm.NewGasBudget(limit, st.msg.GasLimit-limit) + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance) + } + // Deduct the gas cost from the sender's balance st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) return nil } +// preCheck performs all pre-execution validation that does not require +// the EVM to run, then ends by calling buyGas to lock in the gas budget. +// It returns a consensus error if any of the following fail: +// +// - Sender nonce matches state and is not at 2^64-1 (EIP-2681). +// - EIP-7825 per-tx gas-limit cap on Osaka chains pre-Amsterdam +// (the cap also bounds the regular dimension after Amsterdam, but +// it is enforced there via the two-dimensional budget in buyGas). +// - EIP-3607 sender-is-EOA, allowing accounts whose only code is an +// EIP-7702 delegation designator. +// - EIP-1559 fee-cap, tip-cap and base-fee constraints (London+). +// - Blob-tx structural checks: non-nil `To`, non-empty hash list, +// valid KZG versioned hashes, count below `BlobTxMaxBlobs` (Osaka+). +// - Blob fee-cap not below the current blob base fee (Cancun+). +// - EIP-7702 set-code-tx shape: non-nil `To` and non-empty +// authorization list. +// +// The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass +// subsets of these checks for simulation paths (eth_call, eth_estimateGas). func (st *stateTransition) preCheck() error { // Only check transactions that are not fake msg := st.msg @@ -449,11 +508,13 @@ func (st *stateTransition) preCheck() error { msg.From.Hex(), stNonce) } } - isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) - isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) + var ( + isOsaka = st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) + isAmsterdam = st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) + ) if !msg.SkipTransactionChecks { // Verify tx gas limit does not exceed EIP-7825 cap. - if isOsaka && !isAmsterdam && msg.GasLimit > params.MaxTxGas { + if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas { return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) } // Make sure the sender is an EOA @@ -527,40 +588,33 @@ func (st *stateTransition) preCheck() error { return st.buyGas() } -// execute will transition the state by applying the current message and -// returning the evm execution result with following fields. +// execute transitions the state by applying the current message and +// returns the EVM execution result with the following fields: // -// - used gas: total gas used (including gas being refunded) -// - returndata: the returned data from evm -// - concrete execution error: various EVM errors which abort the execution, e.g. -// ErrOutOfGas, ErrExecutionReverted +// - used gas: total gas used, including gas refunded +// - peak used gas: maximum gas used before applying refunds +// - returndata: data returned by the EVM +// - execution error: EVM-level errors that abort execution, such as +// ErrOutOfGas or ErrExecutionReverted // -// However if any consensus issue encountered, return the error directly with -// nil evm execution result. +// If a consensus error is encountered, it is returned directly with a +// nil EVM execution result. func (st *stateTransition) execute() (*ExecutionResult, error) { - // First check this message satisfies all consensus rules before - // applying the message. The rules include these clauses - // - // 1. the nonce of the message caller is correct - // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) - // 3. the amount of gas required is available in the block - // 4. the purchased gas is enough to cover intrinsic usage - // 5. there is no overflow when calculating intrinsic gas - // 6. caller has enough balance to cover asset transfer for **topmost** call - - // Check clauses 1-3, buy gas if everything is correct + // Validate the message and pre-pay gas. if err := st.preCheck(); err != nil { return nil, err } + // Charge intrinsic gas (with overflow detection inside IntrinsicGas). + // Under Amsterdam the cost is two-dimensional and Charge debits both + // regular and state in one step. var ( msg = st.msg rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) contractCreation = msg.To == nil floorDataGas uint64 ) - // Check clauses 4-5, subtract intrinsic gas if everything is correct - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err } @@ -571,15 +625,24 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if st.evm.Config.Tracer.HasGasHook() { st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) } - // Gas limit suffices for the floor data cost (EIP-7623) + + // 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) if err != nil { return nil, err } + // Make sure the transaction has sufficient gas allowance to + // pay the floor cost. if msg.GasLimit < floorDataGas { return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) } + // In Amsterdam, the transaction gas limit is allowed to exceed + // params.MaxTxGas, but the calldata floor cost is capped by it. + if rules.IsAmsterdam && max(cost.RegularGas, floorDataGas) > params.MaxTxGas { + return nil, fmt.Errorf("%w: regular intrisic cost %v, floor: %v", ErrFloorDataGas, cost.RegularGas, floorDataGas) + } } if rules.IsEIP4762 { @@ -590,7 +653,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } - // Check clause 6 + // Top-call affordability, the sender must still be able to cover the value + // transfer of the top frame after gas pre-pay. value := msg.Value if value == nil { value = new(uint256.Int) @@ -599,36 +663,43 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) } - // Check whether the init code size has been exceeded. - if contractCreation { - if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil { - return nil, err - } - } - // Execute the preparatory steps for state transition which includes: // - prepare accessList(post-berlin) // - reset transient storage(EIP-1153) // - enable block-level accessList construction (EIP-7928) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) + // Execute the top-most frame var ( - ret []byte - vmerr error // vm errors do not effect consensus and are therefore not assigned to err + ret []byte + vmerr error // vm errors do not effect consensus and are therefore not assigned to err + result vm.GasBudget + + // Capture the forwarded regular-gas amount BEFORE ForwardAll consumes + // it, so Absorb can back out state-gas spillover from UsedRegularGas + // per EIP-8037. + forwarded = st.gasRemaining.RegularGas ) if contractCreation { - ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) + // Check whether the init code size has been exceeded. + if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil { + return nil, err + } + // Execute the transaction's creation. + ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result, forwarded) + + // If the contract creation failed, refund the account-creation state + // gas pre-charged in IntrinsicGas. + if rules.IsAmsterdam && vmerr != nil { + 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) // 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(&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 @@ -638,44 +709,18 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { st.state.AddAddressToAccessList(addr) } - // Execute the transaction's call. - ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) + ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result, forwarded) } - // Record the gas used excluding gas refunds. This value represents the actual - // gas allowance required to complete execution. - peakGasUsed := st.gasUsed() - - // Compute refund counter, capped to a refund quotient. - st.gasRemaining.Refund(st.calcRefund()) - - if rules.IsPrague { - // After EIP-7623: Data-heavy transactions pay the floor gas. - if used := st.gasUsed(); used < floorDataGas { - prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used}) - if st.evm.Config.Tracer.HasGasHook() { - st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor) - } - } - if peakGasUsed < floorDataGas { - peakGasUsed = floorDataGas - } - } - // Return gas to the user - st.returnGas() - - // Return gas to the gas pool - if rules.IsAmsterdam { - // Refund is excluded for returning - err = st.gp.ReturnGas(st.initialBudget.RegularGas-peakGasUsed, st.gasUsed()) - } else { - // Refund is included for returning - err = st.gp.ReturnGas(st.gasRemaining.RegularGas, st.gasUsed()) - } + // Settle down the gas usage and refund the ETH back if any remaining + gasUsed, peakUsed, err := st.settleGas(rules, floorDataGas) if err != nil { return nil, err } + + // Pay the effective transaction fee to the specific coinbase effectiveTip := msg.GasPrice if rules.IsLondon { baseFee, overflow := uint256.FromBig(st.evm.Context.BaseFee) @@ -684,13 +729,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } effectiveTip = new(uint256.Int).Sub(msg.GasPrice, baseFee) } - if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { // Skip fee payment when NoBaseFee is set and the fee fields // are 0. This avoids a negative effectiveTip being applied to // the coinbase when simulating calls. } else { - fee := new(uint256.Int).SetUint64(st.gasUsed()) + fee := new(uint256.Int).SetUint64(gasUsed) fee.Mul(fee, effectiveTip) st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee) @@ -699,19 +743,105 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64) } } + + // EIP-7708: Emit the ETH-burn logs if rules.IsAmsterdam { for _, log := range st.evm.StateDB.LogsForBurnAccounts() { st.evm.StateDB.AddLog(log) } } return &ExecutionResult{ - UsedGas: st.gasUsed(), - MaxUsedGas: peakGasUsed, + UsedGas: gasUsed, + MaxUsedGas: peakUsed, Err: vmerr, ReturnData: ret, }, nil } +// settleGas finalizes the per-tx gas accounting after EVM execution: +// +// - Snapshots the EIP-8037 block-level 2D figures (tx_regular_gas, +// tx_state_gas) before any refund or floor: +// +// tx_gas_used_before_refund = tx.gas - gas_left - state_gas_reservoir +// tx_state_gas = state_gas_used +// tx_regular_gas = tx_gas_used_before_refund - tx_state_gas +// +// - Computes the receipt scalar tx_gas_used by applying the EIP-3529 +// refund (capped at tx_gas_used_before_refund/5) and the EIP-7623 +// calldata floor: +// +// tx_gas_used = max(tx_gas_used_before_refund - tx_gas_refund, calldata_floor) +// +// - Charges the block gas pool (2D under Amsterdam, scalar pre-Amsterdam). +// +// - Refunds the leftover gas to the sender as ETH. +// +// Returns the receipt-level tx_gas_used and the pre-refund peak (consumed +// by gas-estimation callers via ExecutionResult.MaxUsedGas). UsedStateGas +// should never become negative in the top-most frame, since state-gas +// refunds occur only when state creation is reverted within the same +// transaction and clearing pre-existing state is never refunded. +func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (gasUsed, peakUsed uint64, err error) { + if st.gasRemaining.UsedStateGas < 0 { + return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas) + } + txStateGas := uint64(st.gasRemaining.UsedStateGas) + + // EIP-8037: + // tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir + // tx_state_gas = intrinsic_state_gas + tx_output.execution_state_gas_used + // tx_regular_gas = tx_gas_used_before_refund - tx_state_gas + gasLeft := st.gasRemaining.RegularGas + st.gasRemaining.StateGas + gasUsedBeforeRefund := st.msg.GasLimit - gasLeft + + if gasUsedBeforeRefund < txStateGas { + return 0, 0, fmt.Errorf("negative topmost frame regular gas usage, total: %d, state: %d", gasUsedBeforeRefund, txStateGas) + } + txRegularGas := gasUsedBeforeRefund - txStateGas + + // EIP-3529: tx_gas_refund = min(tx_gas_used_before_refund/5, refund_counter). + refund := st.calcRefund(gasUsedBeforeRefund) + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{Regular: gasLeft + refund}, tracing.GasChangeTxRefunds) + } + gasLeft += refund + gasUsed = gasUsedBeforeRefund - refund + + // EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, calldata_floor). + peakUsed = gasUsedBeforeRefund + if rules.IsPrague && gasUsed < floorDataGas { + diff := floorDataGas - 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) + } + + if rules.IsAmsterdam { + if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil { + return 0, 0, err + } + } else { + if err = st.gp.ChargeGasLegacy(gasLeft, gasUsed); err != nil { + return 0, 0, err + } + } + + // Refund leftover gas to the sender as ETH. + if gasLeft > 0 { + refund := new(uint256.Int).Mul(uint256.NewInt(gasLeft), st.msg.GasPrice) + st.state.AddBalance(st.msg.From, refund, tracing.BalanceIncreaseGasReturn) + + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{}, tracing.GasChangeTxLeftOverReturned) + } + } + return gasUsed, peakUsed, nil +} + // validateAuthorization validates an EIP-7702 authorization against the state. func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) { // Verify chain ID is null or equal to current chain ID. @@ -743,72 +873,93 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio return authority, nil } -// applyAuthorization applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error { +// applyAuthorization applies an EIP-7702 code delegation to the state and, +// under EIP-8037, reconciles the per-authorization intrinsic state-gas pre- +// charge so that, per authority: +// +// - the account portion (AccountCreationSize × CPSB) is charged at most +// once, and only when the account did not exist before the tx +// +// - the delegation-indicator portion (AuthorizationCreationSize × CPSB) is +// charged at most once, and only when the authority ends the tx delegated +// having started it undelegated. +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error { authority, err := st.validateAuthorization(auth) if err != nil { + if rules.IsAmsterdam { + st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) + } return err } + prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) - // If the account already exists in state, refund the new account cost - // charged in the intrinsic calculation. - if st.state.Exist(authority) { - st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) + if !rules.IsAmsterdam { + if st.state.Exist(authority) { + st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) + } + } else { + if st.state.Exist(authority) { + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + } + authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte + + preDelegated, ok := delegates[authority] + if !ok { + preDelegated = curDelegated + delegates[authority] = preDelegated + } + if auth.Address == (common.Address{}) { + // Clearing writes no indicator, refill this auth's state charge. + st.gasRemaining.RefundState(authBase) + + // The indicator was created by an earlier auth within the same + // transaction, refill the state charge as it's no longer justified. + if curDelegated && !preDelegated { + st.gasRemaining.RefundState(authBase) + } + } else if curDelegated || preDelegated { + // The 23-byte slot is already occupied, overwriting it writes no + // new bytes, refill the state charge. + st.gasRemaining.RefundState(authBase) + } } // Update nonce and account code. st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) + + // Delegation to zero address means clear. if auth.Address == (common.Address{}) { - // Delegation to zero address means clear. - st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) + if curDelegated { + st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) + } return nil } - - // Otherwise install delegation to auth.Address. - st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) - + // Install delegation to auth.Address if the delegation changed + if !curDelegated || auth.Address != prevDelegation { + st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + } return nil } -// calcRefund computes refund counter, capped to a refund quotient. -func (st *stateTransition) calcRefund() vm.GasBudget { - var refund uint64 - if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { - // Before EIP-3529: refunds were capped to gasUsed / 2 - refund = st.gasUsed() / params.RefundQuotient - } else { - // After EIP-3529: refunds are capped to gasUsed / 5 - refund = st.gasUsed() / params.RefundQuotientEIP3529 +// applyAuthorizations applies an EIP-7702 code delegation to the state. +func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { + preDelegated := make(map[common.Address]bool) + for _, auth := range auths { + st.applyAuthorization(rules, &auth, preDelegated) } +} + +// calcRefund computes the EIP-3529 refund cap against tx_gas_used_before_refund. +func (st *stateTransition) calcRefund(gasUsedBeforeRefund uint64) uint64 { + quotient := params.RefundQuotient + if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { + quotient = params.RefundQuotientEIP3529 + } + refund := gasUsedBeforeRefund / quotient if refund > st.state.GetRefund() { refund = st.state.GetRefund() } - if refund > 0 && st.evm.Config.Tracer.HasGasHook() { - after := st.gasRemaining - after.RegularGas += refund - - st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds) - } - return vm.NewGasBudget(refund) -} - -// returnGas returns ETH for remaining gas, -// exchanged at the original rate. -func (st *stateTransition) returnGas() { - remaining := uint256.NewInt(st.gasRemaining.RegularGas) - remaining.Mul(remaining, st.msg.GasPrice) - st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) - - if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() { - after := st.gasRemaining - after.RegularGas = 0 - st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned) - } -} - -// gasUsed returns the amount of gas used up by the state transition. -func (st *stateTransition) gasUsed() uint64 { - return st.gasRemaining.Used(st.initialBudget) + return refund } // blobGasUsed returns the amount of blob gas used by the message. diff --git a/core/state_transition_test.go b/core/state_transition_test.go index 8aab016123..be2de7f511 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -155,50 +155,50 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028 bool isEIP3860 bool isAmsterdam bool - want uint64 + want vm.GasCosts }{ { name: "frontier/empty-call", - want: params.TxGas, + want: vm.GasCosts{RegularGas: params.TxGas}, }, { name: "frontier/contract-creation-pre-homestead", creation: true, isHomestead: false, // pre-homestead, contract creation still uses TxGas - want: params.TxGas, + want: vm.GasCosts{RegularGas: params.TxGas}, }, { name: "homestead/contract-creation", creation: true, isHomestead: true, - want: params.TxGasContractCreation, + want: vm.GasCosts{RegularGas: params.TxGasContractCreation}, }, { name: "frontier/non-zero-data", data: bytes.Repeat([]byte{0xff}, 100), // 100 nz bytes * 68 (frontier) - want: params.TxGas + 100*params.TxDataNonZeroGasFrontier, + want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasFrontier}, }, { name: "istanbul/non-zero-data", data: bytes.Repeat([]byte{0xff}, 100), isEIP2028: true, // 100 nz bytes * 16 (post-EIP2028) - want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028, + want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasEIP2028}, }, { name: "istanbul/zero-data", data: bytes.Repeat([]byte{0x00}, 100), isEIP2028: true, // 100 zero bytes * 4 - want: params.TxGas + 100*params.TxDataZeroGas, + want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataZeroGas}, }, { name: "istanbul/mixed-data", data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), isEIP2028: true, - want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028, + want: vm.GasCosts{RegularGas: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028}, }, { name: "shanghai/init-code-word-gas", @@ -208,7 +208,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isEIP3860: true, // TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2 - want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, + want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas}, }, { name: "shanghai/init-code-non-multiple-of-32", @@ -217,7 +217,7 @@ func TestIntrinsicGas(t *testing.T) { isHomestead: true, isEIP2028: true, isEIP3860: true, - want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas, + want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas}, }, { name: "berlin/access-list", @@ -227,7 +227,7 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, // 2 addrs * 2400 + 3 keys * 1900 - want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas, + want: vm.GasCosts{RegularGas: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas}, }, { name: "amsterdam/access-list-extra-cost", @@ -238,9 +238,9 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isAmsterdam: true, // base access-list charge + EIP-7981 extra - want: params.TxGas + + want: vm.GasCosts{RegularGas: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas + - 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost, + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, }, { name: "prague/auth-list", @@ -250,8 +250,54 @@ func TestIntrinsicGas(t *testing.T) { {Address: addr1}, }, isEIP2028: true, - // 3 auths * 25000 - want: params.TxGas + 3*params.CallNewAccountGas, + // 3 auths * 25000 (pre-Amsterdam: CallNewAccountGas per auth tuple) + want: vm.GasCosts{RegularGas: params.TxGas + 3*params.CallNewAccountGas}, + }, + { + name: "amsterdam/contract-creation-empty", + creation: true, + isHomestead: true, + isEIP2028: true, + isAmsterdam: true, + // EIP-8037: creation regular gas is TxGas + CreateGasAmsterdam (not TxGasContractCreation), + // and account-creation cost is moved to state gas. + want: vm.GasCosts{ + RegularGas: params.TxGas + params.CreateGasAmsterdam, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + { + name: "amsterdam/contract-creation-init-code", + data: bytes.Repeat([]byte{0x00}, 64), // 2 words of init code + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, // Shanghai gates init-code word gas + isAmsterdam: true, + want: vm.GasCosts{ + RegularGas: params.TxGas + params.CreateGasAmsterdam + + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + { + name: "amsterdam/contract-creation-with-access-list", + data: bytes.Repeat([]byte{0xff}, 32), // 1 word of non-zero init code + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1}}, + }, + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, + isAmsterdam: true, + want: vm.GasCosts{ + RegularGas: params.TxGas + params.CreateGasAmsterdam + + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, }, { name: "amsterdam/combined", @@ -264,23 +310,34 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - want: params.TxGas + - 100*params.TxDataNonZeroGasEIP2028 + - 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + - 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + - 1*params.CallNewAccountGas, + // EIP-8037 splits the auth-tuple charge into regular + state gas: + // regular: TxAuthTupleRegularGas (7500) per auth + // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth + want: vm.GasCosts{ + RegularGas: params.TxGas + + 100*params.TxDataNonZeroGasEIP2028 + + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + + 1*params.TxAuthTupleRegularGas, + StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + rules := params.Rules{ + IsHomestead: tt.isHomestead, + IsIstanbul: tt.isEIP2028, + IsShanghai: tt.isEIP3860, + IsAmsterdam: tt.isAmsterdam, + } got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, - tt.creation, tt.isHomestead, tt.isEIP2028, tt.isEIP3860, tt.isAmsterdam) + tt.creation, rules, params.CostPerStateByte) if err != nil { t.Fatalf("unexpected error: %v", err) } - want := vm.GasCosts{RegularGas: tt.want} - if got != want { - t.Fatalf("gas mismatch: got %+v, want %+v", got, want) + if got != tt.want { + t.Fatalf("gas mismatch: got %+v, want %+v", got, tt.want) } }) } diff --git a/core/tracing/gen_gas_change_reason_stringer.go b/core/tracing/gen_gas_change_reason_stringer.go index e64be781a6..3d3aa96fad 100644 --- a/core/tracing/gen_gas_change_reason_stringer.go +++ b/core/tracing/gen_gas_change_reason_stringer.go @@ -28,21 +28,22 @@ func _() { _ = x[GasChangeWitnessCodeChunk-17] _ = x[GasChangeWitnessContractCollisionCheck-18] _ = x[GasChangeTxDataFloor-19] + _ = x[GasChangeAccountCreation-20] _ = x[GasChangeIgnored-255] } const ( - _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloor" + _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorAccountCreation" _GasChangeReason_name_1 = "Ignored" ) var ( - _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353} + _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 368} ) func (i GasChangeReason) String() string { switch { - case i <= 19: + case i <= 20: return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]] case i == 255: return _GasChangeReason_name_1 diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 6ea3f7ebbf..78b85fafea 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -472,6 +472,10 @@ const ( // transaction data. This change will always be a negative change. GasChangeTxDataFloor GasChangeReason = 19 + // GasChangeAccountCreation represents the state gas charging for account + // creation inside the call/create frame. + GasChangeAccountCreation GasChangeReason = 20 + // GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as // it will be "manually" tracked by a direct emit of the gas change event. GasChangeIgnored GasChangeReason = 0xFF diff --git a/core/txpool/validation.go b/core/txpool/validation.go index c87bba31ac..3b30dd30ef 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -125,7 +125,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction has more gas than the bare minimum needed to cover // the transaction metadata - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) if err != nil { return err } @@ -138,9 +138,16 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types if err != nil { return err } + // Make sure the transaction has sufficient gas allowance to + // pay the floor cost. if tx.Gas() < floorDataGas { return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrFloorDataGas, tx.Gas(), floorDataGas) } + // In Amsterdam, the transaction gas limit is allowed to exceed + // params.MaxTxGas, but the calldata floor cost is capped by it. + if rules.IsAmsterdam && max(intrGas.RegularGas, floorDataGas) > params.MaxTxGas { + return fmt.Errorf("%w: regular intrisic cost %v, floor: %v", core.ErrFloorDataGas, intrGas.RegularGas, floorDataGas) + } } // Ensure the gasprice is high enough to cover the requirement of the calling pool if tx.GasTipCapIntCmp(opts.MinTip) < 0 { diff --git a/core/vm/common.go b/core/vm/common.go index 2d631f8a55..5059b4af37 100644 --- a/core/vm/common.go +++ b/core/vm/common.go @@ -112,6 +112,5 @@ func toWordSize(size uint64) uint64 { if size > math.MaxUint64-31 { return math.MaxUint64/32 + 1 } - return (size + 31) / 32 } diff --git a/core/vm/contract.go b/core/vm/contract.go index 45c879c80f..9155e9f84a 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -42,6 +42,8 @@ type Contract struct { IsDeployment bool IsSystemCall bool + // Gas carries the unified gas state for this frame: running balance, + // reservoir, and per-frame usage accumulators. See GasBudget. Gas GasBudget value *uint256.Int } @@ -113,7 +115,6 @@ func (c *Contract) GetOp(n uint64) OpCode { if n < uint64(len(c.Code)) { return OpCode(c.Code[n]) } - return STOP } @@ -125,9 +126,10 @@ func (c *Contract) Caller() common.Address { return c.caller } -// UseGas attempts the use gas and subtracts it and returns true on success -func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) { - prior, ok := c.Gas.Charge(cost) +// chargeRegular deducts regular gas only, with tracer integration. +// Returns false on OOG. Delegates the arithmetic to GasBudget.ChargeRegular. +func (c *Contract) chargeRegular(r uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool { + prior, ok := c.Gas.ChargeRegular(r) if !ok { return false } @@ -137,15 +139,44 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G return true } -// RefundGas refunds the leftover gas budget back to the contract. -func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { - prior, changed := c.Gas.Refund(refund) - if !changed { - return +// chargeState deducts state gas (spilling into regular when the reservoir is +// exhausted), with tracer integration. Returns false on OOG. +func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool { + prior, ok := c.Gas.ChargeState(s) + if !ok { + return false } if logger.HasGasHook() && reason != tracing.GasChangeIgnored { logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } + return true +} + +// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state. +func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) { + prior := c.Gas + c.Gas.Absorb(child, forwarded) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) + } +} + +// forwardGas drains `regular` regular gas and the entire state reservoir +// from this contract's running budget and returns the initial GasBudget for +// a child frame. The caller's UsedRegularGas is bumped by the forwarded +// amount so that the absorb-on-return path correctly reclaims the unused +// portion. Thin wrapper around GasBudget.Forward with tracer integration. +// +// Caller must ensure `regular` is no larger than the running balance (the +// opcode's dynamic gas table is expected to validate that before invoking +// the opcode handler). +func (c *Contract) forwardGas(regular uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) GasBudget { + prior := c.Gas + child := c.Gas.Forward(regular) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) + } + return child } // Address returns the contracts address diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 71cfdbc527..6908ffeba1 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -264,9 +264,8 @@ func ActivePrecompiles(rules params.Rules) []common.Address { // - any error that occurred func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, gas GasBudget, logger *tracing.Hooks, rules params.Rules) (ret []byte, remaining GasBudget, err error) { gasCost := p.RequiredGas(input) - prior, ok := gas.Charge(GasCosts{RegularGas: gasCost}) + prior, ok := gas.ChargeRegular(gasCost) if !ok { - gas.Exhaust() return nil, gas, ErrOutOfGas } if logger.HasGasHook() { diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 988cdb91f2..4d28df6a6a 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -37,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas), nil, params.Rules{}) + RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas, 0), nil, params.Rules{}) if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index e7841c8552..c6975bd0a6 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}); err != nil { + if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -122,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := test.Gas - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -139,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -170,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { start := time.Now() for bench.Loop() { copy(data, in) - res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas), nil, params.Rules{}) + res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas, 0), nil, params.Rules{}) } elapsed := uint64(time.Since(start)) if elapsed < 1 { diff --git a/core/vm/eips.go b/core/vm/eips.go index 33af8fd4fd..ba7cbd7461 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -43,6 +43,7 @@ var activators = map[int]func(*JumpTable){ 7939: enable7939, 8024: enable8024, 7843: enable7843, + 8037: enable8037, } // EnableEIP enables the given EIP on the config. @@ -377,7 +378,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er code := evm.StateDB.GetCode(addr) paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64()) consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas) - scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified) + scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -403,7 +404,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // advanced past this boundary. contractAddr := scope.Contract.Address() consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) - scope.Contract.UseGas(GasCosts{RegularGas: wanted}, evm.Config.Tracer, tracing.GasChangeUnspecified) + scope.Contract.chargeRegular(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -430,7 +431,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc { if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall { contractAddr := scope.Contract.Address() consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) - scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified) + scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -590,3 +591,11 @@ func enable7843(jt *JumpTable) { maxStack: maxStack(0, 1), } } + +// enable8037 enables the multidimensional-metering as specified in EIP-8037. +func enable8037(jt *JumpTable) { + jt[CREATE].constantGas = params.CreateGasAmsterdam + jt[CREATE2].constantGas = params.CreateGasAmsterdam + jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037 + jt[SSTORE].dynamicGas = gasSStore8037 +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 832306b9a0..50d9e8ab0c 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -67,6 +67,8 @@ type BlockContext struct { BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) Random *common.Hash // Provides information for PREVRANDAO SlotNum uint64 // Provides information for SLOTNUM + + CostPerStateByte uint64 // CostPerByte for new state after EIP-8037 } // TxContext provides the EVM with information about a transaction. @@ -245,13 +247,13 @@ func isSystemCall(caller common.Address) bool { // parameters. It also handles any necessary value transfer required and takse // the necessary steps to create accounts and reverses the state in case of an // execution error or failed value transfer. -func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) { // Capture the tracer start/end events in debug mode if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALL, caller, addr, input, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { @@ -263,7 +265,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - snapshot := evm.StateDB.Snapshot() + snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas p, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { @@ -275,10 +277,9 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // hash leaf to the access list, then account creation will proceed unimpaired. // Thus, only pay for the creation of the code hash leaf here. wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) - if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok { + if _, ok := gas.ChargeRegular(wgas); !ok { evm.StateDB.RevertToSnapshot(snapshot) - gas.Exhaust() - return nil, gas, ErrOutOfGas + return nil, gas.ExitHalt(reservoir), ErrOutOfGas } } @@ -288,6 +289,16 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } evm.StateDB.CreateAccount(addr) } + if evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) { + prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte) + if !ok { + evm.StateDB.RevertToSnapshot(snapshot) + return nil, gas.ExitHalt(reservoir), ErrOutOfGas + } + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation) + } + } // Perform the value transfer only in non-syscall mode. // Calling this is required even for zero-value transfers, // to ensure the state clearing mechanism is applied. @@ -311,22 +322,19 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g gas = contract.Gas } } - // When an error was returned by the EVM or when setting the creation code - // above we revert to the snapshot and consume any gas remaining. Additionally, - // when we're in homestead this also counts for code storage gas errors. + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, reservoir) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) + if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } - gas.Exhaust() } - // TODO: consider clearing up unused snapshots: - //} else { - // evm.StateDB.DiscardSnapshot(snapshot) } - return ret, gas, err + return ret, exitGas, err } // CallCode executes the contract associated with the addr with the given input @@ -336,26 +344,23 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // // CallCode differs from Call in the sense that it executes the given address' // code with the caller as context. -func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } // Fail if we're trying to transfer more than the available balance - // Note although it's noop to transfer X ether to caller itself. But - // if caller doesn't have enough balance, it would be an error to allow - // over-charging itself. So the check here is necessary. if !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - var snapshot = evm.StateDB.Snapshot() + snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -368,16 +373,19 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt ret, err = evm.Run(contract, input, false) gas = contract.Gas } + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, reservoir) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) + if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } - gas.Exhaust() } } - return ret, gas, err + return ret, exitGas, err } // DelegateCall executes the contract associated with the addr with the given input @@ -385,56 +393,56 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // // DelegateCall differs from CallCode in the sense that it executes the given address' // code with the caller as context and the caller is set to the caller of the caller. -func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { // DELEGATECALL inherits value from parent call - evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } - var snapshot = evm.StateDB.Snapshot() + snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { - // Initialise a new contract and make initialise the delegate values - // - // Note: The value refers to the original value from the parent call. contract := NewContract(originCaller, caller, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, false) gas = contract.Gas } + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, reservoir) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) + if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } - gas.Exhaust() } } - return ret, gas, err + return ret, exitGas, err } // StaticCall executes the contract associated with the addr with the given input // as parameters while disallowing any modifications to the state during the call. // Opcodes that attempt to perform such modifications will result in exceptions // instead of performing the modifications. -func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, result GasBudget, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas.RegularGas, nil) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { @@ -445,7 +453,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // after all empty accounts were deleted, so this is not required. However, if we omit this, // then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json. // We could change this, but for now it's left for legacy reasons - var snapshot = evm.StateDB.Snapshot() + snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas // We do an AddBalance of zero here, just in order to trigger a touch. // This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium, @@ -456,58 +464,59 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b if p, isPrecompile := evm.precompile(addr); isPrecompile { ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { - // Initialise a new contract and set the code that is to be used by the EVM. - // The contract is a scoped environment for this execution context only. contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) - - // When an error was returned by the EVM or when setting the creation code - // above we revert to the snapshot and consume any gas remaining. Additionally - // when we're in Homestead this also counts for code storage gas errors. ret, err = evm.Run(contract, input, true) gas = contract.Gas } + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, reservoir) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } - gas.Exhaust() } } - return ret, gas, err + return ret, exitGas, err } // 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, leftOverGas GasBudget, err error) { - if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, typ, caller, address, code, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) - } +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 // limit. + var nonce uint64 if evm.depth > int(params.CallCreateDepth) { - return nil, common.Address{}, gas, ErrDepth + err = ErrDepth + } else if !evm.Context.CanTransfer(evm.StateDB, caller, value) { + err = ErrInsufficientBalance + } else { + nonce = evm.StateDB.GetNonce(caller) + if nonce+1 < nonce { + err = ErrNonceUintOverflow + } } - if !evm.Context.CanTransfer(evm.StateDB, caller, value) { - return nil, common.Address{}, gas, ErrInsufficientBalance + if evm.Config.Tracer != nil { + evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } - nonce := evm.StateDB.GetNonce(caller) - if nonce+1 < nonce { - return nil, common.Address{}, gas, ErrNonceUintOverflow + if err != nil { + return nil, common.Address{}, gas, err } + // Increment the caller's nonce after passing all validations evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) + reservoir := gas.StateGas // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) if !ok { - gas.Exhaust() - return nil, common.Address{}, gas, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas } if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) @@ -528,11 +537,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { + halt := gas.ExitHalt(reservoir) if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution) } - gas.Exhaust() - return nil, common.Address{}, gas, ErrContractAddressCollision + // EIP-8037 collision rule: the state reservoir is fully preserved on + // address collision while regular gas is burnt. + return nil, common.Address{}, halt, ErrContractAddressCollision } // 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 @@ -540,6 +551,18 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value snapshot := evm.StateDB.Snapshot() if !evm.StateDB.Exist(address) { evm.StateDB.CreateAccount(address) + + if evm.chainRules.IsAmsterdam && evm.depth > 0 { + // Only charge state gas if we are not doing a create transaction. + // Prevents double charging with IntrinsicGas. + prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte) + if !ok { + return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas + } + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation) + } + } } // CreateContract means that regardless of whether the account previously existed // in the state trie or not, it _now_ becomes created as a _contract_ account. @@ -554,8 +577,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) if consumed < wanted { - gas.Exhaust() - return nil, common.Address{}, gas, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) if evm.Config.Tracer.HasGasHook() { @@ -574,13 +596,23 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value contract.IsDeployment = true ret, err = evm.initNewContract(contract, address) + + // Special case: ErrCodeStoreOutOfGas pre-Homestead does NOT roll back + // state and gas is preserved (i.e., treated as success). if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) + + exit := contract.Gas.Exit(err, reservoir) if err != ErrExecutionReverted { - contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) + } } + return ret, address, exit, err } - return ret, address, contract.Gas, err + // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved). + // Both packaged as a success-form GasBudget. + return ret, address, contract.Gas.ExitSuccess(), err } // initNewContract runs a new contract's creation code, performs checks on the @@ -590,30 +622,45 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b if err != nil { return ret, err } - - // Check whether the max code size has been exceeded, assign err if the case. - if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { - return ret, err - } - + // Check prefix before gas calculation. // Reject code starting with 0xEF if EIP-3541 is enabled. if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { return ret, ErrInvalidCode } - - if !evm.chainRules.IsEIP4762 { - createDataGas := uint64(len(ret)) * params.CreateDataGas - if !contract.UseGas(GasCosts{RegularGas: createDataGas}, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { - return ret, ErrCodeStoreOutOfGas - } - } else { + if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas) - contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) if len(ret) > 0 && (consumed < wanted) { return ret, ErrCodeStoreOutOfGas } + if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { + return ret, err + } + } else if evm.chainRules.IsAmsterdam { + // Check max code size BEFORE charging gas so over-max code + // does not consume state gas (which would inflate tx_state). + if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { + return ret, err + } + // Charge regular gas (hash cost) before state gas. + regularCost := toWordSize(uint64(len(ret))) * params.Keccak256WordGas + if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { + return ret, ErrCodeStoreOutOfGas + } + // Charge state gas (code-deposit) afterwards. + stateCost := uint64(len(ret)) * evm.Context.CostPerStateByte + if !contract.chargeState(stateCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { + return ret, ErrCodeStoreOutOfGas + } + } else { + createDataCost := uint64(len(ret)) * params.CreateDataGas + if !contract.chargeRegular(createDataCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { + return ret, ErrCodeStoreOutOfGas + } + if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { + return ret, err + } } - if len(ret) > 0 { evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation) } @@ -621,7 +668,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b } // Create creates a new contract using code as deployment code. -func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) { +func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) { contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller)) return evm.create(caller, code, gas, value, contractAddr, CREATE) } @@ -630,7 +677,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value // // The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:] // instead of the usual sender-and-nonce-hash as the address where the contract is initialized at. -func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) { +func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) { inithash := crypto.Keccak256Hash(code) contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:]) return evm.create(caller, code, gas, endowment, contractAddr, CREATE2) @@ -668,22 +715,20 @@ func (evm *EVM) resolveCodeHash(addr common.Address) common.Hash { // ChainConfig returns the environment's chain configuration func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } -func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas uint64, value *big.Int) { +func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas GasBudget, value *big.Int) { tracer := evm.Config.Tracer if tracer.OnEnter != nil { - tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value) + tracer.OnEnter(depth, byte(typ), from, to, input, startGas.RegularGas, value) } if tracer.HasGasHook() { - initial := NewGasBudget(startGas) - tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance) + tracer.EmitGasChange(tracing.Gas{}, startGas.AsTracing(), tracing.GasChangeCallInitialBalance) } } -func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) { +func (evm *EVM) captureEnd(depth int, startGas GasBudget, leftOverGas GasBudget, ret []byte, err error) { tracer := evm.Config.Tracer - if leftOverGas != 0 && tracer.HasGasHook() { - leftover := NewGasBudget(leftOverGas) - tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned) + if !leftOverGas.IsZero() && tracer.HasGasHook() { + tracer.EmitGasChange(leftOverGas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned) } var reverted bool if err != nil { @@ -693,7 +738,7 @@ func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret [ reverted = false } if tracer.OnExit != nil { - tracer.OnExit(depth, ret, startGas-leftOverGas, VMErrorFromErr(err), reverted) + tracer.OnExit(depth, ret, startGas.RegularGas-leftOverGas.RegularGas, VMErrorFromErr(err), reverted) } } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 046311f9cc..550375c9c0 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -291,10 +291,19 @@ var ( gasMLoad = pureMemoryGascost gasMStore8 = pureMemoryGascost gasMStore = pureMemoryGascost - gasCreate = pureMemoryGascost ) +func gasCreate(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + return pureMemoryGascost(evm, contract, stack, mem, memorySize) +} + func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } gas, err := memoryGasCost(mem, memorySize) if err != nil { return GasCosts{}, err @@ -313,6 +322,9 @@ func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memoryS } func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } gas, err := memoryGasCost(mem, memorySize) if err != nil { return GasCosts{}, err @@ -331,7 +343,11 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m } return GasCosts{RegularGas: gas}, nil } + func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } gas, err := memoryGasCost(mem, memorySize) if err != nil { return GasCosts{}, err @@ -384,17 +400,17 @@ var ( gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic) ) -func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc { +func makeCallVariantGasCost(intrinsicFunc intrinsicGasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize) if err != nil { return GasCosts{}, err } - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic, stack.back(0)) if err != nil { return GasCosts{}, err } - gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp) + gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp) if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -402,19 +418,19 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc { } } -func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( gas uint64 transfersValue = !stack.back(2).IsZero() address = common.Address(stack.back(1).Bytes20()) ) if evm.readOnly && transfersValue { - return GasCosts{}, ErrWriteProtection + return 0, ErrWriteProtection } // Stateless check memoryGas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err } var transferGas uint64 if transfersValue && !evm.chainRules.IsEIP4762 { @@ -422,12 +438,12 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m } var overflow bool if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow { - return GasCosts{}, ErrGasUintOverflow + return 0, ErrGasUintOverflow } // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps. if contract.Gas.RegularGas < gas { - return GasCosts{}, ErrOutOfGas + return 0, ErrOutOfGas } // Stateful check var stateGas uint64 @@ -439,15 +455,15 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m stateGas += params.CallNewAccountGas } if gas, overflow = math.SafeAdd(gas, stateGas); overflow { - return GasCosts{}, ErrGasUintOverflow + return 0, ErrGasUintOverflow } - return GasCosts{RegularGas: gas}, nil + return gas, nil } -func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { memoryGas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err } var ( gas uint64 @@ -457,38 +473,36 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor gas += params.CallValueTransferGas } if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { - return GasCosts{}, ErrGasUintOverflow + return 0, ErrGasUintOverflow } - return GasCosts{RegularGas: gas}, nil + return gas, nil } -func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err } - return GasCosts{RegularGas: gas}, nil + return gas, nil } -func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err } - return GasCosts{RegularGas: gas}, nil + return gas, nil } func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { return GasCosts{}, ErrWriteProtection } - var gas uint64 // EIP150 homestead gas reprice fork: if evm.chainRules.IsEIP150 { gas = params.SelfdestructGasEIP150 var address = common.Address(stack.back(0).Bytes20()) - if evm.chainRules.IsEIP158 { // if empty and transfers value if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { @@ -504,3 +518,104 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me } return GasCosts{RegularGas: gas}, nil } + +func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + var ( + gas GasCosts + address = common.Address(stack.peek().Bytes20()) + ) + if !evm.StateDB.AddressInAccessList(address) { + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddAddressToAccessList(address) + gas.RegularGas = params.ColdAccountAccessCostEIP2929 + } + // Check we have enough regular gas before we add the address to the BAL + if contract.Gas.RegularGas < gas.RegularGas { + return gas, ErrOutOfGas + } + // 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 + // during execution (e.g. via SELFDESTRUCT) and are removed at tx end. + // + // Funding such an account makes it permanent state growth and must be charged. + if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte + } + return gas, nil +} + +func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + // If we fail the minimum gas availability invariant, fail (0) + if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 { + return GasCosts{}, errors.New("not enough gas for reentrancy sentry") + } + // Gas sentry honoured, do the actual gas calculation based on the stored value + var ( + y, x = stack.back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) + cost GasCosts + ) + // Check slot presence in the access list + if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + 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()) + + if current == value { // noop (1) + // EIP 2200 original clause: + // return params.SloadGasEIP2200, nil + return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS + } + if original == current { + if original == (common.Hash{}) { // create slot (2.1.1) + return GasCosts{ + RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929, + StateGas: params.StorageCreationSize * evm.Context.CostPerStateByte, + }, nil + } + if value == (common.Hash{}) { // delete slot (2.1.2b) + evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + // EIP-2200 original clause: + // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + return GasCosts{RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929}, nil // write existing slot (2.1.2) + } + if original != (common.Hash{}) { + if current == (common.Hash{}) { // recreate slot (2.2.1.1) + evm.StateDB.SubRefund(params.SstoreClearsScheduleRefundEIP3529) + } else if value == (common.Hash{}) { // delete slot (2.2.1.2) + evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + } + if original == value { + if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1) + // EIP-8037 point (2): refund state gas directly to the reservoir + // at the SSTORE restoration point (0→x→0 in same tx); not to the + // refund counter, which is capped at gas_used/5. + contract.Gas.RefundState(params.StorageCreationSize * evm.Context.CostPerStateByte) + + // Regular portion of the refund still goes through the refund counter. + evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929) + } else { // reset to original existing slot (2.2.2.2) + // EIP 2200 Original clause: + // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200) + // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST) + // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST + // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST + evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929) + } + } + // EIP-2200 original clause: + //return params.SloadGasEIP2200, nil // dirty update (2.2) + return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2) +} diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index 16ce651a7d..4bf936d07d 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -97,12 +97,12 @@ func TestEIP2200(t *testing.T) { Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, } evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) - initialGas := NewGasBudget(tt.gaspool) - _, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) + initialGas := NewGasBudget(tt.gaspool, 0) + _, result, err := evm.Call(common.Address{}, address, nil, initialGas, new(uint256.Int)) if !errors.Is(err, tt.failure) { t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure) } - if used := leftOver.Used(initialGas); used != tt.used { + if used := result.Used(initialGas); used != tt.used { t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used) } if refund := evm.StateDB.GetRefund(); refund != tt.refund { @@ -157,12 +157,12 @@ func TestCreateGas(t *testing.T) { } evm := NewEVM(vmctx, statedb, chainConfig, config) - initialGas := NewGasBudget(uint64(testGas)) - ret, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) + initialGas := NewGasBudget(uint64(testGas), 0) + ret, result, err := evm.Call(common.Address{}, address, nil, initialGas, new(uint256.Int)) if err != nil { return false } - gasUsed = leftOver.Used(initialGas) + gasUsed = result.Used(initialGas) if len(ret) != 32 { t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret)) } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index ed938ae41f..b1756ab5fe 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -20,11 +20,11 @@ import ( "fmt" "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/log" ) -// GasCosts denotes a vector of gas costs in the -// multidimensional metering paradigm. It represents the cost -// charged by an individual operation. +// GasCosts denotes a vector of gas costs in the multidimensional metering +// paradigm. It represents the cost charged by an individual operation. type GasCosts struct { RegularGas uint64 StateGas uint64 @@ -40,67 +40,254 @@ func (g GasCosts) String() string { return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas) } -// GasBudget denotes a vector of remaining gas allowances available -// for EVM execution in the multidimensional metering paradigm. -// Unlike GasCosts which represents the price of an operation, -// GasBudget tracks how much gas is left to spend. +// GasBudget is the unified gas-state structure used throughout the EVM. +// It carries two pairs of fields: +// +// - RegularGas / StateGas: the running balance during execution, or the +// leftover balance the caller must absorb after a sub-call. +// - UsedRegularGas / UsedStateGas: per-frame accumulators tracking gross +// consumption. UsedStateGas is signed so it can be decremented by inline +// state-gas refunds (e.g., SSTORE 0->A->0). +// +// The same struct serves three roles: +// +// - During execution: Charge / ChargeRegular / ChargeState / RefundState +// and RefundRegular mutate the running balance and the usage accumulators +// in lockstep. +// +// - At frame exit: ExitSuccess / ExitRevert / ExitHalt produce a new +// GasBudget in "leftover" form that packages the result for the caller. +// +// - At absorption: the caller's Absorb method merges the child's leftover +// budget into its own running budget. type GasBudget struct { - RegularGas uint64 // The leftover gas for execution and state gas usage - StateGas uint64 // The state gas reservoir + RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb) + StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb) + UsedRegularGas uint64 // gross regular gas consumed in this frame + UsedStateGas int64 // signed net state-gas consumed in this frame } -// NewGasBudget creates a GasBudget with the given initial regular gas allowance. -func NewGasBudget(gas uint64) GasBudget { - return GasBudget{RegularGas: gas} +// NewGasBudget initializes a fresh GasBudget for execution / forwarding, +// with both usage accumulators set to zero. +func NewGasBudget(regular, state uint64) GasBudget { + return GasBudget{RegularGas: regular, StateGas: state} } -// Used returns the amount of regular gas consumed so far. +// Used returns the total scalar gas consumed relative to an initial budget +// (= (initial.regular + initial.state) − (current.regular + current.state)). +// This is the payment scalar (EIP-8037's tx_gas_used_before_refund). func (g GasBudget) Used(initial GasBudget) uint64 { - return initial.RegularGas - g.RegularGas + return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas) } -// Exhaust sets all remaining gas to zero, preserving the initial amount -// for usage tracking. -func (g *GasBudget) Exhaust() { - g.RegularGas = 0 - g.StateGas = 0 -} - -func (g *GasBudget) Copy() GasBudget { - return GasBudget{RegularGas: g.RegularGas, StateGas: g.StateGas} -} - -// String returns a visual representation of the gas budget vector. +// String returns a visual representation of the budget. func (g GasBudget) String() string { - return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas) + return fmt.Sprintf("<%v,%v,used=<%v,%v>>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas) } -// CanAfford reports whether the budget has sufficient gas to cover the cost. +// CanAfford reports whether the running balance can cover the given cost. +// State-gas charges that exceed the reservoir spill into regular gas. func (g GasBudget) CanAfford(cost GasCosts) bool { - return g.RegularGas >= cost.RegularGas + if g.RegularGas < cost.RegularGas { + return false + } + if cost.StateGas > g.StateGas { + spillover := cost.StateGas - g.StateGas + if spillover > g.RegularGas-cost.RegularGas { + return false + } + } + return true } -// Charge deducts the given gas cost from the budget. It returns the -// pre-charge budget and false if the budget does not have sufficient -// gas to cover the cost. +// Charge deducts a combined regular+state cost from the running balance and +// updates the usage accumulators. State-gas in excess of the reservoir spills +// into regular_gas. func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { prior := *g - if g.RegularGas < cost.RegularGas { + if !g.CanAfford(cost) { return prior, false } + // Charge regular gas g.RegularGas -= cost.RegularGas - return prior, true -} + g.UsedRegularGas += cost.RegularGas -// Refund adds the given gas budget back. It returns the pre-refund budget -// and whether the budget was actually changed. -func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) { - prior := *g - g.RegularGas += other.RegularGas - return prior, g.RegularGas != prior.RegularGas + // Charge state gas + if cost.StateGas > g.StateGas { + spillover := cost.StateGas - g.StateGas + g.StateGas = 0 + g.RegularGas -= spillover + } else { + g.StateGas -= cost.StateGas + } + g.UsedStateGas += int64(cost.StateGas) + return prior, true } // AsTracing converts the GasBudget into the tracing-facing Gas vector. func (g GasBudget) AsTracing() tracing.Gas { return tracing.Gas{Regular: g.RegularGas, State: g.StateGas} } + +// ChargeRegular is a convenience that deducts a regular-only cost. +func (g *GasBudget) ChargeRegular(r uint64) (GasBudget, bool) { + return g.Charge(GasCosts{RegularGas: r}) +} + +// ChargeState is a convenience that deducts a state-only cost (spills to +// regular when the reservoir is exhausted). Returns false on OOG. +func (g *GasBudget) ChargeState(s uint64) (GasBudget, bool) { + return g.Charge(GasCosts{StateGas: s}) +} + +// IsZero returns an indicator if the gas budget has been exhausted. +func (g *GasBudget) IsZero() bool { + return g.RegularGas == 0 && g.StateGas == 0 +} + +// RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0). +// The reservoir is credited and the signed usage counter is decremented +// in lockstep, preserving the per-frame invariant: +// +// StateGas + UsedStateGas == initialStateGas + spillover_so_far +// +// which the revert path relies on for the correct gross refund. +func (g *GasBudget) RefundState(s uint64) { + g.StateGas += s + g.UsedStateGas -= int64(s) +} + +// Forward drains `regular` regular gas and the entire state reservoir from +// the parent's running budget and returns the initial GasBudget for a child +// frame. The parent's UsedRegularGas is bumped by the forwarded amount so +// that the absorb-on-return path correctly reclaims the unused portion. +// +// Used by frame boundaries where the regular forward has NOT been pre- +// deducted: tx-level dispatch (state_transition) and CREATE / CREATE2. The +// CALL family pre-deducts the forward via the dynamic gas table for tracer- +// reporting reasons and therefore constructs its child budget directly. +// +// Caller must ensure `regular` does not exceed the running balance and +// apply any EIP-150 1/64 retention before calling Forward. +func (g *GasBudget) Forward(regular uint64) GasBudget { + g.RegularGas -= regular + g.UsedRegularGas += regular + + child := GasBudget{ + RegularGas: regular, + StateGas: g.StateGas, + } + g.StateGas = 0 + return child +} + +// ForwardAll forwards the parent's full remaining budget (both regular and +// state) to a child frame. Equivalent to Forward(g.RegularGas) — used at +// the tx boundary where there is no 1/64 retention. +func (g *GasBudget) ForwardAll() GasBudget { + return g.Forward(g.RegularGas) +} + +// ============================================================================ +// Exit-form constructors. These take a post-execution running budget and +// produce a new GasBudget in "leftover form", the value the caller should +// absorb to update its own state. +// ============================================================================ + +// ExitSuccess produces the leftover form for a successful frame. Inline +// state-gas refunds have already been folded into StateGas / UsedStateGas +// during execution; the running budget IS the exit budget on success. +func (g GasBudget) ExitSuccess() GasBudget { + return g +} + +// ExitRevert produces the leftover for a REVERT exit. Per EIP-8037, all state +// gas charged by the reverted frame is refunded to the caller's reservoir: +// +// leftover.StateGas = StateGas + UsedStateGas +// +// UsedStateGas is reset since the frame's state changes are discarded. +func (g GasBudget) ExitRevert() GasBudget { + reservoir := int64(g.StateGas) + g.UsedStateGas + if reservoir < 0 { + // Reservoir should never be negative. By construction it equals + // the initial state-gas allocation plus any spillover to regular + // gas. + reservoir = 0 + log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas) + } + return GasBudget{ + RegularGas: g.RegularGas, + StateGas: uint64(reservoir), + UsedRegularGas: g.UsedRegularGas, + UsedStateGas: 0, + } +} + +// ExitHalt produces the leftover for an exceptional halt. +// +// - state_gas_reservoir is reset back to its value at the start of the child frame +// - the gas_left initially given to the child is consumed (set to zero) +func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget { + reservoir := int64(g.StateGas) + g.UsedStateGas + if reservoir < 0 { + // Reservoir should never be negative. By construction it equals + // the initial state-gas allocation plus any spillover to regular + // gas. + reservoir = 0 + log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas) + } + // The portion of state gas charged from regular gas is also burned + // together with the regular gas, rather than being returned to the + // parent's state-gas reservoir. + var spilled uint64 + if uint64(reservoir) > initStateReservoir { + spilled = uint64(reservoir) - initStateReservoir + } + return GasBudget{ + RegularGas: 0, + StateGas: initStateReservoir, + UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled, + UsedStateGas: 0, + } +} + +// Exit dispatches on err to the appropriate exit-form constructor +// for the post-evm.Run path: +// +// - err == nil → ExitSuccess +// - err == ErrExecutionReverted → ExitRevert +// - any other err → ExitHalt +// +// Soft validation failures (occurring BEFORE evm.Run) should call Preserved +// directly instead of going through this dispatcher. +func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget { + switch { + case err == nil: + return g.ExitSuccess() + case err == ErrExecutionReverted: + return g.ExitRevert() + default: + return g.ExitHalt(initStateReservoir) + } +} + +// Absorb merges a sub-call's leftover GasBudget into this (caller's) running +// budget. Additionally, it does an EIP-8037 spillover correction: +// state-gas that spilled into the regular pool inside the child frame is +// excluded from the UsedRegularGas. +// +// spillover = forwarded - child.RegularGas - child.UsedRegularGas +// +// forwarded is the regular-gas amount that was passed to the child at call +// entry (i.e., the regular initial of the child's GasBudget). +func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) { + spillover := forwarded - child.RegularGas - child.UsedRegularGas + + g.UsedRegularGas -= child.RegularGas + g.RegularGas += child.RegularGas + g.StateGas = child.StateGas + g.UsedStateGas += child.UsedStateGas + + g.UsedRegularGas -= spillover +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 4b05092cc7..92c363a356 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -647,25 +647,22 @@ func opSwap16(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - if evm.readOnly { - return nil, ErrWriteProtection - } var ( value = scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop() input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) - gas = scope.Contract.Gas.RegularGas ) + // Apply EIP-150 to the regular gas left after the state charge. + forward := scope.Contract.Gas.RegularGas if evm.chainRules.IsEIP150 { - gas -= gas / 64 + forward -= forward / 64 } // reuse size int for stackvalue stackvalue := size - scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation) - - res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas), &value) + child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation) + res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value) // 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,7 +676,8 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + // Refund the leftover gas back to current frame + scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -690,24 +688,20 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - if evm.readOnly { - return nil, ErrWriteProtection - } var ( endowment = scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop() salt = scope.Stack.pop() input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) - gas = scope.Contract.Gas.RegularGas ) + // Apply EIP-150 to the regular gas left after the state charge. + forward := scope.Contract.Gas.RegularGas + forward -= forward / 64 - // Apply EIP150 - gas -= gas / 64 - scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) // reuse size int for stackvalue stackvalue := size - res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas), - &endowment, &salt) + child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) + res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt) // Push item on the stack based on the returned error. if suberr != nil { stackvalue.Clear() @@ -715,7 +709,9 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue.SetBytes(addr.Bytes()) } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + + // Refund the leftover gas back to current frame + scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -743,7 +739,12 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } - ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value) + + // Regular gas for the forward was already pre-deducted by the dynamic + // gas table (see makeCallVariantGasCallEIP*); only the state reservoir + // needs to be handed off to the child here. + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.Call(scope.Contract.Address(), toAddr, args, childBudget, &value) if err != nil { temp.Clear() @@ -751,11 +752,11 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { temp.SetOne() } stack.push(&temp) + if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -776,8 +777,11 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } - - ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value) + // Regular gas for the forward was already pre-deducted by the dynamic + // gas table, only the state reservoir needs to be handed off to the + // child here. + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.CallCode(scope.Contract.Address(), toAddr, args, childBudget, &value) if err != nil { temp.Clear() } else { @@ -788,7 +792,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -806,7 +810,11 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudget(gas), scope.Contract.value) + // Regular gas for the forward was already pre-deducted by the dynamic + // gas table, only the state reservoir needs to be handed off to the + // child here. + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, childBudget, scope.Contract.value) if err != nil { temp.Clear() } else { @@ -816,8 +824,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -835,7 +842,11 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudget(gas)) + // Regular gas for the forward was already pre-deducted by the dynamic + // gas table, only the state reservoir needs to be handed off to the + // child here. + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, childBudget) if err != nil { temp.Clear() } else { @@ -846,7 +857,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 3994327247..ca33670163 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -174,7 +174,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte // associated costs. contractAddr := contract.Address() consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas) - contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) if consumed < wanted { return nil, ErrOutOfGas } @@ -192,10 +192,8 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack} } // for tracing: this gas consumption event is emitted below in the debug section. - if contract.Gas.RegularGas < cost { + if !contract.chargeRegular(cost, nil, tracing.GasChangeIgnored) { return nil, ErrOutOfGas - } else { - contract.Gas.RegularGas -= cost } // All ops with a dynamic memory usage also has a dynamic gas cost. @@ -224,11 +222,13 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte if err != nil { return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err) } - // for tracing: this gas consumption event is emitted below in the debug section. - if contract.Gas.RegularGas < dynamicCost.RegularGas { + // EIP-8037: charge regular gas before state gas. The state charge + // is a no-op when dynamicCost.StateGas == 0 (e.g., pre-Amsterdam). + if !contract.chargeRegular(dynamicCost.RegularGas, nil, tracing.GasChangeIgnored) { + return nil, ErrOutOfGas + } + if !contract.chargeState(dynamicCost.StateGas, nil, tracing.GasChangeIgnored) { return nil, ErrOutOfGas - } else { - contract.Gas.RegularGas -= dynamicCost.RegularGas } } diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index 868cb12d04..42530b83b7 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) { timeout := make(chan bool) go func(evm *EVM) { - _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64), new(uint256.Int)) + _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64, 0), new(uint256.Int)) errChannel <- err }(evm) @@ -85,7 +85,7 @@ func BenchmarkInterpreter(b *testing.B) { value = uint256.NewInt(0) stack = newStackForTesting() mem = NewMemory() - contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas), nil) + contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas, 0), nil) ) stack.push(uint256.NewInt(123)) stack.push(uint256.NewInt(123)) diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 82fc43ec13..9ea8349e3a 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -23,8 +23,9 @@ import ( ) type ( - executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error) - gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64 + executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error) + gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64 + intrinsicGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64 // memorySizeFunc returns the required size, and whether the operation overflowed a uint64 memorySizeFunc func(*Stack) (size uint64, overflow bool) ) @@ -97,6 +98,7 @@ func newAmsterdamInstructionSet() JumpTable { instructionSet := newOsakaInstructionSet() enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) + enable8037(&instructionSet) // EIP-8037 (State creation gas cost increase) return validate(instructionSet) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 86ac262a93..2206fb95fa 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -168,7 +168,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g evm.StateDB.AddAddressToAccessList(addr) // Charge the remaining difference here already, to correctly calculate available // gas for call - if !contract.UseGas(GasCosts{RegularGas: coldCost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !contract.chargeRegular(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } } @@ -276,7 +276,7 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) } -func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { +func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 @@ -295,7 +295,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // Charge the remaining difference here already, to correctly calculate // available gas for call - if !contract.UseGas(GasCosts{RegularGas: eip2929Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } } @@ -312,7 +312,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps. // It's an essential safeguard before any stateful check. - if contract.Gas.RegularGas < intrinsicCost.RegularGas { + if contract.Gas.RegularGas < intrinsicCost { return GasCosts{}, ErrOutOfGas } @@ -324,13 +324,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { evm.StateDB.AddAddressToAccessList(target) eip7702Cost = params.ColdAccountAccessCostEIP2929 } - if !contract.UseGas(GasCosts{RegularGas: eip7702Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } } // Calculate the gas budget for the nested call. The costs defined by // EIP-2929 and EIP-7702 have already been applied. - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost, stack.back(0)) if err != nil { return GasCosts{}, err } @@ -339,6 +339,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // part of the dynamic gas. This will ensure it is correctly reported to // tracers. contract.Gas.RegularGas += eip2929Cost + eip7702Cost + // Undo the RegularGasUsed increments from the direct UseGas charges, + // since this gas will be re-charged via the returned cost. + contract.Gas.UsedRegularGas -= eip2929Cost + contract.Gas.UsedRegularGas -= eip7702Cost // Aggregate the gas costs from all components, including EIP-2929, EIP-7702, // the CALL opcode itself, and the cost incurred by nested calls. @@ -349,7 +353,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow { return GasCosts{}, ErrGasUintOverflow } - if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow { + if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow { return GasCosts{}, ErrGasUintOverflow } if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow { diff --git a/core/vm/runtime/env.go b/core/vm/runtime/env.go index 4181e08ee8..de3e8d4ae2 100644 --- a/core/vm/runtime/env.go +++ b/core/vm/runtime/env.go @@ -19,6 +19,7 @@ package runtime import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -29,17 +30,18 @@ func NewEnv(cfg *Config) *vm.EVM { BlobHashes: cfg.BlobHashes, } blockContext := vm.BlockContext{ - CanTransfer: core.CanTransfer, - Transfer: core.Transfer, - GetHash: cfg.GetHashFn, - Coinbase: cfg.Coinbase, - BlockNumber: cfg.BlockNumber, - Time: cfg.Time, - Difficulty: cfg.Difficulty, - GasLimit: cfg.GasLimit, - BaseFee: cfg.BaseFee, - BlobBaseFee: cfg.BlobBaseFee, - Random: cfg.Random, + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + GetHash: cfg.GetHashFn, + Coinbase: cfg.Coinbase, + BlockNumber: cfg.BlockNumber, + Time: cfg.Time, + Difficulty: cfg.Difficulty, + GasLimit: cfg.GasLimit, + BaseFee: cfg.BaseFee, + BlobBaseFee: cfg.BlobBaseFee, + Random: cfg.Random, + CostPerStateByte: params.CostPerStateByte, } evm := vm.NewEVM(blockContext, cfg.State, cfg.ChainConfig, cfg.EVMConfig) diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 4fafdf3a50..34dec1bd4a 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -144,15 +144,15 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) { // set the receiver's (the executing contract) code for execution. cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified) // Call the code with the given configuration. - ret, leftOverGas, err := vmenv.Call( + ret, result, err := vmenv.Call( cfg.Origin, common.BytesToAddress([]byte("contract")), input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudget(cfg.GasLimit, 0), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { - cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err) + cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err) } return ret, cfg.State, err } @@ -179,16 +179,16 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { // - reset transient storage(eip 1153) cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil) // Call the code with the given configuration. - code, address, leftOverGas, err := vmenv.Create( + code, address, result, err := vmenv.Create( cfg.Origin, input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudget(cfg.GasLimit, 0), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { - cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err) + cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err) } - return code, address, leftOverGas.RegularGas, err + return code, address, result.RegularGas, err } // Call executes the code given by the contract's address. It will return the @@ -213,15 +213,15 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil) // Call the code with the given configuration. - ret, leftOverGas, err := vmenv.Call( + ret, result, err := vmenv.Call( cfg.Origin, address, input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudget(cfg.GasLimit, 0), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { - cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err) + cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err) } - return ret, leftOverGas.RegularGas, err + return ret, result.RegularGas, err } diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 6570d73575..2fefa46492 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -55,7 +55,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo gasLimit uint64 = 31000 startGas uint64 = 10000 value = uint256.NewInt(0) - contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas), nil) + contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas, 0), nil) ) evm.SetTxContext(vmctx.txCtx) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0} diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go index decdf588e1..73868d22e0 100644 --- a/eth/tracers/logger/logger_test.go +++ b/eth/tracers/logger/logger_test.go @@ -47,7 +47,7 @@ func TestStoreCapture(t *testing.T) { var ( logger = NewStructLogger(nil) evm = vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()}) - contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000), nil) + contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000, 0), nil) ) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} var index common.Hash diff --git a/params/protocol_params.go b/params/protocol_params.go index 3e36b83547..69e10fa5d9 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -88,6 +88,7 @@ const ( LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas. CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction. Create2Gas uint64 = 32000 // Once per CREATE2 operation + CreateGasAmsterdam uint64 = 9000 // Regular gas portion of CREATE in Amsterdam (EIP-8037); state gas is charged separately. CreateNGasEip4762 uint64 = 1000 // Once per CREATEn operations post-verkle SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation. MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL. @@ -100,6 +101,7 @@ const ( TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 + TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037 // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. @@ -196,6 +198,12 @@ const ( // the bound has a small safety margin for system-contract accesses that // don't consume block gas. BALItemCost uint64 = 2000 + + AccountCreationSize = 120 + StorageCreationSize = 64 + AuthorizationCreationSize = 23 + CostPerStateByte = 1530 + SystemMaxSStoresPerCall = 16 ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation diff --git a/tests/state_test.go b/tests/state_test.go index cf1d4bce4c..09c5cad40e 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -325,10 +325,10 @@ func runBenchmark(b *testing.B, t *StateTest) { b.StartTimer() start := time.Now() - initialGas := vm.NewGasBudget(msg.GasLimit) + initialGas := vm.NewGasBudget(msg.GasLimit, 0) // Execute the message. - _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value) + _, result, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas, msg.Value) if err != nil { b.Error(err) return @@ -337,7 +337,7 @@ func runBenchmark(b *testing.B, t *StateTest) { b.StopTimer() elapsed += uint64(time.Since(start)) refund += state.StateDB.GetRefund() - gasUsed += leftOverGas.Used(initialGas) + gasUsed += result.Used(initialGas) state.StateDB.RevertToSnapshot(snapshot) } diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 572c109f1e..91f7d6c3ec 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -80,8 +80,8 @@ func (tt *TransactionTest) Run() error { if err != nil { return } - // Intrinsic gas - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + // Intrinsic cost + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) if err != nil { return }