diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index ca75775be2..e1bdea247e 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -141,6 +141,7 @@ var (
utils.VMTraceJsonConfigFlag,
utils.VMWitnessStatsFlag,
utils.VMStatelessSelfValidationFlag,
+ utils.VMArenaAllocFlag,
utils.NetworkIdFlag,
utils.EthStatsURLFlag,
utils.GpoBlocksFlag,
diff --git a/cmd/keeper/main.go b/cmd/keeper/main.go
index df6881acbf..82d153119f 100644
--- a/cmd/keeper/main.go
+++ b/cmd/keeper/main.go
@@ -23,6 +23,7 @@ import (
"runtime/debug"
"github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@@ -51,7 +52,9 @@ func main() {
fmt.Fprintf(os.Stderr, "failed to get chain config: %v\n", err)
os.Exit(13)
}
- vmConfig := vm.Config{}
+ vmConfig := vm.Config{
+ Allocator: arena.NewBumpAllocator(make([]byte, 32<<20)), // 32 MiB arena
+ }
crossStateRoot, crossReceiptRoot, err := core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness)
if err != nil {
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index e114eb2cd4..00c5072e07 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -621,6 +621,11 @@ var (
Usage: "Generate execution witnesses and self-check against them (testing purpose)",
Category: flags.VMCategory,
}
+ VMArenaAllocFlag = &cli.BoolFlag{
+ Name: "vmarena",
+ Usage: "Use bump/arena allocator for transient EVM allocations during block processing",
+ Category: flags.VMCategory,
+ }
// API options.
RPCGlobalGasCapFlag = &cli.Uint64Flag{
Name: "rpc.gascap",
@@ -1902,6 +1907,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.Bool(VMWitnessStatsFlag.Name) {
cfg.StatelessSelfValidation = true
}
+ if ctx.IsSet(VMArenaAllocFlag.Name) {
+ cfg.EnableArenaAlloc = ctx.Bool(VMArenaAllocFlag.Name)
+ }
if ctx.IsSet(RPCGlobalGasCapFlag.Name) {
cfg.RPCGasCap = ctx.Uint64(RPCGlobalGasCapFlag.Name)
diff --git a/core/arena/allocator.go b/core/arena/allocator.go
new file mode 100644
index 0000000000..27202ad832
--- /dev/null
+++ b/core/arena/allocator.go
@@ -0,0 +1,56 @@
+// Copyright 2026 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 arena
+
+import "unsafe"
+
+// Allocator is the interface for arena-style allocators. RawAlloc returns
+// a pointer to a zeroed region of at least `size` bytes, aligned to `align`.
+// Reset releases all memory allocated since the last Reset (or since creation).
+type Allocator interface {
+ RawAlloc(size, align uintptr) unsafe.Pointer
+ Reset()
+}
+
+// New allocates and zeros a single value of type T from the given allocator.
+// When the allocator is a *HeapAllocator, this uses the standard `new(T)` and
+// involves no unsafe operations.
+func New[T any](a Allocator) *T {
+ if _, ok := a.(*HeapAllocator); ok {
+ return new(T)
+ }
+ var zero T
+ size := unsafe.Sizeof(zero)
+ align := unsafe.Alignof(zero)
+ ptr := a.RawAlloc(size, align)
+ return (*T)(ptr)
+}
+
+// MakeSlice allocates a slice of type []T with the given length and capacity
+// from the allocator. When the allocator is a *HeapAllocator, this uses the
+// standard `make([]T, length, capacity)` and involves no unsafe operations.
+func MakeSlice[T any](a Allocator, length, capacity int) []T {
+ if _, ok := a.(*HeapAllocator); ok {
+ return make([]T, length, capacity)
+ }
+ var zero T
+ elemSize := unsafe.Sizeof(zero)
+ elemAlign := unsafe.Alignof(zero)
+ totalSize := elemSize * uintptr(capacity)
+ ptr := a.RawAlloc(totalSize, elemAlign)
+ return unsafe.Slice((*T)(ptr), capacity)[:length]
+}
diff --git a/core/arena/allocator_test.go b/core/arena/allocator_test.go
new file mode 100644
index 0000000000..8f8c14b446
--- /dev/null
+++ b/core/arena/allocator_test.go
@@ -0,0 +1,257 @@
+// Copyright 2026 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 arena
+
+import (
+ "math/big"
+ "testing"
+ "unsafe"
+)
+
+// testStruct is a struct with mixed field types for alignment testing.
+type testStruct struct {
+ A uint64
+ B uint32
+ C byte
+ D *big.Int
+}
+
+func TestBumpAllocatorAlignment(t *testing.T) {
+ slab := make([]byte, 4096)
+ alloc := NewBumpAllocator(slab)
+
+ // Allocate a byte, then a uint64 — the uint64 must be properly aligned.
+ alloc.RawAlloc(1, 1) // 1 byte, align 1
+
+ var zero uint64
+ ptr := alloc.RawAlloc(unsafe.Sizeof(zero), unsafe.Alignof(zero))
+ addr := uintptr(ptr)
+ if addr%unsafe.Alignof(zero) != 0 {
+ t.Fatalf("uint64 pointer %x not aligned to %d", addr, unsafe.Alignof(zero))
+ }
+}
+
+func TestBumpAllocatorZeroing(t *testing.T) {
+ slab := make([]byte, 4096)
+ alloc := NewBumpAllocator(slab)
+
+ // Dirty the slab.
+ for i := range slab {
+ slab[i] = 0xFF
+ }
+
+ // Allocate and verify zeroed.
+ ptr := alloc.RawAlloc(64, 1)
+ data := unsafe.Slice((*byte)(ptr), 64)
+ for i, b := range data {
+ if b != 0 {
+ t.Fatalf("byte %d not zeroed: got %x", i, b)
+ }
+ }
+}
+
+func TestBumpAllocatorMultiSlab(t *testing.T) {
+ slab := make([]byte, 32)
+ alloc := NewBumpAllocator(slab)
+
+ // Allocation bigger than first slab triggers a new slab.
+ alloc.RawAlloc(64, 1)
+ if alloc.SlabCount() != 2 {
+ t.Fatalf("expected 2 slabs, got %d", alloc.SlabCount())
+ }
+}
+
+func TestBumpAllocatorTotalCap(t *testing.T) {
+ slab := make([]byte, 32)
+ alloc := NewBumpAllocator(slab)
+ alloc.maxTotal = 128 // low cap for testing
+
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatal("expected panic on total cap exceeded, got nil")
+ }
+ }()
+ // First extra slab (32 bytes) → total 64, ok.
+ alloc.RawAlloc(64, 1)
+ // Second extra slab → total would exceed 128.
+ alloc.RawAlloc(128, 1)
+}
+
+func TestBumpAllocatorReset(t *testing.T) {
+ slab := make([]byte, 256)
+ alloc := NewBumpAllocator(slab)
+
+ alloc.RawAlloc(128, 1)
+ if alloc.Used() == 0 {
+ t.Fatal("expected non-zero Used after alloc")
+ }
+
+ alloc.Reset()
+ if alloc.Used() != 0 {
+ t.Fatalf("expected 0 Used after Reset, got %d", alloc.Used())
+ }
+ if alloc.Remaining() != 256 {
+ t.Fatalf("expected 256 Remaining after Reset, got %d", alloc.Remaining())
+ }
+
+ // Should be able to allocate again after Reset.
+ alloc.RawAlloc(128, 1)
+}
+
+func TestNewGenericHeap(t *testing.T) {
+ alloc := &HeapAllocator{}
+
+ v := New[uint64](alloc)
+ if *v != 0 {
+ t.Fatalf("expected zero value, got %d", *v)
+ }
+ *v = 42
+ if *v != 42 {
+ t.Fatal("heap-allocated uint64 not writable")
+ }
+
+ s := New[testStruct](alloc)
+ if s.A != 0 || s.B != 0 || s.C != 0 || s.D != nil {
+ t.Fatal("expected zero struct")
+ }
+ s.A = 1
+ s.D = big.NewInt(99)
+}
+
+func TestNewGenericBump(t *testing.T) {
+ slab := make([]byte, 4096)
+ alloc := NewBumpAllocator(slab)
+
+ v := New[uint64](alloc)
+ if *v != 0 {
+ t.Fatalf("expected zero value, got %d", *v)
+ }
+ *v = 42
+ if *v != 42 {
+ t.Fatal("bump-allocated uint64 not writable")
+ }
+
+ s := New[testStruct](alloc)
+ if s.A != 0 || s.B != 0 || s.C != 0 || s.D != nil {
+ t.Fatal("expected zero struct")
+ }
+ s.A = 1
+
+ // Verify alignment.
+ addr := uintptr(unsafe.Pointer(s))
+ if addr%unsafe.Alignof(testStruct{}) != 0 {
+ t.Fatalf("struct pointer %x not aligned to %d", addr, unsafe.Alignof(testStruct{}))
+ }
+}
+
+func TestMakeSliceHeap(t *testing.T) {
+ alloc := &HeapAllocator{}
+
+ s := MakeSlice[uint64](alloc, 3, 8)
+ if len(s) != 3 || cap(s) != 8 {
+ t.Fatalf("unexpected len/cap: %d/%d", len(s), cap(s))
+ }
+ s[0] = 10
+ s[1] = 20
+ s[2] = 30
+ if s[0] != 10 || s[1] != 20 || s[2] != 30 {
+ t.Fatal("heap slice not writable")
+ }
+}
+
+func TestMakeSliceBump(t *testing.T) {
+ slab := make([]byte, 4096)
+ alloc := NewBumpAllocator(slab)
+
+ s := MakeSlice[uint64](alloc, 3, 8)
+ if len(s) != 3 || cap(s) != 8 {
+ t.Fatalf("unexpected len/cap: %d/%d", len(s), cap(s))
+ }
+ for i := range s {
+ if s[i] != 0 {
+ t.Fatalf("element %d not zeroed: %d", i, s[i])
+ }
+ }
+ s[0] = 10
+ s[1] = 20
+ s[2] = 30
+ if s[0] != 10 || s[1] != 20 || s[2] != 30 {
+ t.Fatal("bump slice not writable")
+ }
+}
+
+func TestHeapAllocatorReset(t *testing.T) {
+ alloc := &HeapAllocator{}
+
+ // RawAlloc pins data; Reset clears pins.
+ alloc.RawAlloc(32, 1)
+ alloc.RawAlloc(64, 1)
+ if len(alloc.pins) != 2 {
+ t.Fatalf("expected 2 pins, got %d", len(alloc.pins))
+ }
+ alloc.Reset()
+ if len(alloc.pins) != 0 {
+ t.Fatalf("expected 0 pins after Reset, got %d", len(alloc.pins))
+ }
+}
+
+func TestDefaultHeap(t *testing.T) {
+ // DefaultHeap should be usable as an Allocator.
+ var a Allocator = DefaultHeap
+ v := New[int](a)
+ if *v != 0 {
+ t.Fatal("expected zero from DefaultHeap")
+ }
+}
+
+func TestBumpAllocatorNewBigInt(t *testing.T) {
+ slab := make([]byte, 4096)
+ alloc := NewBumpAllocator(slab)
+
+ b := New[big.Int](alloc)
+ if b.Sign() != 0 {
+ t.Fatal("expected zero big.Int")
+ }
+ b.SetInt64(123456789)
+ if b.Int64() != 123456789 {
+ t.Fatalf("unexpected value: %d", b.Int64())
+ }
+}
+
+func TestBumpResetAndReuse(t *testing.T) {
+ slab := make([]byte, 256)
+ alloc := NewBumpAllocator(slab)
+
+ // Fill up most of the slab.
+ for i := 0; i < 10; i++ {
+ New[uint64](alloc)
+ }
+ used := alloc.Used()
+ if used == 0 {
+ t.Fatal("expected non-zero usage")
+ }
+
+ alloc.Reset()
+
+ // After reset, should be able to allocate again from the start.
+ for i := 0; i < 10; i++ {
+ v := New[uint64](alloc)
+ if *v != 0 {
+ t.Fatalf("iteration %d: expected zero after reset, got %d", i, *v)
+ }
+ }
+}
diff --git a/core/arena/bump.go b/core/arena/bump.go
new file mode 100644
index 0000000000..7089419542
--- /dev/null
+++ b/core/arena/bump.go
@@ -0,0 +1,141 @@
+// Copyright 2026 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 arena
+
+import (
+ "fmt"
+ "unsafe"
+)
+
+const (
+ defaultSlabSize = 8 << 20 // 8 MiB per slab
+ maxTotalSize = 1 << 30 // 1 GiB total cap across all slabs
+)
+
+// BumpAllocator is a multi-slab bump/arena allocator. It sub-allocates from
+// pre-allocated byte slabs, growing by adding new slabs when the current one
+// is exhausted. It is not thread-safe. All allocated memory is freed at once
+// via Reset.
+//
+// Zeroing is performed lazily at allocation time (not on Reset), using
+// clear() which compiles to an optimized memclr intrinsic.
+type BumpAllocator struct {
+ slabs [][]byte // all slabs (index 0 = first allocated)
+ current int // index of the active slab in slabs
+ offset uintptr // offset within the current slab
+ slabSize int // size of each new slab
+ total uintptr // total bytes allocated across all slabs
+ maxTotal uintptr // maximum total bytes (DoS protection)
+ peak uintptr // high-water mark of Used() across resets
+}
+
+// NewBumpAllocator creates a BumpAllocator with a single initial slab.
+// The slab size and total cap use defaults (8 MiB per slab, 1 GiB cap).
+func NewBumpAllocator(slab []byte) *BumpAllocator {
+ return &BumpAllocator{
+ slabs: [][]byte{slab},
+ slabSize: len(slab),
+ total: uintptr(len(slab)),
+ maxTotal: maxTotalSize,
+ }
+}
+
+// RawAlloc returns a pointer to a zeroed region of at least `size` bytes,
+// aligned to `align`, from the current slab. If the current slab is
+// exhausted, a new slab is allocated. Panics if the total cap is exceeded.
+func (b *BumpAllocator) RawAlloc(size, align uintptr) unsafe.Pointer {
+ slab := b.slabs[b.current]
+
+ // Align the current offset up to the required alignment.
+ aligned := (b.offset + align - 1) &^ (align - 1)
+ end := aligned + size
+
+ if end > uintptr(len(slab)) {
+ // Current slab exhausted — try the next retained slab or allocate a new one.
+ b.current++
+ if b.current < len(b.slabs) {
+ // Reuse a previously allocated slab.
+ slab = b.slabs[b.current]
+ } else {
+ // Allocate a new slab, at least large enough for this request.
+ newSize := b.slabSize
+ if int(size) > newSize {
+ newSize = int(size + align) // oversized allocation
+ }
+ if b.total+uintptr(newSize) > b.maxTotal {
+ panic(fmt.Sprintf("arena: total allocation exceeds %d byte cap", b.maxTotal))
+ }
+ slab = make([]byte, newSize)
+ b.slabs = append(b.slabs, slab)
+ b.total += uintptr(newSize)
+ }
+ b.offset = 0
+ aligned = 0 // offset 0 is always aligned
+ end = size
+ }
+
+ // Zero the region using clear() which compiles to optimized memclr.
+ clear(slab[aligned:end])
+
+ b.offset = end
+ return unsafe.Pointer(&slab[aligned])
+}
+
+// Reset rewinds the allocator to the first slab. Retained slabs are kept
+// for reuse. Zeroing is deferred to the next RawAlloc call, so Reset is O(1).
+func (b *BumpAllocator) Reset() {
+ used := b.Used()
+ if used > b.peak {
+ b.peak = used
+ }
+ b.current = 0
+ b.offset = 0
+}
+
+// Used returns the number of bytes currently allocated (across all active slabs).
+func (b *BumpAllocator) Used() uintptr {
+ var total uintptr
+ for i := 0; i < b.current; i++ {
+ total += uintptr(len(b.slabs[i]))
+ }
+ total += b.offset
+ return total
+}
+
+// Remaining returns the number of bytes left in the current slab.
+func (b *BumpAllocator) Remaining() uintptr {
+ return uintptr(len(b.slabs[b.current])) - b.offset
+}
+
+// SlabCount returns the number of slabs allocated.
+func (b *BumpAllocator) SlabCount() int {
+ return len(b.slabs)
+}
+
+// TotalCapacity returns the total bytes across all slabs.
+func (b *BumpAllocator) TotalCapacity() uintptr {
+ return b.total
+}
+
+// Peak returns the high-water mark of Used() across all resets.
+func (b *BumpAllocator) Peak() uintptr {
+ // Check current usage too (might not have been Reset yet).
+ if used := b.Used(); used > b.peak {
+ return used
+ }
+ return b.peak
+}
diff --git a/core/arena/heap.go b/core/arena/heap.go
new file mode 100644
index 0000000000..9151452304
--- /dev/null
+++ b/core/arena/heap.go
@@ -0,0 +1,45 @@
+// Copyright 2026 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 arena
+
+import "unsafe"
+
+// DefaultHeap is the package-level HeapAllocator used as the nil-allocator default.
+var DefaultHeap = &HeapAllocator{}
+
+// HeapAllocator is a dummy allocator that delegates to Go's built-in heap.
+// The generic helpers (New[T], MakeSlice[T]) detect it via type assertion and
+// use new(T)/make([]T) directly, avoiding any unsafe operations. RawAlloc is
+// provided as a fallback for callers that go through the Allocator interface
+// without the generic helpers.
+type HeapAllocator struct {
+ pins []any // GC roots for objects allocated via RawAlloc
+}
+
+// RawAlloc allocates size bytes on the Go heap and pins the backing array to
+// prevent GC collection. This is only used as a fallback; the generic helpers
+// bypass it entirely via the type assertion fast path.
+func (h *HeapAllocator) RawAlloc(size, align uintptr) unsafe.Pointer {
+ buf := make([]byte, size)
+ h.pins = append(h.pins, buf)
+ return unsafe.Pointer(unsafe.SliceData(buf))
+}
+
+// Reset clears the pins slice, allowing GC to collect all RawAlloc'd memory.
+func (h *HeapAllocator) Reset() {
+ h.pins = h.pins[:0]
+}
diff --git a/core/arena_integration_test.go b/core/arena_integration_test.go
new file mode 100644
index 0000000000..746b7cab04
--- /dev/null
+++ b/core/arena_integration_test.go
@@ -0,0 +1,288 @@
+// Copyright 2026 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 core
+
+import (
+ "context"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus/beacon"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core/arena"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "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/crypto"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/ethereum/go-ethereum/trie"
+)
+
+// TestProcessWithBumpAllocator generates a chain with several types of
+// transactions, then processes each block with both HeapAllocator and
+// BumpAllocator, verifying that the state roots, receipt roots, gas used,
+// and logs match exactly.
+func TestProcessWithBumpAllocator(t *testing.T) {
+ var (
+ key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ key2, _ = crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ addr1 = crypto.PubkeyToAddress(key1.PublicKey)
+ addr2 = crypto.PubkeyToAddress(key2.PublicKey)
+ config = params.MergedTestChainConfig
+ signer = types.LatestSigner(config)
+ gspec = &Genesis{
+ Config: config,
+ Alloc: types.GenesisAlloc{
+ addr1: {Balance: big.NewInt(1_000_000_000_000_000_000)},
+ addr2: {Balance: big.NewInt(1_000_000_000_000_000_000)},
+ },
+ }
+ engine = beacon.New(ethash.NewFaker())
+ )
+
+ // Generate a chain with diverse transactions: value transfers, contract
+ // creates, and dynamic fee transactions.
+ _, blocks, receipts := GenerateChainWithGenesis(gspec, engine, 5, func(i int, gen *BlockGen) {
+ switch i {
+ case 0:
+ // Simple value transfer.
+ tx, _ := types.SignTx(
+ types.NewTransaction(gen.TxNonce(addr1), addr2, big.NewInt(1000), params.TxGas, gen.BaseFee(), nil),
+ signer, key1,
+ )
+ gen.AddTx(tx)
+
+ case 1:
+ // Two value transfers in one block.
+ tx1, _ := types.SignTx(
+ types.NewTransaction(gen.TxNonce(addr1), addr2, big.NewInt(2000), params.TxGas, gen.BaseFee(), nil),
+ signer, key1,
+ )
+ gen.AddTx(tx1)
+ tx2, _ := types.SignTx(
+ types.NewTransaction(gen.TxNonce(addr2), addr1, big.NewInt(500), params.TxGas, gen.BaseFee(), nil),
+ signer, key2,
+ )
+ gen.AddTx(tx2)
+
+ case 2:
+ // Contract creation: deploy a minimal contract (PUSH1 0x42, PUSH1 0, MSTORE, PUSH1 1, PUSH1 31, RETURN).
+ initCode := []byte{
+ 0x60, 0x42, // PUSH1 0x42
+ 0x60, 0x00, // PUSH1 0
+ 0x52, // MSTORE
+ 0x60, 0x01, // PUSH1 1
+ 0x60, 0x1f, // PUSH1 31
+ 0xf3, // RETURN
+ }
+ tx, _ := types.SignTx(
+ types.NewContractCreation(gen.TxNonce(addr1), big.NewInt(0), 100_000, gen.BaseFee(), initCode),
+ signer, key1,
+ )
+ gen.AddTx(tx)
+
+ case 3:
+ // Dynamic fee transaction (EIP-1559).
+ tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{
+ Nonce: gen.TxNonce(addr1),
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: new(big.Int).Add(gen.BaseFee(), big.NewInt(1)),
+ Gas: params.TxGas,
+ To: &addr2,
+ Value: big.NewInt(3000),
+ }), signer, key1)
+ gen.AddTx(tx)
+
+ case 4:
+ // Multiple dynamic fee txs from different senders.
+ tx1, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{
+ Nonce: gen.TxNonce(addr1),
+ GasTipCap: big.NewInt(2),
+ GasFeeCap: new(big.Int).Add(gen.BaseFee(), big.NewInt(2)),
+ Gas: params.TxGas,
+ To: &addr2,
+ Value: big.NewInt(100),
+ }), signer, key1)
+ gen.AddTx(tx1)
+ tx2, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{
+ Nonce: gen.TxNonce(addr2),
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: new(big.Int).Add(gen.BaseFee(), big.NewInt(1)),
+ Gas: params.TxGas,
+ To: &addr1,
+ Value: big.NewInt(200),
+ }), signer, key2)
+ gen.AddTx(tx2)
+ }
+ })
+
+ // Sanity-check that we actually generated transactions.
+ totalTxs := 0
+ for _, r := range receipts {
+ totalTxs += len(r)
+ }
+ if totalTxs == 0 {
+ t.Fatal("no transactions generated")
+ }
+ t.Logf("generated %d blocks with %d total transactions", len(blocks), totalTxs)
+
+ // processBlocks runs the state processor on all blocks with the given
+ // vm.Config and returns per-block state roots, receipt roots, gas used.
+ type blockResult struct {
+ stateRoot common.Hash
+ receiptRoot common.Hash
+ gasUsed uint64
+ logCount int
+ }
+
+ processBlocks := func(cfg vm.Config) ([]blockResult, error) {
+ // Build a fresh blockchain from genesis.
+ db := rawdb.NewMemoryDatabase()
+ triedb := state.NewDatabaseForTesting()
+ gspec.MustCommit(db, triedb.TrieDB())
+ chain := &HeaderChain{
+ config: gspec.Config,
+ chainDb: db,
+ engine: engine,
+ }
+ processor := NewStateProcessor(chain)
+
+ results := make([]blockResult, len(blocks))
+ parentRoot := gspec.ToBlock().Root()
+
+ for i, block := range blocks {
+ statedb, err := state.New(parentRoot, triedb)
+ if err != nil {
+ return nil, err
+ }
+ res, err := processor.Process(context.Background(), block, statedb, cfg)
+ if err != nil {
+ return nil, err
+ }
+ root := statedb.IntermediateRoot(gspec.Config.IsEIP158(block.Number()))
+ receiptRoot := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil))
+
+ results[i] = blockResult{
+ stateRoot: root,
+ receiptRoot: receiptRoot,
+ gasUsed: res.GasUsed,
+ logCount: len(res.Logs),
+ }
+ // Commit so the next block can read from it.
+ statedb.Commit(block.NumberU64(), gspec.Config.IsEIP158(block.Number()), false)
+ parentRoot = root
+ }
+ return results, nil
+ }
+
+ // Process with HeapAllocator (default).
+ heapResults, err := processBlocks(vm.Config{})
+ if err != nil {
+ t.Fatalf("heap processing failed: %v", err)
+ }
+
+ // Process with BumpAllocator.
+ slab := make([]byte, 32<<20) // 32 MiB
+ bumpResults, err := processBlocks(vm.Config{
+ Allocator: arena.NewBumpAllocator(slab),
+ })
+ if err != nil {
+ t.Fatalf("bump processing failed: %v", err)
+ }
+
+ // Compare results block by block.
+ for i := range heapResults {
+ h, b := heapResults[i], bumpResults[i]
+ if h.stateRoot != b.stateRoot {
+ t.Errorf("block %d: state root mismatch: heap=%x bump=%x", i, h.stateRoot, b.stateRoot)
+ }
+ if h.receiptRoot != b.receiptRoot {
+ t.Errorf("block %d: receipt root mismatch: heap=%x bump=%x", i, h.receiptRoot, b.receiptRoot)
+ }
+ if h.gasUsed != b.gasUsed {
+ t.Errorf("block %d: gas used mismatch: heap=%d bump=%d", i, h.gasUsed, b.gasUsed)
+ }
+ if h.logCount != b.logCount {
+ t.Errorf("block %d: log count mismatch: heap=%d bump=%d", i, h.logCount, b.logCount)
+ }
+ }
+}
+
+// TestProcessWithBumpAllocatorResetBetweenBlocks verifies that the arena is
+// properly reset between blocks — i.e., processing block N+1 doesn't corrupt
+// because block N's arena memory was reclaimed.
+func TestProcessWithBumpAllocatorResetBetweenBlocks(t *testing.T) {
+ var (
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ addr = crypto.PubkeyToAddress(key.PublicKey)
+ config = params.MergedTestChainConfig
+ signer = types.LatestSigner(config)
+ engine = beacon.New(ethash.NewFaker())
+ gspec = &Genesis{
+ Config: config,
+ Alloc: types.GenesisAlloc{
+ addr: {Balance: big.NewInt(1_000_000_000_000_000_000)},
+ },
+ }
+ )
+
+ // Generate 10 blocks, each with a transfer, to exercise repeated
+ // alloc/reset cycles on the same slab.
+ _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 10, func(i int, gen *BlockGen) {
+ tx, _ := types.SignTx(
+ types.NewTransaction(gen.TxNonce(addr), common.HexToAddress("0xdead"), big.NewInt(1), params.TxGas, gen.BaseFee(), nil),
+ signer, key,
+ )
+ gen.AddTx(tx)
+ })
+
+ // Use a deliberately small slab (512 KiB) so that without proper Reset,
+ // the allocator would run out of memory across 10 blocks.
+ slab := make([]byte, 512<<10)
+ bumpAlloc := arena.NewBumpAllocator(slab)
+
+ db := rawdb.NewMemoryDatabase()
+ triedb := state.NewDatabaseForTesting()
+ gspec.MustCommit(db, triedb.TrieDB())
+
+ chain := &HeaderChain{
+ config: gspec.Config,
+ chainDb: db,
+ engine: engine,
+ }
+ processor := NewStateProcessor(chain)
+ parentRoot := gspec.ToBlock().Root()
+
+ for i, block := range blocks {
+ statedb, err := state.New(parentRoot, triedb)
+ if err != nil {
+ t.Fatalf("block %d: state.New failed: %v", i, err)
+ }
+ cfg := vm.Config{Allocator: bumpAlloc}
+ res, err := processor.Process(context.Background(), block, statedb, cfg)
+ if err != nil {
+ t.Fatalf("block %d: Process failed: %v", i, err)
+ }
+ if res.GasUsed == 0 {
+ t.Fatalf("block %d: no gas used", i)
+ }
+ parentRoot = statedb.IntermediateRoot(gspec.Config.IsEIP158(block.Number()))
+ statedb.Commit(block.NumberU64(), gspec.Config.IsEIP158(block.Number()), false)
+ }
+}
diff --git a/core/blockchain.go b/core/blockchain.go
index d41f301243..2cb7f1e122 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -2115,9 +2115,12 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
}
}()
go func(start time.Time, throwaway *state.StateDB, block *types.Block) {
- // Disable tracing for prefetcher executions.
+ // Disable tracing and arena allocator for prefetcher executions.
+ // The arena allocator is not thread-safe and must not be shared
+ // with the main processing goroutine.
vmCfg := bc.cfg.VmConfig
vmCfg.Tracer = nil
+ vmCfg.Allocator = nil
bc.prefetcher.Prefetch(block, throwaway, vmCfg, &interrupt)
blockPrefetchExecuteTimer.Update(time.Since(start))
diff --git a/core/evm.go b/core/evm.go
index 7430c0e21f..277e7bba85 100644
--- a/core/evm.go
+++ b/core/evm.go
@@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@@ -39,6 +40,12 @@ type ChainContext interface {
// NewEVMBlockContext creates a new context for use in the EVM.
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
+ return NewEVMBlockContextWithAlloc(header, chain, author, arena.DefaultHeap)
+}
+
+// NewEVMBlockContextWithAlloc creates a new context for use in the EVM, using
+// the provided allocator for transient big.Int allocations.
+func NewEVMBlockContextWithAlloc(header *types.Header, chain ChainContext, author *common.Address, alloc arena.Allocator) vm.BlockContext {
var (
beneficiary common.Address
baseFee *big.Int
@@ -65,15 +72,17 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
if header.SlotNumber != nil {
slotNum = *header.SlotNumber
}
+ blockNumber := new(big.Int).Set(header.Number)
+ difficulty := new(big.Int).Set(header.Difficulty)
return vm.BlockContext{
CanTransfer: CanTransfer,
Transfer: Transfer,
GetHash: GetHashFn(header, chain),
Coinbase: beneficiary,
- BlockNumber: new(big.Int).Set(header.Number),
+ BlockNumber: blockNumber,
Time: header.Time,
- Difficulty: new(big.Int).Set(header.Difficulty),
+ Difficulty: difficulty,
BaseFee: baseFee,
BlobBaseFee: blobBaseFee,
GasLimit: header.GasLimit,
@@ -84,9 +93,15 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
// NewEVMTxContext creates a new transaction context for a single transaction.
func NewEVMTxContext(msg *Message) vm.TxContext {
+ return NewEVMTxContextWithAlloc(msg, arena.DefaultHeap)
+}
+
+// NewEVMTxContextWithAlloc creates a new transaction context, using the
+// provided allocator for transient big.Int allocations.
+func NewEVMTxContextWithAlloc(msg *Message, alloc arena.Allocator) vm.TxContext {
ctx := vm.TxContext{
Origin: msg.From,
- GasPrice: uint256.MustFromBig(msg.GasPrice),
+ GasPrice: new(uint256.Int).Set(&msg.GasPrice),
BlobHashes: msg.BlobHashes,
}
return ctx
diff --git a/core/state_processor.go b/core/state_processor.go
index 6eea74bdd8..c183083b71 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -23,13 +23,16 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/telemetry"
+ "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
)
// StateProcessor is a basic Processor, which takes care of transitioning
@@ -69,7 +72,14 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
blockNumber = block.Number()
allLogs []*types.Log
gp = new(GasPool).AddGas(block.GasLimit())
+ alloc = cfg.GetAllocator()
)
+ defer func() {
+ if ba, ok := alloc.(*arena.BumpAllocator); ok {
+ log.Info("Arena usage after block", "used", common.StorageSize(ba.Used()), "peak", common.StorageSize(ba.Peak()), "slabs", ba.SlabCount(), "total", common.StorageSize(ba.TotalCapacity()))
+ }
+ alloc.Reset()
+ }()
var tracingStateDB = vm.StateDB(statedb)
if hooks := cfg.Tracer; hooks != nil {
@@ -86,7 +96,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
)
// Apply pre-execution system calls.
- context = NewEVMBlockContext(header, p.chain, nil)
+ context = NewEVMBlockContextWithAlloc(header, p.chain, nil, alloc)
evm := vm.NewEVM(context, tracingStateDB, config, cfg)
if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
@@ -98,7 +108,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
- msg, err := TransactionToMessage(tx, signer, header.BaseFee)
+ msg, err := TransactionToMessageWithAlloc(tx, signer, header.BaseFee, alloc)
if err != nil {
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
@@ -247,9 +257,9 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) {
msg := &Message{
From: params.SystemAddress,
GasLimit: 30_000_000,
- GasPrice: common.Big0,
- GasFeeCap: common.Big0,
- GasTipCap: common.Big0,
+ GasPrice: uint256.Int{},
+ GasFeeCap: uint256.Int{},
+ GasTipCap: uint256.Int{},
To: ¶ms.BeaconRootsAddress,
Data: beaconRoot[:],
}
@@ -271,9 +281,9 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
msg := &Message{
From: params.SystemAddress,
GasLimit: 30_000_000,
- GasPrice: common.Big0,
- GasFeeCap: common.Big0,
- GasTipCap: common.Big0,
+ GasPrice: uint256.Int{},
+ GasFeeCap: uint256.Int{},
+ GasTipCap: uint256.Int{},
To: ¶ms.HistoryStorageAddress,
Data: prevHash.Bytes(),
}
@@ -311,9 +321,9 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte
msg := &Message{
From: params.SystemAddress,
GasLimit: 30_000_000,
- GasPrice: common.Big0,
- GasFeeCap: common.Big0,
- GasTipCap: common.Big0,
+ GasPrice: uint256.Int{},
+ GasFeeCap: uint256.Int{},
+ GasTipCap: uint256.Int{},
To: &addr,
}
evm.SetTxContext(NewEVMTxContext(msg))
diff --git a/core/state_transition.go b/core/state_transition.go
index 62474b5f5b..2b956da2be 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -23,6 +23,7 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@@ -146,14 +147,14 @@ type Message struct {
To *common.Address
From common.Address
Nonce uint64
- Value *big.Int
+ Value uint256.Int
GasLimit uint64
- GasPrice *big.Int
- GasFeeCap *big.Int
- GasTipCap *big.Int
+ GasPrice uint256.Int
+ GasFeeCap uint256.Int
+ GasTipCap uint256.Int
Data []byte
AccessList types.AccessList
- BlobGasFeeCap *big.Int
+ BlobGasFeeCap uint256.Int
BlobHashes []common.Hash
SetCodeAuthorizations []types.SetCodeAuthorization
@@ -174,32 +175,68 @@ type Message struct {
// TransactionToMessage converts a transaction into a Message.
func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) {
- msg := &Message{
- Nonce: tx.Nonce(),
- GasLimit: tx.Gas(),
- GasPrice: tx.GasPrice(),
- GasFeeCap: tx.GasFeeCap(),
- GasTipCap: tx.GasTipCap(),
- To: tx.To(),
- Value: tx.Value(),
- Data: tx.Data(),
- AccessList: tx.AccessList(),
- SetCodeAuthorizations: tx.SetCodeAuthorizations(),
- SkipNonceChecks: false,
- SkipTransactionChecks: false,
- BlobHashes: tx.BlobHashes(),
- BlobGasFeeCap: tx.BlobGasFeeCap(),
- }
- // If baseFee provided, set gasPrice to effectiveGasPrice.
- if baseFee != nil {
- msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee)
- if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 {
- msg.GasPrice = msg.GasFeeCap
- }
- }
+ return TransactionToMessageWithAlloc(tx, s, baseFee, arena.DefaultHeap)
+}
+
+// TransactionToMessageWithAlloc converts a transaction into a Message, using
+// the provided allocator for the Message struct and its transient big.Int fields.
+func TransactionToMessageWithAlloc(tx *types.Transaction, s types.Signer, baseFee *big.Int, alloc arena.Allocator) (*Message, error) {
+ msg := arena.New[Message](alloc)
+
var err error
msg.From, err = types.Sender(s, tx)
- return msg, err
+ if err != nil {
+ return nil, err
+ }
+
+ msg.Nonce = tx.Nonce()
+ msg.GasLimit = tx.Gas()
+
+ var (
+ v *uint256.Int
+ overflow bool
+ )
+ if v, overflow = uint256.FromBig(tx.GasPrice()); overflow {
+ return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh,
+ msg.From.Hex(), tx.GasPrice().BitLen())
+ }
+ msg.GasPrice = *v
+ if v, overflow = uint256.FromBig(tx.GasFeeCap()); overflow {
+ return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh,
+ msg.From.Hex(), tx.GasFeeCap().BitLen())
+ }
+ msg.GasFeeCap = *v
+ if v, overflow = uint256.FromBig(tx.GasTipCap()); overflow {
+ return nil, fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh,
+ msg.From.Hex(), tx.GasTipCap().BitLen())
+ }
+ msg.GasTipCap = *v
+
+ msg.To = tx.To()
+ if v, overflow = uint256.FromBig(tx.Value()); overflow {
+ return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex())
+ }
+ msg.Value = *v
+ msg.Data = tx.Data()
+ msg.AccessList = tx.AccessList()
+ msg.SetCodeAuthorizations = tx.SetCodeAuthorizations()
+ msg.BlobHashes = tx.BlobHashes()
+ if tx.BlobGasFeeCap() != nil {
+ if v, overflow = uint256.FromBig(tx.BlobGasFeeCap()); overflow {
+ return nil, fmt.Errorf("%w: blobGasFeeCap exceeds 256 bits", ErrBlobFeeCapTooLow)
+ }
+ msg.BlobGasFeeCap = *v
+ }
+
+ // If baseFee provided, set gasPrice to effectiveGasPrice.
+ if baseFee != nil {
+ baseFeeU256 := uint256.MustFromBig(baseFee)
+ msg.GasPrice.Add(&msg.GasTipCap, baseFeeU256)
+ if msg.GasPrice.Cmp(&msg.GasFeeCap) > 0 {
+ msg.GasPrice.Set(&msg.GasFeeCap)
+ }
+ }
+ return msg, nil
}
// ApplyMessage computes the new state by applying the given message
@@ -210,7 +247,8 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
// indicates a core error meaning that the message would always fail for that particular
// state and would never be accepted within a block.
func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) {
- evm.SetTxContext(NewEVMTxContext(msg))
+ alloc := evm.Config.GetAllocator()
+ evm.SetTxContext(NewEVMTxContextWithAlloc(msg, alloc))
return newStateTransition(evm, msg, gp).execute()
}
@@ -243,16 +281,19 @@ type stateTransition struct {
initialGas uint64
state vm.StateDB
evm *vm.EVM
+ alloc arena.Allocator
}
// newStateTransition initialises and returns a new state transition object.
func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition {
- return &stateTransition{
- gp: gp,
- evm: evm,
- msg: msg,
- state: evm.StateDB,
- }
+ alloc := evm.Config.GetAllocator()
+ st := new(stateTransition)
+ st.gp = gp
+ st.evm = evm
+ st.msg = msg
+ st.state = evm.StateDB
+ st.alloc = alloc
+ return st
}
// to returns the recipient of the message.
@@ -264,20 +305,21 @@ func (st *stateTransition) to() common.Address {
}
func (st *stateTransition) buyGas() error {
+ // Use big.Int for balance check to detect >256-bit overflow correctly.
mgval := new(big.Int).SetUint64(st.msg.GasLimit)
- mgval.Mul(mgval, st.msg.GasPrice)
+ mgval.Mul(mgval, st.msg.GasPrice.ToBig())
balanceCheck := new(big.Int).Set(mgval)
- if st.msg.GasFeeCap != nil {
+ if !st.msg.GasFeeCap.IsZero() {
balanceCheck.SetUint64(st.msg.GasLimit)
- balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap)
+ balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap.ToBig())
}
- balanceCheck.Add(balanceCheck, st.msg.Value)
+ balanceCheck.Add(balanceCheck, st.msg.Value.ToBig())
if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
if blobGas := st.blobGasUsed(); blobGas > 0 {
// Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap
blobBalanceCheck := new(big.Int).SetUint64(blobGas)
- blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap)
+ blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap.ToBig())
balanceCheck.Add(balanceCheck, blobBalanceCheck)
// Pay for blobGasUsed * actual blob fee
blobFee := new(big.Int).SetUint64(blobGas)
@@ -340,25 +382,18 @@ func (st *stateTransition) preCheck() error {
// Make sure that transaction gasFeeCap is greater than the baseFee (post london)
if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
// Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call)
- skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0
+ skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.IsZero() && msg.GasTipCap.IsZero()
if !skipCheck {
- if l := msg.GasFeeCap.BitLen(); l > 256 {
- return fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh,
- msg.From.Hex(), l)
- }
- if l := msg.GasTipCap.BitLen(); l > 256 {
- return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh,
- msg.From.Hex(), l)
- }
- if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 {
+ if msg.GasFeeCap.Cmp(&msg.GasTipCap) < 0 {
return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap,
- msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap)
+ msg.From.Hex(), &msg.GasTipCap, &msg.GasFeeCap)
}
// This will panic if baseFee is nil, but basefee presence is verified
// as part of header validation.
- if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 {
+ baseFeeU256 := uint256.MustFromBig(st.evm.Context.BaseFee)
+ if msg.GasFeeCap.Cmp(baseFeeU256) < 0 {
return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow,
- msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee)
+ msg.From.Hex(), &msg.GasFeeCap, st.evm.Context.BaseFee)
}
}
}
@@ -386,11 +421,12 @@ func (st *stateTransition) preCheck() error {
if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
if st.blobGasUsed() > 0 {
// Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call)
- skipCheck := st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.BitLen() == 0
+ skipCheck := st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.IsZero()
if !skipCheck {
// This will panic if blobBaseFee is nil, but blobBaseFee presence
// is verified as part of header validation.
- if msg.BlobGasFeeCap.Cmp(st.evm.Context.BlobBaseFee) < 0 {
+ blobBaseFeeU256 := uint256.MustFromBig(st.evm.Context.BlobBaseFee)
+ if msg.BlobGasFeeCap.Cmp(blobBaseFeeU256) < 0 {
return fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow,
msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee)
}
@@ -474,11 +510,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
// Check clause 6
- value, overflow := uint256.FromBig(msg.Value)
- if overflow {
- return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex())
- }
- if !value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From, value) {
+ if !msg.Value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From, &msg.Value) {
return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex())
}
@@ -497,7 +529,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
)
if contractCreation {
- ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value)
+ ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, &msg.Value)
} else {
// Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
@@ -520,7 +552,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
// Execute the transaction's call.
- ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
+ ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, &msg.Value)
}
// Record the gas used excluding gas refunds. This value represents the actual
@@ -544,13 +576,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
st.returnGas()
- effectiveTip := msg.GasPrice
+ effectiveTipU256 := new(uint256.Int).Set(&msg.GasPrice)
if rules.IsLondon {
- effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee)
+ baseFee := uint256.MustFromBig(st.evm.Context.BaseFee)
+ effectiveTipU256.Sub(effectiveTipU256, baseFee)
}
- effectiveTipU256, _ := uint256.FromBig(effectiveTip)
- if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 {
+ if st.evm.Config.NoBaseFee && msg.GasFeeCap.IsZero() && msg.GasTipCap.IsZero() {
// 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.
@@ -565,12 +597,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
}
- return &ExecutionResult{
- UsedGas: st.gasUsed(),
- MaxUsedGas: peakGasUsed,
- Err: vmerr,
- ReturnData: ret,
- }, nil
+ result := new(ExecutionResult)
+ result.UsedGas = st.gasUsed()
+ result.MaxUsedGas = peakGasUsed
+ result.Err = vmerr
+ result.ReturnData = ret
+ return result, nil
}
// validateAuthorization validates an EIP-7702 authorization against the state.
@@ -654,7 +686,7 @@ func (st *stateTransition) calcRefund() uint64 {
// exchanged at the original rate.
func (st *stateTransition) returnGas() {
remaining := uint256.NewInt(st.gasRemaining)
- remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
+ remaining.Mul(remaining, &st.msg.GasPrice)
st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn)
if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining > 0 {
diff --git a/core/vm/contract.go b/core/vm/contract.go
index 165ca833f8..112ff000f2 100644
--- a/core/vm/contract.go
+++ b/core/vm/contract.go
@@ -18,6 +18,7 @@ package vm
import (
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/holiman/uint256"
)
@@ -48,17 +49,23 @@ type Contract struct {
// NewContract returns a new contract environment for the execution of EVM.
func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests JumpDestCache) *Contract {
+ return NewContractWithAlloc(caller, address, value, gas, jumpDests, arena.DefaultHeap)
+}
+
+// NewContractWithAlloc returns a new contract environment, allocating the
+// Contract struct from the provided allocator.
+func NewContractWithAlloc(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests JumpDestCache, alloc arena.Allocator) *Contract {
// Initialize the jump analysis cache if it's nil, mostly for tests
if jumpDests == nil {
jumpDests = newMapJumpDests()
}
- return &Contract{
- caller: caller,
- address: address,
- jumpDests: jumpDests,
- Gas: gas,
- value: value,
- }
+ c := new(Contract)
+ c.caller = caller
+ c.address = address
+ c.jumpDests = jumpDests
+ c.Gas = gas
+ c.value = value
+ return c
}
func (c *Contract) validJumpdest(dest *uint256.Int) bool {
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 97ae9468bf..94a480afd3 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -22,6 +22,7 @@ import (
"sync/atomic"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
@@ -298,7 +299,8 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
ret, err = nil, nil // gas is unchanged
} else {
// The contract is a scoped environment for this execution context only.
- contract := NewContract(caller, addr, value, gas, evm.jumpDests)
+ alloc := evm.Config.GetAllocator()
+ contract := NewContractWithAlloc(caller, addr, value, gas, evm.jumpDests, alloc)
contract.IsSystemCall = isSystemCall(caller)
contract.SetCallCode(evm.resolveCodeHash(addr), code)
ret, err = evm.Run(contract, input, false)
@@ -361,7 +363,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
} 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, caller, value, gas, evm.jumpDests)
+ alloc := evm.Config.GetAllocator()
+ contract := NewContractWithAlloc(caller, caller, value, gas, evm.jumpDests, alloc)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
@@ -409,7 +412,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
// 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)
+ alloc := evm.Config.GetAllocator()
+ contract := NewContractWithAlloc(originCaller, caller, value, gas, evm.jumpDests, alloc)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
@@ -464,7 +468,9 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
} 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)
+ alloc := evm.Config.GetAllocator()
+ zeroVal := arena.New[uint256.Int](alloc)
+ contract := NewContractWithAlloc(caller, addr, zeroVal, gas, evm.jumpDests, alloc)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
// When an error was returned by the EVM or when setting the creation code
@@ -571,7 +577,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
// 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, address, value, gas, evm.jumpDests)
+ alloc := evm.Config.GetAllocator()
+ contract := NewContractWithAlloc(caller, address, value, gas, evm.jumpDests, alloc)
// Explicitly set the code to a null hash to prevent caching of jump analysis
// for the initialization code.
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 52dbe83d86..ae5084f2d8 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.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/arena"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/holiman/uint256"
)
@@ -28,14 +29,24 @@ import (
// Config are the configuration options for the Interpreter
type Config struct {
Tracer *tracing.Hooks
- NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
- EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages
- ExtraEips []int // Additional EIPS that are to be enabled
+ NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
+ EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages
+ ExtraEips []int // Additional EIPS that are to be enabled
+ Allocator arena.Allocator // Arena allocator; nil defaults to HeapAllocator
StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose)
EnableWitnessStats bool // Whether trie access statistics collection is enabled
}
+// GetAllocator returns the allocator from the config, defaulting to the
+// heap allocator if none is set.
+func (c Config) GetAllocator() arena.Allocator {
+ if c.Allocator != nil {
+ return c.Allocator
+ }
+ return arena.DefaultHeap
+}
+
// ScopeContext contains the things that are per-call, such as stack and memory,
// but not transients like pc and gas
type ScopeContext struct {
@@ -115,11 +126,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
return nil, nil
}
+ alloc := evm.Config.GetAllocator()
var (
op OpCode // current opcode
jumpTable *JumpTable = evm.table
- mem = NewMemory() // bound memory
- stack = newstack() // local stack
+ mem = NewMemoryWithAlloc(alloc) // bound memory
+ stack = newstackWithAlloc(alloc) // local stack
callContext = &ScopeContext{
Memory: mem,
Stack: stack,
diff --git a/core/vm/memory.go b/core/vm/memory.go
index 54bc2b2849..c0964aeb5c 100644
--- a/core/vm/memory.go
+++ b/core/vm/memory.go
@@ -19,6 +19,7 @@ package vm
import (
"sync"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/holiman/uint256"
)
@@ -32,6 +33,7 @@ var memoryPool = sync.Pool{
type Memory struct {
store []byte
lastGasCost uint64
+ arenaAlloc bool // true if allocated from a BumpAllocator (skip pool return)
}
// NewMemory returns a new memory model.
@@ -39,8 +41,23 @@ func NewMemory() *Memory {
return memoryPool.Get().(*Memory)
}
+// NewMemoryWithAlloc returns a new Memory, allocating from the provided arena.
+// When using a BumpAllocator, the Memory struct is arena-allocated and the pool
+// is bypassed. When using a HeapAllocator, it falls back to the pool.
+func NewMemoryWithAlloc(alloc arena.Allocator) *Memory {
+ if _, ok := alloc.(*arena.BumpAllocator); ok {
+ m := new(Memory)
+ m.arenaAlloc = true
+ return m
+ }
+ return NewMemory()
+}
+
// Free returns the memory to the pool.
func (m *Memory) Free() {
+ if m.arenaAlloc {
+ return // arena-allocated: freed on arena.Reset()
+ }
// To reduce peak allocation, return only smaller memory instances to the pool.
const maxBufferSize = 16 << 10
if cap(m.store) <= maxBufferSize {
diff --git a/core/vm/runtime/arena_test.go b/core/vm/runtime/arena_test.go
new file mode 100644
index 0000000000..2ce8295e66
--- /dev/null
+++ b/core/vm/runtime/arena_test.go
@@ -0,0 +1,233 @@
+// Copyright 2026 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 runtime
+
+import (
+ "bytes"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/arena"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+)
+
+// TestExecuteWithBumpAllocator runs bytecode through the EVM with a
+// BumpAllocator and verifies the result matches the HeapAllocator path.
+func TestExecuteWithBumpAllocator(t *testing.T) {
+ // Bytecode: PUSH1 0x2a, PUSH1 0, MSTORE, PUSH1 32, PUSH1 0, RETURN
+ // Returns the 32-byte big-endian encoding of 42.
+ code := []byte{
+ byte(vm.PUSH1), 0x2a,
+ byte(vm.PUSH1), 0,
+ byte(vm.MSTORE),
+ byte(vm.PUSH1), 32,
+ byte(vm.PUSH1), 0,
+ byte(vm.RETURN),
+ }
+
+ // Run with heap allocator (default).
+ heapRet, _, err := Execute(code, nil, nil)
+ if err != nil {
+ t.Fatalf("heap execute failed: %v", err)
+ }
+
+ // Run with bump allocator.
+ slab := make([]byte, 8<<20) // 8 MiB
+ bumpRet, _, err := Execute(code, nil, &Config{
+ EVMConfig: vm.Config{
+ Allocator: arena.NewBumpAllocator(slab),
+ },
+ })
+ if err != nil {
+ t.Fatalf("bump execute failed: %v", err)
+ }
+
+ if !bytes.Equal(heapRet, bumpRet) {
+ t.Fatalf("results differ:\n heap: %x\n bump: %x", heapRet, bumpRet)
+ }
+}
+
+// TestCallWithBumpAllocator deploys a simple contract, then calls it using
+// both allocator types and verifies matching results.
+func TestCallWithBumpAllocator(t *testing.T) {
+ // Contract code: returns the caller's address (CALLER, PUSH1 0, MSTORE, PUSH1 20, PUSH1 12, RETURN)
+ code := []byte{
+ byte(vm.CALLER),
+ byte(vm.PUSH1), 0,
+ byte(vm.MSTORE),
+ byte(vm.PUSH1), 20,
+ byte(vm.PUSH1), 12,
+ byte(vm.RETURN),
+ }
+ address := common.HexToAddress("0xaa")
+
+ run := func(alloc arena.Allocator) ([]byte, error) {
+ statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ statedb.CreateAccount(address)
+ statedb.SetCode(address, code, tracing.CodeChangeUnspecified)
+ statedb.Finalise(true)
+
+ ret, _, err := Call(address, nil, &Config{
+ State: statedb,
+ EVMConfig: vm.Config{
+ Allocator: alloc,
+ },
+ })
+ return ret, err
+ }
+
+ heapRet, err := run(nil)
+ if err != nil {
+ t.Fatalf("heap call failed: %v", err)
+ }
+
+ slab := make([]byte, 8<<20)
+ bumpRet, err := run(arena.NewBumpAllocator(slab))
+ if err != nil {
+ t.Fatalf("bump call failed: %v", err)
+ }
+
+ if !bytes.Equal(heapRet, bumpRet) {
+ t.Fatalf("results differ:\n heap: %x\n bump: %x", heapRet, bumpRet)
+ }
+}
+
+// TestNestedCallsWithBumpAllocator exercises nested EVM calls (CALL opcode)
+// with a BumpAllocator to stress-test per-call-frame arena allocation of
+// Contract, Memory, Stack, and ScopeContext.
+func TestNestedCallsWithBumpAllocator(t *testing.T) {
+ // Inner contract: returns 0x42.
+ innerCode := []byte{
+ byte(vm.PUSH1), 0x42,
+ byte(vm.PUSH1), 0,
+ byte(vm.MSTORE),
+ byte(vm.PUSH1), 32,
+ byte(vm.PUSH1), 0,
+ byte(vm.RETURN),
+ }
+ innerAddr := common.HexToAddress("0xbb")
+
+ // Outer contract calls inner and returns its result.
+ // PUSH1 32 - retSize
+ // PUSH1 0 - retOffset
+ // PUSH1 0 - argsSize
+ // PUSH1 0 - argsOffset
+ // PUSH1 0 - value
+ // PUSH20 - address
+ // PUSH3 0xffffff - gas
+ // CALL
+ // PUSH1 32 - size
+ // PUSH1 0 - offset
+ // RETURN
+ outerCode := []byte{
+ byte(vm.PUSH1), 32,
+ byte(vm.PUSH1), 0,
+ byte(vm.PUSH1), 0,
+ byte(vm.PUSH1), 0,
+ byte(vm.PUSH1), 0,
+ byte(vm.PUSH20),
+ }
+ outerCode = append(outerCode, innerAddr.Bytes()...)
+ outerCode = append(outerCode,
+ byte(vm.PUSH3), 0xff, 0xff, 0xff,
+ byte(vm.CALL),
+ byte(vm.POP), // pop success flag
+ byte(vm.PUSH1), 32,
+ byte(vm.PUSH1), 0,
+ byte(vm.RETURN),
+ )
+ outerAddr := common.HexToAddress("0xcc")
+
+ run := func(alloc arena.Allocator) ([]byte, error) {
+ statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ statedb.CreateAccount(innerAddr)
+ statedb.SetCode(innerAddr, innerCode, tracing.CodeChangeUnspecified)
+ statedb.CreateAccount(outerAddr)
+ statedb.SetCode(outerAddr, outerCode, tracing.CodeChangeUnspecified)
+ statedb.Finalise(true)
+
+ ret, _, err := Call(outerAddr, nil, &Config{
+ State: statedb,
+ EVMConfig: vm.Config{
+ Allocator: alloc,
+ },
+ })
+ return ret, err
+ }
+
+ heapRet, err := run(nil)
+ if err != nil {
+ t.Fatalf("heap nested call failed: %v", err)
+ }
+
+ slab := make([]byte, 8<<20)
+ bumpRet, err := run(arena.NewBumpAllocator(slab))
+ if err != nil {
+ t.Fatalf("bump nested call failed: %v", err)
+ }
+
+ if !bytes.Equal(heapRet, bumpRet) {
+ t.Fatalf("nested call results differ:\n heap: %x\n bump: %x", heapRet, bumpRet)
+ }
+}
+
+// TestCreateWithBumpAllocator exercises contract creation (CREATE opcode)
+// with a BumpAllocator.
+func TestCreateWithBumpAllocator(t *testing.T) {
+ // Simple init code that returns 0x42 as runtime code.
+ initCode := []byte{
+ byte(vm.PUSH1), 0x42, // runtime code is just "0x42" (1 byte)
+ byte(vm.PUSH1), 0,
+ byte(vm.MSTORE),
+ // Return 1 byte from memory offset 31 (the 0x42 byte in big-endian word).
+ byte(vm.PUSH1), 1,
+ byte(vm.PUSH1), 31,
+ byte(vm.RETURN),
+ }
+
+ run := func(alloc arena.Allocator) ([]byte, common.Address, error) {
+ code, addr, _, err := Create(initCode, &Config{
+ EVMConfig: vm.Config{
+ Allocator: alloc,
+ },
+ Value: big.NewInt(0),
+ })
+ return code, addr, err
+ }
+
+ heapCode, heapAddr, err := run(nil)
+ if err != nil {
+ t.Fatalf("heap create failed: %v", err)
+ }
+
+ slab := make([]byte, 8<<20)
+ bumpCode, bumpAddr, err := run(arena.NewBumpAllocator(slab))
+ if err != nil {
+ t.Fatalf("bump create failed: %v", err)
+ }
+
+ if !bytes.Equal(heapCode, bumpCode) {
+ t.Fatalf("created code differs:\n heap: %x\n bump: %x", heapCode, bumpCode)
+ }
+ if heapAddr != bumpAddr {
+ t.Fatalf("created address differs: heap=%s bump=%s", heapAddr, bumpAddr)
+ }
+}
diff --git a/core/vm/stack.go b/core/vm/stack.go
index 879dc9aa6d..017e154603 100644
--- a/core/vm/stack.go
+++ b/core/vm/stack.go
@@ -19,6 +19,7 @@ package vm
import (
"sync"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/holiman/uint256"
)
@@ -32,14 +33,32 @@ var stackPool = sync.Pool{
// expected to be changed and modified. stack does not take care of adding newly
// initialized objects.
type Stack struct {
- data []uint256.Int
+ data []uint256.Int
+ arenaAlloc bool // true if allocated from a BumpAllocator (skip pool return)
}
func newstack() *Stack {
return stackPool.Get().(*Stack)
}
+// newstackWithAlloc returns a new Stack, allocating from the provided arena.
+// When using a BumpAllocator, the Stack struct is arena-allocated and the
+// backing data slice comes from the arena. When using a HeapAllocator, it
+// falls back to the pool.
+func newstackWithAlloc(alloc arena.Allocator) *Stack {
+ if _, ok := alloc.(*arena.BumpAllocator); ok {
+ s := arena.New[Stack](alloc)
+ s.data = arena.MakeSlice[uint256.Int](alloc, 0, 1024)
+ s.arenaAlloc = true
+ return s
+ }
+ return newstack()
+}
+
func returnStack(s *Stack) {
+ if s.arenaAlloc {
+ return // arena-allocated: freed on arena.Reset()
+ }
s.data = s.data[:0]
stackPool.Put(s)
}
diff --git a/eth/backend.go b/eth/backend.go
index eaa68b501c..7508a00112 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/filtermaps"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/pruner"
@@ -239,6 +240,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
EnablePreimageRecording: config.EnablePreimageRecording,
EnableWitnessStats: config.EnableWitnessStats,
StatelessSelfValidation: config.StatelessSelfValidation,
+ Allocator: newAllocatorFromConfig(config),
},
// Enables file journaling for the trie database. The journal files will be stored
// within the data directory. The corresponding paths will be either:
@@ -597,3 +599,12 @@ func (s *Ethereum) Stop() error {
return nil
}
+
+// newAllocatorFromConfig returns a BumpAllocator if arena allocation is enabled
+// in the config, or nil (which defaults to HeapAllocator) otherwise.
+func newAllocatorFromConfig(config *ethconfig.Config) arena.Allocator {
+ if config.EnableArenaAlloc {
+ return arena.NewBumpAllocator(make([]byte, 8<<20)) // 8 MiB initial slab (grows automatically)
+ }
+ return nil
+}
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index 8aa6e4ef09..2aa3a8f5b6 100644
--- a/eth/ethconfig/config.go
+++ b/eth/ethconfig/config.go
@@ -174,6 +174,9 @@ type Config struct {
// Generate execution witnesses and self-check against them (testing purpose)
StatelessSelfValidation bool
+ // Use bump/arena allocator for transient EVM allocations during block processing
+ EnableArenaAlloc bool
+
// Enables tracking of state size
EnableStateSizeTracking bool
diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go
index 6f94a409e5..1f205c8d04 100644
--- a/eth/ethconfig/gen_config.go
+++ b/eth/ethconfig/gen_config.go
@@ -55,6 +55,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
EnablePreimageRecording bool
EnableWitnessStats bool
StatelessSelfValidation bool
+ EnableArenaAlloc bool
EnableStateSizeTracking bool
VMTrace string
VMTraceJsonConfig string
@@ -108,6 +109,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
enc.EnablePreimageRecording = c.EnablePreimageRecording
enc.EnableWitnessStats = c.EnableWitnessStats
enc.StatelessSelfValidation = c.StatelessSelfValidation
+ enc.EnableArenaAlloc = c.EnableArenaAlloc
enc.EnableStateSizeTracking = c.EnableStateSizeTracking
enc.VMTrace = c.VMTrace
enc.VMTraceJsonConfig = c.VMTraceJsonConfig
@@ -165,6 +167,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
EnablePreimageRecording *bool
EnableWitnessStats *bool
StatelessSelfValidation *bool
+ EnableArenaAlloc *bool
EnableStateSizeTracking *bool
VMTrace *string
VMTraceJsonConfig *string
@@ -297,6 +300,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
if dec.StatelessSelfValidation != nil {
c.StatelessSelfValidation = *dec.StatelessSelfValidation
}
+ if dec.EnableArenaAlloc != nil {
+ c.EnableArenaAlloc = *dec.EnableArenaAlloc
+ }
if dec.EnableStateSizeTracking != nil {
c.EnableStateSizeTracking = *dec.EnableStateSizeTracking
}
diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go
index 6e79fbd62b..694a1f4c90 100644
--- a/eth/gasestimator/gasestimator.go
+++ b/eth/gasestimator/gasestimator.go
@@ -81,10 +81,10 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
// Normalize the max fee per gas the call is willing to spend.
var feeCap *big.Int
- if call.GasFeeCap != nil {
- feeCap = call.GasFeeCap
- } else if call.GasPrice != nil {
- feeCap = call.GasPrice
+ if !call.GasFeeCap.IsZero() {
+ feeCap = call.GasFeeCap.ToBig()
+ } else if !call.GasPrice.IsZero() {
+ feeCap = call.GasPrice.ToBig()
} else {
feeCap = common.Big0
}
@@ -93,17 +93,18 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
balance := opts.State.GetBalance(call.From).ToBig()
available := balance
- if call.Value != nil {
- if call.Value.Cmp(available) >= 0 {
+ if !call.Value.IsZero() {
+ valueBig := call.Value.ToBig()
+ if valueBig.Cmp(available) >= 0 {
return 0, nil, core.ErrInsufficientFundsForTransfer
}
- available.Sub(available, call.Value)
+ available.Sub(available, valueBig)
}
if opts.Config.IsCancun(opts.Header.Number, opts.Header.Time) && len(call.BlobHashes) > 0 {
blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob)
blobBalanceUsage := new(big.Int).SetInt64(int64(len(call.BlobHashes)))
blobBalanceUsage.Mul(blobBalanceUsage, blobGasPerBlob)
- blobBalanceUsage.Mul(blobBalanceUsage, call.BlobGasFeeCap)
+ blobBalanceUsage.Mul(blobBalanceUsage, call.BlobGasFeeCap.ToBig())
if blobBalanceUsage.Cmp(available) >= 0 {
return 0, nil, core.ErrInsufficientFunds
}
@@ -113,9 +114,9 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
// If the allowance is larger than maximum uint64, skip checking
if allowance.IsUint64() && hi > allowance.Uint64() {
- transfer := call.Value
- if transfer == nil {
- transfer = new(big.Int)
+ transfer := new(big.Int)
+ if !call.Value.IsZero() {
+ transfer = call.Value.ToBig()
}
log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance,
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance)
@@ -252,7 +253,7 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio
if call.GasPrice.Sign() == 0 {
evmContext.BaseFee = new(big.Int)
}
- if call.BlobGasFeeCap != nil && call.BlobGasFeeCap.BitLen() == 0 {
+ if call.BlobGasFeeCap.IsZero() {
evmContext.BlobBaseFee = new(big.Int)
}
evm := vm.NewEVM(evmContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true})
diff --git a/eth/tracers/api.go b/eth/tracers/api.go
index 5f2f16627a..b196f663c2 100644
--- a/eth/tracers/api.go
+++ b/eth/tracers/api.go
@@ -994,7 +994,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
if msg.GasPrice.Sign() == 0 {
blockContext.BaseFee = new(big.Int)
}
- if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 {
+ if !msg.BlobGasFeeCap.IsZero() && msg.BlobGasFeeCap.BitLen() == 0 {
blockContext.BlobBaseFee = new(big.Int)
}
if config != nil {
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 4f3071cb03..c4403576fa 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -761,7 +761,7 @@ func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *s
if msg.GasPrice.Sign() == 0 {
blockContext.BaseFee = new(big.Int)
}
- if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 {
+ if !msg.BlobGasFeeCap.IsZero() && msg.BlobGasFeeCap.BitLen() == 0 {
blockContext.BlobBaseFee = new(big.Int)
}
evm := b.GetEVM(ctx, state, header, vmConfig, blockContext)
@@ -1366,7 +1366,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
if msg.GasPrice.Sign() == 0 {
evm.Context.BaseFee = new(big.Int)
}
- if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 {
+ if !msg.BlobGasFeeCap.IsZero() && msg.BlobGasFeeCap.BitLen() == 0 {
evm.Context.BlobBaseFee = new(big.Int)
}
res, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 4fb30e6289..c631737d99 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -477,18 +477,25 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c
if args.AccessList != nil {
accessList = *args.AccessList
}
+ bigToU256 := func(b *big.Int) uint256.Int {
+ if b == nil {
+ return uint256.Int{}
+ }
+ v, _ := uint256.FromBig(b)
+ return *v
+ }
return &core.Message{
From: args.from(),
To: args.To,
- Value: (*big.Int)(args.Value),
+ Value: bigToU256((*big.Int)(args.Value)),
Nonce: uint64(*args.Nonce),
GasLimit: uint64(*args.Gas),
- GasPrice: gasPrice,
- GasFeeCap: gasFeeCap,
- GasTipCap: gasTipCap,
+ GasPrice: bigToU256(gasPrice),
+ GasFeeCap: bigToU256(gasFeeCap),
+ GasTipCap: bigToU256(gasTipCap),
Data: args.data(),
AccessList: accessList,
- BlobGasFeeCap: (*big.Int)(args.BlobFeeCap),
+ BlobGasFeeCap: bigToU256((*big.Int)(args.BlobFeeCap)),
BlobHashes: args.BlobHashes,
SetCodeAuthorizations: args.AuthorizationList,
SkipNonceChecks: skipNonceCheck,
diff --git a/keeper-zisk.elf b/keeper-zisk.elf
new file mode 100644
index 0000000000..3433ca0bb8
Binary files /dev/null and b/keeper-zisk.elf differ
diff --git a/tests/state_test.go b/tests/state_test.go
index f80bda4372..f7ca0cbdc0 100644
--- a/tests/state_test.go
+++ b/tests/state_test.go
@@ -35,7 +35,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
- "github.com/holiman/uint256"
)
func initMatcher(st *testMatcher) {
@@ -316,7 +315,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
start := time.Now()
// Execute the message.
- _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, msg.GasLimit, uint256.MustFromBig(msg.Value))
+ _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, msg.GasLimit, &msg.Value)
if err != nil {
b.Error(err)
return
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index 7525081f84..f4296573ed 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -477,19 +477,23 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Mess
}
}
+ var blobGasFeeCap uint256.Int
+ if tx.BlobGasFeeCap != nil {
+ blobGasFeeCap = *uint256.MustFromBig(tx.BlobGasFeeCap)
+ }
msg := &core.Message{
From: from,
To: to,
Nonce: tx.Nonce,
- Value: value,
+ Value: *uint256.MustFromBig(value),
GasLimit: gasLimit,
- GasPrice: gasPrice,
- GasFeeCap: tx.MaxFeePerGas,
- GasTipCap: tx.MaxPriorityFeePerGas,
+ GasPrice: *uint256.MustFromBig(gasPrice),
+ GasFeeCap: *uint256.MustFromBig(tx.MaxFeePerGas),
+ GasTipCap: *uint256.MustFromBig(tx.MaxPriorityFeePerGas),
Data: data,
AccessList: accessList,
BlobHashes: tx.BlobVersionedHashes,
- BlobGasFeeCap: tx.BlobGasFeeCap,
+ BlobGasFeeCap: blobGasFeeCap,
SetCodeAuthorizations: authList,
}
return msg, nil