core: use an arena allocator in the STF

This commit is contained in:
Guillaume Ballet 2026-02-10 18:20:54 +01:00
parent cee751a1ed
commit 3f5b5dc3a3
No known key found for this signature in database
22 changed files with 1203 additions and 56 deletions

View file

@ -141,6 +141,7 @@ var (
utils.VMTraceJsonConfigFlag,
utils.VMWitnessStatsFlag,
utils.VMStatelessSelfValidationFlag,
utils.VMArenaAllocFlag,
utils.NetworkIdFlag,
utils.EthStatsURLFlag,
utils.GpoBlocksFlag,

View file

@ -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 {

View file

@ -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)

56
core/arena/allocator.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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]
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}
}

141
core/arena/bump.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

45
core/arena/heap.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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]
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -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))

View file

@ -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),

View file

@ -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)
}

View file

@ -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.

View file

@ -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 {

View file

@ -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.

View file

@ -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,

View file

@ -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 {

View file

@ -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 <http://www.gnu.org/licenses/>.
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 <inner> - 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)
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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

BIN
geth Executable file

Binary file not shown.

BIN
keeper-zisk.elf Normal file

Binary file not shown.