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