diff --git a/core/vm/program/program.go b/core/vm/program/program.go
new file mode 100644
index 0000000000..e5b743dc5a
--- /dev/null
+++ b/core/vm/program/program.go
@@ -0,0 +1,364 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The 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.
+//
+// This 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 goevmlab library. If not, see .
+
+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
+}
+
+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) pushBig(val *big.Int) {
+ if val == nil {
+ val = big.NewInt(0)
+ }
+ valBytes := val.Bytes()
+ if len(valBytes) == 0 {
+ valBytes = append(valBytes, 0)
+ }
+ bLen := len(valBytes)
+ if bLen > 32 {
+ panic(fmt.Sprintf("Push value too large, %d bytes", bLen))
+ }
+ 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
+}
+
+// Op appends the given opcode
+func (p *Program) Op(op vm.OpCode) *Program {
+ return p.add(byte(op))
+}
+
+// Ops appends the given opcodes
+func (p *Program) Ops(ops ...vm.OpCode) *Program {
+ for _, op := range ops {
+ p.add(byte(op))
+ }
+ return p
+}
+
+// Push creates a PUSHX instruction with the data provided
+func (p *Program) Push(val any) *Program {
+ switch v := val.(type) {
+ case int:
+ p.pushBig(new(big.Int).SetUint64(uint64(v)))
+ case uint64:
+ p.pushBig(new(big.Int).SetUint64(v))
+ case uint32:
+ p.pushBig(new(big.Int).SetUint64(uint64(v)))
+ case *big.Int:
+ p.pushBig(v)
+ case *uint256.Int:
+ p.pushBig(v.ToBig())
+ case uint256.Int:
+ p.pushBig(v.ToBig())
+ case []byte:
+ p.pushBig(new(big.Int).SetBytes(v))
+ case byte:
+ p.pushBig(new(big.Int).SetUint64(uint64(v)))
+ case interface{ Bytes() []byte }:
+ // Here, we jump through some hovm 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.pushBig(new(big.Int).SetBytes(v.Bytes()))
+ case nil:
+ p.pushBig(nil)
+ default:
+ panic(fmt.Sprintf("unsupported type %v", v))
+ }
+ return p
+}
+
+// Push0 implements PUSH0 (0x5f)
+func (p *Program) Push0() *Program {
+ return p.Op(vm.PUSH0)
+}
+
+// Bytecode returns the Program bytecode
+func (p *Program) Bytecode() []byte {
+ return p.code
+}
+
+// Hex returns the Program bytecode as a hex string
+func (p *Program) Hex() string {
+ return fmt.Sprintf("%02x", p.Bytecode())
+}
+
+// ExtcodeCopy performsa an extcodecopy invocation
+func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) {
+ p.Push(length)
+ p.Push(codeOffset)
+ p.Push(memOffset)
+ p.Push(address)
+ 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 *big.Int, address, value, inOffset, inSize, outOffset, outSize any) {
+ p.Push(outSize)
+ p.Push(outOffset)
+ p.Push(inSize)
+ p.Push(inOffset)
+ p.Push(value)
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.pushBig(gas)
+ }
+ 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 *big.Int, address, inOffset, inSize, outOffset, outSize any) {
+ p.Push(outSize)
+ p.Push(outOffset)
+ p.Push(inSize)
+ p.Push(inOffset)
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.pushBig(gas)
+ }
+ 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 *big.Int, address, inOffset, inSize, outOffset, outSize any) {
+ p.Push(outSize)
+ p.Push(outOffset)
+ p.Push(inSize)
+ p.Push(inOffset)
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.pushBig(gas)
+ }
+ p.Op(vm.STATICCALL)
+}
+
+// StaticCall 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 *big.Int, address, value, inOffset, inSize, outOffset, outSize any) {
+ p.Push(outSize)
+ p.Push(outOffset)
+ p.Push(inSize)
+ p.Push(inOffset)
+ p.Push(value)
+ p.Push(address)
+ if gas == nil {
+ p.Op(vm.GAS)
+ } else {
+ p.pushBig(gas)
+ }
+ 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() uint64 {
+ here := p.Label()
+ p.Op(vm.JUMPDEST)
+ return here
+}
+
+// Jump pushes the destination and adds a JUMP
+func (p *Program) Jump(loc any) {
+ p.Push(loc)
+ p.Op(vm.JUMP)
+}
+
+// Jump pushes the destination and adds a JUMP
+func (p *Program) JumpIf(loc any, condition any) {
+ p.Push(condition)
+ p.Push(loc)
+ p.Op(vm.JUMPI)
+}
+
+func (p *Program) Size() int {
+ return len(p.code)
+}
+
+// InputToMemory 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, ok := big.NewInt(0).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16)
+ if !ok {
+ panic("whoa")
+ }
+ 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
+}
+
+// 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
+}
+
+// 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 pushBig
+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 pushBig
+func (p *Program) Tstore(slot any, value any) *Program {
+ p.Push(value)
+ p.Push(slot)
+ return p.Op(vm.TSTORE)
+}
+
+func (p *Program) Return(offset, len uint32) *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, uint32(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
+ // address: in case of success
+}
+
+// Create2AndCall 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) Create2AndCall(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 0000000000..3050c83f77
--- /dev/null
+++ b/core/vm/program/program_test.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The 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.
+//
+// This 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 goevmlab library. If not, see .
+
+package program
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/vm"
+
+ "bytes"
+ "testing"
+)
+
+func TestCreate2Call(t *testing.T) {
+ // Some runtime code
+ runtime := New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytecode()
+ 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).Bytecode()
+ 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().Create2AndCall(initcode, nil).Bytecode()
+ want = common.FromHex("60606000536030600153606060025360006003536053600453606060055360ff6006536060600753600160085360536009536060600a536002600b536060600c536000600d5360f3600e536000600f60006000f560006000600060006000855af15050")
+ if !bytes.Equal(want, outer) {
+ t.Fatalf("factory error\nwant: %x\nhave: %x\n", want, outer)
+ }
+}