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..3f3c9e47d9 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,6 +93,12 @@ 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), diff --git a/core/state_processor.go b/core/state_processor.go index 6eea74bdd8..b3c73b92a7 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -23,12 +23,14 @@ 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" ) @@ -69,7 +71,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 +95,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 +107,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) } diff --git a/core/state_transition.go b/core/state_transition.go index 62474b5f5b..2b00b2b9fe 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" @@ -174,22 +175,27 @@ 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(), - } + 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 := new(Message) + msg.Nonce = tx.Nonce() + msg.GasLimit = tx.Gas() + msg.GasPrice = tx.GasPrice() + msg.GasFeeCap = tx.GasFeeCap() + msg.GasTipCap = tx.GasTipCap() + + msg.To = tx.To() + msg.Value = tx.Value() + msg.Data = tx.Data() + msg.AccessList = tx.AccessList() + msg.SetCodeAuthorizations = tx.SetCodeAuthorizations() + msg.BlobHashes = tx.BlobHashes() + msg.BlobGasFeeCap = tx.BlobGasFeeCap() + // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee) @@ -210,7 +216,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 +250,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,6 +274,8 @@ func (st *stateTransition) to() common.Address { } func (st *stateTransition) buyGas() error { + // Compute mgval = gasLimit * gasPrice (the gas cost to deduct from sender). + // Use big.Int for balanceCheck to detect >256-bit overflow correctly. mgval := new(big.Int).SetUint64(st.msg.GasLimit) mgval.Mul(mgval, st.msg.GasPrice) balanceCheck := new(big.Int).Set(mgval) @@ -544,11 +556,11 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } st.returnGas() - effectiveTip := msg.GasPrice + effectiveTipU256, _ := uint256.FromBig(msg.GasPrice) if rules.IsLondon { - effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee) + baseFee, _ := uint256.FromBig(st.evm.Context.BaseFee) + effectiveTipU256 = new(uint256.Int).Sub(effectiveTipU256, baseFee) } - effectiveTipU256, _ := uint256.FromBig(effectiveTip) if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { // Skip fee payment when NoBaseFee is set and the fee fields @@ -565,12 +577,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. 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..ab199850cb 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 := new(Stack) + 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/geth b/geth new file mode 100755 index 0000000000..736b7c98ef Binary files /dev/null and b/geth differ 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