diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index e69643272..ea9912d8e 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -2883,6 +2883,7 @@ func testDeleteRecreateSlots(t *testing.T, scheme string) {
chainConfig := *params.TestChainConfig
chainConfig.CancunBlock = nil
+ chainConfig.PragueBlock = nil
gspec := &Genesis{
Config: &chainConfig,
Alloc: GenesisAlloc{
@@ -2976,6 +2977,7 @@ func testDeleteRecreateAccount(t *testing.T, scheme string) {
chainConfig := *params.TestChainConfig
chainConfig.CancunBlock = nil
+ chainConfig.PragueBlock = nil
gspec := &Genesis{
Config: &chainConfig,
Alloc: GenesisAlloc{
@@ -3102,6 +3104,7 @@ func testDeleteRecreateSlotsAcrossManyBlocks(t *testing.T, scheme string) {
t.Logf("Destination address: %x\n", aa)
chainConfig := *params.TestChainConfig
chainConfig.CancunBlock = nil
+ chainConfig.PragueBlock = nil
gspec := &Genesis{
Config: &chainConfig,
Alloc: GenesisAlloc{
@@ -3439,7 +3442,7 @@ func testEIP2718Transition(t *testing.T, scheme string) {
// Expected gas is intrinsic + 2 * pc + hot load + cold load, since only one load is in the access list
expected := params.TxGas + params.TxAccessListAddressGas + params.TxAccessListStorageKeyGas +
- vm.GasQuickStep*2 + params.WarmStorageReadCostEIP2929 + params.ColdSloadCostEIP2929
+ vm.GasQuickStep*2 + params.WarmStorageReadCostEIP2929 + params.ColdSloadCostEIP2929
if block.GasUsed() != expected {
t.Fatalf("incorrect amount of gas spent: expected %d, got %d", expected, block.GasUsed())
@@ -3542,7 +3545,7 @@ func testEIP1559Transition(t *testing.T, scheme string) {
// 1+2: Ensure EIP-1559 access lists are accounted for via gas usage.
expectedGas := params.TxGas + params.TxAccessListAddressGas + params.TxAccessListStorageKeyGas +
- vm.GasQuickStep*2 + params.WarmStorageReadCostEIP2929 + params.ColdSloadCostEIP2929
+ vm.GasQuickStep*2 + params.WarmStorageReadCostEIP2929 + params.ColdSloadCostEIP2929
if block.GasUsed() != expectedGas {
t.Fatalf("incorrect amount of gas spent: expected %d, got %d", expectedGas, block.GasUsed())
}
diff --git a/core/vm/eips.go b/core/vm/eips.go
index c056006f5..049b265d2 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -37,6 +37,7 @@ var activators = map[int]func(*JumpTable){
1884: enable1884,
1344: enable1344,
1153: enable1153,
+ 7702: enable7702,
}
// EnableEIP enables the given EIP on the config.
@@ -319,3 +320,11 @@ func enable6780(jt *JumpTable) {
halts: true,
}
}
+
+// enable7702 the EIP-7702 changes to support delegation designators.
+func enable7702(jt *JumpTable) {
+ jt[CALL].dynamicGas = gasCallEIP7702
+ jt[CALLCODE].dynamicGas = gasCallCodeEIP7702
+ jt[STATICCALL].dynamicGas = gasStaticCallEIP7702
+ jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP7702
+}
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 21e5d1590..0d2e3fd00 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -295,7 +295,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
} 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.
- code := evm.StateDB.GetCode(addr)
+ code := evm.resolveCode(addr)
if len(code) == 0 {
ret, err = nil, nil // gas is unchanged
} else {
@@ -303,7 +303,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
// If the account has no code, we can abort here
// The depth-check is already done, and precompiles handled above
contract := NewContract(caller, AccountRef(addrCopy), value, gas)
- contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code)
+ contract.SetCallCode(&addrCopy, evm.resolveCodeHash(addrCopy), code)
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
}
@@ -363,7 +363,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
// 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, AccountRef(caller.Address()), value, gas)
- contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
+ contract.SetCallCode(&addrCopy, evm.resolveCodeHash(addrCopy), evm.resolveCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
}
@@ -421,7 +421,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
addrCopy := addr
// Initialise a new contract and make initialise the delegate values
contract := NewContract(caller, AccountRef(caller.Address()), nil, gas).AsDelegate()
- contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
+ contract.SetCallCode(&addrCopy, evm.resolveCodeHash(addrCopy), evm.resolveCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
}
@@ -477,7 +477,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
// 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, AccountRef(addrCopy), new(big.Int), gas)
- contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
+ contract.SetCallCode(&addrCopy, evm.resolveCodeHash(addrCopy), evm.resolveCode(addrCopy))
// 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.
@@ -666,17 +666,46 @@ func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *
return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2)
}
+// resolveCode returns the code associated with the provided account. After
+// Prague, it can also resolve code pointed to by a delegation designator.
+func (evm *EVM) resolveCode(addr common.Address) []byte {
+ code := evm.StateDB.GetCode(addr)
+ if !evm.chainRules.IsPrague {
+ return code
+ }
+ if target, ok := types.ParseDelegation(code); ok {
+ // Note we only follow one level of delegation.
+ return evm.StateDB.GetCode(target)
+ }
+ return code
+}
+
+// resolveCodeHash returns the code hash associated with the provided address.
+// After Prague, it can also resolve code hash of the account pointed to by a
+// delegation designator. Although this is not accessible in the EVM it is used
+// internally to associate jumpdest analysis to code.
+func (evm *EVM) resolveCodeHash(addr common.Address) common.Hash {
+ if evm.chainRules.IsPrague {
+ code := evm.StateDB.GetCode(addr)
+ if target, ok := types.ParseDelegation(code); ok {
+ // Note we only follow one level of delegation.
+ return evm.StateDB.GetCodeHash(target)
+ }
+ }
+ return evm.StateDB.GetCodeHash(addr)
+}
+
// ChainConfig returns the environment's chain configuration
func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig }
// PublishEvent executes Publish function from OpEvent if OpCode is found in Context.PublishEvents
func (evm *EVM) PublishEvent(
- opCode OpCode,
- counter uint64,
- from, to common.Address,
- value *big.Int,
- input, output []byte,
- err error,
+ opCode OpCode,
+ counter uint64,
+ from, to common.Address,
+ value *big.Int,
+ input, output []byte,
+ err error,
) {
context := evm.Context
if context.CurrentTransaction == nil {
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 4f669b97f..39e819608 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -77,6 +77,8 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
if cfg.JumpTable[STOP] == nil {
var jt JumpTable
switch {
+ case evm.chainRules.IsPrague:
+ jt = pragueInstructionSet
case evm.chainRules.IsCancun:
jt = cancunInstructionSet
case evm.chainRules.IsShanghai:
diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go
index d18ede350..834e6786d 100644
--- a/core/vm/jump_table.go
+++ b/core/vm/jump_table.go
@@ -60,11 +60,18 @@ var (
londonInstructionSet = newLondonInstructionSet()
shanghaiInstructionSet = newShanghaiInstructionSet()
cancunInstructionSet = newCancunInstructionSet()
+ pragueInstructionSet = newPragueInstructionSet()
)
// JumpTable contains the EVM opcodes supported at a given fork.
type JumpTable [256]*operation
+func newPragueInstructionSet() JumpTable {
+ instructionSet := newCancunInstructionSet()
+ enable7702(&instructionSet) // EIP-7702 Setcode transaction type
+ return instructionSet
+}
+
func newCancunInstructionSet() JumpTable {
instructionSet := newShanghaiInstructionSet()
enable4844(&instructionSet) // EIP-4844 (BLOBHASH opcode)
diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go
index 5b86f6f50..fa4a65fd8 100644
--- a/core/vm/operations_acl.go
+++ b/core/vm/operations_acl.go
@@ -21,6 +21,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
)
@@ -247,3 +248,70 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
}
return gasFunc
}
+
+var (
+ gasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall)
+ gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCall)
+ gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCall)
+ gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCode)
+)
+
+func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc {
+ return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ var (
+ total uint64 // total dynamic gas used
+ addr = common.Address(stack.Back(1).Bytes20())
+ )
+
+ // Check slot presence in the access list
+ if !evm.StateDB.AddressInAccessList(addr) {
+ evm.StateDB.AddAddressToAccessList(addr)
+ // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
+ // the cost to charge for cold access, if any, is Cold - Warm
+ coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
+ // Charge the remaining difference here already, to correctly calculate available
+ // gas for call
+ if !contract.UseGas(coldCost) {
+ return 0, ErrOutOfGas
+ }
+ total += coldCost
+ }
+
+ // Check if code is a delegation and if so, charge for resolution.
+ if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
+ var cost uint64
+ if evm.StateDB.AddressInAccessList(target) {
+ cost = params.WarmStorageReadCostEIP2929
+ } else {
+ evm.StateDB.AddAddressToAccessList(target)
+ cost = params.ColdAccountAccessCostEIP2929
+ }
+ if !contract.UseGas(cost) {
+ return 0, ErrOutOfGas
+ }
+ total += cost
+ }
+
+ // Now call the old calculator, which takes into account
+ // - create new account
+ // - transfer value
+ // - memory expansion
+ // - 63/64ths rule
+ old, err := oldCalculator(evm, contract, stack, mem, memorySize)
+ if err != nil {
+ return old, err
+ }
+
+ // Temporarily add the gas charge back to the contract and return value. By
+ // adding it to the return, it will be charged outside of this function, as
+ // part of the dynamic gas. This will ensure it is correctly reported to
+ // tracers.
+ contract.Gas += total
+
+ var overflow bool
+ if total, overflow = math.SafeAdd(old, total); overflow {
+ return 0, ErrGasUintOverflow
+ }
+ return total, nil
+ }
+}
diff --git a/core/vm/program/program.go b/core/vm/program/program.go
new file mode 100644
index 000000000..5b9cfdcc5
--- /dev/null
+++ b/core/vm/program/program.go
@@ -0,0 +1,431 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// package program is a utility to create EVM bytecode for testing, but _not_ for production. As such:
+//
+// - There are not package guarantees. We might iterate heavily on this package, and do backwards-incompatible changes without warning
+// - There are no quality-guarantees. These utilities may produce evm-code that is non-functional. YMMV.
+// - There are no stability-guarantees. The utility will `panic` if the inputs do not align / make sense.
+
+package program
+
+import (
+ "fmt"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/holiman/uint256"
+)
+
+// Program is a simple bytecode container. It can be used to construct
+// simple EVM programs. Errors during construction of a Program typically
+// cause panics: so avoid using these programs in production settings or on
+// untrusted input.
+// This package is mainly meant to aid in testing. This is not a production
+// -level "compiler".
+type Program struct {
+ code []byte
+}
+
+// New creates a new Program
+func New() *Program {
+ return &Program{
+ code: make([]byte, 0),
+ }
+}
+
+// add adds the op to the code.
+func (p *Program) add(op byte) *Program {
+ p.code = append(p.code, op)
+ return p
+}
+
+// pushBig creates a PUSHX instruction and pushes the given val.
+// - If the val is nil, it pushes zero
+// - If the val is bigger than 32 bytes, it panics
+func (p *Program) doPush(val *uint256.Int) {
+ if val == nil {
+ val = new(uint256.Int)
+ }
+ valBytes := val.Bytes()
+ if len(valBytes) == 0 {
+ valBytes = append(valBytes, 0)
+ }
+ bLen := len(valBytes)
+ p.add(byte(vm.PUSH1) - 1 + byte(bLen))
+ p.Append(valBytes)
+}
+
+// Append appends the given data to the code.
+func (p *Program) Append(data []byte) *Program {
+ p.code = append(p.code, data...)
+ return p
+}
+
+// Bytes returns the Program bytecode. OBS: This is not a copy.
+func (p *Program) Bytes() []byte {
+ return p.code
+}
+
+// SetBytes sets the Program bytecode. The combination of Bytes and SetBytes means
+// that external callers can implement missing functionality:
+//
+// ...
+// prog.Push(1)
+// code := prog.Bytes()
+// manipulate(code)
+// prog.SetBytes(code)
+func (p *Program) SetBytes(code []byte) {
+ p.code = code
+}
+
+// Hex returns the Program bytecode as a hex string.
+func (p *Program) Hex() string {
+ return fmt.Sprintf("%02x", p.Bytes())
+}
+
+// Op appends the given opcode(s).
+func (p *Program) Op(ops ...vm.OpCode) *Program {
+ for _, op := range ops {
+ p.add(byte(op))
+ }
+ return p
+}
+
+// Push creates a PUSHX instruction with the data provided. If zero is being pushed,
+// PUSH0 will be avoided in favour of [PUSH1 0], to ensure backwards compatibility.
+func (p *Program) Push(val any) *Program {
+ switch v := val.(type) {
+ case int:
+ p.doPush(new(uint256.Int).SetUint64(uint64(v)))
+ case uint64:
+ p.doPush(new(uint256.Int).SetUint64(v))
+ case uint32:
+ p.doPush(new(uint256.Int).SetUint64(uint64(v)))
+ case uint16:
+ p.doPush(new(uint256.Int).SetUint64(uint64(v)))
+ case *big.Int:
+ p.doPush(uint256.MustFromBig(v))
+ case *uint256.Int:
+ p.doPush(v)
+ case uint256.Int:
+ p.doPush(&v)
+ case []byte:
+ p.doPush(new(uint256.Int).SetBytes(v))
+ case byte:
+ p.doPush(new(uint256.Int).SetUint64(uint64(v)))
+ case interface{ Bytes() []byte }:
+ // Here, we jump through some hoops in order to avoid depending on
+ // go-ethereum types.Address and common.Hash, and instead use the
+ // interface. This works on both values and pointers!
+ p.doPush(new(uint256.Int).SetBytes(v.Bytes()))
+ case nil:
+ p.doPush(nil)
+ default:
+ panic(fmt.Sprintf("unsupported type %T", v))
+ }
+ return p
+}
+
+// Push0 implements PUSH0 (0x5f).
+func (p *Program) Push0() *Program {
+ return p.Op(vm.PUSH0)
+}
+
+// ExtcodeCopy performs an extcodecopy invocation.
+func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) *Program {
+ p.Push(length)
+ p.Push(codeOffset)
+ p.Push(memOffset)
+ p.Push(address)
+ return p.Op(vm.EXTCODECOPY)
+}
+
+// Call is a convenience function to make a call. If 'gas' is nil, the opcode GAS will
+// be used to provide all gas.
+func (p *Program) Call(gas *uint256.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program {
+ if outOffset == outSize && inSize == outSize && inOffset == outSize && value == outSize {
+ p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1, vm.DUP1)
+ } else {
+ p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset).Push(value)
+ }
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.doPush(gas)
+ }
+ return p.Op(vm.CALL)
+}
+
+// DelegateCall is a convenience function to make a delegatecall. If 'gas' is nil, the opcode GAS will
+// be used to provide all gas.
+func (p *Program) DelegateCall(gas *uint256.Int, address, inOffset, inSize, outOffset, outSize any) *Program {
+ if outOffset == outSize && inSize == outSize && inOffset == outSize {
+ p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1)
+ } else {
+ p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset)
+ }
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.doPush(gas)
+ }
+ return p.Op(vm.DELEGATECALL)
+}
+
+// StaticCall is a convenience function to make a staticcall. If 'gas' is nil, the opcode GAS will
+// be used to provide all gas.
+func (p *Program) StaticCall(gas *uint256.Int, address, inOffset, inSize, outOffset, outSize any) *Program {
+ if outOffset == outSize && inSize == outSize && inOffset == outSize {
+ p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1)
+ } else {
+ p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset)
+ }
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.doPush(gas)
+ }
+ return p.Op(vm.STATICCALL)
+}
+
+// CallCode is a convenience function to make a callcode. If 'gas' is nil, the opcode GAS will
+// be used to provide all gas.
+func (p *Program) CallCode(gas *uint256.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program {
+ if outOffset == outSize && inSize == outSize && inOffset == outSize {
+ p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1)
+ } else {
+ p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset)
+ }
+ p.Push(value)
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.doPush(gas)
+ }
+ return p.Op(vm.CALLCODE)
+}
+
+// Label returns the PC (of the next instruction).
+func (p *Program) Label() uint64 {
+ return uint64(len(p.code))
+}
+
+// Jumpdest adds a JUMPDEST op, and returns the PC of that instruction.
+func (p *Program) Jumpdest() (*Program, uint64) {
+ here := p.Label()
+ p.Op(vm.JUMPDEST)
+ return p, here
+}
+
+// Jump pushes the destination and adds a JUMP.
+func (p *Program) Jump(loc any) *Program {
+ p.Push(loc)
+ p.Op(vm.JUMP)
+ return p
+}
+
+// JumpIf implements JUMPI.
+func (p *Program) JumpIf(loc any, condition any) *Program {
+ p.Push(condition)
+ p.Push(loc)
+ p.Op(vm.JUMPI)
+ return p
+}
+
+// Size returns the current size of the bytecode.
+func (p *Program) Size() int {
+ return len(p.code)
+}
+
+// InputAddressToStack stores the input (calldata) to memory as address (20 bytes).
+func (p *Program) InputAddressToStack(inputOffset uint32) *Program {
+ p.Push(inputOffset)
+ p.Op(vm.CALLDATALOAD) // Loads [n -> n + 32] of input data to stack top
+ mask, _ := big.NewInt(0).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16)
+ p.Push(mask) // turn into address
+ return p.Op(vm.AND)
+}
+
+// Mstore stores the provided data (into the memory area starting at memStart).
+func (p *Program) Mstore(data []byte, memStart uint32) *Program {
+ var idx = 0
+ // We need to store it in chunks of 32 bytes
+ for ; idx+32 <= len(data); idx += 32 {
+ chunk := data[idx : idx+32]
+ // push the value
+ p.Push(chunk)
+ // push the memory index
+ p.Push(uint32(idx) + memStart)
+ p.Op(vm.MSTORE)
+ }
+ // Remainders become stored using MSTORE8
+ for ; idx < len(data); idx++ {
+ b := data[idx]
+ // push the byte
+ p.Push(b)
+ p.Push(uint32(idx) + memStart)
+ p.Op(vm.MSTORE8)
+ }
+ return p
+}
+
+// MstoreSmall stores the provided data, which must be smaller than 32 bytes,
+// into the memory area starting at memStart.
+// The data will be LHS zero-added to align on 32 bytes.
+// For example, providing data 0x1122, it will do a PUSH2:
+// PUSH2 0x1122, resulting in
+// stack: 0x0000000000000000000000000000000000000000000000000000000000001122
+// followed by MSTORE(0,0)
+// And thus, the resulting memory will be
+// [ 0000000000000000000000000000000000000000000000000000000000001122 ]
+func (p *Program) MstoreSmall(data []byte, memStart uint32) *Program {
+ if len(data) > 32 {
+ // For larger sizes, use Mstore instead.
+ panic("only <=32 byte data size supported")
+ }
+ if len(data) == 0 {
+ // Storing 0-length data smells of an error somewhere.
+ panic("data is zero length")
+ }
+ // push the value
+ p.Push(data)
+ // push the memory index
+ p.Push(memStart)
+ p.Op(vm.MSTORE)
+ return p
+}
+
+// MemToStorage copies the given memory area into SSTORE slots,
+// It expects data to be aligned to 32 byte, and does not zero out
+// remainders if some data is not
+// I.e, if given a 1-byte area, it will still copy the full 32 bytes to storage.
+func (p *Program) MemToStorage(memStart, memSize, startSlot int) *Program {
+ // We need to store it in chunks of 32 bytes
+ for idx := memStart; idx < (memStart + memSize); idx += 32 {
+ dataStart := idx
+ // Mload the chunk
+ p.Push(dataStart)
+ p.Op(vm.MLOAD)
+ // Value is now on stack,
+ p.Push(startSlot)
+ p.Op(vm.SSTORE)
+ startSlot++
+ }
+ return p
+}
+
+// ReturnViaCodeCopy utilises CODECOPY to place the given data in the bytecode of
+// p, loads into memory (offset 0) and returns the code.
+// This is a typical "constructor".
+// Note: since all indexing is calculated immediately, the preceding bytecode
+// must not be expanded or shortened.
+func (p *Program) ReturnViaCodeCopy(data []byte) *Program {
+ p.Push(len(data))
+ // For convenience, we'll use PUSH2 for the offset. Then we know we can always
+ // fit, since code is limited to 0xc000
+ p.Op(vm.PUSH2)
+ offsetPos := p.Size() // Need to update this position later on
+ p.Append([]byte{0, 0}) // Offset of the code to be copied
+ p.Push(0) // Offset in memory (destination)
+ p.Op(vm.CODECOPY) // Copy from code[offset:offset+len] to memory[0:]
+ p.Return(0, len(data)) // Return memory[0:len]
+ offset := p.Size()
+ p.Append(data) // And add the data
+
+ // Now, go back and fix the offset
+ p.code[offsetPos] = byte(offset >> 8)
+ p.code[offsetPos+1] = byte(offset)
+ return p
+}
+
+// Sstore stores the given byte array to the given slot.
+// OBS! Does not verify that the value indeed fits into 32 bytes.
+// If it does not, it will panic later on via doPush.
+func (p *Program) Sstore(slot any, value any) *Program {
+ p.Push(value)
+ p.Push(slot)
+ return p.Op(vm.SSTORE)
+}
+
+// Tstore stores the given byte array to the given t-slot.
+// OBS! Does not verify that the value indeed fits into 32 bytes.
+// If it does not, it will panic later on via doPush.
+func (p *Program) Tstore(slot any, value any) *Program {
+ p.Push(value)
+ p.Push(slot)
+ return p.Op(vm.TSTORE)
+}
+
+// Return implements RETURN
+func (p *Program) Return(offset, len int) *Program {
+ p.Push(len)
+ p.Push(offset)
+ return p.Op(vm.RETURN)
+}
+
+// ReturnData loads the given data into memory, and does a return with it
+func (p *Program) ReturnData(data []byte) *Program {
+ p.Mstore(data, 0)
+ return p.Return(0, len(data))
+}
+
+// Create2 uses create2 to construct a contract with the given bytecode.
+// This operation leaves either '0' or address on the stack.
+func (p *Program) Create2(code []byte, salt any) *Program {
+ var (
+ value = 0
+ offset = 0
+ size = len(code)
+ )
+ // Load the code into mem
+ p.Mstore(code, 0)
+ // Create it
+ return p.Push(salt).
+ Push(size).
+ Push(offset).
+ Push(value).
+ Op(vm.CREATE2)
+ // On the stack now, is either
+ // - zero: in case of failure, OR
+ // - address: in case of success
+}
+
+// Create2ThenCall calls create2 with the given initcode and salt, and then calls
+// into the created contract (or calls into zero, if the creation failed).
+func (p *Program) Create2ThenCall(code []byte, salt any) *Program {
+ p.Create2(code, salt)
+ // If there happen to be a zero on the stack, it doesn't matter, we're
+ // not sending any value anyway
+ p.Push(0).Push(0) // mem out
+ p.Push(0).Push(0) // mem in
+ p.Push(0) // value
+ p.Op(vm.DUP6) // address
+ p.Op(vm.GAS)
+ p.Op(vm.CALL)
+ p.Op(vm.POP) // pop the retval
+ return p.Op(vm.POP) // pop the address
+}
+
+// Selfdestruct pushes beneficiary and invokes selfdestruct.
+func (p *Program) Selfdestruct(beneficiary any) *Program {
+ p.Push(beneficiary)
+ return p.Op(vm.SELFDESTRUCT)
+}
diff --git a/core/vm/program/program_test.go b/core/vm/program/program_test.go
new file mode 100644
index 000000000..ceb7d8be6
--- /dev/null
+++ b/core/vm/program/program_test.go
@@ -0,0 +1,311 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package program
+
+import (
+ "bytes"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/holiman/uint256"
+)
+
+func TestPush(t *testing.T) {
+ tests := []struct {
+ input interface{}
+ expected string
+ }{
+ // native ints
+ {0, "6000"},
+ {0xfff, "610fff"},
+ {nil, "6000"},
+ {uint8(1), "6001"},
+ {uint16(1), "6001"},
+ {uint32(1), "6001"},
+ {uint64(1), "6001"},
+ // bigints
+ {big.NewInt(0), "6000"},
+ {big.NewInt(1), "6001"},
+ {big.NewInt(0xfff), "610fff"},
+ // uint256
+ {uint256.NewInt(1), "6001"},
+ {uint256.Int{1, 0, 0, 0}, "6001"},
+ // Addresses
+ {common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), "73deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"},
+ {&common.Address{}, "6000"},
+ }
+ for i, tc := range tests {
+ have := New().Push(tc.input).Hex()
+ if have != tc.expected {
+ t.Errorf("test %d: got %v expected %v", i, have, tc.expected)
+ }
+ }
+}
+
+func TestCall(t *testing.T) {
+ { // Nil gas
+ have := New().Call(nil, common.HexToAddress("0x1337"), big.NewInt(1), 1, 2, 3, 4).Hex()
+ want := "600460036002600160016113375af1"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+ { // Non nil gas
+ have := New().Call(uint256.NewInt(0xffff), common.HexToAddress("0x1337"), big.NewInt(1), 1, 2, 3, 4).Hex()
+ want := "6004600360026001600161133761fffff1"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+}
+
+func TestMstore(t *testing.T) {
+ {
+ have := New().Mstore(common.FromHex("0xaabb"), 0).Hex()
+ want := "60aa60005360bb600153"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+ { // store at offset
+ have := New().Mstore(common.FromHex("0xaabb"), 3).Hex()
+ want := "60aa60035360bb600453"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+ { // 34 bytes
+ data := common.FromHex("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" +
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" +
+ "FFFF")
+
+ have := New().Mstore(data, 0).Hex()
+ want := "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260ff60205360ff602153"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+}
+
+func TestMemToStorage(t *testing.T) {
+ have := New().MemToStorage(0, 33, 1).Hex()
+ want := "600051600155602051600255"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+}
+
+func TestSstore(t *testing.T) {
+ have := New().Sstore(0x1337, []byte("1234")).Hex()
+ want := "633132333461133755"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+}
+
+func TestReturnData(t *testing.T) {
+ {
+ have := New().ReturnData([]byte{0xFF}).Hex()
+ want := "60ff60005360016000f3"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+ {
+ // 32 bytes
+ data := common.FromHex("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
+ have := New().ReturnData(data).Hex()
+ want := "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260206000f3"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+ { // ReturnViaCodeCopy
+ data := common.FromHex("0x6001")
+ have := New().Append([]byte{0x5b, 0x5b, 0x5b}).ReturnViaCodeCopy(data).Hex()
+ want := "5b5b5b600261001060003960026000f36001"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+ { // ReturnViaCodeCopy larger code
+ data := common.FromHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260206000f3")
+ have := New().Append([]byte{0x5b, 0x5b, 0x5b}).ReturnViaCodeCopy(data).Hex()
+ want := "5b5b5b602961001060003960296000f37fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260206000f3"
+ if have != want {
+ t.Errorf("have %v want %v", have, want)
+ }
+ }
+}
+
+func TestCreateAndCall(t *testing.T) {
+ // A constructor that stores a slot
+ ctor := New().Sstore(0, big.NewInt(5))
+
+ // A runtime bytecode which reads the slot and returns
+ deployed := New()
+ deployed.Push(0).Op(vm.SLOAD) // [value] in stack
+ deployed.Push(0) // [value, 0]
+ deployed.Op(vm.MSTORE)
+ deployed.Return(0, 32)
+
+ // Pack them
+ ctor.ReturnData(deployed.Bytes())
+ // Verify constructor + runtime code
+ {
+ want := "6005600055606060005360006001536054600253606060035360006004536052600553606060065360206007536060600853600060095360f3600a53600b6000f3"
+ if got := ctor.Hex(); got != want {
+ t.Fatalf("1: got %v expected %v", got, want)
+ }
+ }
+}
+
+func TestCreate2Call(t *testing.T) {
+ // Some runtime code
+ runtime := New().Op(vm.ADDRESS, vm.SELFDESTRUCT).Bytes()
+ want := common.FromHex("0x30ff")
+ if !bytes.Equal(want, runtime) {
+ t.Fatalf("runtime code error\nwant: %x\nhave: %x\n", want, runtime)
+ }
+ // A constructor returning the runtime code
+ initcode := New().ReturnData(runtime).Bytes()
+ want = common.FromHex("603060005360ff60015360026000f3")
+ if !bytes.Equal(want, initcode) {
+ t.Fatalf("initcode error\nwant: %x\nhave: %x\n", want, initcode)
+ }
+ // A factory invoking the constructor
+ outer := New().Create2ThenCall(initcode, nil).Bytes()
+ want = common.FromHex("60606000536030600153606060025360006003536053600453606060055360ff6006536060600753600160085360536009536060600a536002600b536060600c536000600d5360f3600e536000600f60006000f560006000600060006000855af15050")
+ if !bytes.Equal(want, outer) {
+ t.Fatalf("factory error\nwant: %x\nhave: %x\n", want, outer)
+ }
+}
+
+func TestGenerator(t *testing.T) {
+ for i, tc := range []struct {
+ want []byte
+ haveFn func() []byte
+ }{
+ { // CREATE
+ want: []byte{
+ // Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes)
+ byte(vm.PUSH5),
+ // Init code: PUSH1 0, PUSH1 0, RETURN (3 steps)
+ byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN),
+ byte(vm.PUSH1), 0,
+ byte(vm.MSTORE),
+ // length, offset, value
+ byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0,
+ byte(vm.CREATE),
+ byte(vm.POP),
+ },
+ haveFn: func() []byte {
+ initcode := New().Return(0, 0).Bytes()
+ return New().MstoreSmall(initcode, 0).
+ Push(len(initcode)). // length
+ Push(32 - len(initcode)). // offset
+ Push(0). // value
+ Op(vm.CREATE).
+ Op(vm.POP).Bytes()
+ },
+ },
+ { // CREATE2
+ want: []byte{
+ // Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes)
+ byte(vm.PUSH5),
+ // Init code: PUSH1 0, PUSH1 0, RETURN (3 steps)
+ byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN),
+ byte(vm.PUSH1), 0,
+ byte(vm.MSTORE),
+ // salt, length, offset, value
+ byte(vm.PUSH1), 1, byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0,
+ byte(vm.CREATE2),
+ byte(vm.POP),
+ },
+ haveFn: func() []byte {
+ initcode := New().Return(0, 0).Bytes()
+ return New().MstoreSmall(initcode, 0).
+ Push(1). // salt
+ Push(len(initcode)). // length
+ Push(32 - len(initcode)). // offset
+ Push(0). // value
+ Op(vm.CREATE2).
+ Op(vm.POP).Bytes()
+ },
+ },
+ { // CALL
+ want: []byte{
+ // outsize, outoffset, insize, inoffset
+ byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.DUP1), // value
+ byte(vm.PUSH1), 0xbb, //address
+ byte(vm.GAS), // gas
+ byte(vm.CALL),
+ byte(vm.POP),
+ },
+ haveFn: func() []byte {
+ return New().Call(nil, 0xbb, 0, 0, 0, 0, 0).Op(vm.POP).Bytes()
+ },
+ },
+ { // CALLCODE
+ want: []byte{
+ // outsize, outoffset, insize, inoffset
+ byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0, // value
+ byte(vm.PUSH1), 0xcc, //address
+ byte(vm.GAS), // gas
+ byte(vm.CALLCODE),
+ byte(vm.POP),
+ },
+ haveFn: func() []byte {
+ return New().CallCode(nil, 0xcc, 0, 0, 0, 0, 0).Op(vm.POP).Bytes()
+ },
+ },
+ { // STATICCALL
+ want: []byte{
+ // outsize, outoffset, insize, inoffset
+ byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xdd, //address
+ byte(vm.GAS), // gas
+ byte(vm.STATICCALL),
+ byte(vm.POP),
+ },
+ haveFn: func() []byte {
+ return New().StaticCall(nil, 0xdd, 0, 0, 0, 0).Op(vm.POP).Bytes()
+ },
+ },
+ { // DELEGATECALL
+ want: []byte{
+ // outsize, outoffset, insize, inoffset
+ byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xee, //address
+ byte(vm.GAS), // gas
+ byte(vm.DELEGATECALL),
+ byte(vm.POP),
+ },
+ haveFn: func() []byte {
+ return New().DelegateCall(nil, 0xee, 0, 0, 0, 0).Op(vm.POP).Bytes()
+ },
+ },
+ } {
+ if have := tc.haveFn(); !bytes.Equal(have, tc.want) {
+ t.Fatalf("test %d error\nhave: %x\nwant: %x\n", i, have, tc.want)
+ }
+ }
+}
diff --git a/core/vm/program/readme.md b/core/vm/program/readme.md
new file mode 100644
index 000000000..0e4a54d8f
--- /dev/null
+++ b/core/vm/program/readme.md
@@ -0,0 +1,30 @@
+### What is this
+
+In many cases, we have a need to create somewhat nontrivial bytecode, for testing various
+quirks related to state transition or evm execution.
+
+For example, we want to have a `CREATE2`- op create a contract, which is then invoked, and when invoked does a selfdestruct-to-self.
+
+It is overkill to go full solidity, but it is also a bit tricky do assemble this by concatenating bytes.
+
+This utility takes an approach from [goevmlab](https://github.com/holiman/goevmlab/) where it has been used for several years,
+a go-lang utility to assemble evm bytecode.
+
+Using this utility, the case above can be expressed as:
+```golang
+ // Some runtime code
+ runtime := program.New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytecode()
+ // A constructor returning the runtime code
+ initcode := program.New().ReturnData(runtime).Bytecode()
+ // A factory invoking the constructor
+ outer := program.New().Create2AndCall(initcode, nil).Bytecode()
+```
+
+### Warning
+
+This package is a utility for testing, _not_ for production. As such:
+
+- There are not package guarantees. We might iterate heavily on this package, and do backwards-incompatible changes without warning
+- There are no quality-guarantees. These utilities may produce evm-code that is non-functional. YMMV.
+- There are no stability-guarantees. The utility will `panic` if the inputs do not align / make sense.
+
diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go
index e9969943b..8c86b06f2 100644
--- a/core/vm/runtime/runtime_test.go
+++ b/core/vm/runtime/runtime_test.go
@@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/core/vm/program"
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/params"
@@ -912,3 +913,74 @@ func BenchmarkTracerStepVsCallFrame(b *testing.B) {
benchmarkNonModifyingCode(10000000, code, "tracer-step-10M", stepTracer, b)
benchmarkNonModifyingCode(10000000, code, "tracer-call-frame-10M", callFrameTracer, b)
}
+
+// TestDelegatedAccountAccessCost tests that calling an account with an EIP-7702
+// delegation designator incurs the correct amount of gas based on the tracer.
+func TestDelegatedAccountAccessCost(t *testing.T) {
+ statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
+ statedb.SetCode(common.HexToAddress("0xff"), types.AddressToDelegation(common.HexToAddress("0xaa")))
+ statedb.SetCode(common.HexToAddress("0xaa"), program.New().Return(0, 0).Bytes())
+
+ for i, tc := range []struct {
+ code []byte
+ step int
+ want uint64
+ }{
+ { // CALL(0xff)
+ code: []byte{
+ byte(vm.PUSH1), 0x0,
+ byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xff, byte(vm.DUP1), byte(vm.CALL), byte(vm.POP),
+ },
+ step: 7,
+ want: 5455,
+ },
+ { // CALLCODE(0xff)
+ code: []byte{
+ byte(vm.PUSH1), 0x0,
+ byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xff, byte(vm.DUP1), byte(vm.CALLCODE), byte(vm.POP),
+ },
+ step: 7,
+ want: 5455,
+ },
+ { // DELEGATECALL(0xff)
+ code: []byte{
+ byte(vm.PUSH1), 0x0,
+ byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xff, byte(vm.DUP1), byte(vm.DELEGATECALL), byte(vm.POP),
+ },
+ step: 6,
+ want: 5455,
+ },
+ { // STATICCALL(0xff)
+ code: []byte{
+ byte(vm.PUSH1), 0x0,
+ byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xff, byte(vm.DUP1), byte(vm.STATICCALL), byte(vm.POP),
+ },
+ step: 6,
+ want: 5455,
+ },
+ { // SELFDESTRUCT(0xff): should not be affected by resolution
+ code: []byte{
+ byte(vm.PUSH1), 0xff, byte(vm.SELFDESTRUCT),
+ },
+ step: 1,
+ want: 7600,
+ },
+ } {
+ tracer := logger.NewStructLogger(nil)
+ Execute(tc.code, nil, &Config{
+ ChainConfig: params.TestChainConfig,
+ State: statedb,
+ EVMConfig: vm.Config{
+ Tracer: tracer,
+ },
+ })
+ have := tracer.StructLogs()[tc.step].GasCost
+ if want := tc.want; have != want {
+ t.Fatalf("testcase %d, gas report wrong, step %d, have %d want %d", i, tc.step, have, want)
+ }
+ }
+}